Index: /main/waeup.ikoba/branches/uli-payments/CHANGES.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/CHANGES.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/CHANGES.txt	(revision 12677)
@@ -0,0 +1,19 @@
+Changes
+*******
+
+0.2dev (unreleased)
+===================
+
+* Update bootstrap.py and dependent package versions.
+
+* Add products, documents and customers modules and tons of components.
+
+* Rename 'kofa' 'ikoba', 'university' 'company' and 'student' 'customer'.
+
+* Enucleation: Keep only the portal's framework.
+  Remove university, students, applicants, hostels and accesscodes modules.
+
+0.1 (never released)
+====================
+
+Initial version. Copy from waeup.kofa.
Index: /main/waeup.ikoba/branches/uli-payments/LICENSE
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/LICENSE	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/LICENSE	(revision 12677)
@@ -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.ikoba/branches/uli-payments/README.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/README.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/README.txt	(revision 12677)
@@ -0,0 +1,132 @@
+What is WAeUP.Ikoba ?
+*********************
+
+Ikoba, which means 'bucket' in Bini language, is a multifunctional Application and Registration Web Portal to provide transparent and comprehensive information about application and registration processes.
+
+Ikoba is the second flagships of the West African eUniversity Project (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).
+
+
+Ikoba and Kofa - Similarities and differences
+---------------------------------------------
+
+Ikoba could be described as a customer registration, document verification and contract conclusion portal. Customers are being registered (customer registration workflow), documents are being verified (document verification workflow) and contracts are being applied for and concluded (contract workflow).
+
+Ikoba's user interface and its usability is very similar to WAeUP's Student Management System Kofa. This comprises:
+
+- Responsive webdesign (by Bootstrap)
+- User management
+- Permissions and roles
+- Authentication
+- Portal configuration
+- Data import (batch processing)
+- Data export
+- Logging
+- Reporting
+- Localization
+- Mandates
+- Notification and mailing
+- Data tables
+- etc.
+
+But in Ikoba there are completely new content components (models), views (including pdf slips) and controlling components.
+
+
+Company
+-------
+
+The main web application and container class is called 'Company' which corresponds to the university container in Kofa. The company container contains sub-containers for products, public documents, customers and officers (regular users).
+
+
+Products
+--------
+
+A company offers products usually for sale. A product could be a license, a visa or any purchasable merchandise/article. In the base package a product has eight attributes, unique product id, title, contract title, contract_category, options (set of product options), valid from, valid to, terms and conditions
+
+Products don't have a workflow.
+
+
+Public Documents
+----------------
+
+Public documents are meant for publishing content on the portal. There are three types of public documents:
+
+- PDF documents
+- HTML documents
+- REST documents
+
+The first can be used to provide pdf files for download. The second and third can be used to create multilingual static html pages on the portal. HTML documents expect html coded text as input, REST documents expect reStructuredText which is transformed into html. Public documents have a publication workflow with two states: created and published. Only published documents can be seen by anonymous users.
+
+
+Officers
+--------
+
+Officers are regular users or principals of the system with a set of global and local roles which allows them to view or manage the various sections in Ikoba. In contrast to classical content management systems, officers do not manage their own space where they can add and edit own content. Ikoba officers can just access those parts of the portal which their roles or sets of permissions allow them to access.
+
+An officer has eight attributes: unique login name, title (fullname), public name, description, email address, phone number, set of roles, encrypted password
+
+Officers can't register themselves. They have to be added by the initial system administrator or by officers who have the permission to manage user data.
+
+Officers don't have a workflow.
+
+
+Customers
+---------
+
+The term 'customer' has two different meanings in Ikoba. On the one hand a customer is a container object which contains all the documents and contracts owned by a prospective or registered client or contractor of the company. On the other hand a customer is an authenticated users or principal of the system with a user account adapted to each customer container object.
+
+The attributes of a customer in the base package are: customer id, firstname, middlename, lastname, sex, email, phone, an encrypted password and a unique registration number which can be set by the company.
+
+Customers have a registration workflow with four states: created, started, requested and approved. An email notification is sent after after approval or rejection of the registration.
+
+Customer accounts can be temporarily suspended. Although suspension is not part of the workflow it does appear in the customer's workflow history.
+
+Only approved customers can conclude contracts (see below).
+
+Customers can register themselves. They need a valid email address for self-registration.
+
+
+Customer Documents
+------------------
+
+Customers are requested to upload scanned documents and images required e.g. for the conclusion of contracts. Each uploaded file is connected with a document object which carries the metadata of the scanned document. In the base package a customer document has only a title, a unique document id and an md5 attribute. The latter contains the md5 checksum of the connected (binary) file after verification.
+
+Documents have a verification workflow with five states: created, submitted, verified, rejected and expired.
+
+Various document classes can be implemented and selected by the customer to make customization as flexible as possible.
+
+Once a document has been submitted, it can no longer be edited by customers. Verified documents can neither be edited by customers nor by officers.
+
+The md5 checksum proves the genuineness of the uploaded file.
+
+A pdf document slip can be downloaded. The slip is composed of a covering page and the scanned document badged by a watermark.
+
+
+Contracts
+---------
+
+Contracts are being concluded between the company and the customer. The customer applies for a contract by
+
+1. selecting a proper form (= contract class),
+2. selecting one of the products which belong to the contract class,
+3. filling the contract form,
+4. uploading required documents,
+5. submitting the documents for verification,
+6. linking the submitted documents on the contract form,
+7. paying the contract fees,
+8. submitting the contract form for approval.
+
+A customer has to register first but must not wait for approval before s/he can add contracts. Only after registration the customer can proceed to the contracts section and add, pay and submit contracts. If documents have to be uploaded and attached to the contract, these documents must be at least submitted for verification. Otherwise they cannot be attached. This ensures that documents can't be modified after application (= submission of contracts). Thus, a new customer can register, add and submit documents and then add, pay and submit contracts in just one single session.
+
+Company officers approve the customer registration request first, then verify the customer's documents and finally approve the contracts submitted by the customer. Approving a contract means that the contract is concluded. Contracts can also be rejected or expire.
+
+Each contract class belongs to a contract category. The contract category defines the subset of products which can be selected when configuring the contract (see Products section above).
+
+A contract in the base package has a title, a unique contract id, one document reference, one product reference and list of product options.  A customized contract can have several document references, but it can only refer to a single product offered by the company. The product options determine the fees to be paid before a contract can be concluded.
+
+A contract can only be submitted if all documents attached to the contract have been at least submitted for verification. Contracts can only be approved if the customer registration request has been approved and then all attached documents have been verified by an officer.
+
+The contract workflow has the following states: created, submitted, approved, rejected and expired. An email notification is sent after after approval or rejection of the contract conclusion request.
+
+A pdf contract slip can be downloaded.
Index: /main/waeup.ikoba/branches/uli-payments/bootstrap.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/bootstrap.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/bootstrap.py	(revision 12677)
@@ -0,0 +1,189 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from optparse import OptionParser
+
+tmpeggs = tempfile.mkdtemp()
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --find-links to point to local resources, you can keep
+this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", help="use a specific zc.buildout version")
+
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true", default=False,
+                  help=("Normally, if you do not specify a --version, the "
+                        "bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", "--config-file",
+                  help=("Specify the path to the buildout configuration "
+                        "file to be used."))
+parser.add_option("-f", "--find-links",
+                  help=("Specify a URL to search for buildout releases"))
+parser.add_option("--allow-site-packages",
+                  action="store_true", default=False,
+                  help=("Let bootstrap.py use existing site packages"))
+parser.add_option("--setuptools-version",
+                  help="use a specific setuptools version")
+
+
+options, args = parser.parse_args()
+
+######################################################################
+# load/install setuptools
+
+try:
+    if options.allow_site_packages:
+        import setuptools
+        import pkg_resources
+    from urllib.request import urlopen
+except ImportError:
+    from urllib2 import urlopen
+
+ez = {}
+exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
+
+if not options.allow_site_packages:
+    # ez_setup imports site, which adds site packages
+    # this will remove them from the path to ensure that incompatible versions
+    # of setuptools are not in the path
+    import site
+    # inside a virtualenv, there is no 'getsitepackages'.
+    # We can't remove these reliably
+    if hasattr(site, 'getsitepackages'):
+        for sitepackage_path in site.getsitepackages():
+            sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
+
+setup_args = dict(to_dir=tmpeggs, download_delay=0)
+
+if options.setuptools_version is not None:
+    setup_args['version'] = options.setuptools_version
+
+ez['use_setuptools'](**setup_args)
+import setuptools
+import pkg_resources
+
+# This does not (always?) update the default working set.  We will
+# do it.
+for path in sys.path:
+    if path not in pkg_resources.working_set.entries:
+        pkg_resources.working_set.add_entry(path)
+
+######################################################################
+# Install buildout
+
+ws = pkg_resources.working_set
+
+cmd = [sys.executable, '-c',
+       'from setuptools.command.easy_install import main; main()',
+       '-mZqNxd', tmpeggs]
+
+find_links = os.environ.get(
+    'bootstrap-testing-find-links',
+    options.find_links or
+    ('http://downloads.buildout.org/'
+     if options.accept_buildout_test_releases else None)
+    )
+if find_links:
+    cmd.extend(['-f', find_links])
+
+setuptools_path = ws.find(
+    pkg_resources.Requirement.parse('setuptools')).location
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+
+    def _final_version(parsed_version):
+        try:
+            return not parsed_version.is_prerelease
+        except AttributeError:
+            # Older setuptools
+            for part in parsed_version:
+                if (part[:1] == '*') and (part not in _final_parts):
+                    return False
+            return True
+
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setuptools_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+import subprocess
+if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
+    raise Exception(
+        "Failed to execute command:\n%s" % repr(cmd)[1:-1])
+
+######################################################################
+# Import and run buildout
+
+ws.add_entry(tmpeggs)
+ws.require(requirement)
+import zc.buildout.buildout
+
+if not [a for a in args if '=' not in a]:
+    args.append('bootstrap')
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+    args[0:0] = ['-c', options.config_file]
+
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
Index: /main/waeup.ikoba/branches/uli-payments/buildout-zeo.cfg
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/buildout-zeo.cfg	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/buildout-zeo.cfg	(revision 12677)
@@ -0,0 +1,131 @@
+# A buildout configuration for a ZEO server and two ZEO clients.
+#
+[buildout]
+extends = buildout.cfg
+parts =
+    eggbasket
+    app
+    i18n
+    test
+    mkdirs
+    zpasswd
+    zope_conf
+    site_zcml
+    zdaemon_conf
+    raw_deploy_ini
+    raw_debug_ini
+    themed_deploy_ini
+    themed_debug_ini
+    profile_ini
+    zeo1_ini
+    zeo2_ini
+    waeupdocs
+    coverage-detect
+    coverage-report
+# ikobactl creation must be _after_ app!
+    ikobactl
+    lxml
+    diazo
+# For backward compatibility, telling buildout not to throw away
+# the data and log subdirectories from the parts directory.
+    data
+    log
+
+    zeo_server
+    zope_conf_zeo_1
+    zope_conf_zeo_2
+    zeo1_ini
+    zeo2_ini
+    zeo1_conf
+    zeo2_conf
+    zeo_client1
+    zeo_client2
+
+[zeo_server]
+recipe = zc.zodbrecipes:server
+zeo.conf =
+  <zeo>
+    address ${ikoba_params:server_port}
+  </zeo>
+  <blobstorage 1>
+    blob-dir ${zope_conf:blobstorage}
+    <filestorage 1>
+      path ${zope_conf:filestorage}/Data.fs
+    </filestorage>
+  </blobstorage>
+  <filestorage async>
+    path ${zope_conf:filestorage}/Data.async.fs
+  </filestorage>
+  <eventlog>
+    # This sets up logging to a file.
+    # The "path" setting can be a relative or absolute
+    # filesystem path.
+    <logfile>
+      path ${zope_conf:logfiles}/z3.log
+      formatter zope.exceptions.log.Formatter
+    </logfile>
+    # This sets up logging to to standard output.
+    # The "path" setting can be the tokens STDOUT or STDERR
+    #  <logfile>
+    #    path STDOUT
+    #    formatter zope.exceptions.log.Formatter
+    #  </logfile>
+  </eventlog>
+
+[zope_conf_zeo_1]
+recipe = collective.recipe.template
+input = etc/zope_zeo1.conf.in
+output = ${buildout:parts-directory}/etc/zope_zeo1.conf
+filestorage = ${buildout:directory}/var/filestorage
+blobstorage = ${buildout:directory}/var/blobstorage
+logfiles = ${buildout:directory}/var/log
+extra =
+# extra = <grokwarnings>True</grokwarnings>
+# 'extra' is copied verbatim. Use it for product config sections and so.
+
+[zope_conf_zeo_2]
+recipe = collective.recipe.template
+input = etc/zope_zeo2.conf.in
+output = ${buildout:parts-directory}/etc/zope_zeo2.conf
+filestorage = ${buildout:directory}/var/filestorage
+blobstorage = ${buildout:directory}/var/blobstorage
+logfiles = ${buildout:directory}/var/log
+extra =
+# extra = <grokwarnings>True</grokwarnings>
+# 'extra' is copied verbatim. Use it for product config sections and so.
+
+[zeo1_ini]
+recipe = collective.recipe.template
+input = etc/themed/zeo1.ini.in
+output = ${buildout:parts-directory}/etc/themed-zeo1.ini
+
+[zeo2_ini]
+recipe = collective.recipe.template
+input = etc/themed/zeo2.ini.in
+output = ${buildout:parts-directory}/etc/themed-zeo2.ini
+
+[zeo1_conf]
+recipe = collective.recipe.template
+input = etc/zeo1.conf.in
+output = ${buildout:parts-directory}/etc/zeo1.conf
+
+[zeo2_conf]
+recipe = collective.recipe.template
+input = etc/zeo2.conf.in
+output = ${buildout:parts-directory}/etc/zeo2.conf
+
+
+[zeo_client1]
+# See http://pypi.python.org/pypi/zc.recipe.egg for details...
+recipe = zc.recipe.egg
+eggs = waeup.ikoba [beaker, diazo]
+arguments = "${buildout:parts-directory}/etc/zeo1.conf"
+scripts = ikobactl=zeo_client1
+
+[zeo_client2]
+# See http://pypi.python.org/pypi/zc.recipe.egg for details...
+recipe = zc.recipe.egg
+eggs = waeup.ikoba [beaker, diazo]
+arguments = "${buildout:parts-directory}/etc/zeo2.conf"
+scripts = ikobactl=zeo_client2
+
Index: /main/waeup.ikoba/branches/uli-payments/buildout.cfg
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/buildout.cfg	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/buildout.cfg	(revision 12677)
@@ -0,0 +1,210 @@
+[buildout]
+develop = .
+parts =
+    eggbasket
+    app
+    i18n
+    test
+    mkdirs
+    zpasswd
+    zope_conf
+    site_zcml
+    zdaemon_conf
+    raw_deploy_ini
+    raw_debug_ini
+    themed_deploy_ini
+    themed_debug_ini
+    profile_ini
+    waeupdocs
+    coverage-detect
+    coverage-report
+# ikobactl creation must be _after_ app!
+    ikobactl
+    lxml
+    diazo
+# For backward compatibility, telling buildout not to throw away
+# the data and log subdirectories from the parts directory.
+    data
+    log
+# newest = false
+
+extends = versions.cfg
+show-picked-versions = true
+update-versions-file = versions.cfg
+
+
+# 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.
+
+[ikoba_params]
+## Basic parameters for deployment.
+# default is 127.0.0.1
+host = 0.0.0.0
+# default is 8080
+base_port = 8080
+zeo1_port = 8080
+zeo2_port = 8081
+zeo3_port = 8082
+server_port = 8100
+# default 7. Number of pre-opened ZODB threads/client
+pool_size = 7
+# default 20MB. Cache size of each client (ZEO only)
+client_cache = 20MB
+# default 5000. Number of objects cached.
+object_cache = 5000
+# number of preopened paster threads, default 10
+threadpool_workers = 10
+# package to be developed
+devel_pkg = waeup.ikoba
+
+[app]
+# This creates all scripts in bin/. The ikobactl created here is 'faulty'.
+recipe = zc.recipe.egg
+eggs = waeup.ikoba [beaker, diazo]
+       z3c.evalexception>=2.0
+       Paste
+       PasteScript
+       PasteDeploy
+       repoze.profile
+       ZODB3
+interpreter = python-console
+
+[ikobactl]
+# See http://pypi.python.org/pypi/zc.recipe.egg for details...
+# Here we create the ikobactl script again, this time with a
+# fixed path to zdaemon.conf as argument.
+recipe = zc.recipe.egg
+eggs = waeup.ikoba [beaker, diazo]
+arguments = "${buildout:parts-directory}/etc/zdaemon.conf"
+scripts = ikobactl=ikobactl
+
+[mkdirs]
+recipe = z3c.recipe.mkdir
+paths =
+    ${zope_conf:filestorage}
+    ${zope_conf:logfiles}
+    ${zope_conf:blobstorage}
+    ${buildout:directory}/var/mailqueue/cur
+    ${buildout:directory}/var/mailqueue/tmp
+    ${buildout:directory}/var/mailqueue/new
+    ${buildout:directory}/var/datacenter/media
+    ${buildout:directory}/parts/test/datacenter
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = waeup.ikoba [beaker, test]
+defaults = ['--tests-pattern', '^f?tests$', '-v']
+
+# this section named so that the i18n scripts are called bin/i18n...
+[i18n]
+recipe = z3c.recipe.i18n:i18n
+packages = waeup.ikoba
+eggs = waeup.ikoba
+domain = waeup.ikoba
+output = src/waeup/ikoba/locales
+zcml =
+
+# This section is named so that the zpasswd utility is
+# called `zpasswd`
+[zpasswd]
+recipe = z3c.recipe.dev:script
+eggs = waeup.ikoba
+module = zope.app.server.zpasswd
+method = main
+
+[zope_conf]
+recipe = collective.recipe.template
+input = etc/zope.conf.in
+output = ${buildout:parts-directory}/etc/zope.conf
+filestorage = ${buildout:directory}/var/filestorage
+blobstorage = ${buildout:directory}/var/blobstorage
+logfiles = ${buildout:directory}/var/log
+extra =
+# extra = <grokwarnings>True</grokwarnings>
+# 'extra' is copied verbatim. Use it for product config sections and so.
+
+# The [data] and [log] parts are still in here to instruct buildout to not
+# unintentionally throw away the parts/data and parts/log subdirectories
+# that contain the Data.fs and the log files. These files should be
+# copied to the new locations. See the upgrade notes for more information.
+[data]
+recipe = zc.recipe.filestorage
+
+[log]
+recipe = zc.recipe.filestorage
+
+[mailqueue]
+recipe = zc.recipe.filestorage
+
+[site_zcml]
+recipe = collective.recipe.template
+input = etc/site.zcml.in
+output = ${buildout:parts-directory}/etc/site.zcml
+
+[zdaemon_conf]
+recipe = collective.recipe.template
+input = etc/zdaemon.conf.in
+output = ${buildout:parts-directory}/etc/zdaemon.conf
+
+[raw_debug_ini]
+recipe = collective.recipe.template
+input = etc/raw/debug.ini.in
+output = ${buildout:parts-directory}/etc/raw-debug.ini
+
+[raw_deploy_ini]
+recipe = collective.recipe.template
+input = etc/raw/deploy.ini.in
+output = ${buildout:parts-directory}/etc/raw-deploy.ini
+
+[themed_debug_ini]
+recipe = collective.recipe.template
+input = etc/themed/debug.ini.in
+output = ${buildout:parts-directory}/etc/themed-debug.ini
+
+[themed_deploy_ini]
+recipe = collective.recipe.template
+input = etc/themed/deploy.ini.in
+output = ${buildout:parts-directory}/etc/themed-deploy.ini
+
+[profile_ini]
+recipe = collective.recipe.template
+input = etc/profile.ini.in
+output = ${buildout:parts-directory}/etc/profile.ini
+
+[eggbasket]
+recipe = z3c.recipe.eggbasket
+eggs = grok
+url = http://grok.zope.org/releaseinfo/grok-eggs-1.1.tgz
+
+[waeupdocs]
+recipe = collective.recipe.sphinxbuilder
+eggs = waeup.ikoba[docs]
+source = ${buildout:directory}/docs/source
+build = ${buildout:directory}/docs/build
+
+# Collect test coverage data.
+[coverage-detect]
+recipe = zc.recipe.testrunner
+eggs = waeup.ikoba[beaker]
+defaults = ['--tests-pattern', '^f?tests$', '-v', '--coverage', 'coverage']
+
+# Create a coverage report.
+# Make sure to run bin/coverage-detect to collect the data for the report
+# first!
+[coverage-report]
+recipe = zc.recipe.egg
+eggs = z3c.coverage
+scripts = coveragereport
+arguments = ('${buildout:parts-directory}/coverage-detect/coverage', '${buildout:directory}/coverage-report')
+
+[lxml]
+recipe = z3c.recipe.staticlxml
+egg = lxml
+
+[diazo]
+recipe = zc.recipe.egg
+eggs =
+    diazo [wsgi]
+    PasteScript
+    waeup.ikoba [beaker,diazo]
Index: /main/waeup.ikoba/branches/uli-payments/docs/INSTALL.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/INSTALL.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/INSTALL.txt	(revision 12677)
@@ -0,0 +1,468 @@
+Installation of Ikoba
+#####################
+
+These are generic installation instructions for the WAeUP_ ``Ikoba``
+application and registration portal and customized versions thereof.
+
+For specific aspects of this package please refer to the local README
+file.
+
+Please note, that **only Linux-based installs** are described and
+actively supported. We recommend use of Debian_ / Ubuntu_.
+
+.. note:: This means we do not actively support Windows(tm)!
+
+.. contents::
+
+
+Prerequisites
+*************
+
+The Ikoba packages are based on `Grok`_, which is a Python_ framework
+for agile webapplication development. Grok_ itself is based on
+`Zope`_.
+
+Both, Grok_ and Zope_, are written in the `Python`_ programming
+language (with parts written in C). You therefore have to have
+`Python`_ installed (including `Python`_ headers).
+
+
+Preparing the System
+====================
+
+For a `Ikoba` install we need of course `Python`_ installed as well as
+some standard developer tools like C compilers, C libraries, etc.
+
+What you need (Debian/Ubuntu package names in brackets):
+
+* Python 2.7 (``python2.7``)
+
+* Python 2.7 development files (``python2.7-dev``)
+
+* A C-Compiler with basic C developer libraries (``build-essential``)
+
+* A subversion client (``subversion``)
+
+* XML and XSLT development libraries (``libxml2-dev``, ``libxslt1-dev``)
+
+* The "Foreign Function Interface Library" (FFI, ``libffi-dev``) and openssl
+  libs (``libssl-dev``) because they are needed by the paypal Python SDK
+
+* enscript (``enscript``) [optional]
+
+  This is only needed if you want test coverage reports.
+
+All these packages can be installed on Debian systems like this::
+
+  $ sudo apt-get install python2.7 python2.7-dev subversion \
+                         build-essential enscript libxml2-dev libxslt1-dev \
+                         libffi-dev libssl-dev
+
+Afterwards you should be able to enter::
+
+  $ python2.7
+
+at the commandline and get a Python_ prompt. Quit the interpreter
+pressing <CTRL-D>.
+
+
+Installing `virtualenv`
+=======================
+
+We strongly suggest use of `virtualenv`_ to create Python_ sandboxes
+where you can run your code without touching any other installations.
+
+`virtualenv`_ is also the only possibility to build a `Ikoba` install
+without the need of superuser permissions. In short: `virtualenv`_
+will make your life easier. Use it.
+
+Detailed install instructions for `virtualenv`_ can be found on
+http://www.virtualenv.org/en/latest/virtualenv.html#installation.
+
+The short way for a user install (no superuser perms required) is like
+this::
+
+  $ curl -O https://pypi.python.org/packages/source/v/virtualenv/virtualenv-1.11.4.tar.gz#md5=9accc2d3f0ec1da479ce2c3d1fdff06e
+  $ tar xvfz virtualenv-1.11.4.tar.gz
+  $ cd virtualenv-1.11.4
+  $ python virtualenv.py py27
+
+Superusers can install `virtualenv`_ system-wide much easier. On
+`Debian`_/`Ubuntu`_ systems where you have superuser permissions, you
+can install `virtualenv`_ via::
+
+  $ sudo apt-get install python-virtualenv
+
+*Or*, if `pip` is installed already (superusers can install it via
+ ``sudo apt-get install python-pip`` on `Debian`_/`Ubuntu`_)::
+
+  $ sudo pip install virtualenv
+
+
+Creating a Sandbox
+==================
+
+After installing `virtualenv`_ you can install local sandboxes like
+this (if `virtualenv` is installed system-wide)::
+
+  $ virtualenv --no-site-packages py27
+
+*or* like this::
+
+  $ python /path/to/my/virtualenv.py --no-site-packages py27
+
+where ``py27`` is a directory in the filesystem where your sandbox
+will be created. `virtualenv` will also create this directory for
+you. While you can pick any sandbox name you want, it is recommended
+to use a sandbox name that is short and at the same time reflects the
+Python version used (here: Python 2.7).
+
+By passing the ``no-site-packages`` switch we tell `virtualenv` to
+provide a clean environment without any extra-packages installed
+systemwide. More recent versions of `virtualenv`_ have this option set
+by default.
+
+Another often used option of `virtualenv`_ is ``-p``. With ``-p`` you
+can tell `virtualenv`_ which Python executable to use as base for the
+new sandbox.
+
+You now can activate the sandbox by doing::
+
+  $ source py27/bin/activate
+  (py27)$
+
+You will notice that the input prompt changes, indicating the name of
+the sandbox activated.
+
+To deactivate the sandbox at any time, enter::
+
+  (py27)$ deactivate
+
+and the prompt will be the same as before the activation.
+
+For the following steps make sure the sandbox is active.
+
+
+Getting the Sources
+===================
+
+Now, as the sandbox is activated (see above, if not), we can fetch the
+sources for Ikoba.
+
+As Ikoba is currently available from the Subversion_ repository only,
+we use the Subversion_ client `svn` and checkout the main devel branch
+of the `waeup.ikoba` package like this::
+
+  (py27)$ svn co https://svn.waeup.org/repos/main/waeup.ikoba/trunk
+
+The command should fetch the Ikoba base package sources for you and
+will put everything in a new directory ``trunk/``.
+
+Now enter the new directory::
+
+  (py27)$ cd trunk/
+
+and you can start building the real package.
+
+.. note:: If you not want to work with the `waeup.ikoba` package but a
+          customized package (like ``waeup.aaue`` or similar), then
+          you can use the above checkout command but with
+          ``waeup.ikoba`` replaced by the name of the customized
+          package::
+
+            (py27)$ svn co https://svn.waeup.org/repos/main/<PKG-NAME>/trunk
+
+          For the exact link, please refer to the README.txt file in
+          your package.
+
+
+Bootstrapping the Build
+=======================
+
+After `Creating a Sandbox`_ and `Getting the Sources`_ we have to
+initialize the newly created development environment::
+
+  (py27)$ python bootstrap.py
+
+This should create a local ``bin/`` directory with at least a script
+``buildout`` in it.
+
+If bootstrapping fails, chances are, that you need the `distribute`
+package installed. In a virtual environment you can install it without
+interfering with other packages installed on your server::
+
+  (py27)$ pip install --upgrade distribute
+
+Afterwards you have to retry the bootstrapping, until you get a
+working ``buildout`` script in the local ``bin/`` directory.
+
+Once the ``buildout`` script exists, you do not have to activate the
+virtual envronment any more (although you can).
+
+
+Part 1: Single Client Setup
+***************************
+
+You can run ``Ikoba`` with a single client (useful for evaluation,
+development, etc.) or with multiple clients running in parallel. The
+latter setup is useful in productive environments with many thousands
+users and called a ``ZEO`` install. We will cover both setup
+types. The first is covered here, the latter one is covered in `Part
+2: ZEO Install`_.
+
+
+Building the Package
+====================
+
+In the sources directory (``trunk/``) you have to prepare the
+project to fetch needed components (eggs), compile C-code parts,
+etc. This is done by a single command ``buildout``::
+
+  $ ./bin/buildout
+
+If this is your first install of some Grok-related project, this step
+will need some time as lots of sources have to be fetched, many
+components must be compiled, etc.
+
+This step must be redone whenever you change something in
+``buildout.cfg``, ``buildout-zeo.cfg``, or ``setup.py``.
+
+Note that if you have more than one sandbox for a Zope-based web
+application, it will probably make sense to share the eggs between the
+different sandboxes.  You can tell ``zc.buildout`` to use a central
+eggs directory by creating ``~/.buildout/default.cfg`` with the
+following contents::
+
+  [buildout]
+  eggs-directory = /home/bruno/buildout-eggs
+
+where the given directory should be exist beforehand. All eggs of all
+`buildout` projects will then be stored in this directory (and not
+fetched anew if already existing there).
+
+
+Start the Instance
+==================
+
+You should be able now to start the created instance by doing::
+
+  $ ./bin/ikobactl fg
+
+Alternatively you can do::
+
+  $ bin/paster serve parts/etc/deploy-themed.ini
+
+The port numbers where Ikoba is running on your server are defined in
+`buildout.cfg` under ``[ikoba_params]``.
+
+If you now point a browser to the right port on your server, for example::
+
+  localhost:8080
+
+you should get a login pop-up, where you can login as superuser with
+``grok`` and ``grok`` as username/password (Ikoba base package only!).
+
+You can stop the instance by pressing <CTRL-C>.
+
+If you are connected and logged in,
+you should be able to add the grok-based applications
+(such as ``Company``) from the menu.
+
+Add an instance of ``Company`` and click on the link next to the
+then visible entry in the list of installed applications.
+
+Running the Tests
+=================
+
+All WAeUP_ packages come with comprehensive test suites ensuring the
+software quality also after changes and updates.
+
+The package tests are easily run by executing the test runner
+from the ``bin`` directory::
+
+  $ ./bin/test
+
+Use the ``-c`` option to get coloured output. Use the ``--pdb`` option
+to start the Python_ debugger when a test fails.
+
+
+Part 2: ZEO Install
+*******************
+
+Each ZEO install consists of at least one ZEO server and normally two
+or more ZEO clients. While the ZEO server is meant to manage the ZODB
+database for clients, the clients connect to the outside world, listen
+for request and do the real dataprocessing.
+
+We prepared a `buildout` configuration that sets up one server
+configuration and two client configs. This configuration is in
+``buildout-zeo.cfg``.
+
+Generating the ZEO Setup
+========================
+
+To install Ikoba ZEO-based you can run `buildout` with the given
+(or your own) configuration file like this::
+
+  $ ./bin/buildout -c buildout-zeo.cfg
+
+This should generate all scripts necessary to run servers, clients,
+etc.
+
+Starting ZEO Servers and Clients
+================================
+
+First start the server::
+
+  $ ./bin/zeo_server start
+
+Clients can be started by doing::
+
+  $ ./bin/zeo_client1 start
+  $ ./bin/zeo_client2 start
+
+This will start both clients in daemon mode.
+
+Instead of ``start`` you can, as usually, start an instance in
+foreground (``fg``), etc. You know the drill.
+
+
+Manually Starting ZEO Clients
+=============================
+
+This is normally not neccessary.
+
+``zeo_clientN`` scripts are basically wrappers around calls to
+``bin/paster``. You can bypass this wrapper and start a client
+'manually' like this::
+
+  $ ./bin/paster serve --pid-file var/zeo1.pid --daemon \
+        pars/etc/zeo1.ini
+
+It is important to give a pid-file as `paster` otherwise can not start
+different clients (they would all refer to the same pid file
+`paster.pid` and refuse to start after the first client was started).
+
+
+Setup (paramters, ports, etc.)
+==============================
+
+By default the server will listen on port 8100 for requests from
+localhost (not: from the outside world).
+
+You can change ZEO server settings in the ``[zeo_server]`` section
+of ``buildout-zeo.conf``. Run `buildout` afterwards.
+
+The clients will listen on port 8081 and 8082. You can change settings
+in ``etc/zeo1.ini.in`` and ``etc/zeo2.ini.in`` respectively. Run
+buildout after any change.
+
+If you want to change the paster wrapper for any zeo client, you can
+edit ``etc/zeo1.conf`` and/or ``etc/zeo2.conf``. Run buildout
+afterwards.
+
+
+Stopping ZEO Servers and Clients
+================================
+
+Given everything was started in daemon mode, first stop the clients::
+
+  $ ./bin/zeo_client1 stop
+  $ ./bin/zeo_client2 stop
+
+Then stop the ZEO server::
+
+  $ ./bin/zeo_server stop
+
+
+Creating New ZEO Clients
+========================
+
+You want more ZEO clients to be created by `buildout`? Easy. Three
+steps are neccessary.
+
+1. Create config files in `etc/`
+--------------------------------
+
+Each client needs two configuration files:
+
+  - ``etc/zeoN.conf``
+       configuring the paster wrapper
+
+  - ``etc/zeoN.ini``
+       configuring the runtime config, ports, etc.
+
+Just copy over these files from the already existing zeo1/zeo2 files
+and replace ``zeo1`` or ``zeo2`` with your new name.
+
+2. Update buildout-zeo.cfg
+--------------------------
+
+Here, inside ``buildourt-zeo.cfg`` also three steps are needed.
+
+* 2.1. Create new .ini and .conf entries
+
+  The .conf and .ini files in etc/ are only templates that have to be
+  generated in their really used final location. In buildout-zeo.cfg
+  you can care for this by creating a new ``[zeoN_ini]`` and
+  ``[zeoN_conf]`` option (replacing ``N`` with a number, of course).
+
+  Just copy over existing entries and replace the mentions of ``zeo1``
+  or ``zeo2`` by your ``zeoN``.
+
+* 2.2. Create a new ``zeo_clientN`` entry
+
+  Then you have to create an entry that will generate the
+  ``zeo_clientN`` script. Again, just copy over an existing
+  ``[zeo_client1]`` entry and replace ``1`` withg your client number.
+
+* 2.3. Register the new sections in ``[buildout]`` section
+
+  When done with the above: add the new section in ``[buildout]``::
+
+    [buildout]
+      ...
+      <old entries...>
+      ...
+      zope_conf_zeo_5
+      zeo5_ini
+      zeo_client5
+
+  depending on how you named your new sections.
+
+3. Rerun ``buildout``
+---------------------
+
+When adding or removing client/server instances, make sure to stop all
+running servers/clients before rerunning buildout.
+
+To activate the new setup, rerun buildout::
+
+  $ bin/buildout -c buildout-zeo.cfg
+
+This should generate any new clients and remove older ones or just
+update configuration files.
+
+
+Considerations
+==============
+
+There are some things in the current buildout-zeo.cfg we might do not
+want. It extends the regular ``buildout.cfg`` so that we do not have
+to repeat most sections but the ``parts`` in ``[buildout]`` have to be
+listed.
+
+We need, however, not everything with a ZEO-deploy that is listed in a
+default buildout. We might do not need docs, no profiling, etc. Also a
+regular non-ZEO ikobactl might not make to much sense. Therefore all
+this might be subject to changes.
+
+.. _Debian: http://www.debian.org/
+.. _Grok: http://grok.zope.org/
+.. _Python: http://www.python.org/
+.. _Subversion: http://subversion.apache.org/
+.. _Ubuntu: http://www.ubuntu.com/
+.. _virtualenv: http://www.virtualenv.org/en/latest/
+.. _WAeUP: https://www.waeup.org/
+.. _Zope: http://www.zope.org/
+.. _zc.buildout: http://cheeseshop.python.org/pypi/zc.buildout
Index: /main/waeup.ikoba/branches/uli-payments/docs/Makefile
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/Makefile	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/Makefile	(revision 12677)
@@ -0,0 +1,89 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html      to make standalone HTML files"
+	@echo "  dirhtml   to make HTML files named index.html in directories"
+	@echo "  pickle    to make pickle files"
+	@echo "  json      to make JSON files"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  qthelp    to make HTML files and a qthelp project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WAeUPStudentInformationandRegistrationPortalKOFA.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WAeUPStudentInformationandRegistrationPortalKOFA.qhc"
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
Index: /main/waeup.ikoba/branches/uli-payments/docs/make.bat
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/make.bat	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/make.bat	(revision 12677)
@@ -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\WAeUPIkoba.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WAeUPIkoba.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.ikoba/branches/uli-payments/docs/source/_static/default.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/_static/default.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/_static/default.css	(revision 12677)
@@ -0,0 +1,257 @@
+/*
+ * default.css
+ * ~~~~~~~~~~~~~
+ *
+ * Sphinx stylesheet -- default theme.
+ *
+ * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ * Customized by Henrik Bettermann
+ */
+
+@import url("basic.css");
+
+/* -- page layout ----------------------------------------------------------- */
+
+body {
+    font-family: sans-serif;
+    font-size: 100%;
+    background-color: #11303d;
+    color: #000;
+    margin: 0;
+    padding: 0;
+}
+
+div.document {
+    background-color: #1c4e63;
+}
+
+div.documentwrapper {
+    float: left;
+    width: 100%;
+}
+
+div.bodywrapper {
+    margin: 0 0 0 230px;
+}
+
+div.body {
+    background-color: #ffffff;
+    color: #000000;
+    padding: 0 20px 30px 20px;
+}
+
+div.footer {
+    color: #ffffff;
+    width: 100%;
+    padding: 9px 0 9px 0;
+    text-align: center;
+    font-size: 75%;
+}
+
+div.footer a {
+    color: #ffffff;
+    text-decoration: underline;
+}
+
+div.related {
+    background-color: #133f52;
+    line-height: 30px;
+    color: #ffffff;
+}
+
+div.related a {
+    color: #ffffff;
+}
+
+div.sphinxsidebar {
+}
+
+div.sphinxsidebar h3 {
+    font-family: 'Trebuchet MS', sans-serif;
+    color: #ffffff;
+    font-size: 1.4em;
+    font-weight: normal;
+    margin: 0;
+    padding: 0;
+}
+
+div.sphinxsidebar h3 a {
+    color: #ffffff;
+}
+
+div.sphinxsidebar h4 {
+    font-family: 'Trebuchet MS', sans-serif;
+    color: #ffffff;
+    font-size: 1.3em;
+    font-weight: normal;
+    margin: 5px 0 0 0;
+    padding: 0;
+}
+
+div.sphinxsidebar p {
+    color: #ffffff;
+}
+
+div.sphinxsidebar p.topless {
+    margin: 5px 10px 10px 10px;
+}
+
+div.sphinxsidebar ul {
+    margin: 10px;
+    padding: 0;
+    color: #ffffff;
+}
+
+div.sphinxsidebar a {
+    color: #98dbcc;
+}
+
+div.sphinxsidebar input {
+    border: 1px solid #98dbcc;
+    font-family: sans-serif;
+    font-size: 1em;
+}
+
+
+
+/* -- hyperlink styles ------------------------------------------------------ */
+
+a {
+    color: #355f7c;
+    text-decoration: none;
+}
+
+a:visited {
+    color: #355f7c;
+    text-decoration: none;
+}
+
+a:hover {
+    text-decoration: underline;
+}
+
+
+
+/* -- body styles ----------------------------------------------------------- */
+
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+    font-family: 'Trebuchet MS', sans-serif;
+    background-color: #f2f2f2;
+    font-weight: normal;
+    color: #20435c;
+    border-bottom: 1px solid #ccc;
+    margin: 20px -20px 10px -20px;
+    padding: 3px 0 3px 10px;
+}
+
+div.body h1 { margin-top: 0; font-size: 200%; }
+div.body h2 { font-size: 160%; }
+div.body h3 { font-size: 140%; }
+div.body h4 { font-size: 120%; }
+div.body h5 { font-size: 110%; }
+div.body h6 { font-size: 100%; }
+
+a.headerlink {
+    color: #c60f0f;
+    font-size: 0.8em;
+    padding: 0 4px 0 4px;
+    text-decoration: none;
+}
+
+a.headerlink:hover {
+    background-color: #c60f0f;
+    color: white;
+}
+
+div.body p, div.body dd, div.body li {
+    text-align: left;
+    line-height: 130%;
+}
+
+div.admonition p.admonition-title + p {
+    display: inline;
+}
+
+div.admonition p {
+    margin-bottom: 5px;
+}
+
+div.admonition pre {
+    margin-bottom: 5px;
+}
+
+div.admonition ul, div.admonition ol {
+    margin-bottom: 5px;
+}
+
+div.note {
+    background-color: #eee;
+    border: 1px solid #ccc;
+}
+
+div.seealso {
+    background-color: #ffc;
+    border: 1px solid #ff6;
+}
+
+div.topic {
+    background-color: #eee;
+}
+
+div.warning {
+    background-color: #ffe4e4;
+    border: 1px solid #f66;
+}
+
+p.admonition-title {
+    display: inline;
+}
+
+p.admonition-title:after {
+    content: ":";
+}
+
+pre {
+    padding: 5px;
+    background-color: #eeffcc;
+    color: #333333;
+    line-height: 120%;
+    border: 1px solid #ac9;
+    border-left: none;
+    border-right: none;
+}
+
+tt {
+    background-color: #ecf0f3;
+    padding: 0 1px 0 1px;
+    font-size: 0.95em;
+}
+
+th {
+    background-color: #ede;
+}
+
+.warning tt {
+    background: #efc2c2;
+}
+
+.note tt {
+    background: #d6d6d6;
+}
+
+.viewcode-back {
+    font-family: sans-serif;
+}
+
+div.viewcode-block:target {
+    background-color: #f4debf;
+    border-top: 1px solid #ac9;
+    border-bottom: 1px solid #ac9;
+}
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/_templates/mainpage.template
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/_templates/mainpage.template	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/_templates/mainpage.template	(revision 12677)
@@ -0,0 +1,94 @@
+{% extends "layout.html" %}
+{% set title = 'Overview' %}
+{% block body %}
+
+<h1>{{ docstitle }}</h1>
+<p>
+  This is official documentation for waeup.ikoba {{ 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.ikoba.</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.ikoba.
+    </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.ikoba/branches/uli-payments/docs/source/bugs.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/bugs.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/bugs.rst	(revision 12677)
@@ -0,0 +1,18 @@
+.. _reporting-bugs:
+
+Reporting Bugs in `waeup.ikoba`
+******************************
+
+The developers would like to know of any deficiencies you find in
+`waeup.ikoba`.
+
+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.ikoba/branches/uli-payments/docs/source/changes.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/changes.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/changes.rst	(revision 12677)
@@ -0,0 +1,1 @@
+.. include:: ../../CHANGES.txt
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/conf.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/conf.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/conf.py	(revision 12677)
@@ -0,0 +1,213 @@
+# -*- coding: utf-8 -*-
+#
+# WAeUP Application and Registration Portal (Ikoba) 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.Ikoba'
+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.ikoba').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 = 'WAeUPIkoba'
+
+
+# -- 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', 'WAeUPIkoba.tex', u'WAeUP Application and Registration Portal (Ikoba) 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.ikoba/branches/uli-payments/docs/source/contents.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/contents.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/contents.rst	(revision 12677)
@@ -0,0 +1,25 @@
+.. _contents:
+
+WAeUP.Ikoba 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.ikoba/branches/uli-payments/docs/source/copyright.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/copyright.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/copyright.rst	(revision 12677)
@@ -0,0 +1,10 @@
+Copyright
+*********
+
+`waeup.ikoba` 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.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_app.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_app.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_app.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`app` -- Company Root (Site) Module
+**************************************
+
+.. automodule:: waeup.ikoba.app
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_async.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_async.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_async.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`async` -- Asynchronous Job Handling Module
+*******************************************
+
+.. automodule:: waeup.ikoba.async
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_authentication.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_authentication.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_authentication.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`authentication` -- Authentication Components Module
+****************************************************
+
+.. automodule:: waeup.ikoba.authentication
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_beaker.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_beaker.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_beaker.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`beaker` -- dolmen.beaker Configuration Module
+**********************************************
+
+.. automodule:: waeup.ikoba.beaker
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_browser.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_browser.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_browser.rst	(revision 12677)
@@ -0,0 +1,27 @@
+`browser` -- Viewing Components Package
+***************************************
+
+(see also: :ref:`api-browser-modules`)
+
+.. automodule:: waeup.ikoba.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.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_catalog.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_catalog.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_catalog.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`catalog` -- Cataloging Components Module
+*****************************************
+
+.. automodule:: waeup.ikoba.catalog
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_configuration.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_configuration.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_configuration.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`configuration` -- Portal Configuration Components Module
+**********************************************************
+
+.. automodule:: waeup.ikoba.configuration
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_datacenter.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_datacenter.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_datacenter.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`datacenter` -- Datacenter Module
+*********************************
+
+.. automodule:: waeup.ikoba.datacenter
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_fees.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_fees.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_fees.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`fees` -- Fee Management Components Module
+******************************************
+
+.. automodule:: waeup.ikoba.fees
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_image.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_image.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_image.rst	(revision 12677)
@@ -0,0 +1,22 @@
+`image` -- Image File Components Package
+****************************************
+
+(see also: :ref:`api-image-modules`)
+
+.. automodule:: waeup.ikoba.image
+   :members:
+   :undoc-members:
+
+.. _api-image-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   image/api_image
+   image/api_schema
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_imagestorage.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_imagestorage.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_imagestorage.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`imagestorage` -- Image and File Storage Components Module
+**********************************************************
+
+.. automodule:: waeup.ikoba.imagestorage
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_interfaces.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_interfaces.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_interfaces.rst	(revision 12677)
@@ -0,0 +1,10 @@
+`interfaces` - Main Interfaces Module
+*************************************
+
+.. automodule:: waeup.ikoba.interfaces
+   :members:
+   :undoc-members:
+
+.. autointerface:: waeup.ikoba.interfaces.ICompany
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_language.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_language.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_language.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`language` -- User Language Components Module
+*********************************************
+
+.. automodule:: waeup.ikoba.language
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_maintenance.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_maintenance.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_maintenance.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`maintenance` -- Maintenance Helpers
+************************************
+
+.. automodule:: waeup.ikoba.maintenance
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_mandates.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_mandates.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_mandates.rst	(revision 12677)
@@ -0,0 +1,23 @@
+`mandates` -- Mandates Components Package
+*****************************************
+
+(see also: :ref:`api-mandates-modules`)
+
+.. automodule:: waeup.ikoba.mandates
+   :members:
+   :undoc-members:
+
+.. _api-mandates-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   mandates/api_browser
+   mandates/api_container
+   mandates/api_mandate
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_meta.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_meta.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_meta.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`meta` -- Ikoba Components Grokkers Module
+******************************************
+
+.. automodule:: waeup.ikoba.meta
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_objecthistory.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_objecthistory.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_objecthistory.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`objecthistory` -- Components for Object Histories Module
+*********************************************************
+
+.. automodule:: waeup.ikoba.objecthistory
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_payments.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_payments.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_payments.rst	(revision 12677)
@@ -0,0 +1,23 @@
+`payments` -- Payment Components Package
+****************************************
+
+(see also: :ref:`api-payments-modules`)
+
+.. automodule:: waeup.ikoba.payments
+   :members:
+   :undoc-members:
+
+.. _api-payments-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   payments/api_container
+   payments/api_interfaces
+   payments/api_payment
+   payments/api_paypal
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_permissions.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_permissions.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_permissions.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`permissions` -- Permissions and Roles Components Module
+********************************************************
+
+.. automodule:: waeup.ikoba.permissions
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_reports.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_reports.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_reports.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`reports` -- Report Generation Components Module
+************************************************
+
+.. automodule:: waeup.ikoba.reports
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_schema.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_schema.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_schema.rst	(revision 12677)
@@ -0,0 +1,21 @@
+`schema` -- Special Schema Fields Package
+*****************************************
+
+(see also: :ref:`api-schema-modules`)
+
+.. automodule:: waeup.ikoba.schema
+   :members:
+   :undoc-members:
+
+.. _api-schema-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   schema/api_field
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_schoolgrades.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_schoolgrades.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_schoolgrades.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`schoolgrades` -- School Grades Components Module
+*************************************************
+
+.. automodule:: waeup.ikoba.schoolgrades
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_smtp.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_smtp.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_smtp.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`smtp` -- SMTP Services Components Module
+*****************************************
+
+.. automodule:: waeup.ikoba.smtp
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_sourcefactory.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_sourcefactory.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_sourcefactory.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`sourcefactory` -- Contextual Sources Module
+********************************************
+
+.. automodule:: waeup.ikoba.sourcefactory
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_startup.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_startup.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_startup.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`startup` -- WSGI Application Factories Module
+**********************************************
+
+.. automodule:: waeup.ikoba.startup
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_userscontainer.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_userscontainer.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_userscontainer.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`userscontainer` -- Users (Principals) Module
+*********************************************
+
+.. automodule:: waeup.ikoba.userscontainer
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_utils.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_utils.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_utils.rst	(revision 12677)
@@ -0,0 +1,24 @@
+`utils` -- Utilities Package
+****************************
+
+(see also: :ref:`api-utils-modules`)
+
+.. automodule:: waeup.ikoba.utils
+   :members:
+   :undoc-members:
+
+.. _api-utils-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   utils/api_batching
+   utils/api_converters
+   utils/api_helpers
+   utils/api_logger
+   utils/api_utils
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_webservices.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_webservices.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_webservices.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`webservices` -- Web Services Components Module
+***********************************************
+
+.. automodule:: waeup.ikoba.webservices
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_widgets.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_widgets.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_widgets.rst	(revision 12677)
@@ -0,0 +1,23 @@
+`widgets` -- Special Widgets Package
+************************************
+
+(see also: :ref:`api-widgets-modules`)
+
+.. automodule:: waeup.ikoba.widgets
+   :members:
+   :undoc-members:
+
+.. _api-widgets-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   widgets/api_datewidget
+   widgets/api_phonewidget
+   widgets/api_restwidget
+   widgets/api_objectwidget
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_workflow.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_workflow.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/api_workflow.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`workflow` -- Workflow Components Module
+****************************************
+
+.. automodule:: waeup.ikoba.workflow
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_breadcrumbs.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 12677)
@@ -0,0 +1,7 @@
+`browser.breadcrumbs` - Browser Breadcrumbs
+*******************************************
+
+.. automodule:: waeup.ikoba.browser.breadcrumbs
+   :members:
+   :undoc-members:
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_exceptions.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 12677)
@@ -0,0 +1,7 @@
+`browser.exceptions` - Browser Exceptions
+*****************************************
+
+.. automodule:: waeup.ikoba.browser.exceptions
+   :members:
+   :undoc-members:
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_interfaces.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 12677)
@@ -0,0 +1,18 @@
+`browser.interfaces` - Browser Interfaces
+*****************************************
+
+.. automodule:: waeup.ikoba.browser.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IBreadcrumb
+      :members:
+
+   .. autointerface:: IBreadcrumbIgnorable
+      :members:
+
+   .. autointerface:: IBreadcrumbContainer
+      :members:
+
+   .. autointerface:: ITheme
+      :members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_layout.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_layout.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_layout.rst	(revision 12677)
@@ -0,0 +1,7 @@
+`browser.layout` - Browser Layout
+*********************************
+
+.. automodule:: waeup.ikoba.browser.layout
+   :members:
+   :undoc-members:
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_pages.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_pages.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_pages.rst	(revision 12677)
@@ -0,0 +1,7 @@
+`browser.pages` - Browser Pages (Views)
+***************************************
+
+.. automodule:: waeup.ikoba.browser.pages
+   :members:
+   :undoc-members:
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_resources.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_resources.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_resources.rst	(revision 12677)
@@ -0,0 +1,7 @@
+`browser.resources` - Browser Resources
+***************************************
+
+.. automodule:: waeup.ikoba.browser.resources
+   :members:
+   :undoc-members:
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_theming.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_theming.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_theming.rst	(revision 12677)
@@ -0,0 +1,7 @@
+`browser.theming` - Browser Theming
+***********************************
+
+.. automodule:: waeup.ikoba.browser.theming
+   :members:
+   :undoc-members:
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_viewlets.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 12677)
@@ -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.ikoba.browser.viewlets
+     :members:
+     :undoc-members:
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/image/api_image.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/image/api_image.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/image/api_image.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`image.image` - Image File Components
+*************************************
+
+.. automodule:: waeup.ikoba.image.image
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/image/api_schema.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/image/api_schema.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/image/api_schema.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`image.schema` - Image File Schemas
+***********************************
+
+.. automodule:: waeup.ikoba.image.schema
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/index.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/index.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/index.rst	(revision 12677)
@@ -0,0 +1,45 @@
+API
+***
+
+.. automodule:: waeup.ikoba
+   :members:
+
+Modules and Packages
+--------------------
+
+`waeup.ikoba` provides the following modules and subpackages:
+
+.. toctree::
+   :maxdepth: 1
+
+   api_app
+   api_async
+   api_authentication
+   api_beaker
+   api_browser
+   api_catalog
+   api_configuration
+   api_datacenter
+   api_fees
+   api_image
+   api_imagestorage
+   api_interfaces
+   api_language
+   api_maintenance
+   api_mandates
+   api_meta
+   api_objecthistory
+   api_payments
+   api_permissions
+   api_reports
+   api_schema
+   api_schoolgrades
+   api_smtp
+   api_sourcefactory
+   api_startup
+   api_company
+   api_userscontainer
+   api_utils
+   api_webservices
+   api_widgets
+   api_workflow
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/mandates/api_browser.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/mandates/api_browser.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/mandates/api_browser.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`mandates.browser` - Mandates UI Components
+*******************************************
+
+.. automodule:: waeup.ikoba.mandates.browser
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/mandates/api_container.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/mandates/api_container.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/mandates/api_container.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`mandates.container` - Mandate Container Components
+***************************************************
+
+.. automodule:: waeup.ikoba.mandates.container
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/mandates/api_mandate.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/mandates/api_mandate.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/mandates/api_mandate.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`mandates.mandate` - Mandate Components
+***************************************
+
+.. automodule:: waeup.ikoba.mandates.mandate
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_container.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_container.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_container.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`payments.container` - Payments Container Components
+****************************************************
+
+.. automodule:: waeup.ikoba.payments.container
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_interfaces.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_interfaces.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_interfaces.rst	(revision 12677)
@@ -0,0 +1,18 @@
+`payments.interfaces` - Payment Interfaces
+******************************************
+
+.. automodule:: waeup.ikoba.payments.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IPaymentsContainer
+      :members:
+
+   .. autointerface:: IPayment
+      :members:
+
+   .. autointerface:: ISCPayment
+      :members:
+
+   .. autointerface:: IOnlinePayment
+      :members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_payment.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_payment.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_payment.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`payments.payment` - Payment Components
+***************************************
+
+.. automodule:: waeup.ikoba.payments.payment
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_paypal.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_paypal.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/payments/api_paypal.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`payments.paypal` - Support for PayPal
+**************************************
+
+.. automodule:: waeup.ikoba.payments.paypal
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/schema/api_field.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/schema/api_field.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/schema/api_field.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`schema.field` - Special Schema Fields
+**************************************
+
+.. automodule:: waeup.ikoba.schema.field
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_batching.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_batching.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_batching.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`utils.batching` - Batching Components
+**************************************
+
+.. automodule:: waeup.ikoba.utils.batching
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_converters.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_converters.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_converters.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`utils.converters` - Converter Components
+*****************************************
+
+.. automodule:: waeup.ikoba.utils.converters
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_helpers.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_helpers.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_helpers.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`utils.helpers` - Helper Components
+***********************************
+
+.. automodule:: waeup.ikoba.utils.helpers
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_logger.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_logger.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_logger.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`utils.logger` - Logger Components
+**********************************
+
+.. automodule:: waeup.ikoba.utils.logger
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_utils.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_utils.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/utils/api_utils.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`utils.utils` - Utilities
+*************************
+
+.. automodule:: waeup.ikoba.utils.utils
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_datewidget.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_datewidget.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_datewidget.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`widgets.datewidget` - Date Widget
+**********************************
+
+.. automodule:: waeup.ikoba.widgets.datewidget
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_objectwidget.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_objectwidget.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_objectwidget.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`widgets.objectwidget` - Object Widget
+**************************************
+
+.. automodule:: waeup.ikoba.widgets.objectwidget
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_phonewidget.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_phonewidget.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_phonewidget.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`widgets.phonewidget` - Phone Widget
+************************************
+
+.. automodule:: waeup.ikoba.widgets.phonewidget
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_restwidget.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_restwidget.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/api/widgets/api_restwidget.rst	(revision 12677)
@@ -0,0 +1,6 @@
+`widgets.restwidget` - ReST Widget
+**********************************
+
+.. automodule:: waeup.ikoba.widgets.restwidget
+   :members:
+   :undoc-members:
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/developernotes.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/developernotes.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/developernotes.rst	(revision 12677)
@@ -0,0 +1,150 @@
+Developer Notes
+***************
+
+How to setup a developer instance of the WAeUP.Ikoba, handle tests, docs
+etc.
+
+The new WAeUP.Ikoba is based on `Grok <http://grok.zope.org/>`_.
+
+Installing a developer copy
+===========================
+
+See :ref:`installing_linux`
+
+Documentation
+=============
+
+With the :mod:`waeup.ikoba` package we try to reach high standards in
+both, documentation and testing.
+
+:mod:`waeup.ikoba` makes extensive use of doctests, which this way also
+become both: executable (i.e. testable) examples and documentation.
+
+Building documentation
+----------------------
+
+See :ref:`building_documentation`
+
+Writing documentation
+---------------------
+
+means explaining to other developers what your code does and test it
+at the same time. See the many .txt files in the :mod:`waeup.ikoba`
+package for examples.
+
+Testing
+=======
+
+Tests are most important to the reliability of the :mod:`waeup.ikoba`
+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.ikoba` 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 `WAeUPIkobaUnitTestLayer` defined in `waeup.ikoba.testing`. This
+  layer groks the whole `waeup.ikoba` 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.ikoba.testing.WAeUPIkobaUnitTestLayer
+
+  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.ikoba/branches/uli-payments/docs/source/developerdocs/index.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/index.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/index.rst	(revision 12677)
@@ -0,0 +1,9 @@
+Developer Documentation
+***********************
+
+.. toctree::
+   :maxdepth: 2
+   
+   developernotes
+   api/index
+   profiling
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/profiling.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/profiling.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/developerdocs/profiling.rst	(revision 12677)
@@ -0,0 +1,376 @@
+Profiling
+*********
+
+*or: What Did My Application Do in the Afternoon?*
+
+This how-to explains how you can profile your Grok application or
+Grok/Zope itself. This way you get an idea of the bottlenecks of your
+application or in other words: where it spends its time.
+
+.. note:: This text is an updated version of my how-to as it can seen
+          on the old Grok site (no, I won't give a link to that
+          slightly outdated stuff).
+
+Profiling with Grok
+===================
+
+When it comes to a web framework like Grok, profiling is not as easy
+as with commandline or desktop applications. You normally want to
+trigger certain requests and see, in which parts of your code how much
+time or memory was spent.
+
+Here is how you can do this.
+
+Prerequisites
+=============
+
+We assume you know how to create a Grok project (your application) and
+have one already at hand.
+
+Installing a profiler
+=====================
+
+There are some profiling tools available with every Python
+installation. Usually, they are started together with some desktop- or
+commandline application, wrapping it in a way to measure internal calls
+and other things interesting for developers.
+
+With web-frameworks, however, we often only want to check certain
+requests. Luckily, with `Paste`_ and `WSGI`_ we have a mechanism, that
+can serve in a similar way: we can define a (WSGI_) pipeline of apps
+and filters that wrap each other, similar to the approach of regular
+non-web applications. We also have a profiler already available, that
+*is* some WSGI_ application itself and can profile things when some
+HTTP request comes in: `repoze.profile`_ is even configurable over a
+web frontend.
+
+Install repoze.profile_
+=======================
+
+In the ``buildout.cfg`` of your project add the repoze.profile_ egg to list
+of eggs of your application. Look out for a section named ``[app]``, which
+could read like this or similar::
+
+
+  ...
+  [app]
+  recipe = zc.recipe.egg
+  eggs = cptapp
+         z3c.evalexception>=2.0
+         Paste
+         PasteScript
+         PasteDeploy
+         repoze.profile
+  interpreter = python-console
+  ...
+
+Here the added ``repoze.profile`` stanza is important.
+
+Now run::
+
+  $ bin/buildout
+
+to fetch the egg from the net if it is not already available and to
+make it known to the generated scripts.
+
+Create a ``profiler.ini``
+=========================
+
+To make use of the new egg we must tell paster about it. This is done
+by an appropriate initialization file we create now::
+
+  # file: profiler.ini
+  [app:myapp]
+  use = egg:sample
+
+  [filter:myprofiler]
+  use = egg:repoze.profile#profile
+
+  [pipeline:main]
+  pipeline = myprofiler myapp
+
+  [server:main]
+  use = egg:Paste#http
+  host = 127.0.0.1
+  port = 8080
+
+  [DEFAULT]
+  # set the name of the zope.conf file
+  zope_conf = %(here)s/zope.conf
+
+Here we tell paster (the HTTP server) in the ``pipeline`` to filter
+each web request passing it first to repoze.profile_ which then calls
+our own ``sample`` application. This way the profiler can start its
+job and measure all calls done by the following application call.
+
+It is crucial, that you use the name of *your* project egg here in the
+``[app:myapp]`` section. In the sample config above we assume that the
+project (application) is called ``sample``, so that the egg with our
+application is called the same.
+
+.. note:: modern Grok projects put their paster configuration files as
+          *templates* into the local ``etc/`` directory and do not
+          call them ``debug.ini``, ``profile.ini``, etc. but
+          ``debug.ini.in``, ``profile.ini.in``, etc (note the trailing
+          ``.in``). This is not covered here.
+
+Put this new file in the same directory as where your ``zope.conf``
+lives (*not* ``zope.conf.in``). For newer projects this is
+``parts/etc/``.
+
+Start Profiling
+===============
+
+With the given setup we can start profiling by::
+
+  $ bin/paster serve parts/etc/profiler.ini
+
+If your ``profiler.ini`` file resides elsewhere, you must use a
+different location, of course.
+
+.. note:: you dont't *have* to create a separate profiler config. You
+          can, of course, update an existing ``deploy.ini`` or
+          ``debug.ini``. But as profiling takes more time than calls
+          without profiling, it is a good idea to enable profiling
+          only on request, i.e. when starting paster with a
+          respective configuration.
+
+The server will start as usual and you can do everything you like with
+it.
+
+Browsing the Profiler
+=====================
+
+To get to the profiler, enter the following URL:
+
+    http://localhost:8080/__profile__
+
+This brings us to the profiler web frontend. If you have browsed your
+instance before, you will get some values about the timings of last
+requests. If not, then browse a bit over your application to collect
+some data. The data is collected 'in background' during each request
+and added to the values already collected.
+
+The result might look like this:
+
+.. image:: screenshot01.png
+
+As you can see, there are a few options you can play with. You can
+filter the results by name, sort it by time or other stats, enable
+display of full paths, etc.
+
+When you stop paster, all collected data is removed. On the next start
+you can generate new one.
+
+Profiling a certain view
+========================
+
+Say we want to profile the performance of the ``index`` view created
+by the our application. To do this, we first have to install an
+instance of our application. Let's call it simply ``app``.
+
+So, create an instance of your application under the name ``app`` in
+the grok UI admin interface.
+
+Now we can access
+
+    http://localhost:8080/app
+
+and the usual index page will (hopefully) appear.
+
+If we go back to the profiler, however, we will see the summed up
+values of *all* requests we did up to now - including all the actions
+in the admin interface etc. we are not interested in.
+
+We therefore clear the current data by clicking on ``clear`` on the
+profiler page.
+
+Now we access the page we want to examine directly and go to the above
+URL directly.
+
+When we now go back to the profiler, we only see the values of the
+last request. That's the data we are interested in.
+
+Profiling mass requests
+=======================
+
+Very often a single request to a view does not give us reliable data:
+too many factors can influence the request to make its values not very
+representative. What we often want are *many* requests and the average
+values appearing here.
+
+This means for our view: we want to do several hundreds requests to
+the same view. But as we are lazy, we don't want to press the reload
+button several hundred or even thousand times. Luckily there are tools
+available, which can do that for us.
+
+One of this tools is the apache benchmarking tool ``ab`` from the
+Apache project. On Ubuntu systems it is automatically installed, if
+you have the apache webserver installed.
+
+With ``ab`` (apache benchmarking) we can trigger 1,000 requests to our
+index page with one command::
+
+  $ ab -n1000 -c4 http://127.0.0.1/app/@@index
+
+This will give us 1,000 requests, of which at most four are triggered
+concurrently, to the URL http://127.0.0.1/app/@@index. Please don't do
+this on foreign machines!
+
+The result might look like this::
+
+  Benchmarking 127.0.0.1 (be patient)
+  Completed 100 requests
+  Completed 200 requests
+  Completed 300 requests
+  Completed 400 requests
+  Completed 500 requests
+  Completed 600 requests
+  Completed 700 requests
+  Completed 800 requests
+  Completed 900 requests
+  Finished 1000 requests
+  
+  
+  Server Software:        PasteWSGIServer/0.5
+  Server Hostname:        127.0.0.1
+  Server Port:            8080
+  
+  Document Path:          /app/@@index
+  Document Length:        198 bytes
+  
+  Concurrency Level:      4
+  Time taken for tests:   38.297797 seconds
+  Complete requests:      1000
+  Failed requests:        0
+  Write errors:           0
+  Total transferred:      448000 bytes
+  HTML transferred:       198000 bytes
+  Requests per second:    26.11 [#/sec] (mean)
+  Time per request:       153.191 [ms] (mean)
+  Time per request:       38.298 [ms] (mean, across all concurrent requests)
+  Transfer rate:          11.41 [Kbytes/sec] received
+
+  Connection Times (ms)
+                min  mean[+/-sd] median   max
+  Connect:        0    0   0.0      0       0
+  Processing:    94  152  17.3    151     232
+  Waiting:       86  151  17.3    150     231
+  Total:         94  152  17.3    151     232
+
+  Percentage of the requests served within a certain time (ms)
+    50%    151
+    66%    153
+    75%    156
+    80%    158
+    90%    176
+    95%    189
+    98%    203
+    99%    215
+   100%    232 (longest request)
+
+Also this benchmarking results can be interesting. But we want to know
+more about the functions called during this mass request and how much
+time they spent each. This can be seen, if we now go back to the
+browser and open
+
+    http://localhost:8080/__profile__
+
+again.
+
+.. note:: ``ab``, of course, is a simple, rude method for stress
+          testing. Although it provides many options, you might want
+          to look for more sophisticated methods to generate tons of
+          requests. ``multi-mechanize`` is such a tool that can kill
+          your server with the same power as ``ab`` but does it
+          smarter.
+
+Turning the Data into a Graph
+=============================
+
+All this is very nice, but sometimes a picture tells more than
+thousand words. So let's turn all this data into some graph.
+
+As repoze.profile_ cannot do this for us, we have to 'export' the
+collected data first.
+
+Exporting Profiling Data
+========================
+
+The web frontend provided by repoze.profile_ is very nice for
+analyzing ad-hoc. But sometimes we want to have the data 'exported' to
+process it further with other tools or simply archiving the results.
+
+Luckily we can do so by grabbing the file ``wsgi.prof`` which contains
+all the data presented in the web interface. This file is created by
+repoze.profile_ while working and in the top of the project
+directory. In fact the profiler stores all collected data in that
+``wsgi.prof`` file also for own usage.
+
+Be careful: when you click ``clear`` in the webinterface, then the
+file will vanish. Also stopping ``paster`` will make it disappear. So
+copy it to some secure location where we can process the data further
+while the web server is still running (and after you did all requests
+you want to analyze).
+
+Because repoze.profile_ makes use of the standard Python profiler in
+the :mod:`profile` or :mod:`cProfile` module, the data in the
+``wsgi.prof`` file conforms to output generated by these profilers.
+
+Converting the Data into dot-format
+===================================
+
+One of the more advanced tools to create graphs from profiling
+information is ``dot`` from graphviz_. To make use of it, we first
+have to convert the data in ``wsgi.prof`` file into something
+``dot``-compatible.
+
+There is a tool available, which can do the job for us, a Python
+script named ``GProf2Dot`` which is available at:
+
+    http://code.google.com/p/jrfonseca/wiki/Gprof2Dot
+
+Download the script from:
+
+    http://jrfonseca.googlecode.com/svn/trunk/gprof2dot/gprof2dot.py
+
+We can now turn our profiling data into a ``dot`` graph by doing::
+
+  $ python grprof2dot.py -f pstats -o wsgi.prof.dot wsgi.prof
+
+This will turn our input file ``wsgi.prof`` of format `pstats`
+(=Python stats) into a ``dot``-file named ``wsgi.prof.dot``.
+
+Converting the dot file into Graphics
+=====================================
+
+Now we can do the last step and turn our dot file into a nice graphics
+file. For this we need of course the dot programme, which on Ubuntu
+systems can be easily installed doing::
+
+  $ sudo apt-get install dot
+
+Afterwards we do the final transformation by::
+
+  $ dot -Tpng -omygraph.png wsgi.prof.dot
+
+This will generate a PNG file where we can see in terms of graphs
+where a request (or thousands thereof) spends most of the
+time. ``dot`` can generate **huge** graphs. A tiny snippet of a sample
+is shown below.
+
+.. image:: screenshot02.png
+
+All the used tools (``ab``, ``dot``, ``gprof2dot``) provide a huge
+bunch of options you might want to explore further. This way we can
+generate more or less complete graphs (leaving out functions of little
+impact), coulours etc.
+
+In the end you hopefully know more about your application and where it
+spent its time.
+
+.. _`repoze.profile`: http://pypi.python.org/pypi/repoze.profile
+.. _`Paste`: http://pythonpaste.org/
+.. _`WSGI`: http://www.python.org/peps/pep-0333.html
+.. _`graphviz`: http://www.graphviz.org/
+.. _`GProf2Dot`: http://code.google.com/p/jrfonseca/wiki/Gprof2Dot
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/documentation.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/documentation.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/documentation.rst	(revision 12677)
@@ -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.ikoba` itself.
+
+.. _Sphinx: http://sphinx.org/
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/glossary.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/glossary.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/glossary.rst	(revision 12677)
@@ -0,0 +1,16 @@
+.. _glossary:
+
+Glossary
+********
+
+.. glossary::
+
+   Ikoba
+     Application and Registration Portal
+
+   WAeUP
+     West African eUniversity Project
+
+   waeup.ikoba
+     A Python package explained in this documentation
+
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/index.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/index.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/index.rst	(revision 12677)
@@ -0,0 +1,11 @@
+.. WAeUP Application and Registration Portal (Ikoba) 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.ikoba/branches/uli-payments/docs/source/intro.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/intro.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/intro.rst	(revision 12677)
@@ -0,0 +1,1 @@
+.. include:: ../../README.txt
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/license.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/license.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/license.rst	(revision 12677)
@@ -0,0 +1,11 @@
+.. _license:
+
+***********
+The License
+***********
+
+`waeup.ikoba` 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.ikoba/branches/uli-payments/docs/source/userdocs/buildingdocs.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/userdocs/buildingdocs.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/userdocs/buildingdocs.rst	(revision 12677)
@@ -0,0 +1,62 @@
+.. _building_documentation:
+
+Building Documentation (Linux OS only)
+**************************************
+
+We use the excellent `Sphinx <http://sphinx.pocoo.org/>`_ Python
+documentation generator to generate the docs as HTML pages.
+
+Prerequisites
+=============
+
+We assume you already installed `waeup.ikoba` as explained in
+:ref:`installing_linux`. You ran ``bin/buildout`` and can find a script
+``bin/waeupdocs``.
+
+Building HTML docs
+==================
+
+The documentation of the :mod:`waeup.ikoba` project can easily be
+created doing::
+
+  $ bin/waeupdocs
+
+This will create a tree of HTML pages in
+``docs/build/html`` which you can for
+instance browse by pointing your browser to this location.
+
+An 'official' place in internet for the whole docs is about to come
+but not yet available.
+
+Actually, ``waeupdocs`` is only a wrapper script around the real main
+script ``sphinx-build``, which should also be located in your ``bin/``
+directory.
+
+You therefore can also run ``bin/sphinx-build`` manually, which will
+give you more options, especially, if you want the output to be
+created in a different location or similar.
+
+Run::
+
+   $ bin/sphinx-build --help
+
+to get a list of options available.
+
+Alternatively, you can also go to ``docs/build`` and run::
+
+   $ make html
+
+Other available formats are listed, when you run only::
+
+   $ make
+
+These other formats will include ``latex``, ``doctest``, ``linkcheck``
+and more with a short explanation for each format.
+
+Building PDF docs
+=================
+
+We're currently not prepared for PDF, but support is already planned.
+
+Please be aware, that any PDF support will require a full TeX
+installation on your system to work.
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/userdocs/index.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/userdocs/index.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/userdocs/index.rst	(revision 12677)
@@ -0,0 +1,8 @@
+User Documentation
+******************
+
+.. toctree::
+   :maxdepth: 2
+
+   install
+   buildingdocs
Index: /main/waeup.ikoba/branches/uli-payments/docs/source/userdocs/install.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/docs/source/userdocs/install.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/docs/source/userdocs/install.rst	(revision 12677)
@@ -0,0 +1,3 @@
+.. _installing_linux:
+
+.. include:: ../../INSTALL.txt
Index: /main/waeup.ikoba/branches/uli-payments/etc/README.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/README.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/README.txt	(revision 12677)
@@ -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.ikoba/branches/uli-payments/etc/paypal-testing.conf-sample
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/paypal-testing.conf-sample	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/paypal-testing.conf-sample	(revision 12677)
@@ -0,0 +1,9 @@
+# paypal credentials
+
+[rest-client]
+# id and secret you got from PayPal (long hex strings)
+id = invalid
+secret = invalid
+
+# valid values: sandbox, live
+mode = sandbox
Index: /main/waeup.ikoba/branches/uli-payments/etc/profile.ini.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/profile.ini.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/profile.ini.in	(revision 12677)
@@ -0,0 +1,93 @@
+# profile.ini
+#
+# Profiling configuration for use with paster/WSGI
+#
+# Call http://localhost:8080/__profile__ to see the profiler in action
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[filter:profiler]
+use = egg:repoze.profile#profile
+
+[app:app]
+use = egg:${ikoba_params:devel_pkg}
+
+[pipeline:main]
+pipeline = translogger profiler app
+
+[server:main]
+use = egg:Paste#http
+host = ${ikoba_params:host}
+port = ${ikoba_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
Index: /main/waeup.ikoba/branches/uli-payments/etc/raw/debug.ini.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/raw/debug.ini.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/raw/debug.ini.in	(revision 12677)
@@ -0,0 +1,94 @@
+# debug.ini
+#
+# Debugging configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi
+
+[handlers]
+keys = console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[filter-app:main]
+# Change the last part from 'ajax' to 'pdb' for a post-mortem debugger
+# on the console:
+use = egg:z3c.evalexception#ajax
+next = zope
+
+[app:zope]
+use = egg:${ikoba_params:devel_pkg}#debug
+filter-with = translogger
+exempt-exceptions = zope.security.interfaces.IUnauthorized
+
+[server:main]
+use = egg:Paste#http
+host = ${ikoba_params:host}
+port = ${ikoba_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
Index: /main/waeup.ikoba/branches/uli-payments/etc/raw/deploy.ini.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/raw/deploy.ini.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/raw/deploy.ini.in	(revision 12677)
@@ -0,0 +1,94 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.ikoba.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:main]
+use = egg:${ikoba_params:devel_pkg}
+filter-with = translogger
+
+[server:main]
+use = egg:Paste#http
+host = ${ikoba_params:host}
+port = ${ikoba_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
Index: /main/waeup.ikoba/branches/uli-payments/etc/site.zcml.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/site.zcml.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/site.zcml.in	(revision 12677)
@@ -0,0 +1,86 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:ikoba="http://namespaces.waeup.org/ikoba"
+           i18n_domain="${ikoba_params:devel_pkg}">
+
+  <include package="${ikoba_params:devel_pkg}" />
+  <include package="${ikoba_params:devel_pkg}" file="mail.zcml" />
+  <!-- install job container
+
+       install a job container for asynchronous jobs. Pick one of the
+       two possibilites: 'single' for installation in the one big ZODB
+       or 'multidb' for setup with a dedicated ZODB for job handling.
+  -->
+  <!-- include package="waeup.ikoba" file="async_single.zcml" / -->
+  <include package="waeup.ikoba" file="async_multidb.zcml" />
+
+  <!-- install dispatcher and other needed async components
+
+       install other needed components for asynchronous jobs. Pick one
+       of the two possibilities: 'basic_dispatcher' for single ZODB
+       setup, 'multidb_dispatcher' for setup with a dedicated ZODB for
+       job handling. Make sure the setting matches the setting picked
+       above. -->
+  <!-- include package="zc.async" file="basic_dispatcher_policy.zcml" / -->
+  <include package="zc.async" file="multidb_dispatcher_policy.zcml" />
+
+  <!-- Where should the datacenter reside by default? -->
+  <ikoba:datacenter
+      path="${buildout:directory}/var/datacenter" />
+
+  <!-- Where can the Paypal config be found? -->
+  <ikoba:paypalconf
+      path="${buildout:directory}/etc/paypal.conf" />
+
+    <configure i18n_domain="${ikoba_params:devel_pkg}">
+
+
+      <unauthenticatedPrincipal id="zope.anybody"
+                                title="Unauthenticated User" />
+      <unauthenticatedGroup id="zope.Anybody"
+                            title="Unauthenticated Users" />
+      <authenticatedGroup id="zope.Authenticated"
+                        title="Authenticated Users" />
+      <everybodyGroup id="zope.Everybody"
+                      title="All Users" />
+      <principal id="zope.manager"
+                 title="Manager"
+                 login="grok"
+                 password_manager="SHA1"
+                 password="321b9cd3739bd029629243732eb682db005a7541f7509622"
+                 />
+
+      <!-- XMLRPC user for demo and testing
+           Plaintext password is xmlrpcuser1
+           Don't use it in real deployment -->
+      <principal id="zope.xmlrpcuser1"
+                 title="XMLRPC User 1"
+                 login="xmlrpcuser1"
+                 password="{SSHA}ZaL4-UiwmSu6AbLYY-yCS_8vtKpH5Hc6"
+                 password_manager="SSHA"
+                 />
+
+      <grant role="waeup.xmlrpcusers1"
+             principal="zope.xmlrpcuser1" />
+
+
+      <!-- Replace the following directive if you do not want
+           public access -->
+      <grant permission="zope.app.dublincore.view"
+             principal="zope.Anybody" />
+      <grant permission="zope.View"
+             principal="zope.Authenticated" />
+      <grant permission="zope.app.dublincore.view"
+             principal="zope.Authenticated" />
+      <grant permission="waeup.Public"
+             principal="zope.Everybody" />
+      <grant permission="waeup.Anonymous"
+             principal="zope.Anybody" />
+      <grant permission="waeup.Authenticated"
+             principal="zope.Authenticated" />
+
+      <role id="zope.Manager" title="Site Manager" />
+      <grantAll role="zope.Manager" />
+      <grant role="zope.Manager"
+             principal="zope.manager" />
+   </configure>
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/etc/themed/debug.ini.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/themed/debug.ini.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/themed/debug.ini.in	(revision 12677)
@@ -0,0 +1,118 @@
+# debug.ini
+#
+# Debugging configuration for use with paster/WSGI and diazo
+#
+
+[loggers]
+keys = root, wsgi
+
+[handlers]
+keys = console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[filter:debugger]
+# Change the last part from 'ajax' to 'pdb' for a post-mortem debugger
+# on the console:
+use = egg:z3c.evalexception#ajax
+
+[app:zope]
+use = egg:${ikoba_params:devel_pkg}#debug
+exempt-exceptions = zope.security.interfaces.IUnauthorized
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[filter:theme]
+use = egg:diazo
+rules = file:///%(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to false, if you don't want rules.xml being rebuild on each request
+debug = true
+
+[pipeline:default]
+pipeline = theme
+           translogger
+           debugger
+           zope
+
+[server:main]
+use = egg:Paste#http
+host = ${ikoba_params:host}
+port = ${ikoba_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
+# the directory where all real diazo config is done (no trailing slash)
+LAYOUT_DIR = ${buildout:directory}/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: /main/waeup.ikoba/branches/uli-payments/etc/themed/deploy.ini.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/themed/deploy.ini.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/themed/deploy.ini.in	(revision 12677)
@@ -0,0 +1,118 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.ikoba.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:content]
+use = egg:${ikoba_params:devel_pkg}
+filter-with = translogger
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static/
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[server:main]
+use = egg:Paste#http
+host = ${ikoba_params:host}
+port = ${ikoba_params:base_port}
+
+[filter:theme]
+use = egg:diazo
+rules = file:///%(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to true, if you want rules.xml being rebuild on each request
+debug = false
+
+[pipeline:default]
+pipeline = theme
+           content
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
+# the directory where all real diazo config is done (no trailing slash)
+LAYOUT_DIR = ${buildout:directory}/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: /main/waeup.ikoba/branches/uli-payments/etc/themed/zeo1.ini.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/themed/zeo1.ini.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/themed/zeo1.ini.in	(revision 12677)
@@ -0,0 +1,119 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.ikoba.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo1.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo1_async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo1_async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:zope]
+use = egg:${ikoba_params:devel_pkg}
+filter-with = translogger
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static/
+
+[pipeline:default]
+pipeline = theme
+           zope
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[filter:theme]
+use = egg:diazo
+rules = %(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to true, if you want rules.xml being rebuild on each request
+debug = false
+
+[server:main]
+use = egg:Paste#http
+host = ${ikoba_params:host}
+port = ${ikoba_params:zeo1_port}
+threadpool_workers = ${ikoba_params:threadpool_workers}
+use_threadpool = True
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope_zeo1.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
+LAYOUT_DIR = ${buildout:directory}/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: /main/waeup.ikoba/branches/uli-payments/etc/themed/zeo2.ini.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/themed/zeo2.ini.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/themed/zeo2.ini.in	(revision 12677)
@@ -0,0 +1,119 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.ikoba.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo2.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo2_async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo2_async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:zope]
+use = egg:${ikoba_params:devel_pkg}
+filter-with = translogger
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static
+
+[pipeline:default]
+pipeline = theme
+           zope
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[filter:theme]
+use = egg:diazo
+rules = %(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to true, if you want rules.xml being rebuild on each request
+debug = false
+
+[server:main]
+use = egg:Paste#http
+host = ${ikoba_params:host}
+port = ${ikoba_params:zeo2_port}
+threadpool_workers = ${ikoba_params:threadpool_workers}
+use_threadpool = True
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope_zeo2.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid2.txt
+LAYOUT_DIR = ${buildout:directory}/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: /main/waeup.ikoba/branches/uli-payments/etc/zdaemon.conf.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/zdaemon.conf.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/zdaemon.conf.in	(revision 12677)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve ${buildout:directory}/parts/etc/themed-deploy.ini
+  daemon on
+  transcript ${zope_conf:logfiles}/zdaemon.log
+  socket-name ${zope_conf:logfiles}/zdaemonsock
+  # Enable this to run the child process as a different user
+  # user zope
+</runner>
Index: /main/waeup.ikoba/branches/uli-payments/etc/zeo1.conf.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/zeo1.conf.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/zeo1.conf.in	(revision 12677)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve --pid-file ${buildout:directory}/var/zeo1.pid ${buildout:directory}/parts/etc/themed-zeo1.ini
+  daemon on
+  transcript ${zope_conf:logfiles}/zdaemon-zeo1.log
+  socket-name ${zope_conf:logfiles}/zeo1.sock
+  # Enable this to run the child process as a different user
+  # user zope
+</runner>
Index: /main/waeup.ikoba/branches/uli-payments/etc/zeo2.conf.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/zeo2.conf.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/zeo2.conf.in	(revision 12677)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve --pid-file ${buildout:directory}/var/zeo2.pid ${buildout:directory}/parts/etc/themed-zeo2.ini
+  daemon on
+  transcript ${zope_conf:logfiles}/zdaemon-zeo2.log
+  socket-name ${zope_conf:logfiles}/zeo2.sock
+  # Enable this to run the child process as a different user
+  # user zope
+</runner>
Index: /main/waeup.ikoba/branches/uli-payments/etc/zope.conf.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/zope.conf.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/zope.conf.in	(revision 12677)
@@ -0,0 +1,62 @@
+# Identify the component configuration used to define the site:
+site-definition ${site_zcml:output}
+
+<zodb>
+  # Standard blob storage
+  <blobstorage>
+    blob-dir ${zope_conf:blobstorage}
+    <filestorage>
+      path ${zope_conf:filestorage}/Data.fs
+    </filestorage>
+  </blobstorage>
+
+# Uncomment this if you want a blob-less standard file storage instead:
+#  <filestorage>
+#       path ${zope_conf:filestorage}
+#  </filestorage>
+
+# Uncomment this if you want to connect to a ZEO server instead:
+#  <zeoclient>
+#    server localhost:8100
+#    storage 1
+#    # ZEO client cache, in bytes
+#    cache-size 20MB
+#    # Uncomment to have a persistent disk cache
+#    #client zeo1
+#  </zeoclient>
+
+</zodb>
+
+<zodb async>
+  <filestorage>
+    # create true
+    path ${zope_conf:filestorage}/Data.async.fs
+  </filestorage>
+</zodb>
+
+<eventlog>
+  # This sets up logging to a file.
+  # The "path" setting can be a relative or absolute
+  # filesystem path.
+
+  <logfile>
+    path ${zope_conf:logfiles}/z3.log
+    formatter zope.exceptions.log.Formatter
+  </logfile>
+
+  # This sets up logging to to standard output.
+  # The "path" setting can be the tokens STDOUT or STDERR
+  
+#  <logfile>
+#    path STDOUT
+#    formatter zope.exceptions.log.Formatter
+#  </logfile>
+</eventlog>
+
+# Comment this line to disable developer mode.  This should be done in
+# production
+# devmode on
+
+# Extra configuration lines can be added to zope_conf's extra option. Put for 
+# instance productconf sections in here.
+${zope_conf:extra}
Index: /main/waeup.ikoba/branches/uli-payments/etc/zope_zeo1.conf.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/zope_zeo1.conf.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/zope_zeo1.conf.in	(revision 12677)
@@ -0,0 +1,100 @@
+# Identify the component configuration used to define the site:
+site-definition ${site_zcml:output}
+
+<zodb>
+
+  #cache-size
+
+  # num of concurrent pre-opened ZODB threads; default: 7
+  pool-size 25
+
+  # Standard blob storage
+#  <blobstorage>
+#    blob-dir ${zope_conf:blobstorage}
+#    <filestorage>
+#      path ${zope_conf:filestorage}/Data.fs
+#    </filestorage>
+#  </blobstorage>
+
+# Uncomment this if you want a blob-less standard file storage instead:
+#  <filestorage>
+#       path ${zope_conf:filestorage}
+#  </filestorage>
+
+# Uncomment this if you want to connect to a ZEO server instead:
+  <zeoclient>
+    server localhost:8100
+    storage 1
+     # ZEO client cache, in bytes
+    cache-size 20MB
+    # Uncomment to have a persistent disk cache
+    client ${zope_conf_zeo_1:filestorage}/zeo1
+  </zeoclient>
+
+# Uncomment this if you want to run a ZEO server instead:
+#  <zeo>
+#    # required; 'host:port' for TCP/IP, path to a socket file for UDP
+#    address localhost:8090
+#
+#    # optional; 'address' remarks apply
+#    #monitor-address localhost:8091
+#
+#    # optional; keep queue of last N transactions. Speeds up client cache verif.
+#    #invalidation-queue-size 10
+#
+#    # optional; is this a read-only instance? true or false
+#    #read-only false
+#
+#    # optional; seconds to wait for client to commit after acquiring lock
+#    #transaction-timeout 20
+#
+#    # optional; authentication protocol. With ZEO only 'digest' supported
+#    #authentication-protocol digest 
+#
+#    # optional; Path of the database containing authentication credentials.
+#    #authentication-database
+#
+#    # optional; the authentication realm of server
+#    #authentication-realm myrealm
+#  </zeo>
+
+</zodb>
+
+<zodb async>
+  <zeoclient>
+    server localhost:8100
+    storage async
+    name async
+    # ZEO client cache, in bytes
+    cache-size 20MB
+    # Uncomment to have a persistent disk cache
+    client ${zope_conf_zeo_1:filestorage}/zeo1_async
+  </zeoclient>
+</zodb>
+
+<eventlog>
+  # This sets up logging to a file.
+  # The "path" setting can be a relative or absolute
+  # filesystem path.
+
+  <logfile>
+    path ${zope_conf:logfiles}/zeo1_z3.log
+    formatter zope.exceptions.log.Formatter
+  </logfile>
+
+  # This sets up logging to to standard output.
+  # The "path" setting can be the tokens STDOUT or STDERR
+  
+#  <logfile>
+#    path STDOUT
+#    formatter zope.exceptions.log.Formatter
+#  </logfile>
+</eventlog>
+
+# Comment this line to disable developer mode.  This should be done in
+# production
+#devmode on
+
+# Extra configuration lines can be added to zope_conf's extra option. Put for 
+# instance productconf sections in here.
+${zope_conf:extra}
Index: /main/waeup.ikoba/branches/uli-payments/etc/zope_zeo2.conf.in
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/etc/zope_zeo2.conf.in	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/etc/zope_zeo2.conf.in	(revision 12677)
@@ -0,0 +1,101 @@
+# Identify the component configuration used to define the site:
+site-definition ${site_zcml:output}
+
+<zodb>
+
+  #cache-size
+
+  # num of concurrent pre-opened ZODB threads; default: 7
+  pool-size 7
+
+  # Standard blob storage
+#  <blobstorage>
+#    blob-dir ${zope_conf:blobstorage}
+#    <filestorage>
+#      path ${zope_conf:filestorage}/Data.fs
+#    </filestorage>
+#  </blobstorage>
+
+# Uncomment this if you want a blob-less standard file storage instead:
+#  <filestorage>
+#       path ${zope_conf:filestorage}
+#  </filestorage>
+
+# Uncomment this if you want to connect to a ZEO server instead:
+  <zeoclient>
+    server localhost:8100
+    storage 1
+    # ZEO client cache, in bytes
+    cache-size 20MB
+    # Uncomment to have a persistent disk cache
+    client ${zope_conf_zeo_2:filestorage}/zeo2
+  </zeoclient>
+
+# Uncomment this if you want to run a ZEO server instead:
+#  <zeo>
+#    # required; 'host:port' for TCP/IP, path to a socket file for UDP
+#    address localhost:8090
+#
+#    # optional; 'address' remarks apply
+#    #monitor-address localhost:8091
+#
+#    # optional; keep queue of last N transactions. Speeds up client cache verif.
+#    #invalidation-queue-size 10
+#
+#    # optional; is this a read-only instance? true or false
+#    #read-only false
+#
+#    # optional; seconds to wait for client to commit after acquiring lock
+#    #transaction-timeout 20
+#
+#    # optional; authentication protocol. With ZEO only 'digest' supported
+#    #authentication-protocol digest 
+#
+#    # optional; Path of the database containing authentication credentials.
+#    #authentication-database
+#
+#    # optional; the authentication realm of server
+#    #authentication-realm myrealm
+#  </zeo>
+
+</zodb>
+
+<zodb async>
+  <zeoclient>
+    server localhost:8100
+    storage async
+    name async
+    # ZEO client cache, in bytes
+    cache-size 20MB
+    # Uncomment to have a persistent disk cache
+    client ${zope_conf_zeo_2:filestorage}/zeo2_async
+  </zeoclient>
+</zodb>
+
+
+<eventlog>
+  # This sets up logging to a file.
+  # The "path" setting can be a relative or absolute
+  # filesystem path.
+
+  <logfile>
+    path ${zope_conf:logfiles}/zeo2_z3.log
+    formatter zope.exceptions.log.Formatter
+  </logfile>
+
+  # This sets up logging to to standard output.
+  # The "path" setting can be the tokens STDOUT or STDERR
+  
+#  <logfile>
+#    path STDOUT
+#    formatter zope.exceptions.log.Formatter
+#  </logfile>
+</eventlog>
+
+# Comment this line to disable developer mode.  This should be done in
+# production
+#devmode on
+
+# Extra configuration lines can be added to zope_conf's extra option. Put for 
+# instance productconf sections in here.
+${zope_conf:extra}
Index: /main/waeup.ikoba/branches/uli-payments/layout/error-zope.html
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/error-zope.html	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/error-zope.html	(revision 12677)
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>waeup.ikoba Portal - Error</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="">
+    <meta name="author" content="">
+    <link rel="shortcut icon" href="static/ico/favicon.ico">
+
+    <!-- stylesheets -->
+    <link href="static/bootstrap/css/bootstrap.min.css"
+	  rel="stylesheet">
+    <link href="static/css/base.css" rel="stylesheet">
+  </head>
+  <body id="ikoba-body" class="errorpage">
+    <div class="container">
+      <div class="jumbotron">
+        <h1 id="ikoba-error-heading">Error</h1>
+        <div id="ikoba-error-message">Error Message</div>
+        <div>We are terribly sorry.</div>
+      </div>
+    </div>
+
+  </body>
+</html>
Index: /main/waeup.ikoba/branches/uli-payments/layout/rules.xml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/rules.xml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/rules.xml	(revision 12677)
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rules
+    xmlns="http://namespaces.plone.org/diazo"
+    xmlns:css="http://namespaces.plone.org/diazo/css"
+    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+    xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <!-- Cases where we do not want themes... -->
+  <notheme if-path="/++grokui++" />  <!-- do not theme GrokUI -->
+  <notheme if-path="/_debug" /><!-- output by debugger -->
+  <notheme css:if-content="div#error-area" />
+
+  <!-- use error page if there is a <table class="layout">
+       tag in content. This tag is rendered in Zope 3 errors. -->
+  <rules css:if-content="table#layout">
+    <theme href="error-zope.html" />
+    <replace css:theme-children="#ikoba-error-message"
+             css:content-children="h3" />
+  </rules>
+  <!-- use error page if there is no <div> tag in content.
+       This happens if something in the application went wrong. -->
+  <rules css:if-not-content="div">
+    <theme href="error-zope.html" />
+    <replace css:theme-children="#ikoba-error-message"
+             css:content-children="body" />
+  </rules>
+
+  <!-- regular rules -->
+  <rules css:if-content="body.ikoba-body">
+
+    <theme href="theme.html" />
+
+    <!-- head elements -->
+    <replace css:theme="html head title"
+             css:content="html head title" />
+
+    <!-- brand (upper left) -->
+    <copy attributes="href" css:theme="a.ikoba-brand-link"
+          css:content="a.ikoba-brand-link" />
+    <replace css:theme-children="a.ikoba-brand-link"
+             css:content-children="a.ikoba-brand-link" />
+
+    <!-- left top navigation -->
+    <replace css:theme-children="ul.ikoba-navbar-left"
+             css:content-children="ul.ikoba-navbar-left" />
+
+    <!-- right side top navigation -->
+    <replace css:theme-children="ul.ikoba-navbar-right"
+             css:content-children="ul.ikoba-navbar-right" />
+
+    <!-- breadcrumbs -->
+    <drop css:theme="div.ikoba-breadcrumbs-box-long"
+          css:if-not-content="ul.breadcrumb" />
+    <drop css:theme="div.ikoba-breadcrumbs-box-long"
+          css:if-content="div.wfstatus" />
+    <drop css:theme="div.ikoba-breadcrumbs-box-short"
+          css:if-not-content="ul.breadcrumb" />
+    <drop css:theme="div.ikoba-breadcrumbs-box-short"
+          css:if-not-content="div.wfstatus" />
+    <replace css:theme-children="ol.ikoba-breadcrumbs"
+             css:content-children="ul.breadcrumb" />
+
+    <!-- customer status -->
+    <drop css:theme="div.ikoba-customer-status-box"
+          css:if-not-content="div.wfstatus" />
+    <replace css:theme-children="div.wfstatus"
+             css:content-children="div.wfstatus" />
+
+    <!-- sidebar -->
+
+    <drop css:theme="div.ikoba-sidebar"
+          css:if-not-content="div.sidebar" />
+    <replace css:theme-children="div.sidebar-nav"
+             css:content-children="div.sidebar" />
+
+    <!-- content -->
+
+    <!-- drop one of the both content areas, depending on sidebar -->
+
+    <drop css:theme="div.ikoba-content-narrow"
+          css:if-not-content="div.sidebar" />
+    <drop css:theme="div.ikoba-content-wide"
+          css:if-content="div.sidebar" />
+    <replace css:theme-children="div.ikoba-content"
+             css:content-children="div.ikoba-content" />
+
+
+    <!-- scripts -->
+    <drop css:theme="script.ikoba-script-tooltip"
+          css:if-not-content="[data-toggle='tooltip']" />
+
+    <drop css:theme="script.ikoba-script-datepicker"
+          css:if-not-content="input.datepicker-le-year" />
+
+    <drop css:theme="script.ikoba-script-datatable"
+          css:if-not-content="table.ikoba-data-table" />
+
+<!--
+
+    <rules css:if-not-content="input.datepicker-le-year">
+        <drop css:theme="script.jquery-ui"
+              css:if-not-content="[data-toggle='tooltip']" />
+    </rules>
+
+-->
+<!--
+    <after css:theme-children="html head"
+           css:content="html head meta" />
+-->
+<!--
+    <after css:theme-children="html head"
+           css:content="html head base" />
+-->
+
+    <!-- top navigation -->
+    <!-- left top navigation -->
+<!--
+    <replace css:theme-children="ul.ikoba-navbar-left"
+             css:content-children="ul.ikoba-navbar-left" />
+-->
+
+    <!-- the language selector
+
+      quite tricky. We insert a <li> element at beginning of
+      ul.ikoba-navbar-right. This <li> has a header element
+      (the 'Language' button) and the a number of <li> elements
+      representing the supported languages.
+      We change the input content of format
+
+         <a href="foo" title="Lang">Abbrev</a>
+
+      to
+
+         <a href="foo">Lang</a>
+
+      creating new nodes (<xsl:element>) with attributes copied over.
+    -->
+    <before css:theme-children="ul.ikoba-navbar-right">
+      <li class="dropdown ikoba-langselect">
+        <a href="#" class="dropdown-toggle"
+           data-toggle="dropdown">
+           <img src="/static/img/earth.png" />
+        </a>
+        <ul class="dropdown-menu">
+          <xsl:for-each css:select=" .language a">
+            <li class="ikoba-language">
+              <xsl:element name="a">
+                <xsl:attribute name="href">
+                  <xsl:value-of select="@href" />
+                </xsl:attribute>
+                <xsl:value-of select="@title" />
+              </xsl:element>
+            </li>
+          </xsl:for-each>
+        </ul>
+      </li>
+    </before>
+
+    <!-- Make all data tables responsive -->
+
+    <replace css:content="table.ikoba-data-table">
+      <div class="table-responsive">
+        <xsl:apply-templates css:select="table.ikoba-data-table" mode="raw"/>
+      </div>
+    </replace>
+
+   </rules>
+
+</rules>
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap-theme.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap-theme.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap-theme.css	(revision 12677)
@@ -0,0 +1,397 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2014 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+.btn-default,
+.btn-primary,
+.btn-success,
+.btn-info,
+.btn-warning,
+.btn-danger {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.btn-default:active,
+.btn-primary:active,
+.btn-success:active,
+.btn-info:active,
+.btn-warning:active,
+.btn-danger:active,
+.btn-default.active,
+.btn-primary.active,
+.btn-success.active,
+.btn-info.active,
+.btn-warning.active,
+.btn-danger.active {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+
+.btn:active,
+.btn.active {
+  background-image: none;
+}
+
+.btn-default {
+  text-shadow: 0 1px 0 #fff;
+  background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);
+  background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%);
+  background-repeat: repeat-x;
+  border-color: #dbdbdb;
+  border-color: #ccc;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-default:hover,
+.btn-default:focus {
+  background-color: #e0e0e0;
+  background-position: 0 -15px;
+}
+
+.btn-default:active,
+.btn-default.active {
+  background-color: #e0e0e0;
+  border-color: #dbdbdb;
+}
+
+.btn-primary {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
+  background-repeat: repeat-x;
+  border-color: #2b669a;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-primary:hover,
+.btn-primary:focus {
+  background-color: #2d6ca2;
+  background-position: 0 -15px;
+}
+
+.btn-primary:active,
+.btn-primary.active {
+  background-color: #2d6ca2;
+  border-color: #2b669a;
+}
+
+.btn-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
+  background-repeat: repeat-x;
+  border-color: #3e8f3e;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-success:hover,
+.btn-success:focus {
+  background-color: #419641;
+  background-position: 0 -15px;
+}
+
+.btn-success:active,
+.btn-success.active {
+  background-color: #419641;
+  border-color: #3e8f3e;
+}
+
+.btn-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
+  background-repeat: repeat-x;
+  border-color: #e38d13;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-warning:hover,
+.btn-warning:focus {
+  background-color: #eb9316;
+  background-position: 0 -15px;
+}
+
+.btn-warning:active,
+.btn-warning.active {
+  background-color: #eb9316;
+  border-color: #e38d13;
+}
+
+.btn-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
+  background-repeat: repeat-x;
+  border-color: #b92c28;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-danger:hover,
+.btn-danger:focus {
+  background-color: #c12e2a;
+  background-position: 0 -15px;
+}
+
+.btn-danger:active,
+.btn-danger.active {
+  background-color: #c12e2a;
+  border-color: #b92c28;
+}
+
+.btn-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
+  background-repeat: repeat-x;
+  border-color: #28a4c9;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-info:hover,
+.btn-info:focus {
+  background-color: #2aabd2;
+  background-position: 0 -15px;
+}
+
+.btn-info:active,
+.btn-info.active {
+  background-color: #2aabd2;
+  border-color: #28a4c9;
+}
+
+.thumbnail,
+.img-thumbnail {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+}
+
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  background-color: #e8e8e8;
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+}
+
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  background-color: #357ebd;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+}
+
+.navbar-default {
+  background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
+  background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);
+  background-repeat: repeat-x;
+  border-radius: 4px;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
+}
+
+.navbar-default .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
+  background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
+}
+
+.navbar-brand,
+.navbar-nav > li > a {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
+}
+
+.navbar-inverse {
+  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%);
+  background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.navbar-inverse .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #222222 0%, #282828 100%);
+  background-image: linear-gradient(to bottom, #222222 0%, #282828 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
+}
+
+.navbar-inverse .navbar-brand,
+.navbar-inverse .navbar-nav > li > a {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+
+.navbar-static-top,
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  border-radius: 0;
+}
+
+.alert {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.alert-success {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
+  background-repeat: repeat-x;
+  border-color: #b2dba1;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
+}
+
+.alert-info {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
+  background-repeat: repeat-x;
+  border-color: #9acfea;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
+}
+
+.alert-warning {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
+  background-repeat: repeat-x;
+  border-color: #f5e79e;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
+}
+
+.alert-danger {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
+  background-repeat: repeat-x;
+  border-color: #dca7a7;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
+}
+
+.progress {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
+}
+
+.progress-bar {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
+}
+
+.progress-bar-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
+}
+
+.progress-bar-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
+}
+
+.progress-bar-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
+}
+
+.progress-bar-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
+}
+
+.list-group {
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+}
+
+.list-group-item.active,
+.list-group-item.active:hover,
+.list-group-item.active:focus {
+  text-shadow: 0 -1px 0 #3071a9;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
+  background-repeat: repeat-x;
+  border-color: #3278b3;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
+}
+
+.panel {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.panel-default > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+}
+
+.panel-primary > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+}
+
+.panel-success > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
+}
+
+.panel-info > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
+}
+
+.panel-warning > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
+}
+
+.panel-danger > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
+}
+
+.well {
+  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
+  background-repeat: repeat-x;
+  border-color: #dcdcdc;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
+          box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
+}
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap-theme.min.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap-theme.min.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap-theme.min.css	(revision 12677)
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2014 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)}
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap.css	(revision 12677)
@@ -0,0 +1,7118 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2014 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/*! normalize.css v2.1.3 | MIT License | git.io/normalize */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section,
+summary {
+  display: block;
+}
+
+audio,
+canvas,
+video {
+  display: inline-block;
+}
+
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+
+[hidden],
+template {
+  display: none;
+}
+
+html {
+  font-family: sans-serif;
+  -webkit-text-size-adjust: 100%;
+      -ms-text-size-adjust: 100%;
+}
+
+body {
+  margin: 0;
+}
+
+a {
+  background: transparent;
+}
+
+a:focus {
+  outline: thin dotted;
+}
+
+a:active,
+a:hover {
+  outline: 0;
+}
+
+h1 {
+  margin: 0.67em 0;
+  font-size: 2em;
+}
+
+abbr[title] {
+  border-bottom: 1px dotted;
+}
+
+b,
+strong {
+  font-weight: bold;
+}
+
+dfn {
+  font-style: italic;
+}
+
+hr {
+  height: 0;
+  -moz-box-sizing: content-box;
+       box-sizing: content-box;
+}
+
+mark {
+  color: #000;
+  background: #ff0;
+}
+
+code,
+kbd,
+pre,
+samp {
+  font-family: monospace, serif;
+  font-size: 1em;
+}
+
+pre {
+  white-space: pre-wrap;
+}
+
+q {
+  quotes: "\201C" "\201D" "\2018" "\2019";
+}
+
+small {
+  font-size: 80%;
+}
+
+sub,
+sup {
+  position: relative;
+  font-size: 75%;
+  line-height: 0;
+  vertical-align: baseline;
+}
+
+sup {
+  top: -0.5em;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+img {
+  border: 0;
+}
+
+svg:not(:root) {
+  overflow: hidden;
+}
+
+figure {
+  margin: 0;
+}
+
+fieldset {
+  padding: 0.35em 0.625em 0.75em;
+  margin: 0 2px;
+  border: 1px solid #c0c0c0;
+}
+
+legend {
+  padding: 0;
+  border: 0;
+}
+
+button,
+input,
+select,
+textarea {
+  margin: 0;
+  font-family: inherit;
+  font-size: 100%;
+}
+
+button,
+input {
+  line-height: normal;
+}
+
+button,
+select {
+  text-transform: none;
+}
+
+button,
+html input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+  cursor: pointer;
+  -webkit-appearance: button;
+}
+
+button[disabled],
+html input[disabled] {
+  cursor: default;
+}
+
+input[type="checkbox"],
+input[type="radio"] {
+  padding: 0;
+  box-sizing: border-box;
+}
+
+input[type="search"] {
+  -webkit-box-sizing: content-box;
+     -moz-box-sizing: content-box;
+          box-sizing: content-box;
+  -webkit-appearance: textfield;
+}
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+  padding: 0;
+  border: 0;
+}
+
+textarea {
+  overflow: auto;
+  vertical-align: top;
+}
+
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+@media print {
+  * {
+    color: #000 !important;
+    text-shadow: none !important;
+    background: transparent !important;
+    box-shadow: none !important;
+  }
+  a,
+  a:visited {
+    text-decoration: underline;
+  }
+  a[href]:after {
+    content: " (" attr(href) ")";
+  }
+  abbr[title]:after {
+    content: " (" attr(title) ")";
+  }
+  a[href^="javascript:"]:after,
+  a[href^="#"]:after {
+    content: "";
+  }
+  pre,
+  blockquote {
+    border: 1px solid #999;
+    page-break-inside: avoid;
+  }
+  thead {
+    display: table-header-group;
+  }
+  tr,
+  img {
+    page-break-inside: avoid;
+  }
+  img {
+    max-width: 100% !important;
+  }
+  @page  {
+    margin: 2cm .5cm;
+  }
+  p,
+  h2,
+  h3 {
+    orphans: 3;
+    widows: 3;
+  }
+  h2,
+  h3 {
+    page-break-after: avoid;
+  }
+  select {
+    background: #fff !important;
+  }
+  .navbar {
+    display: none;
+  }
+  .table td,
+  .table th {
+    background-color: #fff !important;
+  }
+  .btn > .caret,
+  .dropup > .btn > .caret {
+    border-top-color: #000 !important;
+  }
+  .label {
+    border: 1px solid #000;
+  }
+  .table {
+    border-collapse: collapse !important;
+  }
+  .table-bordered th,
+  .table-bordered td {
+    border: 1px solid #ddd !important;
+  }
+}
+
+*,
+*:before,
+*:after {
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
+}
+
+html {
+  font-size: 62.5%;
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+body {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 14px;
+  line-height: 1.428571429;
+  color: #333333;
+  background-color: #ffffff;
+}
+
+input,
+button,
+select,
+textarea {
+  font-family: inherit;
+  font-size: inherit;
+  line-height: inherit;
+}
+
+a {
+  color: #428bca;
+  text-decoration: none;
+}
+
+a:hover,
+a:focus {
+  color: #2a6496;
+  text-decoration: underline;
+}
+
+a:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+img {
+  vertical-align: middle;
+}
+
+.img-responsive {
+  display: block;
+  height: auto;
+  max-width: 100%;
+}
+
+.img-rounded {
+  border-radius: 6px;
+}
+
+.img-thumbnail {
+  display: inline-block;
+  height: auto;
+  max-width: 100%;
+  padding: 4px;
+  line-height: 1.428571429;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+  border-radius: 4px;
+  -webkit-transition: all 0.2s ease-in-out;
+          transition: all 0.2s ease-in-out;
+}
+
+.img-circle {
+  border-radius: 50%;
+}
+
+hr {
+  margin-top: 20px;
+  margin-bottom: 20px;
+  border: 0;
+  border-top: 1px solid #eeeeee;
+}
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  border: 0;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+.h1,
+.h2,
+.h3,
+.h4,
+.h5,
+.h6 {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-weight: 500;
+  line-height: 1.1;
+  color: inherit;
+}
+
+h1 small,
+h2 small,
+h3 small,
+h4 small,
+h5 small,
+h6 small,
+.h1 small,
+.h2 small,
+.h3 small,
+.h4 small,
+.h5 small,
+.h6 small,
+h1 .small,
+h2 .small,
+h3 .small,
+h4 .small,
+h5 .small,
+h6 .small,
+.h1 .small,
+.h2 .small,
+.h3 .small,
+.h4 .small,
+.h5 .small,
+.h6 .small {
+  font-weight: normal;
+  line-height: 1;
+  color: #999999;
+}
+
+h1,
+h2,
+h3 {
+  margin-top: 20px;
+  margin-bottom: 10px;
+}
+
+h1 small,
+h2 small,
+h3 small,
+h1 .small,
+h2 .small,
+h3 .small {
+  font-size: 65%;
+}
+
+h4,
+h5,
+h6 {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+h4 small,
+h5 small,
+h6 small,
+h4 .small,
+h5 .small,
+h6 .small {
+  font-size: 75%;
+}
+
+h1,
+.h1 {
+  font-size: 36px;
+}
+
+h2,
+.h2 {
+  font-size: 30px;
+}
+
+h3,
+.h3 {
+  font-size: 24px;
+}
+
+h4,
+.h4 {
+  font-size: 18px;
+}
+
+h5,
+.h5 {
+  font-size: 14px;
+}
+
+h6,
+.h6 {
+  font-size: 12px;
+}
+
+p {
+  margin: 0 0 10px;
+}
+
+.lead {
+  margin-bottom: 20px;
+  font-size: 16px;
+  font-weight: 200;
+  line-height: 1.4;
+}
+
+@media (min-width: 768px) {
+  .lead {
+    font-size: 21px;
+  }
+}
+
+small,
+.small {
+  font-size: 85%;
+}
+
+cite {
+  font-style: normal;
+}
+
+.text-muted {
+  color: #999999;
+}
+
+.text-primary {
+  color: #428bca;
+}
+
+.text-primary:hover {
+  color: #3071a9;
+}
+
+.text-warning {
+  color: #8a6d3b;
+}
+
+.text-warning:hover {
+  color: #66512c;
+}
+
+.text-danger {
+  color: #a94442;
+}
+
+.text-danger:hover {
+  color: #843534;
+}
+
+.text-success {
+  color: #3c763d;
+}
+
+.text-success:hover {
+  color: #2b542c;
+}
+
+.text-info {
+  color: #31708f;
+}
+
+.text-info:hover {
+  color: #245269;
+}
+
+.text-left {
+  text-align: left;
+}
+
+.text-right {
+  text-align: right;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.page-header {
+  padding-bottom: 9px;
+  margin: 40px 0 20px;
+  border-bottom: 1px solid #eeeeee;
+}
+
+ul,
+ol {
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+
+ul ul,
+ol ul,
+ul ol,
+ol ol {
+  margin-bottom: 0;
+}
+
+.list-unstyled {
+  padding-left: 0;
+  list-style: none;
+}
+
+.list-inline {
+  padding-left: 0;
+  list-style: none;
+}
+
+.list-inline > li {
+  display: inline-block;
+  padding-right: 5px;
+  padding-left: 5px;
+}
+
+.list-inline > li:first-child {
+  padding-left: 0;
+}
+
+dl {
+  margin-top: 0;
+  margin-bottom: 20px;
+}
+
+dt,
+dd {
+  line-height: 1.428571429;
+}
+
+dt {
+  font-weight: bold;
+}
+
+dd {
+  margin-left: 0;
+}
+
+@media (min-width: 768px) {
+  .dl-horizontal dt {
+    float: left;
+    width: 160px;
+    overflow: hidden;
+    clear: left;
+    text-align: right;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  .dl-horizontal dd {
+    margin-left: 180px;
+  }
+  .dl-horizontal dd:before,
+  .dl-horizontal dd:after {
+    display: table;
+    content: " ";
+  }
+  .dl-horizontal dd:after {
+    clear: both;
+  }
+  .dl-horizontal dd:before,
+  .dl-horizontal dd:after {
+    display: table;
+    content: " ";
+  }
+  .dl-horizontal dd:after {
+    clear: both;
+  }
+}
+
+abbr[title],
+abbr[data-original-title] {
+  cursor: help;
+  border-bottom: 1px dotted #999999;
+}
+
+.initialism {
+  font-size: 90%;
+  text-transform: uppercase;
+}
+
+blockquote {
+  padding: 10px 20px;
+  margin: 0 0 20px;
+  border-left: 5px solid #eeeeee;
+}
+
+blockquote p {
+  font-size: 17.5px;
+  font-weight: 300;
+  line-height: 1.25;
+}
+
+blockquote p:last-child {
+  margin-bottom: 0;
+}
+
+blockquote small,
+blockquote .small {
+  display: block;
+  line-height: 1.428571429;
+  color: #999999;
+}
+
+blockquote small:before,
+blockquote .small:before {
+  content: '\2014 \00A0';
+}
+
+blockquote.pull-right {
+  padding-right: 15px;
+  padding-left: 0;
+  border-right: 5px solid #eeeeee;
+  border-left: 0;
+}
+
+blockquote.pull-right p,
+blockquote.pull-right small,
+blockquote.pull-right .small {
+  text-align: right;
+}
+
+blockquote.pull-right small:before,
+blockquote.pull-right .small:before {
+  content: '';
+}
+
+blockquote.pull-right small:after,
+blockquote.pull-right .small:after {
+  content: '\00A0 \2014';
+}
+
+blockquote:before,
+blockquote:after {
+  content: "";
+}
+
+address {
+  margin-bottom: 20px;
+  font-style: normal;
+  line-height: 1.428571429;
+}
+
+code,
+kbd,
+pre,
+samp {
+  font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
+}
+
+code {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: #c7254e;
+  white-space: nowrap;
+  background-color: #f9f2f4;
+  border-radius: 4px;
+}
+
+pre {
+  display: block;
+  padding: 9.5px;
+  margin: 0 0 10px;
+  font-size: 13px;
+  line-height: 1.428571429;
+  color: #333333;
+  word-break: break-all;
+  word-wrap: break-word;
+  background-color: #f5f5f5;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+}
+
+pre code {
+  padding: 0;
+  font-size: inherit;
+  color: inherit;
+  white-space: pre-wrap;
+  background-color: transparent;
+  border-radius: 0;
+}
+
+.pre-scrollable {
+  max-height: 340px;
+  overflow-y: scroll;
+}
+
+.container {
+  padding-right: 15px;
+  padding-left: 15px;
+  margin-right: auto;
+  margin-left: auto;
+}
+
+.container:before,
+.container:after {
+  display: table;
+  content: " ";
+}
+
+.container:after {
+  clear: both;
+}
+
+.container:before,
+.container:after {
+  display: table;
+  content: " ";
+}
+
+.container:after {
+  clear: both;
+}
+
+@media (min-width: 768px) {
+  .container {
+    width: 750px;
+  }
+}
+
+@media (min-width: 992px) {
+  .container {
+    width: 970px;
+  }
+}
+
+@media (min-width: 1200px) {
+  .container {
+    width: 1170px;
+  }
+}
+
+.row {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+
+.row:before,
+.row:after {
+  display: table;
+  content: " ";
+}
+
+.row:after {
+  clear: both;
+}
+
+.row:before,
+.row:after {
+  display: table;
+  content: " ";
+}
+
+.row:after {
+  clear: both;
+}
+
+.col-xs-1,
+.col-sm-1,
+.col-md-1,
+.col-lg-1,
+.col-xs-2,
+.col-sm-2,
+.col-md-2,
+.col-lg-2,
+.col-xs-3,
+.col-sm-3,
+.col-md-3,
+.col-lg-3,
+.col-xs-4,
+.col-sm-4,
+.col-md-4,
+.col-lg-4,
+.col-xs-5,
+.col-sm-5,
+.col-md-5,
+.col-lg-5,
+.col-xs-6,
+.col-sm-6,
+.col-md-6,
+.col-lg-6,
+.col-xs-7,
+.col-sm-7,
+.col-md-7,
+.col-lg-7,
+.col-xs-8,
+.col-sm-8,
+.col-md-8,
+.col-lg-8,
+.col-xs-9,
+.col-sm-9,
+.col-md-9,
+.col-lg-9,
+.col-xs-10,
+.col-sm-10,
+.col-md-10,
+.col-lg-10,
+.col-xs-11,
+.col-sm-11,
+.col-md-11,
+.col-lg-11,
+.col-xs-12,
+.col-sm-12,
+.col-md-12,
+.col-lg-12 {
+  position: relative;
+  min-height: 1px;
+  padding-right: 15px;
+  padding-left: 15px;
+}
+
+.col-xs-1,
+.col-xs-2,
+.col-xs-3,
+.col-xs-4,
+.col-xs-5,
+.col-xs-6,
+.col-xs-7,
+.col-xs-8,
+.col-xs-9,
+.col-xs-10,
+.col-xs-11,
+.col-xs-12 {
+  float: left;
+}
+
+.col-xs-12 {
+  width: 100%;
+}
+
+.col-xs-11 {
+  width: 91.66666666666666%;
+}
+
+.col-xs-10 {
+  width: 83.33333333333334%;
+}
+
+.col-xs-9 {
+  width: 75%;
+}
+
+.col-xs-8 {
+  width: 66.66666666666666%;
+}
+
+.col-xs-7 {
+  width: 58.333333333333336%;
+}
+
+.col-xs-6 {
+  width: 50%;
+}
+
+.col-xs-5 {
+  width: 41.66666666666667%;
+}
+
+.col-xs-4 {
+  width: 33.33333333333333%;
+}
+
+.col-xs-3 {
+  width: 25%;
+}
+
+.col-xs-2 {
+  width: 16.666666666666664%;
+}
+
+.col-xs-1 {
+  width: 8.333333333333332%;
+}
+
+.col-xs-pull-12 {
+  right: 100%;
+}
+
+.col-xs-pull-11 {
+  right: 91.66666666666666%;
+}
+
+.col-xs-pull-10 {
+  right: 83.33333333333334%;
+}
+
+.col-xs-pull-9 {
+  right: 75%;
+}
+
+.col-xs-pull-8 {
+  right: 66.66666666666666%;
+}
+
+.col-xs-pull-7 {
+  right: 58.333333333333336%;
+}
+
+.col-xs-pull-6 {
+  right: 50%;
+}
+
+.col-xs-pull-5 {
+  right: 41.66666666666667%;
+}
+
+.col-xs-pull-4 {
+  right: 33.33333333333333%;
+}
+
+.col-xs-pull-3 {
+  right: 25%;
+}
+
+.col-xs-pull-2 {
+  right: 16.666666666666664%;
+}
+
+.col-xs-pull-1 {
+  right: 8.333333333333332%;
+}
+
+.col-xs-pull-0 {
+  right: 0;
+}
+
+.col-xs-push-12 {
+  left: 100%;
+}
+
+.col-xs-push-11 {
+  left: 91.66666666666666%;
+}
+
+.col-xs-push-10 {
+  left: 83.33333333333334%;
+}
+
+.col-xs-push-9 {
+  left: 75%;
+}
+
+.col-xs-push-8 {
+  left: 66.66666666666666%;
+}
+
+.col-xs-push-7 {
+  left: 58.333333333333336%;
+}
+
+.col-xs-push-6 {
+  left: 50%;
+}
+
+.col-xs-push-5 {
+  left: 41.66666666666667%;
+}
+
+.col-xs-push-4 {
+  left: 33.33333333333333%;
+}
+
+.col-xs-push-3 {
+  left: 25%;
+}
+
+.col-xs-push-2 {
+  left: 16.666666666666664%;
+}
+
+.col-xs-push-1 {
+  left: 8.333333333333332%;
+}
+
+.col-xs-push-0 {
+  left: 0;
+}
+
+.col-xs-offset-12 {
+  margin-left: 100%;
+}
+
+.col-xs-offset-11 {
+  margin-left: 91.66666666666666%;
+}
+
+.col-xs-offset-10 {
+  margin-left: 83.33333333333334%;
+}
+
+.col-xs-offset-9 {
+  margin-left: 75%;
+}
+
+.col-xs-offset-8 {
+  margin-left: 66.66666666666666%;
+}
+
+.col-xs-offset-7 {
+  margin-left: 58.333333333333336%;
+}
+
+.col-xs-offset-6 {
+  margin-left: 50%;
+}
+
+.col-xs-offset-5 {
+  margin-left: 41.66666666666667%;
+}
+
+.col-xs-offset-4 {
+  margin-left: 33.33333333333333%;
+}
+
+.col-xs-offset-3 {
+  margin-left: 25%;
+}
+
+.col-xs-offset-2 {
+  margin-left: 16.666666666666664%;
+}
+
+.col-xs-offset-1 {
+  margin-left: 8.333333333333332%;
+}
+
+.col-xs-offset-0 {
+  margin-left: 0;
+}
+
+@media (min-width: 768px) {
+  .col-sm-1,
+  .col-sm-2,
+  .col-sm-3,
+  .col-sm-4,
+  .col-sm-5,
+  .col-sm-6,
+  .col-sm-7,
+  .col-sm-8,
+  .col-sm-9,
+  .col-sm-10,
+  .col-sm-11,
+  .col-sm-12 {
+    float: left;
+  }
+  .col-sm-12 {
+    width: 100%;
+  }
+  .col-sm-11 {
+    width: 91.66666666666666%;
+  }
+  .col-sm-10 {
+    width: 83.33333333333334%;
+  }
+  .col-sm-9 {
+    width: 75%;
+  }
+  .col-sm-8 {
+    width: 66.66666666666666%;
+  }
+  .col-sm-7 {
+    width: 58.333333333333336%;
+  }
+  .col-sm-6 {
+    width: 50%;
+  }
+  .col-sm-5 {
+    width: 41.66666666666667%;
+  }
+  .col-sm-4 {
+    width: 33.33333333333333%;
+  }
+  .col-sm-3 {
+    width: 25%;
+  }
+  .col-sm-2 {
+    width: 16.666666666666664%;
+  }
+  .col-sm-1 {
+    width: 8.333333333333332%;
+  }
+  .col-sm-pull-12 {
+    right: 100%;
+  }
+  .col-sm-pull-11 {
+    right: 91.66666666666666%;
+  }
+  .col-sm-pull-10 {
+    right: 83.33333333333334%;
+  }
+  .col-sm-pull-9 {
+    right: 75%;
+  }
+  .col-sm-pull-8 {
+    right: 66.66666666666666%;
+  }
+  .col-sm-pull-7 {
+    right: 58.333333333333336%;
+  }
+  .col-sm-pull-6 {
+    right: 50%;
+  }
+  .col-sm-pull-5 {
+    right: 41.66666666666667%;
+  }
+  .col-sm-pull-4 {
+    right: 33.33333333333333%;
+  }
+  .col-sm-pull-3 {
+    right: 25%;
+  }
+  .col-sm-pull-2 {
+    right: 16.666666666666664%;
+  }
+  .col-sm-pull-1 {
+    right: 8.333333333333332%;
+  }
+  .col-sm-pull-0 {
+    right: 0;
+  }
+  .col-sm-push-12 {
+    left: 100%;
+  }
+  .col-sm-push-11 {
+    left: 91.66666666666666%;
+  }
+  .col-sm-push-10 {
+    left: 83.33333333333334%;
+  }
+  .col-sm-push-9 {
+    left: 75%;
+  }
+  .col-sm-push-8 {
+    left: 66.66666666666666%;
+  }
+  .col-sm-push-7 {
+    left: 58.333333333333336%;
+  }
+  .col-sm-push-6 {
+    left: 50%;
+  }
+  .col-sm-push-5 {
+    left: 41.66666666666667%;
+  }
+  .col-sm-push-4 {
+    left: 33.33333333333333%;
+  }
+  .col-sm-push-3 {
+    left: 25%;
+  }
+  .col-sm-push-2 {
+    left: 16.666666666666664%;
+  }
+  .col-sm-push-1 {
+    left: 8.333333333333332%;
+  }
+  .col-sm-push-0 {
+    left: 0;
+  }
+  .col-sm-offset-12 {
+    margin-left: 100%;
+  }
+  .col-sm-offset-11 {
+    margin-left: 91.66666666666666%;
+  }
+  .col-sm-offset-10 {
+    margin-left: 83.33333333333334%;
+  }
+  .col-sm-offset-9 {
+    margin-left: 75%;
+  }
+  .col-sm-offset-8 {
+    margin-left: 66.66666666666666%;
+  }
+  .col-sm-offset-7 {
+    margin-left: 58.333333333333336%;
+  }
+  .col-sm-offset-6 {
+    margin-left: 50%;
+  }
+  .col-sm-offset-5 {
+    margin-left: 41.66666666666667%;
+  }
+  .col-sm-offset-4 {
+    margin-left: 33.33333333333333%;
+  }
+  .col-sm-offset-3 {
+    margin-left: 25%;
+  }
+  .col-sm-offset-2 {
+    margin-left: 16.666666666666664%;
+  }
+  .col-sm-offset-1 {
+    margin-left: 8.333333333333332%;
+  }
+  .col-sm-offset-0 {
+    margin-left: 0;
+  }
+}
+
+@media (min-width: 992px) {
+  .col-md-1,
+  .col-md-2,
+  .col-md-3,
+  .col-md-4,
+  .col-md-5,
+  .col-md-6,
+  .col-md-7,
+  .col-md-8,
+  .col-md-9,
+  .col-md-10,
+  .col-md-11,
+  .col-md-12 {
+    float: left;
+  }
+  .col-md-12 {
+    width: 100%;
+  }
+  .col-md-11 {
+    width: 91.66666666666666%;
+  }
+  .col-md-10 {
+    width: 83.33333333333334%;
+  }
+  .col-md-9 {
+    width: 75%;
+  }
+  .col-md-8 {
+    width: 66.66666666666666%;
+  }
+  .col-md-7 {
+    width: 58.333333333333336%;
+  }
+  .col-md-6 {
+    width: 50%;
+  }
+  .col-md-5 {
+    width: 41.66666666666667%;
+  }
+  .col-md-4 {
+    width: 33.33333333333333%;
+  }
+  .col-md-3 {
+    width: 25%;
+  }
+  .col-md-2 {
+    width: 16.666666666666664%;
+  }
+  .col-md-1 {
+    width: 8.333333333333332%;
+  }
+  .col-md-pull-12 {
+    right: 100%;
+  }
+  .col-md-pull-11 {
+    right: 91.66666666666666%;
+  }
+  .col-md-pull-10 {
+    right: 83.33333333333334%;
+  }
+  .col-md-pull-9 {
+    right: 75%;
+  }
+  .col-md-pull-8 {
+    right: 66.66666666666666%;
+  }
+  .col-md-pull-7 {
+    right: 58.333333333333336%;
+  }
+  .col-md-pull-6 {
+    right: 50%;
+  }
+  .col-md-pull-5 {
+    right: 41.66666666666667%;
+  }
+  .col-md-pull-4 {
+    right: 33.33333333333333%;
+  }
+  .col-md-pull-3 {
+    right: 25%;
+  }
+  .col-md-pull-2 {
+    right: 16.666666666666664%;
+  }
+  .col-md-pull-1 {
+    right: 8.333333333333332%;
+  }
+  .col-md-pull-0 {
+    right: 0;
+  }
+  .col-md-push-12 {
+    left: 100%;
+  }
+  .col-md-push-11 {
+    left: 91.66666666666666%;
+  }
+  .col-md-push-10 {
+    left: 83.33333333333334%;
+  }
+  .col-md-push-9 {
+    left: 75%;
+  }
+  .col-md-push-8 {
+    left: 66.66666666666666%;
+  }
+  .col-md-push-7 {
+    left: 58.333333333333336%;
+  }
+  .col-md-push-6 {
+    left: 50%;
+  }
+  .col-md-push-5 {
+    left: 41.66666666666667%;
+  }
+  .col-md-push-4 {
+    left: 33.33333333333333%;
+  }
+  .col-md-push-3 {
+    left: 25%;
+  }
+  .col-md-push-2 {
+    left: 16.666666666666664%;
+  }
+  .col-md-push-1 {
+    left: 8.333333333333332%;
+  }
+  .col-md-push-0 {
+    left: 0;
+  }
+  .col-md-offset-12 {
+    margin-left: 100%;
+  }
+  .col-md-offset-11 {
+    margin-left: 91.66666666666666%;
+  }
+  .col-md-offset-10 {
+    margin-left: 83.33333333333334%;
+  }
+  .col-md-offset-9 {
+    margin-left: 75%;
+  }
+  .col-md-offset-8 {
+    margin-left: 66.66666666666666%;
+  }
+  .col-md-offset-7 {
+    margin-left: 58.333333333333336%;
+  }
+  .col-md-offset-6 {
+    margin-left: 50%;
+  }
+  .col-md-offset-5 {
+    margin-left: 41.66666666666667%;
+  }
+  .col-md-offset-4 {
+    margin-left: 33.33333333333333%;
+  }
+  .col-md-offset-3 {
+    margin-left: 25%;
+  }
+  .col-md-offset-2 {
+    margin-left: 16.666666666666664%;
+  }
+  .col-md-offset-1 {
+    margin-left: 8.333333333333332%;
+  }
+  .col-md-offset-0 {
+    margin-left: 0;
+  }
+}
+
+@media (min-width: 1200px) {
+  .col-lg-1,
+  .col-lg-2,
+  .col-lg-3,
+  .col-lg-4,
+  .col-lg-5,
+  .col-lg-6,
+  .col-lg-7,
+  .col-lg-8,
+  .col-lg-9,
+  .col-lg-10,
+  .col-lg-11,
+  .col-lg-12 {
+    float: left;
+  }
+  .col-lg-12 {
+    width: 100%;
+  }
+  .col-lg-11 {
+    width: 91.66666666666666%;
+  }
+  .col-lg-10 {
+    width: 83.33333333333334%;
+  }
+  .col-lg-9 {
+    width: 75%;
+  }
+  .col-lg-8 {
+    width: 66.66666666666666%;
+  }
+  .col-lg-7 {
+    width: 58.333333333333336%;
+  }
+  .col-lg-6 {
+    width: 50%;
+  }
+  .col-lg-5 {
+    width: 41.66666666666667%;
+  }
+  .col-lg-4 {
+    width: 33.33333333333333%;
+  }
+  .col-lg-3 {
+    width: 25%;
+  }
+  .col-lg-2 {
+    width: 16.666666666666664%;
+  }
+  .col-lg-1 {
+    width: 8.333333333333332%;
+  }
+  .col-lg-pull-12 {
+    right: 100%;
+  }
+  .col-lg-pull-11 {
+    right: 91.66666666666666%;
+  }
+  .col-lg-pull-10 {
+    right: 83.33333333333334%;
+  }
+  .col-lg-pull-9 {
+    right: 75%;
+  }
+  .col-lg-pull-8 {
+    right: 66.66666666666666%;
+  }
+  .col-lg-pull-7 {
+    right: 58.333333333333336%;
+  }
+  .col-lg-pull-6 {
+    right: 50%;
+  }
+  .col-lg-pull-5 {
+    right: 41.66666666666667%;
+  }
+  .col-lg-pull-4 {
+    right: 33.33333333333333%;
+  }
+  .col-lg-pull-3 {
+    right: 25%;
+  }
+  .col-lg-pull-2 {
+    right: 16.666666666666664%;
+  }
+  .col-lg-pull-1 {
+    right: 8.333333333333332%;
+  }
+  .col-lg-pull-0 {
+    right: 0;
+  }
+  .col-lg-push-12 {
+    left: 100%;
+  }
+  .col-lg-push-11 {
+    left: 91.66666666666666%;
+  }
+  .col-lg-push-10 {
+    left: 83.33333333333334%;
+  }
+  .col-lg-push-9 {
+    left: 75%;
+  }
+  .col-lg-push-8 {
+    left: 66.66666666666666%;
+  }
+  .col-lg-push-7 {
+    left: 58.333333333333336%;
+  }
+  .col-lg-push-6 {
+    left: 50%;
+  }
+  .col-lg-push-5 {
+    left: 41.66666666666667%;
+  }
+  .col-lg-push-4 {
+    left: 33.33333333333333%;
+  }
+  .col-lg-push-3 {
+    left: 25%;
+  }
+  .col-lg-push-2 {
+    left: 16.666666666666664%;
+  }
+  .col-lg-push-1 {
+    left: 8.333333333333332%;
+  }
+  .col-lg-push-0 {
+    left: 0;
+  }
+  .col-lg-offset-12 {
+    margin-left: 100%;
+  }
+  .col-lg-offset-11 {
+    margin-left: 91.66666666666666%;
+  }
+  .col-lg-offset-10 {
+    margin-left: 83.33333333333334%;
+  }
+  .col-lg-offset-9 {
+    margin-left: 75%;
+  }
+  .col-lg-offset-8 {
+    margin-left: 66.66666666666666%;
+  }
+  .col-lg-offset-7 {
+    margin-left: 58.333333333333336%;
+  }
+  .col-lg-offset-6 {
+    margin-left: 50%;
+  }
+  .col-lg-offset-5 {
+    margin-left: 41.66666666666667%;
+  }
+  .col-lg-offset-4 {
+    margin-left: 33.33333333333333%;
+  }
+  .col-lg-offset-3 {
+    margin-left: 25%;
+  }
+  .col-lg-offset-2 {
+    margin-left: 16.666666666666664%;
+  }
+  .col-lg-offset-1 {
+    margin-left: 8.333333333333332%;
+  }
+  .col-lg-offset-0 {
+    margin-left: 0;
+  }
+}
+
+table {
+  max-width: 100%;
+  background-color: transparent;
+}
+
+th {
+  text-align: left;
+}
+
+.table {
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.table > thead > tr > th,
+.table > tbody > tr > th,
+.table > tfoot > tr > th,
+.table > thead > tr > td,
+.table > tbody > tr > td,
+.table > tfoot > tr > td {
+  padding: 8px;
+  line-height: 1.428571429;
+  vertical-align: top;
+  border-top: 1px solid #dddddd;
+}
+
+.table > thead > tr > th {
+  vertical-align: bottom;
+  border-bottom: 2px solid #dddddd;
+}
+
+.table > caption + thead > tr:first-child > th,
+.table > colgroup + thead > tr:first-child > th,
+.table > thead:first-child > tr:first-child > th,
+.table > caption + thead > tr:first-child > td,
+.table > colgroup + thead > tr:first-child > td,
+.table > thead:first-child > tr:first-child > td {
+  border-top: 0;
+}
+
+.table > tbody + tbody {
+  border-top: 2px solid #dddddd;
+}
+
+.table .table {
+  background-color: #ffffff;
+}
+
+.table-condensed > thead > tr > th,
+.table-condensed > tbody > tr > th,
+.table-condensed > tfoot > tr > th,
+.table-condensed > thead > tr > td,
+.table-condensed > tbody > tr > td,
+.table-condensed > tfoot > tr > td {
+  padding: 5px;
+}
+
+.table-bordered {
+  border: 1px solid #dddddd;
+}
+
+.table-bordered > thead > tr > th,
+.table-bordered > tbody > tr > th,
+.table-bordered > tfoot > tr > th,
+.table-bordered > thead > tr > td,
+.table-bordered > tbody > tr > td,
+.table-bordered > tfoot > tr > td {
+  border: 1px solid #dddddd;
+}
+
+.table-bordered > thead > tr > th,
+.table-bordered > thead > tr > td {
+  border-bottom-width: 2px;
+}
+
+.table-striped > tbody > tr:nth-child(odd) > td,
+.table-striped > tbody > tr:nth-child(odd) > th {
+  background-color: #f9f9f9;
+}
+
+.table-hover > tbody > tr:hover > td,
+.table-hover > tbody > tr:hover > th {
+  background-color: #f5f5f5;
+}
+
+table col[class*="col-"] {
+  position: static;
+  display: table-column;
+  float: none;
+}
+
+table td[class*="col-"],
+table th[class*="col-"] {
+  display: table-cell;
+  float: none;
+}
+
+.table > thead > tr > .active,
+.table > tbody > tr > .active,
+.table > tfoot > tr > .active,
+.table > thead > .active > td,
+.table > tbody > .active > td,
+.table > tfoot > .active > td,
+.table > thead > .active > th,
+.table > tbody > .active > th,
+.table > tfoot > .active > th {
+  background-color: #f5f5f5;
+}
+
+.table-hover > tbody > tr > .active:hover,
+.table-hover > tbody > .active:hover > td,
+.table-hover > tbody > .active:hover > th {
+  background-color: #e8e8e8;
+}
+
+.table > thead > tr > .success,
+.table > tbody > tr > .success,
+.table > tfoot > tr > .success,
+.table > thead > .success > td,
+.table > tbody > .success > td,
+.table > tfoot > .success > td,
+.table > thead > .success > th,
+.table > tbody > .success > th,
+.table > tfoot > .success > th {
+  background-color: #dff0d8;
+}
+
+.table-hover > tbody > tr > .success:hover,
+.table-hover > tbody > .success:hover > td,
+.table-hover > tbody > .success:hover > th {
+  background-color: #d0e9c6;
+}
+
+.table > thead > tr > .danger,
+.table > tbody > tr > .danger,
+.table > tfoot > tr > .danger,
+.table > thead > .danger > td,
+.table > tbody > .danger > td,
+.table > tfoot > .danger > td,
+.table > thead > .danger > th,
+.table > tbody > .danger > th,
+.table > tfoot > .danger > th {
+  background-color: #f2dede;
+}
+
+.table-hover > tbody > tr > .danger:hover,
+.table-hover > tbody > .danger:hover > td,
+.table-hover > tbody > .danger:hover > th {
+  background-color: #ebcccc;
+}
+
+.table > thead > tr > .warning,
+.table > tbody > tr > .warning,
+.table > tfoot > tr > .warning,
+.table > thead > .warning > td,
+.table > tbody > .warning > td,
+.table > tfoot > .warning > td,
+.table > thead > .warning > th,
+.table > tbody > .warning > th,
+.table > tfoot > .warning > th {
+  background-color: #fcf8e3;
+}
+
+.table-hover > tbody > tr > .warning:hover,
+.table-hover > tbody > .warning:hover > td,
+.table-hover > tbody > .warning:hover > th {
+  background-color: #faf2cc;
+}
+
+@media (max-width: 767px) {
+  .table-responsive {
+    width: 100%;
+    margin-bottom: 15px;
+    overflow-x: scroll;
+    overflow-y: hidden;
+    border: 1px solid #dddddd;
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    -webkit-overflow-scrolling: touch;
+  }
+  .table-responsive > .table {
+    margin-bottom: 0;
+  }
+  .table-responsive > .table > thead > tr > th,
+  .table-responsive > .table > tbody > tr > th,
+  .table-responsive > .table > tfoot > tr > th,
+  .table-responsive > .table > thead > tr > td,
+  .table-responsive > .table > tbody > tr > td,
+  .table-responsive > .table > tfoot > tr > td {
+    white-space: nowrap;
+  }
+  .table-responsive > .table-bordered {
+    border: 0;
+  }
+  .table-responsive > .table-bordered > thead > tr > th:first-child,
+  .table-responsive > .table-bordered > tbody > tr > th:first-child,
+  .table-responsive > .table-bordered > tfoot > tr > th:first-child,
+  .table-responsive > .table-bordered > thead > tr > td:first-child,
+  .table-responsive > .table-bordered > tbody > tr > td:first-child,
+  .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+    border-left: 0;
+  }
+  .table-responsive > .table-bordered > thead > tr > th:last-child,
+  .table-responsive > .table-bordered > tbody > tr > th:last-child,
+  .table-responsive > .table-bordered > tfoot > tr > th:last-child,
+  .table-responsive > .table-bordered > thead > tr > td:last-child,
+  .table-responsive > .table-bordered > tbody > tr > td:last-child,
+  .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+    border-right: 0;
+  }
+  .table-responsive > .table-bordered > tbody > tr:last-child > th,
+  .table-responsive > .table-bordered > tfoot > tr:last-child > th,
+  .table-responsive > .table-bordered > tbody > tr:last-child > td,
+  .table-responsive > .table-bordered > tfoot > tr:last-child > td {
+    border-bottom: 0;
+  }
+}
+
+fieldset {
+  padding: 0;
+  margin: 0;
+  border: 0;
+}
+
+legend {
+  display: block;
+  width: 100%;
+  padding: 0;
+  margin-bottom: 20px;
+  font-size: 21px;
+  line-height: inherit;
+  color: #333333;
+  border: 0;
+  border-bottom: 1px solid #e5e5e5;
+}
+
+label {
+  display: inline-block;
+  margin-bottom: 5px;
+  font-weight: bold;
+}
+
+input[type="search"] {
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
+}
+
+input[type="radio"],
+input[type="checkbox"] {
+  margin: 4px 0 0;
+  margin-top: 1px \9;
+  /* IE8-9 */
+
+  line-height: normal;
+}
+
+input[type="file"] {
+  display: block;
+}
+
+select[multiple],
+select[size] {
+  height: auto;
+}
+
+select optgroup {
+  font-family: inherit;
+  font-size: inherit;
+  font-style: inherit;
+}
+
+input[type="file"]:focus,
+input[type="radio"]:focus,
+input[type="checkbox"]:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+input[type="number"]::-webkit-outer-spin-button,
+input[type="number"]::-webkit-inner-spin-button {
+  height: auto;
+}
+
+output {
+  display: block;
+  padding-top: 7px;
+  font-size: 14px;
+  line-height: 1.428571429;
+  color: #555555;
+  vertical-align: middle;
+}
+
+.form-control {
+  display: block;
+  width: 100%;
+  height: 34px;
+  padding: 6px 12px;
+  font-size: 14px;
+  line-height: 1.428571429;
+  color: #555555;
+  vertical-align: middle;
+  background-color: #ffffff;
+  background-image: none;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+          transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+}
+
+.form-control:focus {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+
+.form-control:-moz-placeholder {
+  color: #999999;
+}
+
+.form-control::-moz-placeholder {
+  color: #999999;
+  opacity: 1;
+}
+
+.form-control:-ms-input-placeholder {
+  color: #999999;
+}
+
+.form-control::-webkit-input-placeholder {
+  color: #999999;
+}
+
+.form-control[disabled],
+.form-control[readonly],
+fieldset[disabled] .form-control {
+  cursor: not-allowed;
+  background-color: #eeeeee;
+}
+
+textarea.form-control {
+  height: auto;
+}
+
+.form-group {
+  margin-bottom: 15px;
+}
+
+.radio,
+.checkbox {
+  display: block;
+  min-height: 20px;
+  padding-left: 20px;
+  margin-top: 10px;
+  margin-bottom: 10px;
+  vertical-align: middle;
+}
+
+.radio label,
+.checkbox label {
+  display: inline;
+  margin-bottom: 0;
+  font-weight: normal;
+  cursor: pointer;
+}
+
+.radio input[type="radio"],
+.radio-inline input[type="radio"],
+.checkbox input[type="checkbox"],
+.checkbox-inline input[type="checkbox"] {
+  float: left;
+  margin-left: -20px;
+}
+
+.radio + .radio,
+.checkbox + .checkbox {
+  margin-top: -5px;
+}
+
+.radio-inline,
+.checkbox-inline {
+  display: inline-block;
+  padding-left: 20px;
+  margin-bottom: 0;
+  font-weight: normal;
+  vertical-align: middle;
+  cursor: pointer;
+}
+
+.radio-inline + .radio-inline,
+.checkbox-inline + .checkbox-inline {
+  margin-top: 0;
+  margin-left: 10px;
+}
+
+input[type="radio"][disabled],
+input[type="checkbox"][disabled],
+.radio[disabled],
+.radio-inline[disabled],
+.checkbox[disabled],
+.checkbox-inline[disabled],
+fieldset[disabled] input[type="radio"],
+fieldset[disabled] input[type="checkbox"],
+fieldset[disabled] .radio,
+fieldset[disabled] .radio-inline,
+fieldset[disabled] .checkbox,
+fieldset[disabled] .checkbox-inline {
+  cursor: not-allowed;
+}
+
+.input-sm {
+  height: 30px;
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+select.input-sm {
+  height: 30px;
+  line-height: 30px;
+}
+
+textarea.input-sm {
+  height: auto;
+}
+
+.input-lg {
+  height: 46px;
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+
+select.input-lg {
+  height: 46px;
+  line-height: 46px;
+}
+
+textarea.input-lg {
+  height: auto;
+}
+
+.has-warning .help-block,
+.has-warning .control-label,
+.has-warning .radio,
+.has-warning .checkbox,
+.has-warning .radio-inline,
+.has-warning .checkbox-inline {
+  color: #8a6d3b;
+}
+
+.has-warning .form-control {
+  border-color: #8a6d3b;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.has-warning .form-control:focus {
+  border-color: #66512c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+}
+
+.has-warning .input-group-addon {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #8a6d3b;
+}
+
+.has-error .help-block,
+.has-error .control-label,
+.has-error .radio,
+.has-error .checkbox,
+.has-error .radio-inline,
+.has-error .checkbox-inline {
+  color: #a94442;
+}
+
+.has-error .form-control {
+  border-color: #a94442;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.has-error .form-control:focus {
+  border-color: #843534;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+}
+
+.has-error .input-group-addon {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #a94442;
+}
+
+.has-success .help-block,
+.has-success .control-label,
+.has-success .radio,
+.has-success .checkbox,
+.has-success .radio-inline,
+.has-success .checkbox-inline {
+  color: #3c763d;
+}
+
+.has-success .form-control {
+  border-color: #3c763d;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.has-success .form-control:focus {
+  border-color: #2b542c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+}
+
+.has-success .input-group-addon {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #3c763d;
+}
+
+.form-control-static {
+  margin-bottom: 0;
+}
+
+.help-block {
+  display: block;
+  margin-top: 5px;
+  margin-bottom: 10px;
+  color: #737373;
+}
+
+@media (min-width: 768px) {
+  .form-inline .form-group {
+    display: inline-block;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .form-inline .form-control {
+    display: inline-block;
+  }
+  .form-inline select.form-control {
+    width: auto;
+  }
+  .form-inline .radio,
+  .form-inline .checkbox {
+    display: inline-block;
+    padding-left: 0;
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+  .form-inline .radio input[type="radio"],
+  .form-inline .checkbox input[type="checkbox"] {
+    float: none;
+    margin-left: 0;
+  }
+}
+
+.form-horizontal .control-label,
+.form-horizontal .radio,
+.form-horizontal .checkbox,
+.form-horizontal .radio-inline,
+.form-horizontal .checkbox-inline {
+  padding-top: 7px;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.form-horizontal .radio,
+.form-horizontal .checkbox {
+  min-height: 27px;
+}
+
+.form-horizontal .form-group {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+
+.form-horizontal .form-group:before,
+.form-horizontal .form-group:after {
+  display: table;
+  content: " ";
+}
+
+.form-horizontal .form-group:after {
+  clear: both;
+}
+
+.form-horizontal .form-group:before,
+.form-horizontal .form-group:after {
+  display: table;
+  content: " ";
+}
+
+.form-horizontal .form-group:after {
+  clear: both;
+}
+
+.form-horizontal .form-control-static {
+  padding-top: 7px;
+}
+
+@media (min-width: 768px) {
+  .form-horizontal .control-label {
+    text-align: right;
+  }
+}
+
+.btn {
+  display: inline-block;
+  padding: 6px 12px;
+  margin-bottom: 0;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 1.428571429;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: middle;
+  cursor: pointer;
+  background-image: none;
+  border: 1px solid transparent;
+  border-radius: 4px;
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+       -o-user-select: none;
+          user-select: none;
+}
+
+.btn:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+.btn:hover,
+.btn:focus {
+  color: #333333;
+  text-decoration: none;
+}
+
+.btn:active,
+.btn.active {
+  background-image: none;
+  outline: 0;
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+
+.btn.disabled,
+.btn[disabled],
+fieldset[disabled] .btn {
+  pointer-events: none;
+  cursor: not-allowed;
+  opacity: 0.65;
+  filter: alpha(opacity=65);
+  -webkit-box-shadow: none;
+          box-shadow: none;
+}
+
+.btn-default {
+  color: #333333;
+  background-color: #ffffff;
+  border-color: #cccccc;
+}
+
+.btn-default:hover,
+.btn-default:focus,
+.btn-default:active,
+.btn-default.active,
+.open .dropdown-toggle.btn-default {
+  color: #333333;
+  background-color: #ebebeb;
+  border-color: #adadad;
+}
+
+.btn-default:active,
+.btn-default.active,
+.open .dropdown-toggle.btn-default {
+  background-image: none;
+}
+
+.btn-default.disabled,
+.btn-default[disabled],
+fieldset[disabled] .btn-default,
+.btn-default.disabled:hover,
+.btn-default[disabled]:hover,
+fieldset[disabled] .btn-default:hover,
+.btn-default.disabled:focus,
+.btn-default[disabled]:focus,
+fieldset[disabled] .btn-default:focus,
+.btn-default.disabled:active,
+.btn-default[disabled]:active,
+fieldset[disabled] .btn-default:active,
+.btn-default.disabled.active,
+.btn-default[disabled].active,
+fieldset[disabled] .btn-default.active {
+  background-color: #ffffff;
+  border-color: #cccccc;
+}
+
+.btn-default .badge {
+  color: #ffffff;
+  background-color: #fff;
+}
+
+.btn-primary {
+  color: #ffffff;
+  background-color: #428bca;
+  border-color: #357ebd;
+}
+
+.btn-primary:hover,
+.btn-primary:focus,
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+  color: #ffffff;
+  background-color: #3276b1;
+  border-color: #285e8e;
+}
+
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+  background-image: none;
+}
+
+.btn-primary.disabled,
+.btn-primary[disabled],
+fieldset[disabled] .btn-primary,
+.btn-primary.disabled:hover,
+.btn-primary[disabled]:hover,
+fieldset[disabled] .btn-primary:hover,
+.btn-primary.disabled:focus,
+.btn-primary[disabled]:focus,
+fieldset[disabled] .btn-primary:focus,
+.btn-primary.disabled:active,
+.btn-primary[disabled]:active,
+fieldset[disabled] .btn-primary:active,
+.btn-primary.disabled.active,
+.btn-primary[disabled].active,
+fieldset[disabled] .btn-primary.active {
+  background-color: #428bca;
+  border-color: #357ebd;
+}
+
+.btn-primary .badge {
+  color: #428bca;
+  background-color: #fff;
+}
+
+.btn-warning {
+  color: #ffffff;
+  background-color: #f0ad4e;
+  border-color: #eea236;
+}
+
+.btn-warning:hover,
+.btn-warning:focus,
+.btn-warning:active,
+.btn-warning.active,
+.open .dropdown-toggle.btn-warning {
+  color: #ffffff;
+  background-color: #ed9c28;
+  border-color: #d58512;
+}
+
+.btn-warning:active,
+.btn-warning.active,
+.open .dropdown-toggle.btn-warning {
+  background-image: none;
+}
+
+.btn-warning.disabled,
+.btn-warning[disabled],
+fieldset[disabled] .btn-warning,
+.btn-warning.disabled:hover,
+.btn-warning[disabled]:hover,
+fieldset[disabled] .btn-warning:hover,
+.btn-warning.disabled:focus,
+.btn-warning[disabled]:focus,
+fieldset[disabled] .btn-warning:focus,
+.btn-warning.disabled:active,
+.btn-warning[disabled]:active,
+fieldset[disabled] .btn-warning:active,
+.btn-warning.disabled.active,
+.btn-warning[disabled].active,
+fieldset[disabled] .btn-warning.active {
+  background-color: #f0ad4e;
+  border-color: #eea236;
+}
+
+.btn-warning .badge {
+  color: #f0ad4e;
+  background-color: #fff;
+}
+
+.btn-danger {
+  color: #ffffff;
+  background-color: #d9534f;
+  border-color: #d43f3a;
+}
+
+.btn-danger:hover,
+.btn-danger:focus,
+.btn-danger:active,
+.btn-danger.active,
+.open .dropdown-toggle.btn-danger {
+  color: #ffffff;
+  background-color: #d2322d;
+  border-color: #ac2925;
+}
+
+.btn-danger:active,
+.btn-danger.active,
+.open .dropdown-toggle.btn-danger {
+  background-image: none;
+}
+
+.btn-danger.disabled,
+.btn-danger[disabled],
+fieldset[disabled] .btn-danger,
+.btn-danger.disabled:hover,
+.btn-danger[disabled]:hover,
+fieldset[disabled] .btn-danger:hover,
+.btn-danger.disabled:focus,
+.btn-danger[disabled]:focus,
+fieldset[disabled] .btn-danger:focus,
+.btn-danger.disabled:active,
+.btn-danger[disabled]:active,
+fieldset[disabled] .btn-danger:active,
+.btn-danger.disabled.active,
+.btn-danger[disabled].active,
+fieldset[disabled] .btn-danger.active {
+  background-color: #d9534f;
+  border-color: #d43f3a;
+}
+
+.btn-danger .badge {
+  color: #d9534f;
+  background-color: #fff;
+}
+
+.btn-success {
+  color: #ffffff;
+  background-color: #5cb85c;
+  border-color: #4cae4c;
+}
+
+.btn-success:hover,
+.btn-success:focus,
+.btn-success:active,
+.btn-success.active,
+.open .dropdown-toggle.btn-success {
+  color: #ffffff;
+  background-color: #47a447;
+  border-color: #398439;
+}
+
+.btn-success:active,
+.btn-success.active,
+.open .dropdown-toggle.btn-success {
+  background-image: none;
+}
+
+.btn-success.disabled,
+.btn-success[disabled],
+fieldset[disabled] .btn-success,
+.btn-success.disabled:hover,
+.btn-success[disabled]:hover,
+fieldset[disabled] .btn-success:hover,
+.btn-success.disabled:focus,
+.btn-success[disabled]:focus,
+fieldset[disabled] .btn-success:focus,
+.btn-success.disabled:active,
+.btn-success[disabled]:active,
+fieldset[disabled] .btn-success:active,
+.btn-success.disabled.active,
+.btn-success[disabled].active,
+fieldset[disabled] .btn-success.active {
+  background-color: #5cb85c;
+  border-color: #4cae4c;
+}
+
+.btn-success .badge {
+  color: #5cb85c;
+  background-color: #fff;
+}
+
+.btn-info {
+  color: #ffffff;
+  background-color: #5bc0de;
+  border-color: #46b8da;
+}
+
+.btn-info:hover,
+.btn-info:focus,
+.btn-info:active,
+.btn-info.active,
+.open .dropdown-toggle.btn-info {
+  color: #ffffff;
+  background-color: #39b3d7;
+  border-color: #269abc;
+}
+
+.btn-info:active,
+.btn-info.active,
+.open .dropdown-toggle.btn-info {
+  background-image: none;
+}
+
+.btn-info.disabled,
+.btn-info[disabled],
+fieldset[disabled] .btn-info,
+.btn-info.disabled:hover,
+.btn-info[disabled]:hover,
+fieldset[disabled] .btn-info:hover,
+.btn-info.disabled:focus,
+.btn-info[disabled]:focus,
+fieldset[disabled] .btn-info:focus,
+.btn-info.disabled:active,
+.btn-info[disabled]:active,
+fieldset[disabled] .btn-info:active,
+.btn-info.disabled.active,
+.btn-info[disabled].active,
+fieldset[disabled] .btn-info.active {
+  background-color: #5bc0de;
+  border-color: #46b8da;
+}
+
+.btn-info .badge {
+  color: #5bc0de;
+  background-color: #fff;
+}
+
+.btn-link {
+  font-weight: normal;
+  color: #428bca;
+  cursor: pointer;
+  border-radius: 0;
+}
+
+.btn-link,
+.btn-link:active,
+.btn-link[disabled],
+fieldset[disabled] .btn-link {
+  background-color: transparent;
+  -webkit-box-shadow: none;
+          box-shadow: none;
+}
+
+.btn-link,
+.btn-link:hover,
+.btn-link:focus,
+.btn-link:active {
+  border-color: transparent;
+}
+
+.btn-link:hover,
+.btn-link:focus {
+  color: #2a6496;
+  text-decoration: underline;
+  background-color: transparent;
+}
+
+.btn-link[disabled]:hover,
+fieldset[disabled] .btn-link:hover,
+.btn-link[disabled]:focus,
+fieldset[disabled] .btn-link:focus {
+  color: #999999;
+  text-decoration: none;
+}
+
+.btn-lg {
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+
+.btn-sm {
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-xs {
+  padding: 1px 5px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-block {
+  display: block;
+  width: 100%;
+  padding-right: 0;
+  padding-left: 0;
+}
+
+.btn-block + .btn-block {
+  margin-top: 5px;
+}
+
+input[type="submit"].btn-block,
+input[type="reset"].btn-block,
+input[type="button"].btn-block {
+  width: 100%;
+}
+
+.fade {
+  opacity: 0;
+  -webkit-transition: opacity 0.15s linear;
+          transition: opacity 0.15s linear;
+}
+
+.fade.in {
+  opacity: 1;
+}
+
+.collapse {
+  display: none;
+}
+
+.collapse.in {
+  display: block;
+}
+
+.collapsing {
+  position: relative;
+  height: 0;
+  overflow: hidden;
+  -webkit-transition: height 0.35s ease;
+          transition: height 0.35s ease;
+}
+
+@font-face {
+  font-family: 'Glyphicons Halflings';
+  src: url('../fonts/glyphicons-halflings-regular.eot');
+  src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg');
+}
+
+.glyphicon {
+  position: relative;
+  top: 1px;
+  display: inline-block;
+  font-family: 'Glyphicons Halflings';
+  -webkit-font-smoothing: antialiased;
+  font-style: normal;
+  font-weight: normal;
+  line-height: 1;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.glyphicon:empty {
+  width: 1em;
+}
+
+.glyphicon-asterisk:before {
+  content: "\2a";
+}
+
+.glyphicon-plus:before {
+  content: "\2b";
+}
+
+.glyphicon-euro:before {
+  content: "\20ac";
+}
+
+.glyphicon-minus:before {
+  content: "\2212";
+}
+
+.glyphicon-cloud:before {
+  content: "\2601";
+}
+
+.glyphicon-envelope:before {
+  content: "\2709";
+}
+
+.glyphicon-pencil:before {
+  content: "\270f";
+}
+
+.glyphicon-glass:before {
+  content: "\e001";
+}
+
+.glyphicon-music:before {
+  content: "\e002";
+}
+
+.glyphicon-search:before {
+  content: "\e003";
+}
+
+.glyphicon-heart:before {
+  content: "\e005";
+}
+
+.glyphicon-star:before {
+  content: "\e006";
+}
+
+.glyphicon-star-empty:before {
+  content: "\e007";
+}
+
+.glyphicon-user:before {
+  content: "\e008";
+}
+
+.glyphicon-film:before {
+  content: "\e009";
+}
+
+.glyphicon-th-large:before {
+  content: "\e010";
+}
+
+.glyphicon-th:before {
+  content: "\e011";
+}
+
+.glyphicon-th-list:before {
+  content: "\e012";
+}
+
+.glyphicon-ok:before {
+  content: "\e013";
+}
+
+.glyphicon-remove:before {
+  content: "\e014";
+}
+
+.glyphicon-zoom-in:before {
+  content: "\e015";
+}
+
+.glyphicon-zoom-out:before {
+  content: "\e016";
+}
+
+.glyphicon-off:before {
+  content: "\e017";
+}
+
+.glyphicon-signal:before {
+  content: "\e018";
+}
+
+.glyphicon-cog:before {
+  content: "\e019";
+}
+
+.glyphicon-trash:before {
+  content: "\e020";
+}
+
+.glyphicon-home:before {
+  content: "\e021";
+}
+
+.glyphicon-file:before {
+  content: "\e022";
+}
+
+.glyphicon-time:before {
+  content: "\e023";
+}
+
+.glyphicon-road:before {
+  content: "\e024";
+}
+
+.glyphicon-download-alt:before {
+  content: "\e025";
+}
+
+.glyphicon-download:before {
+  content: "\e026";
+}
+
+.glyphicon-upload:before {
+  content: "\e027";
+}
+
+.glyphicon-inbox:before {
+  content: "\e028";
+}
+
+.glyphicon-play-circle:before {
+  content: "\e029";
+}
+
+.glyphicon-repeat:before {
+  content: "\e030";
+}
+
+.glyphicon-refresh:before {
+  content: "\e031";
+}
+
+.glyphicon-list-alt:before {
+  content: "\e032";
+}
+
+.glyphicon-lock:before {
+  content: "\e033";
+}
+
+.glyphicon-flag:before {
+  content: "\e034";
+}
+
+.glyphicon-headphones:before {
+  content: "\e035";
+}
+
+.glyphicon-volume-off:before {
+  content: "\e036";
+}
+
+.glyphicon-volume-down:before {
+  content: "\e037";
+}
+
+.glyphicon-volume-up:before {
+  content: "\e038";
+}
+
+.glyphicon-qrcode:before {
+  content: "\e039";
+}
+
+.glyphicon-barcode:before {
+  content: "\e040";
+}
+
+.glyphicon-tag:before {
+  content: "\e041";
+}
+
+.glyphicon-tags:before {
+  content: "\e042";
+}
+
+.glyphicon-book:before {
+  content: "\e043";
+}
+
+.glyphicon-bookmark:before {
+  content: "\e044";
+}
+
+.glyphicon-print:before {
+  content: "\e045";
+}
+
+.glyphicon-camera:before {
+  content: "\e046";
+}
+
+.glyphicon-font:before {
+  content: "\e047";
+}
+
+.glyphicon-bold:before {
+  content: "\e048";
+}
+
+.glyphicon-italic:before {
+  content: "\e049";
+}
+
+.glyphicon-text-height:before {
+  content: "\e050";
+}
+
+.glyphicon-text-width:before {
+  content: "\e051";
+}
+
+.glyphicon-align-left:before {
+  content: "\e052";
+}
+
+.glyphicon-align-center:before {
+  content: "\e053";
+}
+
+.glyphicon-align-right:before {
+  content: "\e054";
+}
+
+.glyphicon-align-justify:before {
+  content: "\e055";
+}
+
+.glyphicon-list:before {
+  content: "\e056";
+}
+
+.glyphicon-indent-left:before {
+  content: "\e057";
+}
+
+.glyphicon-indent-right:before {
+  content: "\e058";
+}
+
+.glyphicon-facetime-video:before {
+  content: "\e059";
+}
+
+.glyphicon-picture:before {
+  content: "\e060";
+}
+
+.glyphicon-map-marker:before {
+  content: "\e062";
+}
+
+.glyphicon-adjust:before {
+  content: "\e063";
+}
+
+.glyphicon-tint:before {
+  content: "\e064";
+}
+
+.glyphicon-edit:before {
+  content: "\e065";
+}
+
+.glyphicon-share:before {
+  content: "\e066";
+}
+
+.glyphicon-check:before {
+  content: "\e067";
+}
+
+.glyphicon-move:before {
+  content: "\e068";
+}
+
+.glyphicon-step-backward:before {
+  content: "\e069";
+}
+
+.glyphicon-fast-backward:before {
+  content: "\e070";
+}
+
+.glyphicon-backward:before {
+  content: "\e071";
+}
+
+.glyphicon-play:before {
+  content: "\e072";
+}
+
+.glyphicon-pause:before {
+  content: "\e073";
+}
+
+.glyphicon-stop:before {
+  content: "\e074";
+}
+
+.glyphicon-forward:before {
+  content: "\e075";
+}
+
+.glyphicon-fast-forward:before {
+  content: "\e076";
+}
+
+.glyphicon-step-forward:before {
+  content: "\e077";
+}
+
+.glyphicon-eject:before {
+  content: "\e078";
+}
+
+.glyphicon-chevron-left:before {
+  content: "\e079";
+}
+
+.glyphicon-chevron-right:before {
+  content: "\e080";
+}
+
+.glyphicon-plus-sign:before {
+  content: "\e081";
+}
+
+.glyphicon-minus-sign:before {
+  content: "\e082";
+}
+
+.glyphicon-remove-sign:before {
+  content: "\e083";
+}
+
+.glyphicon-ok-sign:before {
+  content: "\e084";
+}
+
+.glyphicon-question-sign:before {
+  content: "\e085";
+}
+
+.glyphicon-info-sign:before {
+  content: "\e086";
+}
+
+.glyphicon-screenshot:before {
+  content: "\e087";
+}
+
+.glyphicon-remove-circle:before {
+  content: "\e088";
+}
+
+.glyphicon-ok-circle:before {
+  content: "\e089";
+}
+
+.glyphicon-ban-circle:before {
+  content: "\e090";
+}
+
+.glyphicon-arrow-left:before {
+  content: "\e091";
+}
+
+.glyphicon-arrow-right:before {
+  content: "\e092";
+}
+
+.glyphicon-arrow-up:before {
+  content: "\e093";
+}
+
+.glyphicon-arrow-down:before {
+  content: "\e094";
+}
+
+.glyphicon-share-alt:before {
+  content: "\e095";
+}
+
+.glyphicon-resize-full:before {
+  content: "\e096";
+}
+
+.glyphicon-resize-small:before {
+  content: "\e097";
+}
+
+.glyphicon-exclamation-sign:before {
+  content: "\e101";
+}
+
+.glyphicon-gift:before {
+  content: "\e102";
+}
+
+.glyphicon-leaf:before {
+  content: "\e103";
+}
+
+.glyphicon-fire:before {
+  content: "\e104";
+}
+
+.glyphicon-eye-open:before {
+  content: "\e105";
+}
+
+.glyphicon-eye-close:before {
+  content: "\e106";
+}
+
+.glyphicon-warning-sign:before {
+  content: "\e107";
+}
+
+.glyphicon-plane:before {
+  content: "\e108";
+}
+
+.glyphicon-calendar:before {
+  content: "\e109";
+}
+
+.glyphicon-random:before {
+  content: "\e110";
+}
+
+.glyphicon-comment:before {
+  content: "\e111";
+}
+
+.glyphicon-magnet:before {
+  content: "\e112";
+}
+
+.glyphicon-chevron-up:before {
+  content: "\e113";
+}
+
+.glyphicon-chevron-down:before {
+  content: "\e114";
+}
+
+.glyphicon-retweet:before {
+  content: "\e115";
+}
+
+.glyphicon-shopping-cart:before {
+  content: "\e116";
+}
+
+.glyphicon-folder-close:before {
+  content: "\e117";
+}
+
+.glyphicon-folder-open:before {
+  content: "\e118";
+}
+
+.glyphicon-resize-vertical:before {
+  content: "\e119";
+}
+
+.glyphicon-resize-horizontal:before {
+  content: "\e120";
+}
+
+.glyphicon-hdd:before {
+  content: "\e121";
+}
+
+.glyphicon-bullhorn:before {
+  content: "\e122";
+}
+
+.glyphicon-bell:before {
+  content: "\e123";
+}
+
+.glyphicon-certificate:before {
+  content: "\e124";
+}
+
+.glyphicon-thumbs-up:before {
+  content: "\e125";
+}
+
+.glyphicon-thumbs-down:before {
+  content: "\e126";
+}
+
+.glyphicon-hand-right:before {
+  content: "\e127";
+}
+
+.glyphicon-hand-left:before {
+  content: "\e128";
+}
+
+.glyphicon-hand-up:before {
+  content: "\e129";
+}
+
+.glyphicon-hand-down:before {
+  content: "\e130";
+}
+
+.glyphicon-circle-arrow-right:before {
+  content: "\e131";
+}
+
+.glyphicon-circle-arrow-left:before {
+  content: "\e132";
+}
+
+.glyphicon-circle-arrow-up:before {
+  content: "\e133";
+}
+
+.glyphicon-circle-arrow-down:before {
+  content: "\e134";
+}
+
+.glyphicon-globe:before {
+  content: "\e135";
+}
+
+.glyphicon-wrench:before {
+  content: "\e136";
+}
+
+.glyphicon-tasks:before {
+  content: "\e137";
+}
+
+.glyphicon-filter:before {
+  content: "\e138";
+}
+
+.glyphicon-briefcase:before {
+  content: "\e139";
+}
+
+.glyphicon-fullscreen:before {
+  content: "\e140";
+}
+
+.glyphicon-dashboard:before {
+  content: "\e141";
+}
+
+.glyphicon-paperclip:before {
+  content: "\e142";
+}
+
+.glyphicon-heart-empty:before {
+  content: "\e143";
+}
+
+.glyphicon-link:before {
+  content: "\e144";
+}
+
+.glyphicon-phone:before {
+  content: "\e145";
+}
+
+.glyphicon-pushpin:before {
+  content: "\e146";
+}
+
+.glyphicon-usd:before {
+  content: "\e148";
+}
+
+.glyphicon-gbp:before {
+  content: "\e149";
+}
+
+.glyphicon-sort:before {
+  content: "\e150";
+}
+
+.glyphicon-sort-by-alphabet:before {
+  content: "\e151";
+}
+
+.glyphicon-sort-by-alphabet-alt:before {
+  content: "\e152";
+}
+
+.glyphicon-sort-by-order:before {
+  content: "\e153";
+}
+
+.glyphicon-sort-by-order-alt:before {
+  content: "\e154";
+}
+
+.glyphicon-sort-by-attributes:before {
+  content: "\e155";
+}
+
+.glyphicon-sort-by-attributes-alt:before {
+  content: "\e156";
+}
+
+.glyphicon-unchecked:before {
+  content: "\e157";
+}
+
+.glyphicon-expand:before {
+  content: "\e158";
+}
+
+.glyphicon-collapse-down:before {
+  content: "\e159";
+}
+
+.glyphicon-collapse-up:before {
+  content: "\e160";
+}
+
+.glyphicon-log-in:before {
+  content: "\e161";
+}
+
+.glyphicon-flash:before {
+  content: "\e162";
+}
+
+.glyphicon-log-out:before {
+  content: "\e163";
+}
+
+.glyphicon-new-window:before {
+  content: "\e164";
+}
+
+.glyphicon-record:before {
+  content: "\e165";
+}
+
+.glyphicon-save:before {
+  content: "\e166";
+}
+
+.glyphicon-open:before {
+  content: "\e167";
+}
+
+.glyphicon-saved:before {
+  content: "\e168";
+}
+
+.glyphicon-import:before {
+  content: "\e169";
+}
+
+.glyphicon-export:before {
+  content: "\e170";
+}
+
+.glyphicon-send:before {
+  content: "\e171";
+}
+
+.glyphicon-floppy-disk:before {
+  content: "\e172";
+}
+
+.glyphicon-floppy-saved:before {
+  content: "\e173";
+}
+
+.glyphicon-floppy-remove:before {
+  content: "\e174";
+}
+
+.glyphicon-floppy-save:before {
+  content: "\e175";
+}
+
+.glyphicon-floppy-open:before {
+  content: "\e176";
+}
+
+.glyphicon-credit-card:before {
+  content: "\e177";
+}
+
+.glyphicon-transfer:before {
+  content: "\e178";
+}
+
+.glyphicon-cutlery:before {
+  content: "\e179";
+}
+
+.glyphicon-header:before {
+  content: "\e180";
+}
+
+.glyphicon-compressed:before {
+  content: "\e181";
+}
+
+.glyphicon-earphone:before {
+  content: "\e182";
+}
+
+.glyphicon-phone-alt:before {
+  content: "\e183";
+}
+
+.glyphicon-tower:before {
+  content: "\e184";
+}
+
+.glyphicon-stats:before {
+  content: "\e185";
+}
+
+.glyphicon-sd-video:before {
+  content: "\e186";
+}
+
+.glyphicon-hd-video:before {
+  content: "\e187";
+}
+
+.glyphicon-subtitles:before {
+  content: "\e188";
+}
+
+.glyphicon-sound-stereo:before {
+  content: "\e189";
+}
+
+.glyphicon-sound-dolby:before {
+  content: "\e190";
+}
+
+.glyphicon-sound-5-1:before {
+  content: "\e191";
+}
+
+.glyphicon-sound-6-1:before {
+  content: "\e192";
+}
+
+.glyphicon-sound-7-1:before {
+  content: "\e193";
+}
+
+.glyphicon-copyright-mark:before {
+  content: "\e194";
+}
+
+.glyphicon-registration-mark:before {
+  content: "\e195";
+}
+
+.glyphicon-cloud-download:before {
+  content: "\e197";
+}
+
+.glyphicon-cloud-upload:before {
+  content: "\e198";
+}
+
+.glyphicon-tree-conifer:before {
+  content: "\e199";
+}
+
+.glyphicon-tree-deciduous:before {
+  content: "\e200";
+}
+
+.caret {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  margin-left: 2px;
+  vertical-align: middle;
+  border-top: 4px solid;
+  border-right: 4px solid transparent;
+  border-left: 4px solid transparent;
+}
+
+.dropdown {
+  position: relative;
+}
+
+.dropdown-toggle:focus {
+  outline: 0;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 1000;
+  display: none;
+  float: left;
+  min-width: 160px;
+  padding: 5px 0;
+  margin: 2px 0 0;
+  font-size: 14px;
+  list-style: none;
+  background-color: #ffffff;
+  border: 1px solid #cccccc;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  border-radius: 4px;
+  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+          box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+  background-clip: padding-box;
+}
+
+.dropdown-menu.pull-right {
+  right: 0;
+  left: auto;
+}
+
+.dropdown-menu .divider {
+  height: 1px;
+  margin: 9px 0;
+  overflow: hidden;
+  background-color: #e5e5e5;
+}
+
+.dropdown-menu > li > a {
+  display: block;
+  padding: 3px 20px;
+  clear: both;
+  font-weight: normal;
+  line-height: 1.428571429;
+  color: #333333;
+  white-space: nowrap;
+}
+
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  color: #262626;
+  text-decoration: none;
+  background-color: #f5f5f5;
+}
+
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  color: #ffffff;
+  text-decoration: none;
+  background-color: #428bca;
+  outline: 0;
+}
+
+.dropdown-menu > .disabled > a,
+.dropdown-menu > .disabled > a:hover,
+.dropdown-menu > .disabled > a:focus {
+  color: #999999;
+}
+
+.dropdown-menu > .disabled > a:hover,
+.dropdown-menu > .disabled > a:focus {
+  text-decoration: none;
+  cursor: not-allowed;
+  background-color: transparent;
+  background-image: none;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.open > .dropdown-menu {
+  display: block;
+}
+
+.open > a {
+  outline: 0;
+}
+
+.dropdown-header {
+  display: block;
+  padding: 3px 20px;
+  font-size: 12px;
+  line-height: 1.428571429;
+  color: #999999;
+}
+
+.dropdown-backdrop {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 990;
+}
+
+.pull-right > .dropdown-menu {
+  right: 0;
+  left: auto;
+}
+
+.dropup .caret,
+.navbar-fixed-bottom .dropdown .caret {
+  border-top: 0;
+  border-bottom: 4px solid;
+  content: "";
+}
+
+.dropup .dropdown-menu,
+.navbar-fixed-bottom .dropdown .dropdown-menu {
+  top: auto;
+  bottom: 100%;
+  margin-bottom: 1px;
+}
+
+@media (min-width: 768px) {
+  .navbar-right .dropdown-menu {
+    right: 0;
+    left: auto;
+  }
+}
+
+.btn-group,
+.btn-group-vertical {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.btn-group > .btn,
+.btn-group-vertical > .btn {
+  position: relative;
+  float: left;
+}
+
+.btn-group > .btn:hover,
+.btn-group-vertical > .btn:hover,
+.btn-group > .btn:focus,
+.btn-group-vertical > .btn:focus,
+.btn-group > .btn:active,
+.btn-group-vertical > .btn:active,
+.btn-group > .btn.active,
+.btn-group-vertical > .btn.active {
+  z-index: 2;
+}
+
+.btn-group > .btn:focus,
+.btn-group-vertical > .btn:focus {
+  outline: none;
+}
+
+.btn-group .btn + .btn,
+.btn-group .btn + .btn-group,
+.btn-group .btn-group + .btn,
+.btn-group .btn-group + .btn-group {
+  margin-left: -1px;
+}
+
+.btn-toolbar:before,
+.btn-toolbar:after {
+  display: table;
+  content: " ";
+}
+
+.btn-toolbar:after {
+  clear: both;
+}
+
+.btn-toolbar:before,
+.btn-toolbar:after {
+  display: table;
+  content: " ";
+}
+
+.btn-toolbar:after {
+  clear: both;
+}
+
+.btn-toolbar .btn-group {
+  float: left;
+}
+
+.btn-toolbar > .btn + .btn,
+.btn-toolbar > .btn-group + .btn,
+.btn-toolbar > .btn + .btn-group,
+.btn-toolbar > .btn-group + .btn-group {
+  margin-left: 5px;
+}
+
+.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
+  border-radius: 0;
+}
+
+.btn-group > .btn:first-child {
+  margin-left: 0;
+}
+
+.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.btn-group > .btn:last-child:not(:first-child),
+.btn-group > .dropdown-toggle:not(:first-child) {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group > .btn-group {
+  float: left;
+}
+
+.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+
+.btn-group > .btn-group:first-child > .btn:last-child,
+.btn-group > .btn-group:first-child > .dropdown-toggle {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.btn-group > .btn-group:last-child > .btn:first-child {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group .dropdown-toggle:active,
+.btn-group.open .dropdown-toggle {
+  outline: 0;
+}
+
+.btn-group-xs > .btn {
+  padding: 1px 5px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-group-sm > .btn {
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-group-lg > .btn {
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+
+.btn-group > .btn + .dropdown-toggle {
+  padding-right: 8px;
+  padding-left: 8px;
+}
+
+.btn-group > .btn-lg + .dropdown-toggle {
+  padding-right: 12px;
+  padding-left: 12px;
+}
+
+.btn-group.open .dropdown-toggle {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+
+.btn-group.open .dropdown-toggle.btn-link {
+  -webkit-box-shadow: none;
+          box-shadow: none;
+}
+
+.btn .caret {
+  margin-left: 0;
+}
+
+.btn-lg .caret {
+  border-width: 5px 5px 0;
+  border-bottom-width: 0;
+}
+
+.dropup .btn-lg .caret {
+  border-width: 0 5px 5px;
+}
+
+.btn-group-vertical > .btn,
+.btn-group-vertical > .btn-group,
+.btn-group-vertical > .btn-group > .btn {
+  display: block;
+  float: none;
+  width: 100%;
+  max-width: 100%;
+}
+
+.btn-group-vertical > .btn-group:before,
+.btn-group-vertical > .btn-group:after {
+  display: table;
+  content: " ";
+}
+
+.btn-group-vertical > .btn-group:after {
+  clear: both;
+}
+
+.btn-group-vertical > .btn-group:before,
+.btn-group-vertical > .btn-group:after {
+  display: table;
+  content: " ";
+}
+
+.btn-group-vertical > .btn-group:after {
+  clear: both;
+}
+
+.btn-group-vertical > .btn-group > .btn {
+  float: none;
+}
+
+.btn-group-vertical > .btn + .btn,
+.btn-group-vertical > .btn + .btn-group,
+.btn-group-vertical > .btn-group + .btn,
+.btn-group-vertical > .btn-group + .btn-group {
+  margin-top: -1px;
+  margin-left: 0;
+}
+
+.btn-group-vertical > .btn:not(:first-child):not(:last-child) {
+  border-radius: 0;
+}
+
+.btn-group-vertical > .btn:first-child:not(:last-child) {
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.btn-group-vertical > .btn:last-child:not(:first-child) {
+  border-top-right-radius: 0;
+  border-bottom-left-radius: 4px;
+  border-top-left-radius: 0;
+}
+
+.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+
+.btn-group-vertical > .btn-group:first-child > .btn:last-child,
+.btn-group-vertical > .btn-group:first-child > .dropdown-toggle {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.btn-group-vertical > .btn-group:last-child > .btn:first-child {
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group-justified {
+  display: table;
+  width: 100%;
+  border-collapse: separate;
+  table-layout: fixed;
+}
+
+.btn-group-justified > .btn,
+.btn-group-justified > .btn-group {
+  display: table-cell;
+  float: none;
+  width: 1%;
+}
+
+.btn-group-justified > .btn-group .btn {
+  width: 100%;
+}
+
+[data-toggle="buttons"] > .btn > input[type="radio"],
+[data-toggle="buttons"] > .btn > input[type="checkbox"] {
+  display: none;
+}
+
+.input-group {
+  position: relative;
+  display: table;
+  border-collapse: separate;
+}
+
+.input-group[class*="col-"] {
+  float: none;
+  padding-right: 0;
+  padding-left: 0;
+}
+
+.input-group .form-control {
+  width: 100%;
+  margin-bottom: 0;
+}
+
+.input-group-lg > .form-control,
+.input-group-lg > .input-group-addon,
+.input-group-lg > .input-group-btn > .btn {
+  height: 46px;
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+
+select.input-group-lg > .form-control,
+select.input-group-lg > .input-group-addon,
+select.input-group-lg > .input-group-btn > .btn {
+  height: 46px;
+  line-height: 46px;
+}
+
+textarea.input-group-lg > .form-control,
+textarea.input-group-lg > .input-group-addon,
+textarea.input-group-lg > .input-group-btn > .btn {
+  height: auto;
+}
+
+.input-group-sm > .form-control,
+.input-group-sm > .input-group-addon,
+.input-group-sm > .input-group-btn > .btn {
+  height: 30px;
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+select.input-group-sm > .form-control,
+select.input-group-sm > .input-group-addon,
+select.input-group-sm > .input-group-btn > .btn {
+  height: 30px;
+  line-height: 30px;
+}
+
+textarea.input-group-sm > .form-control,
+textarea.input-group-sm > .input-group-addon,
+textarea.input-group-sm > .input-group-btn > .btn {
+  height: auto;
+}
+
+.input-group-addon,
+.input-group-btn,
+.input-group .form-control {
+  display: table-cell;
+}
+
+.input-group-addon:not(:first-child):not(:last-child),
+.input-group-btn:not(:first-child):not(:last-child),
+.input-group .form-control:not(:first-child):not(:last-child) {
+  border-radius: 0;
+}
+
+.input-group-addon,
+.input-group-btn {
+  width: 1%;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+
+.input-group-addon {
+  padding: 6px 12px;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 1;
+  color: #555555;
+  text-align: center;
+  background-color: #eeeeee;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+}
+
+.input-group-addon.input-sm {
+  padding: 5px 10px;
+  font-size: 12px;
+  border-radius: 3px;
+}
+
+.input-group-addon.input-lg {
+  padding: 10px 16px;
+  font-size: 18px;
+  border-radius: 6px;
+}
+
+.input-group-addon input[type="radio"],
+.input-group-addon input[type="checkbox"] {
+  margin-top: 0;
+}
+
+.input-group .form-control:first-child,
+.input-group-addon:first-child,
+.input-group-btn:first-child > .btn,
+.input-group-btn:first-child > .dropdown-toggle,
+.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.input-group-addon:first-child {
+  border-right: 0;
+}
+
+.input-group .form-control:last-child,
+.input-group-addon:last-child,
+.input-group-btn:last-child > .btn,
+.input-group-btn:last-child > .dropdown-toggle,
+.input-group-btn:first-child > .btn:not(:first-child) {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.input-group-addon:last-child {
+  border-left: 0;
+}
+
+.input-group-btn {
+  position: relative;
+  white-space: nowrap;
+}
+
+.input-group-btn:first-child > .btn {
+  margin-right: -1px;
+}
+
+.input-group-btn:last-child > .btn {
+  margin-left: -1px;
+}
+
+.input-group-btn > .btn {
+  position: relative;
+}
+
+.input-group-btn > .btn + .btn {
+  margin-left: -4px;
+}
+
+.input-group-btn > .btn:hover,
+.input-group-btn > .btn:active {
+  z-index: 2;
+}
+
+.nav {
+  padding-left: 0;
+  margin-bottom: 0;
+  list-style: none;
+}
+
+.nav:before,
+.nav:after {
+  display: table;
+  content: " ";
+}
+
+.nav:after {
+  clear: both;
+}
+
+.nav:before,
+.nav:after {
+  display: table;
+  content: " ";
+}
+
+.nav:after {
+  clear: both;
+}
+
+.nav > li {
+  position: relative;
+  display: block;
+}
+
+.nav > li > a {
+  position: relative;
+  display: block;
+  padding: 10px 15px;
+}
+
+.nav > li > a:hover,
+.nav > li > a:focus {
+  text-decoration: none;
+  background-color: #eeeeee;
+}
+
+.nav > li.disabled > a {
+  color: #999999;
+}
+
+.nav > li.disabled > a:hover,
+.nav > li.disabled > a:focus {
+  color: #999999;
+  text-decoration: none;
+  cursor: not-allowed;
+  background-color: transparent;
+}
+
+.nav .open > a,
+.nav .open > a:hover,
+.nav .open > a:focus {
+  background-color: #eeeeee;
+  border-color: #428bca;
+}
+
+.nav .nav-divider {
+  height: 1px;
+  margin: 9px 0;
+  overflow: hidden;
+  background-color: #e5e5e5;
+}
+
+.nav > li > a > img {
+  max-width: none;
+}
+
+.nav-tabs {
+  border-bottom: 1px solid #dddddd;
+}
+
+.nav-tabs > li {
+  float: left;
+  margin-bottom: -1px;
+}
+
+.nav-tabs > li > a {
+  margin-right: 2px;
+  line-height: 1.428571429;
+  border: 1px solid transparent;
+  border-radius: 4px 4px 0 0;
+}
+
+.nav-tabs > li > a:hover {
+  border-color: #eeeeee #eeeeee #dddddd;
+}
+
+.nav-tabs > li.active > a,
+.nav-tabs > li.active > a:hover,
+.nav-tabs > li.active > a:focus {
+  color: #555555;
+  cursor: default;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+  border-bottom-color: transparent;
+}
+
+.nav-tabs.nav-justified {
+  width: 100%;
+  border-bottom: 0;
+}
+
+.nav-tabs.nav-justified > li {
+  float: none;
+}
+
+.nav-tabs.nav-justified > li > a {
+  margin-bottom: 5px;
+  text-align: center;
+}
+
+.nav-tabs.nav-justified > .dropdown .dropdown-menu {
+  top: auto;
+  left: auto;
+}
+
+@media (min-width: 768px) {
+  .nav-tabs.nav-justified > li {
+    display: table-cell;
+    width: 1%;
+  }
+  .nav-tabs.nav-justified > li > a {
+    margin-bottom: 0;
+  }
+}
+
+.nav-tabs.nav-justified > li > a {
+  margin-right: 0;
+  border-radius: 4px;
+}
+
+.nav-tabs.nav-justified > .active > a,
+.nav-tabs.nav-justified > .active > a:hover,
+.nav-tabs.nav-justified > .active > a:focus {
+  border: 1px solid #dddddd;
+}
+
+@media (min-width: 768px) {
+  .nav-tabs.nav-justified > li > a {
+    border-bottom: 1px solid #dddddd;
+    border-radius: 4px 4px 0 0;
+  }
+  .nav-tabs.nav-justified > .active > a,
+  .nav-tabs.nav-justified > .active > a:hover,
+  .nav-tabs.nav-justified > .active > a:focus {
+    border-bottom-color: #ffffff;
+  }
+}
+
+.nav-pills > li {
+  float: left;
+}
+
+.nav-pills > li > a {
+  border-radius: 4px;
+}
+
+.nav-pills > li + li {
+  margin-left: 2px;
+}
+
+.nav-pills > li.active > a,
+.nav-pills > li.active > a:hover,
+.nav-pills > li.active > a:focus {
+  color: #ffffff;
+  background-color: #428bca;
+}
+
+.nav-stacked > li {
+  float: none;
+}
+
+.nav-stacked > li + li {
+  margin-top: 2px;
+  margin-left: 0;
+}
+
+.nav-justified {
+  width: 100%;
+}
+
+.nav-justified > li {
+  float: none;
+}
+
+.nav-justified > li > a {
+  margin-bottom: 5px;
+  text-align: center;
+}
+
+.nav-justified > .dropdown .dropdown-menu {
+  top: auto;
+  left: auto;
+}
+
+@media (min-width: 768px) {
+  .nav-justified > li {
+    display: table-cell;
+    width: 1%;
+  }
+  .nav-justified > li > a {
+    margin-bottom: 0;
+  }
+}
+
+.nav-tabs-justified {
+  border-bottom: 0;
+}
+
+.nav-tabs-justified > li > a {
+  margin-right: 0;
+  border-radius: 4px;
+}
+
+.nav-tabs-justified > .active > a,
+.nav-tabs-justified > .active > a:hover,
+.nav-tabs-justified > .active > a:focus {
+  border: 1px solid #dddddd;
+}
+
+@media (min-width: 768px) {
+  .nav-tabs-justified > li > a {
+    border-bottom: 1px solid #dddddd;
+    border-radius: 4px 4px 0 0;
+  }
+  .nav-tabs-justified > .active > a,
+  .nav-tabs-justified > .active > a:hover,
+  .nav-tabs-justified > .active > a:focus {
+    border-bottom-color: #ffffff;
+  }
+}
+
+.tab-content > .tab-pane {
+  display: none;
+}
+
+.tab-content > .active {
+  display: block;
+}
+
+.nav-tabs .dropdown-menu {
+  margin-top: -1px;
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.navbar {
+  position: relative;
+  min-height: 50px;
+  margin-bottom: 20px;
+  border: 1px solid transparent;
+}
+
+.navbar:before,
+.navbar:after {
+  display: table;
+  content: " ";
+}
+
+.navbar:after {
+  clear: both;
+}
+
+.navbar:before,
+.navbar:after {
+  display: table;
+  content: " ";
+}
+
+.navbar:after {
+  clear: both;
+}
+
+@media (min-width: 768px) {
+  .navbar {
+    border-radius: 4px;
+  }
+}
+
+.navbar-header:before,
+.navbar-header:after {
+  display: table;
+  content: " ";
+}
+
+.navbar-header:after {
+  clear: both;
+}
+
+.navbar-header:before,
+.navbar-header:after {
+  display: table;
+  content: " ";
+}
+
+.navbar-header:after {
+  clear: both;
+}
+
+@media (min-width: 768px) {
+  .navbar-header {
+    float: left;
+  }
+}
+
+.navbar-collapse {
+  max-height: 340px;
+  padding-right: 15px;
+  padding-left: 15px;
+  overflow-x: visible;
+  border-top: 1px solid transparent;
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
+  -webkit-overflow-scrolling: touch;
+}
+
+.navbar-collapse:before,
+.navbar-collapse:after {
+  display: table;
+  content: " ";
+}
+
+.navbar-collapse:after {
+  clear: both;
+}
+
+.navbar-collapse:before,
+.navbar-collapse:after {
+  display: table;
+  content: " ";
+}
+
+.navbar-collapse:after {
+  clear: both;
+}
+
+.navbar-collapse.in {
+  overflow-y: auto;
+}
+
+@media (min-width: 768px) {
+  .navbar-collapse {
+    width: auto;
+    border-top: 0;
+    box-shadow: none;
+  }
+  .navbar-collapse.collapse {
+    display: block !important;
+    height: auto !important;
+    padding-bottom: 0;
+    overflow: visible !important;
+  }
+  .navbar-collapse.in {
+    overflow-y: visible;
+  }
+  .navbar-fixed-top .navbar-collapse,
+  .navbar-static-top .navbar-collapse,
+  .navbar-fixed-bottom .navbar-collapse {
+    padding-right: 0;
+    padding-left: 0;
+  }
+}
+
+.container > .navbar-header,
+.container > .navbar-collapse {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+
+@media (min-width: 768px) {
+  .container > .navbar-header,
+  .container > .navbar-collapse {
+    margin-right: 0;
+    margin-left: 0;
+  }
+}
+
+.navbar-static-top {
+  z-index: 1000;
+  border-width: 0 0 1px;
+}
+
+@media (min-width: 768px) {
+  .navbar-static-top {
+    border-radius: 0;
+  }
+}
+
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  position: fixed;
+  right: 0;
+  left: 0;
+  z-index: 1030;
+}
+
+@media (min-width: 768px) {
+  .navbar-fixed-top,
+  .navbar-fixed-bottom {
+    border-radius: 0;
+  }
+}
+
+.navbar-fixed-top {
+  top: 0;
+  border-width: 0 0 1px;
+}
+
+.navbar-fixed-bottom {
+  bottom: 0;
+  margin-bottom: 0;
+  border-width: 1px 0 0;
+}
+
+.navbar-brand {
+  float: left;
+  padding: 15px 15px;
+  font-size: 18px;
+  line-height: 20px;
+}
+
+.navbar-brand:hover,
+.navbar-brand:focus {
+  text-decoration: none;
+}
+
+@media (min-width: 768px) {
+  .navbar > .container .navbar-brand {
+    margin-left: -15px;
+  }
+}
+
+.navbar-toggle {
+  position: relative;
+  float: right;
+  padding: 9px 10px;
+  margin-top: 8px;
+  margin-right: 15px;
+  margin-bottom: 8px;
+  background-color: transparent;
+  background-image: none;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}
+
+.navbar-toggle .icon-bar {
+  display: block;
+  width: 22px;
+  height: 2px;
+  border-radius: 1px;
+}
+
+.navbar-toggle .icon-bar + .icon-bar {
+  margin-top: 4px;
+}
+
+@media (min-width: 768px) {
+  .navbar-toggle {
+    display: none;
+  }
+}
+
+.navbar-nav {
+  margin: 7.5px -15px;
+}
+
+.navbar-nav > li > a {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  line-height: 20px;
+}
+
+@media (max-width: 767px) {
+  .navbar-nav .open .dropdown-menu {
+    position: static;
+    float: none;
+    width: auto;
+    margin-top: 0;
+    background-color: transparent;
+    border: 0;
+    box-shadow: none;
+  }
+  .navbar-nav .open .dropdown-menu > li > a,
+  .navbar-nav .open .dropdown-menu .dropdown-header {
+    padding: 5px 15px 5px 25px;
+  }
+  .navbar-nav .open .dropdown-menu > li > a {
+    line-height: 20px;
+  }
+  .navbar-nav .open .dropdown-menu > li > a:hover,
+  .navbar-nav .open .dropdown-menu > li > a:focus {
+    background-image: none;
+  }
+}
+
+@media (min-width: 768px) {
+  .navbar-nav {
+    float: left;
+    margin: 0;
+  }
+  .navbar-nav > li {
+    float: left;
+  }
+  .navbar-nav > li > a {
+    padding-top: 15px;
+    padding-bottom: 15px;
+  }
+  .navbar-nav.navbar-right:last-child {
+    margin-right: -15px;
+  }
+}
+
+@media (min-width: 768px) {
+  .navbar-left {
+    float: left !important;
+  }
+  .navbar-right {
+    float: right !important;
+  }
+}
+
+.navbar-form {
+  padding: 10px 15px;
+  margin-top: 8px;
+  margin-right: -15px;
+  margin-bottom: 8px;
+  margin-left: -15px;
+  border-top: 1px solid transparent;
+  border-bottom: 1px solid transparent;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+}
+
+@media (min-width: 768px) {
+  .navbar-form .form-group {
+    display: inline-block;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .navbar-form .form-control {
+    display: inline-block;
+  }
+  .navbar-form select.form-control {
+    width: auto;
+  }
+  .navbar-form .radio,
+  .navbar-form .checkbox {
+    display: inline-block;
+    padding-left: 0;
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+  .navbar-form .radio input[type="radio"],
+  .navbar-form .checkbox input[type="checkbox"] {
+    float: none;
+    margin-left: 0;
+  }
+}
+
+@media (max-width: 767px) {
+  .navbar-form .form-group {
+    margin-bottom: 5px;
+  }
+}
+
+@media (min-width: 768px) {
+  .navbar-form {
+    width: auto;
+    padding-top: 0;
+    padding-bottom: 0;
+    margin-right: 0;
+    margin-left: 0;
+    border: 0;
+    -webkit-box-shadow: none;
+            box-shadow: none;
+  }
+  .navbar-form.navbar-right:last-child {
+    margin-right: -15px;
+  }
+}
+
+.navbar-nav > li > .dropdown-menu {
+  margin-top: 0;
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.navbar-nav.pull-right > li > .dropdown-menu,
+.navbar-nav > li > .dropdown-menu.pull-right {
+  right: 0;
+  left: auto;
+}
+
+.navbar-btn {
+  margin-top: 8px;
+  margin-bottom: 8px;
+}
+
+.navbar-btn.btn-sm {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+.navbar-btn.btn-xs {
+  margin-top: 14px;
+  margin-bottom: 14px;
+}
+
+.navbar-text {
+  margin-top: 15px;
+  margin-bottom: 15px;
+}
+
+@media (min-width: 768px) {
+  .navbar-text {
+    float: left;
+    margin-right: 15px;
+    margin-left: 15px;
+  }
+  .navbar-text.navbar-right:last-child {
+    margin-right: 0;
+  }
+}
+
+.navbar-default {
+  background-color: #f8f8f8;
+  border-color: #e7e7e7;
+}
+
+.navbar-default .navbar-brand {
+  color: #777777;
+}
+
+.navbar-default .navbar-brand:hover,
+.navbar-default .navbar-brand:focus {
+  color: #5e5e5e;
+  background-color: transparent;
+}
+
+.navbar-default .navbar-text {
+  color: #777777;
+}
+
+.navbar-default .navbar-nav > li > a {
+  color: #777777;
+}
+
+.navbar-default .navbar-nav > li > a:hover,
+.navbar-default .navbar-nav > li > a:focus {
+  color: #333333;
+  background-color: transparent;
+}
+
+.navbar-default .navbar-nav > .active > a,
+.navbar-default .navbar-nav > .active > a:hover,
+.navbar-default .navbar-nav > .active > a:focus {
+  color: #555555;
+  background-color: #e7e7e7;
+}
+
+.navbar-default .navbar-nav > .disabled > a,
+.navbar-default .navbar-nav > .disabled > a:hover,
+.navbar-default .navbar-nav > .disabled > a:focus {
+  color: #cccccc;
+  background-color: transparent;
+}
+
+.navbar-default .navbar-toggle {
+  border-color: #dddddd;
+}
+
+.navbar-default .navbar-toggle:hover,
+.navbar-default .navbar-toggle:focus {
+  background-color: #dddddd;
+}
+
+.navbar-default .navbar-toggle .icon-bar {
+  background-color: #cccccc;
+}
+
+.navbar-default .navbar-collapse,
+.navbar-default .navbar-form {
+  border-color: #e7e7e7;
+}
+
+.navbar-default .navbar-nav > .open > a,
+.navbar-default .navbar-nav > .open > a:hover,
+.navbar-default .navbar-nav > .open > a:focus {
+  color: #555555;
+  background-color: #e7e7e7;
+}
+
+@media (max-width: 767px) {
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a {
+    color: #777777;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
+    color: #333333;
+    background-color: transparent;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a,
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #555555;
+    background-color: #e7e7e7;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+    color: #cccccc;
+    background-color: transparent;
+  }
+}
+
+.navbar-default .navbar-link {
+  color: #777777;
+}
+
+.navbar-default .navbar-link:hover {
+  color: #333333;
+}
+
+.navbar-inverse {
+  background-color: #222222;
+  border-color: #080808;
+}
+
+.navbar-inverse .navbar-brand {
+  color: #999999;
+}
+
+.navbar-inverse .navbar-brand:hover,
+.navbar-inverse .navbar-brand:focus {
+  color: #ffffff;
+  background-color: transparent;
+}
+
+.navbar-inverse .navbar-text {
+  color: #999999;
+}
+
+.navbar-inverse .navbar-nav > li > a {
+  color: #999999;
+}
+
+.navbar-inverse .navbar-nav > li > a:hover,
+.navbar-inverse .navbar-nav > li > a:focus {
+  color: #ffffff;
+  background-color: transparent;
+}
+
+.navbar-inverse .navbar-nav > .active > a,
+.navbar-inverse .navbar-nav > .active > a:hover,
+.navbar-inverse .navbar-nav > .active > a:focus {
+  color: #ffffff;
+  background-color: #080808;
+}
+
+.navbar-inverse .navbar-nav > .disabled > a,
+.navbar-inverse .navbar-nav > .disabled > a:hover,
+.navbar-inverse .navbar-nav > .disabled > a:focus {
+  color: #444444;
+  background-color: transparent;
+}
+
+.navbar-inverse .navbar-toggle {
+  border-color: #333333;
+}
+
+.navbar-inverse .navbar-toggle:hover,
+.navbar-inverse .navbar-toggle:focus {
+  background-color: #333333;
+}
+
+.navbar-inverse .navbar-toggle .icon-bar {
+  background-color: #ffffff;
+}
+
+.navbar-inverse .navbar-collapse,
+.navbar-inverse .navbar-form {
+  border-color: #101010;
+}
+
+.navbar-inverse .navbar-nav > .open > a,
+.navbar-inverse .navbar-nav > .open > a:hover,
+.navbar-inverse .navbar-nav > .open > a:focus {
+  color: #ffffff;
+  background-color: #080808;
+}
+
+@media (max-width: 767px) {
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {
+    border-color: #080808;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu .divider {
+    background-color: #080808;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
+    color: #999999;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {
+    color: #ffffff;
+    background-color: transparent;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #ffffff;
+    background-color: #080808;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+    color: #444444;
+    background-color: transparent;
+  }
+}
+
+.navbar-inverse .navbar-link {
+  color: #999999;
+}
+
+.navbar-inverse .navbar-link:hover {
+  color: #ffffff;
+}
+
+.breadcrumb {
+  padding: 8px 15px;
+  margin-bottom: 20px;
+  list-style: none;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+
+.breadcrumb > li {
+  display: inline-block;
+}
+
+.breadcrumb > li + li:before {
+  padding: 0 5px;
+  color: #cccccc;
+  content: "/\00a0";
+}
+
+.breadcrumb > .active {
+  color: #999999;
+}
+
+.pagination {
+  display: inline-block;
+  padding-left: 0;
+  margin: 20px 0;
+  border-radius: 4px;
+}
+
+.pagination > li {
+  display: inline;
+}
+
+.pagination > li > a,
+.pagination > li > span {
+  position: relative;
+  float: left;
+  padding: 6px 12px;
+  margin-left: -1px;
+  line-height: 1.428571429;
+  text-decoration: none;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+}
+
+.pagination > li:first-child > a,
+.pagination > li:first-child > span {
+  margin-left: 0;
+  border-bottom-left-radius: 4px;
+  border-top-left-radius: 4px;
+}
+
+.pagination > li:last-child > a,
+.pagination > li:last-child > span {
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 4px;
+}
+
+.pagination > li > a:hover,
+.pagination > li > span:hover,
+.pagination > li > a:focus,
+.pagination > li > span:focus {
+  background-color: #eeeeee;
+}
+
+.pagination > .active > a,
+.pagination > .active > span,
+.pagination > .active > a:hover,
+.pagination > .active > span:hover,
+.pagination > .active > a:focus,
+.pagination > .active > span:focus {
+  z-index: 2;
+  color: #ffffff;
+  cursor: default;
+  background-color: #428bca;
+  border-color: #428bca;
+}
+
+.pagination > .disabled > span,
+.pagination > .disabled > span:hover,
+.pagination > .disabled > span:focus,
+.pagination > .disabled > a,
+.pagination > .disabled > a:hover,
+.pagination > .disabled > a:focus {
+  color: #999999;
+  cursor: not-allowed;
+  background-color: #ffffff;
+  border-color: #dddddd;
+}
+
+.pagination-lg > li > a,
+.pagination-lg > li > span {
+  padding: 10px 16px;
+  font-size: 18px;
+}
+
+.pagination-lg > li:first-child > a,
+.pagination-lg > li:first-child > span {
+  border-bottom-left-radius: 6px;
+  border-top-left-radius: 6px;
+}
+
+.pagination-lg > li:last-child > a,
+.pagination-lg > li:last-child > span {
+  border-top-right-radius: 6px;
+  border-bottom-right-radius: 6px;
+}
+
+.pagination-sm > li > a,
+.pagination-sm > li > span {
+  padding: 5px 10px;
+  font-size: 12px;
+}
+
+.pagination-sm > li:first-child > a,
+.pagination-sm > li:first-child > span {
+  border-bottom-left-radius: 3px;
+  border-top-left-radius: 3px;
+}
+
+.pagination-sm > li:last-child > a,
+.pagination-sm > li:last-child > span {
+  border-top-right-radius: 3px;
+  border-bottom-right-radius: 3px;
+}
+
+.pager {
+  padding-left: 0;
+  margin: 20px 0;
+  text-align: center;
+  list-style: none;
+}
+
+.pager:before,
+.pager:after {
+  display: table;
+  content: " ";
+}
+
+.pager:after {
+  clear: both;
+}
+
+.pager:before,
+.pager:after {
+  display: table;
+  content: " ";
+}
+
+.pager:after {
+  clear: both;
+}
+
+.pager li {
+  display: inline;
+}
+
+.pager li > a,
+.pager li > span {
+  display: inline-block;
+  padding: 5px 14px;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+  border-radius: 15px;
+}
+
+.pager li > a:hover,
+.pager li > a:focus {
+  text-decoration: none;
+  background-color: #eeeeee;
+}
+
+.pager .next > a,
+.pager .next > span {
+  float: right;
+}
+
+.pager .previous > a,
+.pager .previous > span {
+  float: left;
+}
+
+.pager .disabled > a,
+.pager .disabled > a:hover,
+.pager .disabled > a:focus,
+.pager .disabled > span {
+  color: #999999;
+  cursor: not-allowed;
+  background-color: #ffffff;
+}
+
+.label {
+  display: inline;
+  padding: .2em .6em .3em;
+  font-size: 75%;
+  font-weight: bold;
+  line-height: 1;
+  color: #ffffff;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  border-radius: .25em;
+}
+
+.label[href]:hover,
+.label[href]:focus {
+  color: #ffffff;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+.label:empty {
+  display: none;
+}
+
+.btn .label {
+  position: relative;
+  top: -1px;
+}
+
+.label-default {
+  background-color: #999999;
+}
+
+.label-default[href]:hover,
+.label-default[href]:focus {
+  background-color: #808080;
+}
+
+.label-primary {
+  background-color: #428bca;
+}
+
+.label-primary[href]:hover,
+.label-primary[href]:focus {
+  background-color: #3071a9;
+}
+
+.label-success {
+  background-color: #5cb85c;
+}
+
+.label-success[href]:hover,
+.label-success[href]:focus {
+  background-color: #449d44;
+}
+
+.label-info {
+  background-color: #5bc0de;
+}
+
+.label-info[href]:hover,
+.label-info[href]:focus {
+  background-color: #31b0d5;
+}
+
+.label-warning {
+  background-color: #f0ad4e;
+}
+
+.label-warning[href]:hover,
+.label-warning[href]:focus {
+  background-color: #ec971f;
+}
+
+.label-danger {
+  background-color: #d9534f;
+}
+
+.label-danger[href]:hover,
+.label-danger[href]:focus {
+  background-color: #c9302c;
+}
+
+.badge {
+  display: inline-block;
+  min-width: 10px;
+  padding: 3px 7px;
+  font-size: 12px;
+  font-weight: bold;
+  line-height: 1;
+  color: #ffffff;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  background-color: #999999;
+  border-radius: 10px;
+}
+
+.badge:empty {
+  display: none;
+}
+
+.btn .badge {
+  position: relative;
+  top: -1px;
+}
+
+a.badge:hover,
+a.badge:focus {
+  color: #ffffff;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+a.list-group-item.active > .badge,
+.nav-pills > .active > a > .badge {
+  color: #428bca;
+  background-color: #ffffff;
+}
+
+.nav-pills > li > a > .badge {
+  margin-left: 3px;
+}
+
+.jumbotron {
+  padding: 30px;
+  margin-bottom: 30px;
+  font-size: 21px;
+  font-weight: 200;
+  line-height: 2.1428571435;
+  color: inherit;
+  background-color: #eeeeee;
+}
+
+.jumbotron h1,
+.jumbotron .h1 {
+  line-height: 1;
+  color: inherit;
+}
+
+.jumbotron p {
+  line-height: 1.4;
+}
+
+.container .jumbotron {
+  border-radius: 6px;
+}
+
+.jumbotron .container {
+  max-width: 100%;
+}
+
+@media screen and (min-width: 768px) {
+  .jumbotron {
+    padding-top: 48px;
+    padding-bottom: 48px;
+  }
+  .container .jumbotron {
+    padding-right: 60px;
+    padding-left: 60px;
+  }
+  .jumbotron h1,
+  .jumbotron .h1 {
+    font-size: 63px;
+  }
+}
+
+.thumbnail {
+  display: block;
+  padding: 4px;
+  margin-bottom: 20px;
+  line-height: 1.428571429;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+  border-radius: 4px;
+  -webkit-transition: all 0.2s ease-in-out;
+          transition: all 0.2s ease-in-out;
+}
+
+.thumbnail > img,
+.thumbnail a > img {
+  display: block;
+  height: auto;
+  max-width: 100%;
+  margin-right: auto;
+  margin-left: auto;
+}
+
+a.thumbnail:hover,
+a.thumbnail:focus,
+a.thumbnail.active {
+  border-color: #428bca;
+}
+
+.thumbnail .caption {
+  padding: 9px;
+  color: #333333;
+}
+
+.alert {
+  padding: 15px;
+  margin-bottom: 20px;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}
+
+.alert h4 {
+  margin-top: 0;
+  color: inherit;
+}
+
+.alert .alert-link {
+  font-weight: bold;
+}
+
+.alert > p,
+.alert > ul {
+  margin-bottom: 0;
+}
+
+.alert > p + p {
+  margin-top: 5px;
+}
+
+.alert-dismissable {
+  padding-right: 35px;
+}
+
+.alert-dismissable .close {
+  position: relative;
+  top: -2px;
+  right: -21px;
+  color: inherit;
+}
+
+.alert-success {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #d6e9c6;
+}
+
+.alert-success hr {
+  border-top-color: #c9e2b3;
+}
+
+.alert-success .alert-link {
+  color: #2b542c;
+}
+
+.alert-info {
+  color: #31708f;
+  background-color: #d9edf7;
+  border-color: #bce8f1;
+}
+
+.alert-info hr {
+  border-top-color: #a6e1ec;
+}
+
+.alert-info .alert-link {
+  color: #245269;
+}
+
+.alert-warning {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #faebcc;
+}
+
+.alert-warning hr {
+  border-top-color: #f7e1b5;
+}
+
+.alert-warning .alert-link {
+  color: #66512c;
+}
+
+.alert-danger {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #ebccd1;
+}
+
+.alert-danger hr {
+  border-top-color: #e4b9c0;
+}
+
+.alert-danger .alert-link {
+  color: #843534;
+}
+
+@-webkit-keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+
+@keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+
+.progress {
+  height: 20px;
+  margin-bottom: 20px;
+  overflow: hidden;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+          box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.progress-bar {
+  float: left;
+  width: 0;
+  height: 100%;
+  font-size: 12px;
+  line-height: 20px;
+  color: #ffffff;
+  text-align: center;
+  background-color: #428bca;
+  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+          box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  -webkit-transition: width 0.6s ease;
+          transition: width 0.6s ease;
+}
+
+.progress-striped .progress-bar {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-size: 40px 40px;
+}
+
+.progress.active .progress-bar {
+  -webkit-animation: progress-bar-stripes 2s linear infinite;
+          animation: progress-bar-stripes 2s linear infinite;
+}
+
+.progress-bar-success {
+  background-color: #5cb85c;
+}
+
+.progress-striped .progress-bar-success {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-bar-info {
+  background-color: #5bc0de;
+}
+
+.progress-striped .progress-bar-info {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-bar-warning {
+  background-color: #f0ad4e;
+}
+
+.progress-striped .progress-bar-warning {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-bar-danger {
+  background-color: #d9534f;
+}
+
+.progress-striped .progress-bar-danger {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.media,
+.media-body {
+  overflow: hidden;
+  zoom: 1;
+}
+
+.media,
+.media .media {
+  margin-top: 15px;
+}
+
+.media:first-child {
+  margin-top: 0;
+}
+
+.media-object {
+  display: block;
+}
+
+.media-heading {
+  margin: 0 0 5px;
+}
+
+.media > .pull-left {
+  margin-right: 10px;
+}
+
+.media > .pull-right {
+  margin-left: 10px;
+}
+
+.media-list {
+  padding-left: 0;
+  list-style: none;
+}
+
+.list-group {
+  padding-left: 0;
+  margin-bottom: 20px;
+}
+
+.list-group-item {
+  position: relative;
+  display: block;
+  padding: 10px 15px;
+  margin-bottom: -1px;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+}
+
+.list-group-item:first-child {
+  border-top-right-radius: 4px;
+  border-top-left-radius: 4px;
+}
+
+.list-group-item:last-child {
+  margin-bottom: 0;
+  border-bottom-right-radius: 4px;
+  border-bottom-left-radius: 4px;
+}
+
+.list-group-item > .badge {
+  float: right;
+}
+
+.list-group-item > .badge + .badge {
+  margin-right: 5px;
+}
+
+a.list-group-item {
+  color: #555555;
+}
+
+a.list-group-item .list-group-item-heading {
+  color: #333333;
+}
+
+a.list-group-item:hover,
+a.list-group-item:focus {
+  text-decoration: none;
+  background-color: #f5f5f5;
+}
+
+a.list-group-item.active,
+a.list-group-item.active:hover,
+a.list-group-item.active:focus {
+  z-index: 2;
+  color: #ffffff;
+  background-color: #428bca;
+  border-color: #428bca;
+}
+
+a.list-group-item.active .list-group-item-heading,
+a.list-group-item.active:hover .list-group-item-heading,
+a.list-group-item.active:focus .list-group-item-heading {
+  color: inherit;
+}
+
+a.list-group-item.active .list-group-item-text,
+a.list-group-item.active:hover .list-group-item-text,
+a.list-group-item.active:focus .list-group-item-text {
+  color: #e1edf7;
+}
+
+.list-group-item-heading {
+  margin-top: 0;
+  margin-bottom: 5px;
+}
+
+.list-group-item-text {
+  margin-bottom: 0;
+  line-height: 1.3;
+}
+
+.panel {
+  margin-bottom: 20px;
+  background-color: #ffffff;
+  border: 1px solid transparent;
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+          box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.panel-body {
+  padding: 15px;
+}
+
+.panel-body:before,
+.panel-body:after {
+  display: table;
+  content: " ";
+}
+
+.panel-body:after {
+  clear: both;
+}
+
+.panel-body:before,
+.panel-body:after {
+  display: table;
+  content: " ";
+}
+
+.panel-body:after {
+  clear: both;
+}
+
+.panel > .list-group {
+  margin-bottom: 0;
+}
+
+.panel > .list-group .list-group-item {
+  border-width: 1px 0;
+}
+
+.panel > .list-group .list-group-item:first-child {
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.panel > .list-group .list-group-item:last-child {
+  border-bottom: 0;
+}
+
+.panel-heading + .list-group .list-group-item:first-child {
+  border-top-width: 0;
+}
+
+.panel > .table,
+.panel > .table-responsive > .table {
+  margin-bottom: 0;
+}
+
+.panel > .panel-body + .table,
+.panel > .panel-body + .table-responsive {
+  border-top: 1px solid #dddddd;
+}
+
+.panel > .table > tbody:first-child th,
+.panel > .table > tbody:first-child td {
+  border-top: 0;
+}
+
+.panel > .table-bordered,
+.panel > .table-responsive > .table-bordered {
+  border: 0;
+}
+
+.panel > .table-bordered > thead > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,
+.panel > .table-bordered > tbody > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,
+.panel > .table-bordered > tfoot > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,
+.panel > .table-bordered > thead > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,
+.panel > .table-bordered > tbody > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,
+.panel > .table-bordered > tfoot > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+  border-left: 0;
+}
+
+.panel > .table-bordered > thead > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,
+.panel > .table-bordered > tbody > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,
+.panel > .table-bordered > tfoot > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,
+.panel > .table-bordered > thead > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,
+.panel > .table-bordered > tbody > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,
+.panel > .table-bordered > tfoot > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+  border-right: 0;
+}
+
+.panel > .table-bordered > thead > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > thead > tr:last-child > th,
+.panel > .table-bordered > tbody > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,
+.panel > .table-bordered > tfoot > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th,
+.panel > .table-bordered > thead > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > thead > tr:last-child > td,
+.panel > .table-bordered > tbody > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,
+.panel > .table-bordered > tfoot > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td {
+  border-bottom: 0;
+}
+
+.panel > .table-responsive {
+  margin-bottom: 0;
+  border: 0;
+}
+
+.panel-heading {
+  padding: 10px 15px;
+  border-bottom: 1px solid transparent;
+  border-top-right-radius: 3px;
+  border-top-left-radius: 3px;
+}
+
+.panel-heading > .dropdown .dropdown-toggle {
+  color: inherit;
+}
+
+.panel-title {
+  margin-top: 0;
+  margin-bottom: 0;
+  font-size: 16px;
+  color: inherit;
+}
+
+.panel-title > a {
+  color: inherit;
+}
+
+.panel-footer {
+  padding: 10px 15px;
+  background-color: #f5f5f5;
+  border-top: 1px solid #dddddd;
+  border-bottom-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+
+.panel-group .panel {
+  margin-bottom: 0;
+  overflow: hidden;
+  border-radius: 4px;
+}
+
+.panel-group .panel + .panel {
+  margin-top: 5px;
+}
+
+.panel-group .panel-heading {
+  border-bottom: 0;
+}
+
+.panel-group .panel-heading + .panel-collapse .panel-body {
+  border-top: 1px solid #dddddd;
+}
+
+.panel-group .panel-footer {
+  border-top: 0;
+}
+
+.panel-group .panel-footer + .panel-collapse .panel-body {
+  border-bottom: 1px solid #dddddd;
+}
+
+.panel-default {
+  border-color: #dddddd;
+}
+
+.panel-default > .panel-heading {
+  color: #333333;
+  background-color: #f5f5f5;
+  border-color: #dddddd;
+}
+
+.panel-default > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #dddddd;
+}
+
+.panel-default > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #dddddd;
+}
+
+.panel-primary {
+  border-color: #428bca;
+}
+
+.panel-primary > .panel-heading {
+  color: #ffffff;
+  background-color: #428bca;
+  border-color: #428bca;
+}
+
+.panel-primary > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #428bca;
+}
+
+.panel-primary > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #428bca;
+}
+
+.panel-success {
+  border-color: #d6e9c6;
+}
+
+.panel-success > .panel-heading {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #d6e9c6;
+}
+
+.panel-success > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #d6e9c6;
+}
+
+.panel-success > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #d6e9c6;
+}
+
+.panel-warning {
+  border-color: #faebcc;
+}
+
+.panel-warning > .panel-heading {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #faebcc;
+}
+
+.panel-warning > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #faebcc;
+}
+
+.panel-warning > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #faebcc;
+}
+
+.panel-danger {
+  border-color: #ebccd1;
+}
+
+.panel-danger > .panel-heading {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #ebccd1;
+}
+
+.panel-danger > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #ebccd1;
+}
+
+.panel-danger > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #ebccd1;
+}
+
+.panel-info {
+  border-color: #bce8f1;
+}
+
+.panel-info > .panel-heading {
+  color: #31708f;
+  background-color: #d9edf7;
+  border-color: #bce8f1;
+}
+
+.panel-info > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #bce8f1;
+}
+
+.panel-info > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #bce8f1;
+}
+
+.well {
+  min-height: 20px;
+  padding: 19px;
+  margin-bottom: 20px;
+  background-color: #f5f5f5;
+  border: 1px solid #e3e3e3;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.well blockquote {
+  border-color: #ddd;
+  border-color: rgba(0, 0, 0, 0.15);
+}
+
+.well-lg {
+  padding: 24px;
+  border-radius: 6px;
+}
+
+.well-sm {
+  padding: 9px;
+  border-radius: 3px;
+}
+
+.close {
+  float: right;
+  font-size: 21px;
+  font-weight: bold;
+  line-height: 1;
+  color: #000000;
+  text-shadow: 0 1px 0 #ffffff;
+  opacity: 0.2;
+  filter: alpha(opacity=20);
+}
+
+.close:hover,
+.close:focus {
+  color: #000000;
+  text-decoration: none;
+  cursor: pointer;
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+}
+
+button.close {
+  padding: 0;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+  -webkit-appearance: none;
+}
+
+.modal-open {
+  overflow: hidden;
+}
+
+.modal {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1040;
+  display: none;
+  overflow: auto;
+  overflow-y: scroll;
+}
+
+.modal.fade .modal-dialog {
+  -webkit-transform: translate(0, -25%);
+      -ms-transform: translate(0, -25%);
+          transform: translate(0, -25%);
+  -webkit-transition: -webkit-transform 0.3s ease-out;
+     -moz-transition: -moz-transform 0.3s ease-out;
+       -o-transition: -o-transform 0.3s ease-out;
+          transition: transform 0.3s ease-out;
+}
+
+.modal.in .modal-dialog {
+  -webkit-transform: translate(0, 0);
+      -ms-transform: translate(0, 0);
+          transform: translate(0, 0);
+}
+
+.modal-dialog {
+  position: relative;
+  z-index: 1050;
+  width: auto;
+  margin: 10px;
+}
+
+.modal-content {
+  position: relative;
+  background-color: #ffffff;
+  border: 1px solid #999999;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  border-radius: 6px;
+  outline: none;
+  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
+          box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
+  background-clip: padding-box;
+}
+
+.modal-backdrop {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1030;
+  background-color: #000000;
+}
+
+.modal-backdrop.fade {
+  opacity: 0;
+  filter: alpha(opacity=0);
+}
+
+.modal-backdrop.in {
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+}
+
+.modal-header {
+  min-height: 16.428571429px;
+  padding: 15px;
+  border-bottom: 1px solid #e5e5e5;
+}
+
+.modal-header .close {
+  margin-top: -2px;
+}
+
+.modal-title {
+  margin: 0;
+  line-height: 1.428571429;
+}
+
+.modal-body {
+  position: relative;
+  padding: 20px;
+}
+
+.modal-footer {
+  padding: 19px 20px 20px;
+  margin-top: 15px;
+  text-align: right;
+  border-top: 1px solid #e5e5e5;
+}
+
+.modal-footer:before,
+.modal-footer:after {
+  display: table;
+  content: " ";
+}
+
+.modal-footer:after {
+  clear: both;
+}
+
+.modal-footer:before,
+.modal-footer:after {
+  display: table;
+  content: " ";
+}
+
+.modal-footer:after {
+  clear: both;
+}
+
+.modal-footer .btn + .btn {
+  margin-bottom: 0;
+  margin-left: 5px;
+}
+
+.modal-footer .btn-group .btn + .btn {
+  margin-left: -1px;
+}
+
+.modal-footer .btn-block + .btn-block {
+  margin-left: 0;
+}
+
+@media screen and (min-width: 768px) {
+  .modal-dialog {
+    width: 600px;
+    margin: 30px auto;
+  }
+  .modal-content {
+    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
+            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
+  }
+}
+
+.tooltip {
+  position: absolute;
+  z-index: 1030;
+  display: block;
+  font-size: 12px;
+  line-height: 1.4;
+  opacity: 0;
+  filter: alpha(opacity=0);
+  visibility: visible;
+}
+
+.tooltip.in {
+  opacity: 0.9;
+  filter: alpha(opacity=90);
+}
+
+.tooltip.top {
+  padding: 5px 0;
+  margin-top: -3px;
+}
+
+.tooltip.right {
+  padding: 0 5px;
+  margin-left: 3px;
+}
+
+.tooltip.bottom {
+  padding: 5px 0;
+  margin-top: 3px;
+}
+
+.tooltip.left {
+  padding: 0 5px;
+  margin-left: -3px;
+}
+
+.tooltip-inner {
+  max-width: 200px;
+  padding: 3px 8px;
+  color: #ffffff;
+  text-align: center;
+  text-decoration: none;
+  background-color: #000000;
+  border-radius: 4px;
+}
+
+.tooltip-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+
+.tooltip.top .tooltip-arrow {
+  bottom: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-top-color: #000000;
+  border-width: 5px 5px 0;
+}
+
+.tooltip.top-left .tooltip-arrow {
+  bottom: 0;
+  left: 5px;
+  border-top-color: #000000;
+  border-width: 5px 5px 0;
+}
+
+.tooltip.top-right .tooltip-arrow {
+  right: 5px;
+  bottom: 0;
+  border-top-color: #000000;
+  border-width: 5px 5px 0;
+}
+
+.tooltip.right .tooltip-arrow {
+  top: 50%;
+  left: 0;
+  margin-top: -5px;
+  border-right-color: #000000;
+  border-width: 5px 5px 5px 0;
+}
+
+.tooltip.left .tooltip-arrow {
+  top: 50%;
+  right: 0;
+  margin-top: -5px;
+  border-left-color: #000000;
+  border-width: 5px 0 5px 5px;
+}
+
+.tooltip.bottom .tooltip-arrow {
+  top: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-bottom-color: #000000;
+  border-width: 0 5px 5px;
+}
+
+.tooltip.bottom-left .tooltip-arrow {
+  top: 0;
+  left: 5px;
+  border-bottom-color: #000000;
+  border-width: 0 5px 5px;
+}
+
+.tooltip.bottom-right .tooltip-arrow {
+  top: 0;
+  right: 5px;
+  border-bottom-color: #000000;
+  border-width: 0 5px 5px;
+}
+
+.popover {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1010;
+  display: none;
+  max-width: 276px;
+  padding: 1px;
+  text-align: left;
+  white-space: normal;
+  background-color: #ffffff;
+  border: 1px solid #cccccc;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  border-radius: 6px;
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+          box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  background-clip: padding-box;
+}
+
+.popover.top {
+  margin-top: -10px;
+}
+
+.popover.right {
+  margin-left: 10px;
+}
+
+.popover.bottom {
+  margin-top: 10px;
+}
+
+.popover.left {
+  margin-left: -10px;
+}
+
+.popover-title {
+  padding: 8px 14px;
+  margin: 0;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 18px;
+  background-color: #f7f7f7;
+  border-bottom: 1px solid #ebebeb;
+  border-radius: 5px 5px 0 0;
+}
+
+.popover-content {
+  padding: 9px 14px;
+}
+
+.popover .arrow,
+.popover .arrow:after {
+  position: absolute;
+  display: block;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+
+.popover .arrow {
+  border-width: 11px;
+}
+
+.popover .arrow:after {
+  border-width: 10px;
+  content: "";
+}
+
+.popover.top .arrow {
+  bottom: -11px;
+  left: 50%;
+  margin-left: -11px;
+  border-top-color: #999999;
+  border-top-color: rgba(0, 0, 0, 0.25);
+  border-bottom-width: 0;
+}
+
+.popover.top .arrow:after {
+  bottom: 1px;
+  margin-left: -10px;
+  border-top-color: #ffffff;
+  border-bottom-width: 0;
+  content: " ";
+}
+
+.popover.right .arrow {
+  top: 50%;
+  left: -11px;
+  margin-top: -11px;
+  border-right-color: #999999;
+  border-right-color: rgba(0, 0, 0, 0.25);
+  border-left-width: 0;
+}
+
+.popover.right .arrow:after {
+  bottom: -10px;
+  left: 1px;
+  border-right-color: #ffffff;
+  border-left-width: 0;
+  content: " ";
+}
+
+.popover.bottom .arrow {
+  top: -11px;
+  left: 50%;
+  margin-left: -11px;
+  border-bottom-color: #999999;
+  border-bottom-color: rgba(0, 0, 0, 0.25);
+  border-top-width: 0;
+}
+
+.popover.bottom .arrow:after {
+  top: 1px;
+  margin-left: -10px;
+  border-bottom-color: #ffffff;
+  border-top-width: 0;
+  content: " ";
+}
+
+.popover.left .arrow {
+  top: 50%;
+  right: -11px;
+  margin-top: -11px;
+  border-left-color: #999999;
+  border-left-color: rgba(0, 0, 0, 0.25);
+  border-right-width: 0;
+}
+
+.popover.left .arrow:after {
+  right: 1px;
+  bottom: -10px;
+  border-left-color: #ffffff;
+  border-right-width: 0;
+  content: " ";
+}
+
+.carousel {
+  position: relative;
+}
+
+.carousel-inner {
+  position: relative;
+  width: 100%;
+  overflow: hidden;
+}
+
+.carousel-inner > .item {
+  position: relative;
+  display: none;
+  -webkit-transition: 0.6s ease-in-out left;
+          transition: 0.6s ease-in-out left;
+}
+
+.carousel-inner > .item > img,
+.carousel-inner > .item > a > img {
+  display: block;
+  height: auto;
+  max-width: 100%;
+  line-height: 1;
+}
+
+.carousel-inner > .active,
+.carousel-inner > .next,
+.carousel-inner > .prev {
+  display: block;
+}
+
+.carousel-inner > .active {
+  left: 0;
+}
+
+.carousel-inner > .next,
+.carousel-inner > .prev {
+  position: absolute;
+  top: 0;
+  width: 100%;
+}
+
+.carousel-inner > .next {
+  left: 100%;
+}
+
+.carousel-inner > .prev {
+  left: -100%;
+}
+
+.carousel-inner > .next.left,
+.carousel-inner > .prev.right {
+  left: 0;
+}
+
+.carousel-inner > .active.left {
+  left: -100%;
+}
+
+.carousel-inner > .active.right {
+  left: 100%;
+}
+
+.carousel-control {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  width: 15%;
+  font-size: 20px;
+  color: #ffffff;
+  text-align: center;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+}
+
+.carousel-control.left {
+  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.5) 0), color-stop(rgba(0, 0, 0, 0.0001) 100%));
+  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.0001) 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);
+}
+
+.carousel-control.right {
+  right: 0;
+  left: auto;
+  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.0001) 0), color-stop(rgba(0, 0, 0, 0.5) 100%));
+  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0, rgba(0, 0, 0, 0.5) 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);
+}
+
+.carousel-control:hover,
+.carousel-control:focus {
+  color: #ffffff;
+  text-decoration: none;
+  outline: none;
+  opacity: 0.9;
+  filter: alpha(opacity=90);
+}
+
+.carousel-control .icon-prev,
+.carousel-control .icon-next,
+.carousel-control .glyphicon-chevron-left,
+.carousel-control .glyphicon-chevron-right {
+  position: absolute;
+  top: 50%;
+  z-index: 5;
+  display: inline-block;
+}
+
+.carousel-control .icon-prev,
+.carousel-control .glyphicon-chevron-left {
+  left: 50%;
+}
+
+.carousel-control .icon-next,
+.carousel-control .glyphicon-chevron-right {
+  right: 50%;
+}
+
+.carousel-control .icon-prev,
+.carousel-control .icon-next {
+  width: 20px;
+  height: 20px;
+  margin-top: -10px;
+  margin-left: -10px;
+  font-family: serif;
+}
+
+.carousel-control .icon-prev:before {
+  content: '\2039';
+}
+
+.carousel-control .icon-next:before {
+  content: '\203a';
+}
+
+.carousel-indicators {
+  position: absolute;
+  bottom: 10px;
+  left: 50%;
+  z-index: 15;
+  width: 60%;
+  padding-left: 0;
+  margin-left: -30%;
+  text-align: center;
+  list-style: none;
+}
+
+.carousel-indicators li {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  margin: 1px;
+  text-indent: -999px;
+  cursor: pointer;
+  background-color: #000 \9;
+  background-color: rgba(0, 0, 0, 0);
+  border: 1px solid #ffffff;
+  border-radius: 10px;
+}
+
+.carousel-indicators .active {
+  width: 12px;
+  height: 12px;
+  margin: 0;
+  background-color: #ffffff;
+}
+
+.carousel-caption {
+  position: absolute;
+  right: 15%;
+  bottom: 20px;
+  left: 15%;
+  z-index: 10;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  color: #ffffff;
+  text-align: center;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+}
+
+.carousel-caption .btn {
+  text-shadow: none;
+}
+
+@media screen and (min-width: 768px) {
+  .carousel-control .glyphicons-chevron-left,
+  .carousel-control .glyphicons-chevron-right,
+  .carousel-control .icon-prev,
+  .carousel-control .icon-next {
+    width: 30px;
+    height: 30px;
+    margin-top: -15px;
+    margin-left: -15px;
+    font-size: 30px;
+  }
+  .carousel-caption {
+    right: 20%;
+    left: 20%;
+    padding-bottom: 30px;
+  }
+  .carousel-indicators {
+    bottom: 20px;
+  }
+}
+
+.clearfix:before,
+.clearfix:after {
+  display: table;
+  content: " ";
+}
+
+.clearfix:after {
+  clear: both;
+}
+
+.center-block {
+  display: block;
+  margin-right: auto;
+  margin-left: auto;
+}
+
+.pull-right {
+  float: right !important;
+}
+
+.pull-left {
+  float: left !important;
+}
+
+.hide {
+  display: none !important;
+}
+
+.show {
+  display: block !important;
+}
+
+.invisible {
+  visibility: hidden;
+}
+
+.text-hide {
+  font: 0/0 a;
+  color: transparent;
+  text-shadow: none;
+  background-color: transparent;
+  border: 0;
+}
+
+.hidden {
+  display: none !important;
+  visibility: hidden !important;
+}
+
+.affix {
+  position: fixed;
+}
+
+@-ms-viewport {
+  width: device-width;
+}
+
+.visible-xs,
+tr.visible-xs,
+th.visible-xs,
+td.visible-xs {
+  display: none !important;
+}
+
+@media (max-width: 767px) {
+  .visible-xs {
+    display: block !important;
+  }
+  table.visible-xs {
+    display: table;
+  }
+  tr.visible-xs {
+    display: table-row !important;
+  }
+  th.visible-xs,
+  td.visible-xs {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-xs.visible-sm {
+    display: block !important;
+  }
+  table.visible-xs.visible-sm {
+    display: table;
+  }
+  tr.visible-xs.visible-sm {
+    display: table-row !important;
+  }
+  th.visible-xs.visible-sm,
+  td.visible-xs.visible-sm {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-xs.visible-md {
+    display: block !important;
+  }
+  table.visible-xs.visible-md {
+    display: table;
+  }
+  tr.visible-xs.visible-md {
+    display: table-row !important;
+  }
+  th.visible-xs.visible-md,
+  td.visible-xs.visible-md {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-xs.visible-lg {
+    display: block !important;
+  }
+  table.visible-xs.visible-lg {
+    display: table;
+  }
+  tr.visible-xs.visible-lg {
+    display: table-row !important;
+  }
+  th.visible-xs.visible-lg,
+  td.visible-xs.visible-lg {
+    display: table-cell !important;
+  }
+}
+
+.visible-sm,
+tr.visible-sm,
+th.visible-sm,
+td.visible-sm {
+  display: none !important;
+}
+
+@media (max-width: 767px) {
+  .visible-sm.visible-xs {
+    display: block !important;
+  }
+  table.visible-sm.visible-xs {
+    display: table;
+  }
+  tr.visible-sm.visible-xs {
+    display: table-row !important;
+  }
+  th.visible-sm.visible-xs,
+  td.visible-sm.visible-xs {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-sm {
+    display: block !important;
+  }
+  table.visible-sm {
+    display: table;
+  }
+  tr.visible-sm {
+    display: table-row !important;
+  }
+  th.visible-sm,
+  td.visible-sm {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-sm.visible-md {
+    display: block !important;
+  }
+  table.visible-sm.visible-md {
+    display: table;
+  }
+  tr.visible-sm.visible-md {
+    display: table-row !important;
+  }
+  th.visible-sm.visible-md,
+  td.visible-sm.visible-md {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-sm.visible-lg {
+    display: block !important;
+  }
+  table.visible-sm.visible-lg {
+    display: table;
+  }
+  tr.visible-sm.visible-lg {
+    display: table-row !important;
+  }
+  th.visible-sm.visible-lg,
+  td.visible-sm.visible-lg {
+    display: table-cell !important;
+  }
+}
+
+.visible-md,
+tr.visible-md,
+th.visible-md,
+td.visible-md {
+  display: none !important;
+}
+
+@media (max-width: 767px) {
+  .visible-md.visible-xs {
+    display: block !important;
+  }
+  table.visible-md.visible-xs {
+    display: table;
+  }
+  tr.visible-md.visible-xs {
+    display: table-row !important;
+  }
+  th.visible-md.visible-xs,
+  td.visible-md.visible-xs {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-md.visible-sm {
+    display: block !important;
+  }
+  table.visible-md.visible-sm {
+    display: table;
+  }
+  tr.visible-md.visible-sm {
+    display: table-row !important;
+  }
+  th.visible-md.visible-sm,
+  td.visible-md.visible-sm {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-md {
+    display: block !important;
+  }
+  table.visible-md {
+    display: table;
+  }
+  tr.visible-md {
+    display: table-row !important;
+  }
+  th.visible-md,
+  td.visible-md {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-md.visible-lg {
+    display: block !important;
+  }
+  table.visible-md.visible-lg {
+    display: table;
+  }
+  tr.visible-md.visible-lg {
+    display: table-row !important;
+  }
+  th.visible-md.visible-lg,
+  td.visible-md.visible-lg {
+    display: table-cell !important;
+  }
+}
+
+.visible-lg,
+tr.visible-lg,
+th.visible-lg,
+td.visible-lg {
+  display: none !important;
+}
+
+@media (max-width: 767px) {
+  .visible-lg.visible-xs {
+    display: block !important;
+  }
+  table.visible-lg.visible-xs {
+    display: table;
+  }
+  tr.visible-lg.visible-xs {
+    display: table-row !important;
+  }
+  th.visible-lg.visible-xs,
+  td.visible-lg.visible-xs {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-lg.visible-sm {
+    display: block !important;
+  }
+  table.visible-lg.visible-sm {
+    display: table;
+  }
+  tr.visible-lg.visible-sm {
+    display: table-row !important;
+  }
+  th.visible-lg.visible-sm,
+  td.visible-lg.visible-sm {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-lg.visible-md {
+    display: block !important;
+  }
+  table.visible-lg.visible-md {
+    display: table;
+  }
+  tr.visible-lg.visible-md {
+    display: table-row !important;
+  }
+  th.visible-lg.visible-md,
+  td.visible-lg.visible-md {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-lg {
+    display: block !important;
+  }
+  table.visible-lg {
+    display: table;
+  }
+  tr.visible-lg {
+    display: table-row !important;
+  }
+  th.visible-lg,
+  td.visible-lg {
+    display: table-cell !important;
+  }
+}
+
+.hidden-xs {
+  display: block !important;
+}
+
+table.hidden-xs {
+  display: table;
+}
+
+tr.hidden-xs {
+  display: table-row !important;
+}
+
+th.hidden-xs,
+td.hidden-xs {
+  display: table-cell !important;
+}
+
+@media (max-width: 767px) {
+  .hidden-xs,
+  tr.hidden-xs,
+  th.hidden-xs,
+  td.hidden-xs {
+    display: none !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-xs.hidden-sm,
+  tr.hidden-xs.hidden-sm,
+  th.hidden-xs.hidden-sm,
+  td.hidden-xs.hidden-sm {
+    display: none !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-xs.hidden-md,
+  tr.hidden-xs.hidden-md,
+  th.hidden-xs.hidden-md,
+  td.hidden-xs.hidden-md {
+    display: none !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .hidden-xs.hidden-lg,
+  tr.hidden-xs.hidden-lg,
+  th.hidden-xs.hidden-lg,
+  td.hidden-xs.hidden-lg {
+    display: none !important;
+  }
+}
+
+.hidden-sm {
+  display: block !important;
+}
+
+table.hidden-sm {
+  display: table;
+}
+
+tr.hidden-sm {
+  display: table-row !important;
+}
+
+th.hidden-sm,
+td.hidden-sm {
+  display: table-cell !important;
+}
+
+@media (max-width: 767px) {
+  .hidden-sm.hidden-xs,
+  tr.hidden-sm.hidden-xs,
+  th.hidden-sm.hidden-xs,
+  td.hidden-sm.hidden-xs {
+    display: none !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-sm,
+  tr.hidden-sm,
+  th.hidden-sm,
+  td.hidden-sm {
+    display: none !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-sm.hidden-md,
+  tr.hidden-sm.hidden-md,
+  th.hidden-sm.hidden-md,
+  td.hidden-sm.hidden-md {
+    display: none !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .hidden-sm.hidden-lg,
+  tr.hidden-sm.hidden-lg,
+  th.hidden-sm.hidden-lg,
+  td.hidden-sm.hidden-lg {
+    display: none !important;
+  }
+}
+
+.hidden-md {
+  display: block !important;
+}
+
+table.hidden-md {
+  display: table;
+}
+
+tr.hidden-md {
+  display: table-row !important;
+}
+
+th.hidden-md,
+td.hidden-md {
+  display: table-cell !important;
+}
+
+@media (max-width: 767px) {
+  .hidden-md.hidden-xs,
+  tr.hidden-md.hidden-xs,
+  th.hidden-md.hidden-xs,
+  td.hidden-md.hidden-xs {
+    display: none !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-md.hidden-sm,
+  tr.hidden-md.hidden-sm,
+  th.hidden-md.hidden-sm,
+  td.hidden-md.hidden-sm {
+    display: none !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-md,
+  tr.hidden-md,
+  th.hidden-md,
+  td.hidden-md {
+    display: none !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .hidden-md.hidden-lg,
+  tr.hidden-md.hidden-lg,
+  th.hidden-md.hidden-lg,
+  td.hidden-md.hidden-lg {
+    display: none !important;
+  }
+}
+
+.hidden-lg {
+  display: block !important;
+}
+
+table.hidden-lg {
+  display: table;
+}
+
+tr.hidden-lg {
+  display: table-row !important;
+}
+
+th.hidden-lg,
+td.hidden-lg {
+  display: table-cell !important;
+}
+
+@media (max-width: 767px) {
+  .hidden-lg.hidden-xs,
+  tr.hidden-lg.hidden-xs,
+  th.hidden-lg.hidden-xs,
+  td.hidden-lg.hidden-xs {
+    display: none !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-lg.hidden-sm,
+  tr.hidden-lg.hidden-sm,
+  th.hidden-lg.hidden-sm,
+  td.hidden-lg.hidden-sm {
+    display: none !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-lg.hidden-md,
+  tr.hidden-lg.hidden-md,
+  th.hidden-lg.hidden-md,
+  td.hidden-lg.hidden-md {
+    display: none !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .hidden-lg,
+  tr.hidden-lg,
+  th.hidden-lg,
+  td.hidden-lg {
+    display: none !important;
+  }
+}
+
+.visible-print,
+tr.visible-print,
+th.visible-print,
+td.visible-print {
+  display: none !important;
+}
+
+@media print {
+  .visible-print {
+    display: block !important;
+  }
+  table.visible-print {
+    display: table;
+  }
+  tr.visible-print {
+    display: table-row !important;
+  }
+  th.visible-print,
+  td.visible-print {
+    display: table-cell !important;
+  }
+  .hidden-print,
+  tr.hidden-print,
+  th.hidden-print,
+  td.hidden-print {
+    display: none !important;
+  }
+}
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap.min.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap.min.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/css/bootstrap.min.css	(revision 12677)
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2014 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10px;margin-bottom:10px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-primary:hover{color:#3071a9}.text-warning{color:#8a6d3b}.text-warning:hover{color:#66512c}.text-danger{color:#a94442}.text-danger:hover{color:#843534}.text-success{color:#3c763d}.text-success:hover{color:#2b542c}.text-info{color:#31708f}.text-info:hover{color:#245269}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small,blockquote .small{display:block;line-height:1.428571429;color:#999}blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>.active,.table>tbody>tr>.active,.table>tfoot>tr>.active,.table>thead>.active>td,.table>tbody>.active>td,.table>tfoot>.active>td,.table>thead>.active>th,.table>tbody>.active>th,.table>tfoot>.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>.active:hover,.table-hover>tbody>.active:hover>td,.table-hover>tbody>.active:hover>th{background-color:#e8e8e8}.table>thead>tr>.success,.table>tbody>tr>.success,.table>tfoot>tr>.success,.table>thead>.success>td,.table>tbody>.success>td,.table>tfoot>.success>td,.table>thead>.success>th,.table>tbody>.success>th,.table>tfoot>.success>th{background-color:#dff0d8}.table-hover>tbody>tr>.success:hover,.table-hover>tbody>.success:hover>td,.table-hover>tbody>.success:hover>th{background-color:#d0e9c6}.table>thead>tr>.danger,.table>tbody>tr>.danger,.table>tfoot>tr>.danger,.table>thead>.danger>td,.table>tbody>.danger>td,.table>tfoot>.danger>td,.table>thead>.danger>th,.table>tbody>.danger>th,.table>tfoot>.danger>th{background-color:#f2dede}.table-hover>tbody>tr>.danger:hover,.table-hover>tbody>.danger:hover>td,.table-hover>tbody>.danger:hover>th{background-color:#ebcccc}.table>thead>tr>.warning,.table>tbody>tr>.warning,.table>tfoot>tr>.warning,.table>thead>.warning>td,.table>tbody>.warning>td,.table>tfoot>.warning>td,.table>thead>.warning>th,.table>tbody>.warning>th,.table>tfoot>.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>.warning:hover,.table-hover>tbody>.warning:hover>td,.table-hover>tbody>.warning:hover>th{background-color:#faf2cc}@media(max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}output{display:block;padding-top:7px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline select.form-control{width:auto}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#fff}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-link{font-weight:normal;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1;-moz-osx-font-smoothing:grayscale}.glyphicon:empty{width:1em}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn:first-child>.btn{margin-right:-1px}.input-group-btn:last-child>.btn{margin-left:-1px}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form select.form-control{width:auto}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;height:auto;max-width:100%;margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child th,.panel>.table>tbody:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;z-index:1050;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;outline:0;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}table.visible-xs.visible-sm{display:table}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}table.visible-xs.visible-md{display:table}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}table.visible-xs.visible-lg{display:table}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}table.visible-sm.visible-xs{display:table}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}table.visible-sm.visible-md{display:table}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}table.visible-sm.visible-lg{display:table}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}table.visible-md.visible-xs{display:table}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}table.visible-md.visible-sm{display:table}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}table.visible-md.visible-lg{display:table}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}table.visible-lg.visible-xs{display:table}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}table.visible-lg.visible-sm{display:table}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}table.visible-lg.visible-md{display:table}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}table.hidden-xs{display:table}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm,tr.hidden-xs.hidden-sm,th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md,tr.hidden-xs.hidden-md,th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg,tr.hidden-xs.hidden-lg,th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}table.hidden-sm{display:table}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs,tr.hidden-sm.hidden-xs,th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md,tr.hidden-sm.hidden-md,th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg,tr.hidden-sm.hidden-lg,th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}table.hidden-md{display:table}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs,tr.hidden-md.hidden-xs,th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm,tr.hidden-md.hidden-sm,th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg,tr.hidden-md.hidden-lg,th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}table.hidden-lg{display:table}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs,tr.hidden-lg.hidden-xs,th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm,tr.hidden-lg.hidden-sm,th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md,tr.hidden-lg.hidden-md,th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}}
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/fonts/glyphicons-halflings-regular.svg
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/fonts/glyphicons-halflings-regular.svg	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/fonts/glyphicons-halflings-regular.svg	(revision 12677)
@@ -0,0 +1,229 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
+<font-face units-per-em="1200" ascent="960" descent="-240" />
+<missing-glyph horiz-adv-x="500" />
+<glyph />
+<glyph />
+<glyph unicode="&#xd;" />
+<glyph unicode=" " />
+<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
+<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
+<glyph unicode="&#xa0;" />
+<glyph unicode="&#x2000;" horiz-adv-x="652" />
+<glyph unicode="&#x2001;" horiz-adv-x="1304" />
+<glyph unicode="&#x2002;" horiz-adv-x="652" />
+<glyph unicode="&#x2003;" horiz-adv-x="1304" />
+<glyph unicode="&#x2004;" horiz-adv-x="434" />
+<glyph unicode="&#x2005;" horiz-adv-x="326" />
+<glyph unicode="&#x2006;" horiz-adv-x="217" />
+<glyph unicode="&#x2007;" horiz-adv-x="217" />
+<glyph unicode="&#x2008;" horiz-adv-x="163" />
+<glyph unicode="&#x2009;" horiz-adv-x="260" />
+<glyph unicode="&#x200a;" horiz-adv-x="72" />
+<glyph unicode="&#x202f;" horiz-adv-x="260" />
+<glyph unicode="&#x205f;" horiz-adv-x="326" />
+<glyph unicode="&#x20ac;" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
+<glyph unicode="&#x2212;" d="M200 400h900v300h-900v-300z" />
+<glyph unicode="&#x2601;" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
+<glyph unicode="&#x2709;" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
+<glyph unicode="&#x270f;" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
+<glyph unicode="&#xe000;" horiz-adv-x="500" d="M0 0z" />
+<glyph unicode="&#xe001;" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
+<glyph unicode="&#xe002;" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q17 -55 85.5 -75.5t147.5 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
+<glyph unicode="&#xe003;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
+<glyph unicode="&#xe005;" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
+<glyph unicode="&#xe006;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
+<glyph unicode="&#xe007;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
+<glyph unicode="&#xe008;" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
+<glyph unicode="&#xe009;" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
+<glyph unicode="&#xe010;" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe011;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe012;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe013;" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
+<glyph unicode="&#xe014;" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
+<glyph unicode="&#xe015;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
+<glyph unicode="&#xe016;" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 299q-120 -77 -261 -77q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
+<glyph unicode="&#xe017;" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
+<glyph unicode="&#xe018;" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
+<glyph unicode="&#xe019;" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
+<glyph unicode="&#xe020;" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
+<glyph unicode="&#xe021;" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
+<glyph unicode="&#xe022;" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
+<glyph unicode="&#xe023;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
+<glyph unicode="&#xe024;" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
+<glyph unicode="&#xe025;" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
+<glyph unicode="&#xe026;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
+<glyph unicode="&#xe027;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
+<glyph unicode="&#xe028;" d="M0 25v475l200 700h800q199 -700 200 -700v-475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
+<glyph unicode="&#xe029;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
+<glyph unicode="&#xe030;" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
+<glyph unicode="&#xe031;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
+<glyph unicode="&#xe032;" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
+<glyph unicode="&#xe033;" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
+<glyph unicode="&#xe034;" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
+<glyph unicode="&#xe035;" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
+<glyph unicode="&#xe036;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
+<glyph unicode="&#xe037;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
+<glyph unicode="&#xe038;" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
+<glyph unicode="&#xe039;" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
+<glyph unicode="&#xe040;" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
+<glyph unicode="&#xe041;" d="M1 700v475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
+<glyph unicode="&#xe042;" d="M2 700v475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
+<glyph unicode="&#xe043;" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
+<glyph unicode="&#xe044;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
+<glyph unicode="&#xe045;" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
+<glyph unicode="&#xe046;" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
+<glyph unicode="&#xe047;" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
+<glyph unicode="&#xe048;" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v70h471q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
+<glyph unicode="&#xe049;" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
+<glyph unicode="&#xe050;" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
+<glyph unicode="&#xe051;" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
+<glyph unicode="&#xe052;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe053;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe054;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe055;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe056;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe057;" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe058;" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
+<glyph unicode="&#xe059;" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
+<glyph unicode="&#xe060;" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
+<glyph unicode="&#xe062;" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" />
+<glyph unicode="&#xe063;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" />
+<glyph unicode="&#xe064;" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 138.5t-64 210.5zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
+<glyph unicode="&#xe065;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" />
+<glyph unicode="&#xe066;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
+<glyph unicode="&#xe067;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l566 567l-136 137l-430 -431l-147 147z" />
+<glyph unicode="&#xe068;" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
+<glyph unicode="&#xe069;" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe070;" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe071;" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
+<glyph unicode="&#xe072;" d="M200 0l900 550l-900 550v-1100z" />
+<glyph unicode="&#xe073;" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
+<glyph unicode="&#xe074;" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
+<glyph unicode="&#xe075;" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
+<glyph unicode="&#xe076;" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
+<glyph unicode="&#xe077;" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
+<glyph unicode="&#xe078;" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
+<glyph unicode="&#xe079;" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
+<glyph unicode="&#xe080;" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
+<glyph unicode="&#xe081;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
+<glyph unicode="&#xe082;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM300 500h600v200h-600v-200z" />
+<glyph unicode="&#xe083;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141z" />
+<glyph unicode="&#xe084;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
+<glyph unicode="&#xe085;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM363 700h144q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5q19 0 30 -10t11 -26 q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-105 0 -172 -56t-67 -183zM500 300h200v100h-200v-100z" />
+<glyph unicode="&#xe086;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" />
+<glyph unicode="&#xe087;" d="M0 500v200h194q15 60 36 104.5t55.5 86t88 69t126.5 40.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200 v-206q149 48 201 206h-201v200h200q-25 74 -76 127.5t-124 76.5v-204h-200v203q-75 -24 -130 -77.5t-79 -125.5h209v-200h-210z" />
+<glyph unicode="&#xe088;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" />
+<glyph unicode="&#xe089;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" />
+<glyph unicode="&#xe090;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" />
+<glyph unicode="&#xe091;" d="M0 547l600 453v-300h600v-300h-600v-301z" />
+<glyph unicode="&#xe092;" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
+<glyph unicode="&#xe093;" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
+<glyph unicode="&#xe094;" d="M104 600h296v600h300v-600h298l-449 -600z" />
+<glyph unicode="&#xe095;" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" />
+<glyph unicode="&#xe096;" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
+<glyph unicode="&#xe097;" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
+<glyph unicode="&#xe101;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-33 14.5h-207q-20 0 -32 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
+<glyph unicode="&#xe102;" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111v6t-1 15t-3 18l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6h-111v-100z M100 0h400v400h-400v-400zM200 900q-3 0 14 48t35 96l18 47l214 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
+<glyph unicode="&#xe103;" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
+<glyph unicode="&#xe104;" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
+<glyph unicode="&#xe105;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
+<glyph unicode="&#xe106;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
+<glyph unicode="&#xe107;" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 33 -48 36t-48 -29l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
+<glyph unicode="&#xe108;" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -21 -13 -29t-32 1l-94 78h-222l-94 -78q-19 -9 -32 -1t-13 29v64 q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
+<glyph unicode="&#xe109;" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
+<glyph unicode="&#xe110;" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
+<glyph unicode="&#xe111;" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
+<glyph unicode="&#xe112;" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
+<glyph unicode="&#xe113;" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
+<glyph unicode="&#xe114;" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
+<glyph unicode="&#xe115;" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
+<glyph unicode="&#xe116;" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
+<glyph unicode="&#xe117;" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
+<glyph unicode="&#xe118;" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
+<glyph unicode="&#xe119;" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
+<glyph unicode="&#xe120;" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
+<glyph unicode="&#xe121;" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
+<glyph unicode="&#xe122;" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM99 500v250v5q0 13 0.5 18.5t2.5 13t8 10.5t15 3h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35q-56 337 -56 351z M1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe123;" d="M74 350q0 21 13.5 35.5t33.5 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5l-18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-22 -9 -63 -23t-167.5 -37 t-251.5 -23t-245.5 20.5t-178.5 41.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
+<glyph unicode="&#xe124;" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
+<glyph unicode="&#xe125;" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q123 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 212l100 213h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" />
+<glyph unicode="&#xe126;" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q123 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" />
+<glyph unicode="&#xe127;" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" />
+<glyph unicode="&#xe128;" d="M-101 651q0 72 54 110t139 37h302l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 16.5 -10.5t26 -26t16.5 -36.5v-526q0 -13 -85.5 -93.5t-93.5 -80.5h-342q-15 0 -28.5 20t-19.5 41l-131 339h-106q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l106 89v502l-342 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM999 201v600h200v-600h-200z" />
+<glyph unicode="&#xe129;" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6v7.5v7v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" />
+<glyph unicode="&#xe130;" d="M1 585q-15 -31 7 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85l-1 -302q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM76 565l237 339h503l89 -100v-294l-340 -130 q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
+<glyph unicode="&#xe131;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 500h300l-2 -194l402 294l-402 298v-197h-298v-201z" />
+<glyph unicode="&#xe132;" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l400 -294v194h302v201h-300v197z" />
+<glyph unicode="&#xe133;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" />
+<glyph unicode="&#xe134;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" />
+<glyph unicode="&#xe135;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -34 5.5 -93t7.5 -87q0 -9 17 -44t16 -60q12 0 23 -5.5 t23 -15t20 -13.5q20 -10 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55.5t-20 -57.5q12 -21 22.5 -34.5t28 -27t36.5 -17.5q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q101 -2 221 111q31 30 47 48t34 49t21 62q-14 9 -37.5 9.5t-35.5 7.5q-14 7 -49 15t-52 19 q-9 0 -39.5 -0.5t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q8 16 22 22q6 -1 26 -1.5t33.5 -4.5t19.5 -13q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5 t5.5 57.5q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 41 1 44q31 -13 58.5 -14.5t39.5 3.5l11 4q6 36 -17 53.5t-64 28.5t-56 23 q-19 -3 -37 0q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -46 0t-45 -3q-20 -6 -51.5 -25.5t-34.5 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -91t-29.5 -79zM518 915q3 12 16 30.5t16 25.5q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -18 8 -42.5t16.5 -44 t9.5 -23.5q-6 1 -39 5t-53.5 10t-36.5 16z" />
+<glyph unicode="&#xe136;" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
+<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
+<glyph unicode="&#xe138;" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
+<glyph unicode="&#xe139;" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
+<glyph unicode="&#xe140;" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
+<glyph unicode="&#xe141;" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM513 609q0 32 21 56.5t52 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-16 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5q-37 0 -62.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" />
+<glyph unicode="&#xe142;" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -79.5 -17t-67.5 -51l-388 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23q38 0 53 -36 q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60l517 511 q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" />
+<glyph unicode="&#xe143;" d="M79 784q0 131 99 229.5t230 98.5q144 0 242 -129q103 129 245 129q130 0 227 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100l-84.5 84.5t-68 74t-60 78t-33.5 70.5t-15 78z M250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-106 48.5q-73 0 -131 -83l-118 -171l-114 174q-51 80 -124 80q-59 0 -108.5 -49.5t-49.5 -118.5z" />
+<glyph unicode="&#xe144;" d="M57 353q0 -94 66 -160l141 -141q66 -66 159 -66q95 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-12 12 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141l19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -18q46 -46 77 -99l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
+<glyph unicode="&#xe145;" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
+<glyph unicode="&#xe146;" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
+<glyph unicode="&#xe148;" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335l-27 7q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5v-307l64 -14 q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5zM700 237 q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
+<glyph unicode="&#xe149;" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -11 2.5 -24.5t5.5 -24t9.5 -26.5t10.5 -25t14 -27.5t14 -25.5 t15.5 -27t13.5 -24h242v-100h-197q8 -50 -2.5 -115t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q32 1 102 -16t104 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10 t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5t-30 142.5h-221z" />
+<glyph unicode="&#xe150;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
+<glyph unicode="&#xe151;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
+<glyph unicode="&#xe152;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
+<glyph unicode="&#xe153;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
+<glyph unicode="&#xe154;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
+<glyph unicode="&#xe155;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
+<glyph unicode="&#xe156;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
+<glyph unicode="&#xe157;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
+<glyph unicode="&#xe158;" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
+<glyph unicode="&#xe159;" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
+<glyph unicode="&#xe160;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
+<glyph unicode="&#xe161;" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" />
+<glyph unicode="&#xe162;" d="M216 519q10 -19 32 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8l9 -1q13 0 26 16l538 630q15 19 6 36q-8 18 -32 16h-300q1 4 78 219.5t79 227.5q2 17 -6 27l-8 8h-9q-16 0 -25 -15q-4 -5 -98.5 -111.5t-228 -257t-209.5 -238.5q-17 -19 -7 -40z" />
+<glyph unicode="&#xe163;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " />
+<glyph unicode="&#xe164;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" />
+<glyph unicode="&#xe165;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" />
+<glyph unicode="&#xe166;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe167;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe168;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe169;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 401h700v699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe170;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l248 -237v700h-699zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe171;" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
+<glyph unicode="&#xe172;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" />
+<glyph unicode="&#xe173;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" />
+<glyph unicode="&#xe174;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" />
+<glyph unicode="&#xe175;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" />
+<glyph unicode="&#xe176;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" />
+<glyph unicode="&#xe177;" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" />
+<glyph unicode="&#xe178;" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" />
+<glyph unicode="&#xe179;" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -117q-25 -16 -43.5 -50.5t-18.5 -65.5v-359z" />
+<glyph unicode="&#xe180;" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" />
+<glyph unicode="&#xe181;" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" />
+<glyph unicode="&#xe182;" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q16 17 13 40.5t-22 37.5l-192 136q-19 14 -45 12t-42 -19l-119 -118q-143 103 -267 227q-126 126 -227 268l118 118q17 17 20 41.5 t-11 44.5l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" />
+<glyph unicode="&#xe183;" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -15 -35.5t-35 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" />
+<glyph unicode="&#xe184;" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
+<glyph unicode="&#xe185;" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" />
+<glyph unicode="&#xe186;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
+<glyph unicode="&#xe187;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
+<glyph unicode="&#xe188;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" />
+<glyph unicode="&#xe189;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" />
+<glyph unicode="&#xe190;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" />
+<glyph unicode="&#xe191;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
+<glyph unicode="&#xe192;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
+<glyph unicode="&#xe193;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" />
+<glyph unicode="&#xe194;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" />
+<glyph unicode="&#xe195;" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" />
+<glyph unicode="&#xe197;" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300 h200l-300 -300z" />
+<glyph unicode="&#xe198;" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104t60.5 178q0 121 -85 207.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" />
+<glyph unicode="&#xe199;" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
+<glyph unicode="&#xe200;" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -12t1 -11q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
+</font>
+</defs></svg> 
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/js/bootstrap.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/js/bootstrap.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/js/bootstrap.js	(revision 12677)
@@ -0,0 +1,2006 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2013 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+if (typeof jQuery === "undefined") { throw new Error("Bootstrap requires jQuery") }
+
+/* ========================================================================
+ * Bootstrap: transition.js v3.0.3
+ * http://getbootstrap.com/javascript/#transitions
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
+  // ============================================================
+
+  function transitionEnd() {
+    var el = document.createElement('bootstrap')
+
+    var transEndEventNames = {
+      'WebkitTransition' : 'webkitTransitionEnd'
+    , 'MozTransition'    : 'transitionend'
+    , 'OTransition'      : 'oTransitionEnd otransitionend'
+    , 'transition'       : 'transitionend'
+    }
+
+    for (var name in transEndEventNames) {
+      if (el.style[name] !== undefined) {
+        return { end: transEndEventNames[name] }
+      }
+    }
+  }
+
+  // http://blog.alexmaccaw.com/css-transitions
+  $.fn.emulateTransitionEnd = function (duration) {
+    var called = false, $el = this
+    $(this).one($.support.transition.end, function () { called = true })
+    var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
+    setTimeout(callback, duration)
+    return this
+  }
+
+  $(function () {
+    $.support.transition = transitionEnd()
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: alert.js v3.0.3
+ * http://getbootstrap.com/javascript/#alerts
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // ALERT CLASS DEFINITION
+  // ======================
+
+  var dismiss = '[data-dismiss="alert"]'
+  var Alert   = function (el) {
+    $(el).on('click', dismiss, this.close)
+  }
+
+  Alert.prototype.close = function (e) {
+    var $this    = $(this)
+    var selector = $this.attr('data-target')
+
+    if (!selector) {
+      selector = $this.attr('href')
+      selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
+    }
+
+    var $parent = $(selector)
+
+    if (e) e.preventDefault()
+
+    if (!$parent.length) {
+      $parent = $this.hasClass('alert') ? $this : $this.parent()
+    }
+
+    $parent.trigger(e = $.Event('close.bs.alert'))
+
+    if (e.isDefaultPrevented()) return
+
+    $parent.removeClass('in')
+
+    function removeElement() {
+      $parent.trigger('closed.bs.alert').remove()
+    }
+
+    $.support.transition && $parent.hasClass('fade') ?
+      $parent
+        .one($.support.transition.end, removeElement)
+        .emulateTransitionEnd(150) :
+      removeElement()
+  }
+
+
+  // ALERT PLUGIN DEFINITION
+  // =======================
+
+  var old = $.fn.alert
+
+  $.fn.alert = function (option) {
+    return this.each(function () {
+      var $this = $(this)
+      var data  = $this.data('bs.alert')
+
+      if (!data) $this.data('bs.alert', (data = new Alert(this)))
+      if (typeof option == 'string') data[option].call($this)
+    })
+  }
+
+  $.fn.alert.Constructor = Alert
+
+
+  // ALERT NO CONFLICT
+  // =================
+
+  $.fn.alert.noConflict = function () {
+    $.fn.alert = old
+    return this
+  }
+
+
+  // ALERT DATA-API
+  // ==============
+
+  $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close)
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: button.js v3.0.3
+ * http://getbootstrap.com/javascript/#buttons
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // BUTTON PUBLIC CLASS DEFINITION
+  // ==============================
+
+  var Button = function (element, options) {
+    this.$element = $(element)
+    this.options  = $.extend({}, Button.DEFAULTS, options)
+  }
+
+  Button.DEFAULTS = {
+    loadingText: 'loading...'
+  }
+
+  Button.prototype.setState = function (state) {
+    var d    = 'disabled'
+    var $el  = this.$element
+    var val  = $el.is('input') ? 'val' : 'html'
+    var data = $el.data()
+
+    state = state + 'Text'
+
+    if (!data.resetText) $el.data('resetText', $el[val]())
+
+    $el[val](data[state] || this.options[state])
+
+    // push to event loop to allow forms to submit
+    setTimeout(function () {
+      state == 'loadingText' ?
+        $el.addClass(d).attr(d, d) :
+        $el.removeClass(d).removeAttr(d);
+    }, 0)
+  }
+
+  Button.prototype.toggle = function () {
+    var $parent = this.$element.closest('[data-toggle="buttons"]')
+    var changed = true
+
+    if ($parent.length) {
+      var $input = this.$element.find('input')
+      if ($input.prop('type') === 'radio') {
+        // see if clicking on current one
+        if ($input.prop('checked') && this.$element.hasClass('active'))
+          changed = false
+        else
+          $parent.find('.active').removeClass('active')
+      }
+      if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
+    }
+
+    if (changed) this.$element.toggleClass('active')
+  }
+
+
+  // BUTTON PLUGIN DEFINITION
+  // ========================
+
+  var old = $.fn.button
+
+  $.fn.button = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.button')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.button', (data = new Button(this, options)))
+
+      if (option == 'toggle') data.toggle()
+      else if (option) data.setState(option)
+    })
+  }
+
+  $.fn.button.Constructor = Button
+
+
+  // BUTTON NO CONFLICT
+  // ==================
+
+  $.fn.button.noConflict = function () {
+    $.fn.button = old
+    return this
+  }
+
+
+  // BUTTON DATA-API
+  // ===============
+
+  $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) {
+    var $btn = $(e.target)
+    if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
+    $btn.button('toggle')
+    e.preventDefault()
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: carousel.js v3.0.3
+ * http://getbootstrap.com/javascript/#carousel
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // CAROUSEL CLASS DEFINITION
+  // =========================
+
+  var Carousel = function (element, options) {
+    this.$element    = $(element)
+    this.$indicators = this.$element.find('.carousel-indicators')
+    this.options     = options
+    this.paused      =
+    this.sliding     =
+    this.interval    =
+    this.$active     =
+    this.$items      = null
+
+    this.options.pause == 'hover' && this.$element
+      .on('mouseenter', $.proxy(this.pause, this))
+      .on('mouseleave', $.proxy(this.cycle, this))
+  }
+
+  Carousel.DEFAULTS = {
+    interval: 5000
+  , pause: 'hover'
+  , wrap: true
+  }
+
+  Carousel.prototype.cycle =  function (e) {
+    e || (this.paused = false)
+
+    this.interval && clearInterval(this.interval)
+
+    this.options.interval
+      && !this.paused
+      && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
+
+    return this
+  }
+
+  Carousel.prototype.getActiveIndex = function () {
+    this.$active = this.$element.find('.item.active')
+    this.$items  = this.$active.parent().children()
+
+    return this.$items.index(this.$active)
+  }
+
+  Carousel.prototype.to = function (pos) {
+    var that        = this
+    var activeIndex = this.getActiveIndex()
+
+    if (pos > (this.$items.length - 1) || pos < 0) return
+
+    if (this.sliding)       return this.$element.one('slid.bs.carousel', function () { that.to(pos) })
+    if (activeIndex == pos) return this.pause().cycle()
+
+    return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
+  }
+
+  Carousel.prototype.pause = function (e) {
+    e || (this.paused = true)
+
+    if (this.$element.find('.next, .prev').length && $.support.transition.end) {
+      this.$element.trigger($.support.transition.end)
+      this.cycle(true)
+    }
+
+    this.interval = clearInterval(this.interval)
+
+    return this
+  }
+
+  Carousel.prototype.next = function () {
+    if (this.sliding) return
+    return this.slide('next')
+  }
+
+  Carousel.prototype.prev = function () {
+    if (this.sliding) return
+    return this.slide('prev')
+  }
+
+  Carousel.prototype.slide = function (type, next) {
+    var $active   = this.$element.find('.item.active')
+    var $next     = next || $active[type]()
+    var isCycling = this.interval
+    var direction = type == 'next' ? 'left' : 'right'
+    var fallback  = type == 'next' ? 'first' : 'last'
+    var that      = this
+
+    if (!$next.length) {
+      if (!this.options.wrap) return
+      $next = this.$element.find('.item')[fallback]()
+    }
+
+    this.sliding = true
+
+    isCycling && this.pause()
+
+    var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction })
+
+    if ($next.hasClass('active')) return
+
+    if (this.$indicators.length) {
+      this.$indicators.find('.active').removeClass('active')
+      this.$element.one('slid.bs.carousel', function () {
+        var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
+        $nextIndicator && $nextIndicator.addClass('active')
+      })
+    }
+
+    if ($.support.transition && this.$element.hasClass('slide')) {
+      this.$element.trigger(e)
+      if (e.isDefaultPrevented()) return
+      $next.addClass(type)
+      $next[0].offsetWidth // force reflow
+      $active.addClass(direction)
+      $next.addClass(direction)
+      $active
+        .one($.support.transition.end, function () {
+          $next.removeClass([type, direction].join(' ')).addClass('active')
+          $active.removeClass(['active', direction].join(' '))
+          that.sliding = false
+          setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0)
+        })
+        .emulateTransitionEnd(600)
+    } else {
+      this.$element.trigger(e)
+      if (e.isDefaultPrevented()) return
+      $active.removeClass('active')
+      $next.addClass('active')
+      this.sliding = false
+      this.$element.trigger('slid.bs.carousel')
+    }
+
+    isCycling && this.cycle()
+
+    return this
+  }
+
+
+  // CAROUSEL PLUGIN DEFINITION
+  // ==========================
+
+  var old = $.fn.carousel
+
+  $.fn.carousel = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.carousel')
+      var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option)
+      var action  = typeof option == 'string' ? option : options.slide
+
+      if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
+      if (typeof option == 'number') data.to(option)
+      else if (action) data[action]()
+      else if (options.interval) data.pause().cycle()
+    })
+  }
+
+  $.fn.carousel.Constructor = Carousel
+
+
+  // CAROUSEL NO CONFLICT
+  // ====================
+
+  $.fn.carousel.noConflict = function () {
+    $.fn.carousel = old
+    return this
+  }
+
+
+  // CAROUSEL DATA-API
+  // =================
+
+  $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
+    var $this   = $(this), href
+    var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
+    var options = $.extend({}, $target.data(), $this.data())
+    var slideIndex = $this.attr('data-slide-to')
+    if (slideIndex) options.interval = false
+
+    $target.carousel(options)
+
+    if (slideIndex = $this.attr('data-slide-to')) {
+      $target.data('bs.carousel').to(slideIndex)
+    }
+
+    e.preventDefault()
+  })
+
+  $(window).on('load', function () {
+    $('[data-ride="carousel"]').each(function () {
+      var $carousel = $(this)
+      $carousel.carousel($carousel.data())
+    })
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: collapse.js v3.0.3
+ * http://getbootstrap.com/javascript/#collapse
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // COLLAPSE PUBLIC CLASS DEFINITION
+  // ================================
+
+  var Collapse = function (element, options) {
+    this.$element      = $(element)
+    this.options       = $.extend({}, Collapse.DEFAULTS, options)
+    this.transitioning = null
+
+    if (this.options.parent) this.$parent = $(this.options.parent)
+    if (this.options.toggle) this.toggle()
+  }
+
+  Collapse.DEFAULTS = {
+    toggle: true
+  }
+
+  Collapse.prototype.dimension = function () {
+    var hasWidth = this.$element.hasClass('width')
+    return hasWidth ? 'width' : 'height'
+  }
+
+  Collapse.prototype.show = function () {
+    if (this.transitioning || this.$element.hasClass('in')) return
+
+    var startEvent = $.Event('show.bs.collapse')
+    this.$element.trigger(startEvent)
+    if (startEvent.isDefaultPrevented()) return
+
+    var actives = this.$parent && this.$parent.find('> .panel > .in')
+
+    if (actives && actives.length) {
+      var hasData = actives.data('bs.collapse')
+      if (hasData && hasData.transitioning) return
+      actives.collapse('hide')
+      hasData || actives.data('bs.collapse', null)
+    }
+
+    var dimension = this.dimension()
+
+    this.$element
+      .removeClass('collapse')
+      .addClass('collapsing')
+      [dimension](0)
+
+    this.transitioning = 1
+
+    var complete = function () {
+      this.$element
+        .removeClass('collapsing')
+        .addClass('in')
+        [dimension]('auto')
+      this.transitioning = 0
+      this.$element.trigger('shown.bs.collapse')
+    }
+
+    if (!$.support.transition) return complete.call(this)
+
+    var scrollSize = $.camelCase(['scroll', dimension].join('-'))
+
+    this.$element
+      .one($.support.transition.end, $.proxy(complete, this))
+      .emulateTransitionEnd(350)
+      [dimension](this.$element[0][scrollSize])
+  }
+
+  Collapse.prototype.hide = function () {
+    if (this.transitioning || !this.$element.hasClass('in')) return
+
+    var startEvent = $.Event('hide.bs.collapse')
+    this.$element.trigger(startEvent)
+    if (startEvent.isDefaultPrevented()) return
+
+    var dimension = this.dimension()
+
+    this.$element
+      [dimension](this.$element[dimension]())
+      [0].offsetHeight
+
+    this.$element
+      .addClass('collapsing')
+      .removeClass('collapse')
+      .removeClass('in')
+
+    this.transitioning = 1
+
+    var complete = function () {
+      this.transitioning = 0
+      this.$element
+        .trigger('hidden.bs.collapse')
+        .removeClass('collapsing')
+        .addClass('collapse')
+    }
+
+    if (!$.support.transition) return complete.call(this)
+
+    this.$element
+      [dimension](0)
+      .one($.support.transition.end, $.proxy(complete, this))
+      .emulateTransitionEnd(350)
+  }
+
+  Collapse.prototype.toggle = function () {
+    this[this.$element.hasClass('in') ? 'hide' : 'show']()
+  }
+
+
+  // COLLAPSE PLUGIN DEFINITION
+  // ==========================
+
+  var old = $.fn.collapse
+
+  $.fn.collapse = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.collapse')
+      var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
+
+      if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.collapse.Constructor = Collapse
+
+
+  // COLLAPSE NO CONFLICT
+  // ====================
+
+  $.fn.collapse.noConflict = function () {
+    $.fn.collapse = old
+    return this
+  }
+
+
+  // COLLAPSE DATA-API
+  // =================
+
+  $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) {
+    var $this   = $(this), href
+    var target  = $this.attr('data-target')
+        || e.preventDefault()
+        || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
+    var $target = $(target)
+    var data    = $target.data('bs.collapse')
+    var option  = data ? 'toggle' : $this.data()
+    var parent  = $this.attr('data-parent')
+    var $parent = parent && $(parent)
+
+    if (!data || !data.transitioning) {
+      if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed')
+      $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
+    }
+
+    $target.collapse(option)
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: dropdown.js v3.0.3
+ * http://getbootstrap.com/javascript/#dropdowns
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // DROPDOWN CLASS DEFINITION
+  // =========================
+
+  var backdrop = '.dropdown-backdrop'
+  var toggle   = '[data-toggle=dropdown]'
+  var Dropdown = function (element) {
+    $(element).on('click.bs.dropdown', this.toggle)
+  }
+
+  Dropdown.prototype.toggle = function (e) {
+    var $this = $(this)
+
+    if ($this.is('.disabled, :disabled')) return
+
+    var $parent  = getParent($this)
+    var isActive = $parent.hasClass('open')
+
+    clearMenus()
+
+    if (!isActive) {
+      if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
+        // if mobile we use a backdrop because click events don't delegate
+        $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus)
+      }
+
+      $parent.trigger(e = $.Event('show.bs.dropdown'))
+
+      if (e.isDefaultPrevented()) return
+
+      $parent
+        .toggleClass('open')
+        .trigger('shown.bs.dropdown')
+
+      $this.focus()
+    }
+
+    return false
+  }
+
+  Dropdown.prototype.keydown = function (e) {
+    if (!/(38|40|27)/.test(e.keyCode)) return
+
+    var $this = $(this)
+
+    e.preventDefault()
+    e.stopPropagation()
+
+    if ($this.is('.disabled, :disabled')) return
+
+    var $parent  = getParent($this)
+    var isActive = $parent.hasClass('open')
+
+    if (!isActive || (isActive && e.keyCode == 27)) {
+      if (e.which == 27) $parent.find(toggle).focus()
+      return $this.click()
+    }
+
+    var $items = $('[role=menu] li:not(.divider):visible a', $parent)
+
+    if (!$items.length) return
+
+    var index = $items.index($items.filter(':focus'))
+
+    if (e.keyCode == 38 && index > 0)                 index--                        // up
+    if (e.keyCode == 40 && index < $items.length - 1) index++                        // down
+    if (!~index)                                      index=0
+
+    $items.eq(index).focus()
+  }
+
+  function clearMenus() {
+    $(backdrop).remove()
+    $(toggle).each(function (e) {
+      var $parent = getParent($(this))
+      if (!$parent.hasClass('open')) return
+      $parent.trigger(e = $.Event('hide.bs.dropdown'))
+      if (e.isDefaultPrevented()) return
+      $parent.removeClass('open').trigger('hidden.bs.dropdown')
+    })
+  }
+
+  function getParent($this) {
+    var selector = $this.attr('data-target')
+
+    if (!selector) {
+      selector = $this.attr('href')
+      selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+    }
+
+    var $parent = selector && $(selector)
+
+    return $parent && $parent.length ? $parent : $this.parent()
+  }
+
+
+  // DROPDOWN PLUGIN DEFINITION
+  // ==========================
+
+  var old = $.fn.dropdown
+
+  $.fn.dropdown = function (option) {
+    return this.each(function () {
+      var $this = $(this)
+      var data  = $this.data('bs.dropdown')
+
+      if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
+      if (typeof option == 'string') data[option].call($this)
+    })
+  }
+
+  $.fn.dropdown.Constructor = Dropdown
+
+
+  // DROPDOWN NO CONFLICT
+  // ====================
+
+  $.fn.dropdown.noConflict = function () {
+    $.fn.dropdown = old
+    return this
+  }
+
+
+  // APPLY TO STANDARD DROPDOWN ELEMENTS
+  // ===================================
+
+  $(document)
+    .on('click.bs.dropdown.data-api', clearMenus)
+    .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
+    .on('click.bs.dropdown.data-api'  , toggle, Dropdown.prototype.toggle)
+    .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: modal.js v3.0.3
+ * http://getbootstrap.com/javascript/#modals
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // MODAL CLASS DEFINITION
+  // ======================
+
+  var Modal = function (element, options) {
+    this.options   = options
+    this.$element  = $(element)
+    this.$backdrop =
+    this.isShown   = null
+
+    if (this.options.remote) this.$element.load(this.options.remote)
+  }
+
+  Modal.DEFAULTS = {
+      backdrop: true
+    , keyboard: true
+    , show: true
+  }
+
+  Modal.prototype.toggle = function (_relatedTarget) {
+    return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
+  }
+
+  Modal.prototype.show = function (_relatedTarget) {
+    var that = this
+    var e    = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
+
+    this.$element.trigger(e)
+
+    if (this.isShown || e.isDefaultPrevented()) return
+
+    this.isShown = true
+
+    this.escape()
+
+    this.$element.on('click.dismiss.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
+
+    this.backdrop(function () {
+      var transition = $.support.transition && that.$element.hasClass('fade')
+
+      if (!that.$element.parent().length) {
+        that.$element.appendTo(document.body) // don't move modals dom position
+      }
+
+      that.$element.show()
+
+      if (transition) {
+        that.$element[0].offsetWidth // force reflow
+      }
+
+      that.$element
+        .addClass('in')
+        .attr('aria-hidden', false)
+
+      that.enforceFocus()
+
+      var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
+
+      transition ?
+        that.$element.find('.modal-dialog') // wait for modal to slide in
+          .one($.support.transition.end, function () {
+            that.$element.focus().trigger(e)
+          })
+          .emulateTransitionEnd(300) :
+        that.$element.focus().trigger(e)
+    })
+  }
+
+  Modal.prototype.hide = function (e) {
+    if (e) e.preventDefault()
+
+    e = $.Event('hide.bs.modal')
+
+    this.$element.trigger(e)
+
+    if (!this.isShown || e.isDefaultPrevented()) return
+
+    this.isShown = false
+
+    this.escape()
+
+    $(document).off('focusin.bs.modal')
+
+    this.$element
+      .removeClass('in')
+      .attr('aria-hidden', true)
+      .off('click.dismiss.modal')
+
+    $.support.transition && this.$element.hasClass('fade') ?
+      this.$element
+        .one($.support.transition.end, $.proxy(this.hideModal, this))
+        .emulateTransitionEnd(300) :
+      this.hideModal()
+  }
+
+  Modal.prototype.enforceFocus = function () {
+    $(document)
+      .off('focusin.bs.modal') // guard against infinite focus loop
+      .on('focusin.bs.modal', $.proxy(function (e) {
+        if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
+          this.$element.focus()
+        }
+      }, this))
+  }
+
+  Modal.prototype.escape = function () {
+    if (this.isShown && this.options.keyboard) {
+      this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {
+        e.which == 27 && this.hide()
+      }, this))
+    } else if (!this.isShown) {
+      this.$element.off('keyup.dismiss.bs.modal')
+    }
+  }
+
+  Modal.prototype.hideModal = function () {
+    var that = this
+    this.$element.hide()
+    this.backdrop(function () {
+      that.removeBackdrop()
+      that.$element.trigger('hidden.bs.modal')
+    })
+  }
+
+  Modal.prototype.removeBackdrop = function () {
+    this.$backdrop && this.$backdrop.remove()
+    this.$backdrop = null
+  }
+
+  Modal.prototype.backdrop = function (callback) {
+    var that    = this
+    var animate = this.$element.hasClass('fade') ? 'fade' : ''
+
+    if (this.isShown && this.options.backdrop) {
+      var doAnimate = $.support.transition && animate
+
+      this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
+        .appendTo(document.body)
+
+      this.$element.on('click.dismiss.modal', $.proxy(function (e) {
+        if (e.target !== e.currentTarget) return
+        this.options.backdrop == 'static'
+          ? this.$element[0].focus.call(this.$element[0])
+          : this.hide.call(this)
+      }, this))
+
+      if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
+
+      this.$backdrop.addClass('in')
+
+      if (!callback) return
+
+      doAnimate ?
+        this.$backdrop
+          .one($.support.transition.end, callback)
+          .emulateTransitionEnd(150) :
+        callback()
+
+    } else if (!this.isShown && this.$backdrop) {
+      this.$backdrop.removeClass('in')
+
+      $.support.transition && this.$element.hasClass('fade')?
+        this.$backdrop
+          .one($.support.transition.end, callback)
+          .emulateTransitionEnd(150) :
+        callback()
+
+    } else if (callback) {
+      callback()
+    }
+  }
+
+
+  // MODAL PLUGIN DEFINITION
+  // =======================
+
+  var old = $.fn.modal
+
+  $.fn.modal = function (option, _relatedTarget) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.modal')
+      var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
+
+      if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
+      if (typeof option == 'string') data[option](_relatedTarget)
+      else if (options.show) data.show(_relatedTarget)
+    })
+  }
+
+  $.fn.modal.Constructor = Modal
+
+
+  // MODAL NO CONFLICT
+  // =================
+
+  $.fn.modal.noConflict = function () {
+    $.fn.modal = old
+    return this
+  }
+
+
+  // MODAL DATA-API
+  // ==============
+
+  $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
+    var $this   = $(this)
+    var href    = $this.attr('href')
+    var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
+    var option  = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
+
+    e.preventDefault()
+
+    $target
+      .modal(option, this)
+      .one('hide', function () {
+        $this.is(':visible') && $this.focus()
+      })
+  })
+
+  $(document)
+    .on('show.bs.modal',  '.modal', function () { $(document.body).addClass('modal-open') })
+    .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: tooltip.js v3.0.3
+ * http://getbootstrap.com/javascript/#tooltip
+ * Inspired by the original jQuery.tipsy by Jason Frame
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // TOOLTIP PUBLIC CLASS DEFINITION
+  // ===============================
+
+  var Tooltip = function (element, options) {
+    this.type       =
+    this.options    =
+    this.enabled    =
+    this.timeout    =
+    this.hoverState =
+    this.$element   = null
+
+    this.init('tooltip', element, options)
+  }
+
+  Tooltip.DEFAULTS = {
+    animation: true
+  , placement: 'top'
+  , selector: false
+  , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+  , trigger: 'hover focus'
+  , title: ''
+  , delay: 0
+  , html: false
+  , container: false
+  }
+
+  Tooltip.prototype.init = function (type, element, options) {
+    this.enabled  = true
+    this.type     = type
+    this.$element = $(element)
+    this.options  = this.getOptions(options)
+
+    var triggers = this.options.trigger.split(' ')
+
+    for (var i = triggers.length; i--;) {
+      var trigger = triggers[i]
+
+      if (trigger == 'click') {
+        this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
+      } else if (trigger != 'manual') {
+        var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focus'
+        var eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
+
+        this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
+        this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
+      }
+    }
+
+    this.options.selector ?
+      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
+      this.fixTitle()
+  }
+
+  Tooltip.prototype.getDefaults = function () {
+    return Tooltip.DEFAULTS
+  }
+
+  Tooltip.prototype.getOptions = function (options) {
+    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
+
+    if (options.delay && typeof options.delay == 'number') {
+      options.delay = {
+        show: options.delay
+      , hide: options.delay
+      }
+    }
+
+    return options
+  }
+
+  Tooltip.prototype.getDelegateOptions = function () {
+    var options  = {}
+    var defaults = this.getDefaults()
+
+    this._options && $.each(this._options, function (key, value) {
+      if (defaults[key] != value) options[key] = value
+    })
+
+    return options
+  }
+
+  Tooltip.prototype.enter = function (obj) {
+    var self = obj instanceof this.constructor ?
+      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
+
+    clearTimeout(self.timeout)
+
+    self.hoverState = 'in'
+
+    if (!self.options.delay || !self.options.delay.show) return self.show()
+
+    self.timeout = setTimeout(function () {
+      if (self.hoverState == 'in') self.show()
+    }, self.options.delay.show)
+  }
+
+  Tooltip.prototype.leave = function (obj) {
+    var self = obj instanceof this.constructor ?
+      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
+
+    clearTimeout(self.timeout)
+
+    self.hoverState = 'out'
+
+    if (!self.options.delay || !self.options.delay.hide) return self.hide()
+
+    self.timeout = setTimeout(function () {
+      if (self.hoverState == 'out') self.hide()
+    }, self.options.delay.hide)
+  }
+
+  Tooltip.prototype.show = function () {
+    var e = $.Event('show.bs.'+ this.type)
+
+    if (this.hasContent() && this.enabled) {
+      this.$element.trigger(e)
+
+      if (e.isDefaultPrevented()) return
+
+      var $tip = this.tip()
+
+      this.setContent()
+
+      if (this.options.animation) $tip.addClass('fade')
+
+      var placement = typeof this.options.placement == 'function' ?
+        this.options.placement.call(this, $tip[0], this.$element[0]) :
+        this.options.placement
+
+      var autoToken = /\s?auto?\s?/i
+      var autoPlace = autoToken.test(placement)
+      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
+
+      $tip
+        .detach()
+        .css({ top: 0, left: 0, display: 'block' })
+        .addClass(placement)
+
+      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
+
+      var pos          = this.getPosition()
+      var actualWidth  = $tip[0].offsetWidth
+      var actualHeight = $tip[0].offsetHeight
+
+      if (autoPlace) {
+        var $parent = this.$element.parent()
+
+        var orgPlacement = placement
+        var docScroll    = document.documentElement.scrollTop || document.body.scrollTop
+        var parentWidth  = this.options.container == 'body' ? window.innerWidth  : $parent.outerWidth()
+        var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
+        var parentLeft   = this.options.container == 'body' ? 0 : $parent.offset().left
+
+        placement = placement == 'bottom' && pos.top   + pos.height  + actualHeight - docScroll > parentHeight  ? 'top'    :
+                    placement == 'top'    && pos.top   - docScroll   - actualHeight < 0                         ? 'bottom' :
+                    placement == 'right'  && pos.right + actualWidth > parentWidth                              ? 'left'   :
+                    placement == 'left'   && pos.left  - actualWidth < parentLeft                               ? 'right'  :
+                    placement
+
+        $tip
+          .removeClass(orgPlacement)
+          .addClass(placement)
+      }
+
+      var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
+
+      this.applyPlacement(calculatedOffset, placement)
+      this.$element.trigger('shown.bs.' + this.type)
+    }
+  }
+
+  Tooltip.prototype.applyPlacement = function(offset, placement) {
+    var replace
+    var $tip   = this.tip()
+    var width  = $tip[0].offsetWidth
+    var height = $tip[0].offsetHeight
+
+    // manually read margins because getBoundingClientRect includes difference
+    var marginTop = parseInt($tip.css('margin-top'), 10)
+    var marginLeft = parseInt($tip.css('margin-left'), 10)
+
+    // we must check for NaN for ie 8/9
+    if (isNaN(marginTop))  marginTop  = 0
+    if (isNaN(marginLeft)) marginLeft = 0
+
+    offset.top  = offset.top  + marginTop
+    offset.left = offset.left + marginLeft
+
+    $tip
+      .offset(offset)
+      .addClass('in')
+
+    // check to see if placing tip in new offset caused the tip to resize itself
+    var actualWidth  = $tip[0].offsetWidth
+    var actualHeight = $tip[0].offsetHeight
+
+    if (placement == 'top' && actualHeight != height) {
+      replace = true
+      offset.top = offset.top + height - actualHeight
+    }
+
+    if (/bottom|top/.test(placement)) {
+      var delta = 0
+
+      if (offset.left < 0) {
+        delta       = offset.left * -2
+        offset.left = 0
+
+        $tip.offset(offset)
+
+        actualWidth  = $tip[0].offsetWidth
+        actualHeight = $tip[0].offsetHeight
+      }
+
+      this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
+    } else {
+      this.replaceArrow(actualHeight - height, actualHeight, 'top')
+    }
+
+    if (replace) $tip.offset(offset)
+  }
+
+  Tooltip.prototype.replaceArrow = function(delta, dimension, position) {
+    this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + "%") : '')
+  }
+
+  Tooltip.prototype.setContent = function () {
+    var $tip  = this.tip()
+    var title = this.getTitle()
+
+    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
+    $tip.removeClass('fade in top bottom left right')
+  }
+
+  Tooltip.prototype.hide = function () {
+    var that = this
+    var $tip = this.tip()
+    var e    = $.Event('hide.bs.' + this.type)
+
+    function complete() {
+      if (that.hoverState != 'in') $tip.detach()
+    }
+
+    this.$element.trigger(e)
+
+    if (e.isDefaultPrevented()) return
+
+    $tip.removeClass('in')
+
+    $.support.transition && this.$tip.hasClass('fade') ?
+      $tip
+        .one($.support.transition.end, complete)
+        .emulateTransitionEnd(150) :
+      complete()
+
+    this.$element.trigger('hidden.bs.' + this.type)
+
+    return this
+  }
+
+  Tooltip.prototype.fixTitle = function () {
+    var $e = this.$element
+    if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
+      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
+    }
+  }
+
+  Tooltip.prototype.hasContent = function () {
+    return this.getTitle()
+  }
+
+  Tooltip.prototype.getPosition = function () {
+    var el = this.$element[0]
+    return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
+      width: el.offsetWidth
+    , height: el.offsetHeight
+    }, this.$element.offset())
+  }
+
+  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
+    return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } :
+           placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } :
+           placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
+        /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }
+  }
+
+  Tooltip.prototype.getTitle = function () {
+    var title
+    var $e = this.$element
+    var o  = this.options
+
+    title = $e.attr('data-original-title')
+      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)
+
+    return title
+  }
+
+  Tooltip.prototype.tip = function () {
+    return this.$tip = this.$tip || $(this.options.template)
+  }
+
+  Tooltip.prototype.arrow = function () {
+    return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
+  }
+
+  Tooltip.prototype.validate = function () {
+    if (!this.$element[0].parentNode) {
+      this.hide()
+      this.$element = null
+      this.options  = null
+    }
+  }
+
+  Tooltip.prototype.enable = function () {
+    this.enabled = true
+  }
+
+  Tooltip.prototype.disable = function () {
+    this.enabled = false
+  }
+
+  Tooltip.prototype.toggleEnabled = function () {
+    this.enabled = !this.enabled
+  }
+
+  Tooltip.prototype.toggle = function (e) {
+    var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
+    self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
+  }
+
+  Tooltip.prototype.destroy = function () {
+    this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
+  }
+
+
+  // TOOLTIP PLUGIN DEFINITION
+  // =========================
+
+  var old = $.fn.tooltip
+
+  $.fn.tooltip = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.tooltip')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.tooltip.Constructor = Tooltip
+
+
+  // TOOLTIP NO CONFLICT
+  // ===================
+
+  $.fn.tooltip.noConflict = function () {
+    $.fn.tooltip = old
+    return this
+  }
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: popover.js v3.0.3
+ * http://getbootstrap.com/javascript/#popovers
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // POPOVER PUBLIC CLASS DEFINITION
+  // ===============================
+
+  var Popover = function (element, options) {
+    this.init('popover', element, options)
+  }
+
+  if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
+
+  Popover.DEFAULTS = $.extend({} , $.fn.tooltip.Constructor.DEFAULTS, {
+    placement: 'right'
+  , trigger: 'click'
+  , content: ''
+  , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
+  })
+
+
+  // NOTE: POPOVER EXTENDS tooltip.js
+  // ================================
+
+  Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
+
+  Popover.prototype.constructor = Popover
+
+  Popover.prototype.getDefaults = function () {
+    return Popover.DEFAULTS
+  }
+
+  Popover.prototype.setContent = function () {
+    var $tip    = this.tip()
+    var title   = this.getTitle()
+    var content = this.getContent()
+
+    $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
+    $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content)
+
+    $tip.removeClass('fade top bottom left right in')
+
+    // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
+    // this manually by checking the contents.
+    if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
+  }
+
+  Popover.prototype.hasContent = function () {
+    return this.getTitle() || this.getContent()
+  }
+
+  Popover.prototype.getContent = function () {
+    var $e = this.$element
+    var o  = this.options
+
+    return $e.attr('data-content')
+      || (typeof o.content == 'function' ?
+            o.content.call($e[0]) :
+            o.content)
+  }
+
+  Popover.prototype.arrow = function () {
+    return this.$arrow = this.$arrow || this.tip().find('.arrow')
+  }
+
+  Popover.prototype.tip = function () {
+    if (!this.$tip) this.$tip = $(this.options.template)
+    return this.$tip
+  }
+
+
+  // POPOVER PLUGIN DEFINITION
+  // =========================
+
+  var old = $.fn.popover
+
+  $.fn.popover = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.popover')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.popover.Constructor = Popover
+
+
+  // POPOVER NO CONFLICT
+  // ===================
+
+  $.fn.popover.noConflict = function () {
+    $.fn.popover = old
+    return this
+  }
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: scrollspy.js v3.0.3
+ * http://getbootstrap.com/javascript/#scrollspy
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // SCROLLSPY CLASS DEFINITION
+  // ==========================
+
+  function ScrollSpy(element, options) {
+    var href
+    var process  = $.proxy(this.process, this)
+
+    this.$element       = $(element).is('body') ? $(window) : $(element)
+    this.$body          = $('body')
+    this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process)
+    this.options        = $.extend({}, ScrollSpy.DEFAULTS, options)
+    this.selector       = (this.options.target
+      || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
+      || '') + ' .nav li > a'
+    this.offsets        = $([])
+    this.targets        = $([])
+    this.activeTarget   = null
+
+    this.refresh()
+    this.process()
+  }
+
+  ScrollSpy.DEFAULTS = {
+    offset: 10
+  }
+
+  ScrollSpy.prototype.refresh = function () {
+    var offsetMethod = this.$element[0] == window ? 'offset' : 'position'
+
+    this.offsets = $([])
+    this.targets = $([])
+
+    var self     = this
+    var $targets = this.$body
+      .find(this.selector)
+      .map(function () {
+        var $el   = $(this)
+        var href  = $el.data('target') || $el.attr('href')
+        var $href = /^#\w/.test(href) && $(href)
+
+        return ($href
+          && $href.length
+          && [[ $href[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]]) || null
+      })
+      .sort(function (a, b) { return a[0] - b[0] })
+      .each(function () {
+        self.offsets.push(this[0])
+        self.targets.push(this[1])
+      })
+  }
+
+  ScrollSpy.prototype.process = function () {
+    var scrollTop    = this.$scrollElement.scrollTop() + this.options.offset
+    var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight
+    var maxScroll    = scrollHeight - this.$scrollElement.height()
+    var offsets      = this.offsets
+    var targets      = this.targets
+    var activeTarget = this.activeTarget
+    var i
+
+    if (scrollTop >= maxScroll) {
+      return activeTarget != (i = targets.last()[0]) && this.activate(i)
+    }
+
+    for (i = offsets.length; i--;) {
+      activeTarget != targets[i]
+        && scrollTop >= offsets[i]
+        && (!offsets[i + 1] || scrollTop <= offsets[i + 1])
+        && this.activate( targets[i] )
+    }
+  }
+
+  ScrollSpy.prototype.activate = function (target) {
+    this.activeTarget = target
+
+    $(this.selector)
+      .parents('.active')
+      .removeClass('active')
+
+    var selector = this.selector
+      + '[data-target="' + target + '"],'
+      + this.selector + '[href="' + target + '"]'
+
+    var active = $(selector)
+      .parents('li')
+      .addClass('active')
+
+    if (active.parent('.dropdown-menu').length)  {
+      active = active
+        .closest('li.dropdown')
+        .addClass('active')
+    }
+
+    active.trigger('activate.bs.scrollspy')
+  }
+
+
+  // SCROLLSPY PLUGIN DEFINITION
+  // ===========================
+
+  var old = $.fn.scrollspy
+
+  $.fn.scrollspy = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.scrollspy')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.scrollspy.Constructor = ScrollSpy
+
+
+  // SCROLLSPY NO CONFLICT
+  // =====================
+
+  $.fn.scrollspy.noConflict = function () {
+    $.fn.scrollspy = old
+    return this
+  }
+
+
+  // SCROLLSPY DATA-API
+  // ==================
+
+  $(window).on('load', function () {
+    $('[data-spy="scroll"]').each(function () {
+      var $spy = $(this)
+      $spy.scrollspy($spy.data())
+    })
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: tab.js v3.0.3
+ * http://getbootstrap.com/javascript/#tabs
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // TAB CLASS DEFINITION
+  // ====================
+
+  var Tab = function (element) {
+    this.element = $(element)
+  }
+
+  Tab.prototype.show = function () {
+    var $this    = this.element
+    var $ul      = $this.closest('ul:not(.dropdown-menu)')
+    var selector = $this.data('target')
+
+    if (!selector) {
+      selector = $this.attr('href')
+      selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+    }
+
+    if ($this.parent('li').hasClass('active')) return
+
+    var previous = $ul.find('.active:last a')[0]
+    var e        = $.Event('show.bs.tab', {
+      relatedTarget: previous
+    })
+
+    $this.trigger(e)
+
+    if (e.isDefaultPrevented()) return
+
+    var $target = $(selector)
+
+    this.activate($this.parent('li'), $ul)
+    this.activate($target, $target.parent(), function () {
+      $this.trigger({
+        type: 'shown.bs.tab'
+      , relatedTarget: previous
+      })
+    })
+  }
+
+  Tab.prototype.activate = function (element, container, callback) {
+    var $active    = container.find('> .active')
+    var transition = callback
+      && $.support.transition
+      && $active.hasClass('fade')
+
+    function next() {
+      $active
+        .removeClass('active')
+        .find('> .dropdown-menu > .active')
+        .removeClass('active')
+
+      element.addClass('active')
+
+      if (transition) {
+        element[0].offsetWidth // reflow for transition
+        element.addClass('in')
+      } else {
+        element.removeClass('fade')
+      }
+
+      if (element.parent('.dropdown-menu')) {
+        element.closest('li.dropdown').addClass('active')
+      }
+
+      callback && callback()
+    }
+
+    transition ?
+      $active
+        .one($.support.transition.end, next)
+        .emulateTransitionEnd(150) :
+      next()
+
+    $active.removeClass('in')
+  }
+
+
+  // TAB PLUGIN DEFINITION
+  // =====================
+
+  var old = $.fn.tab
+
+  $.fn.tab = function ( option ) {
+    return this.each(function () {
+      var $this = $(this)
+      var data  = $this.data('bs.tab')
+
+      if (!data) $this.data('bs.tab', (data = new Tab(this)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.tab.Constructor = Tab
+
+
+  // TAB NO CONFLICT
+  // ===============
+
+  $.fn.tab.noConflict = function () {
+    $.fn.tab = old
+    return this
+  }
+
+
+  // TAB DATA-API
+  // ============
+
+  $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
+    e.preventDefault()
+    $(this).tab('show')
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: affix.js v3.0.3
+ * http://getbootstrap.com/javascript/#affix
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // AFFIX CLASS DEFINITION
+  // ======================
+
+  var Affix = function (element, options) {
+    this.options = $.extend({}, Affix.DEFAULTS, options)
+    this.$window = $(window)
+      .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
+      .on('click.bs.affix.data-api',  $.proxy(this.checkPositionWithEventLoop, this))
+
+    this.$element = $(element)
+    this.affixed  =
+    this.unpin    = null
+
+    this.checkPosition()
+  }
+
+  Affix.RESET = 'affix affix-top affix-bottom'
+
+  Affix.DEFAULTS = {
+    offset: 0
+  }
+
+  Affix.prototype.checkPositionWithEventLoop = function () {
+    setTimeout($.proxy(this.checkPosition, this), 1)
+  }
+
+  Affix.prototype.checkPosition = function () {
+    if (!this.$element.is(':visible')) return
+
+    var scrollHeight = $(document).height()
+    var scrollTop    = this.$window.scrollTop()
+    var position     = this.$element.offset()
+    var offset       = this.options.offset
+    var offsetTop    = offset.top
+    var offsetBottom = offset.bottom
+
+    if (typeof offset != 'object')         offsetBottom = offsetTop = offset
+    if (typeof offsetTop == 'function')    offsetTop    = offset.top()
+    if (typeof offsetBottom == 'function') offsetBottom = offset.bottom()
+
+    var affix = this.unpin   != null && (scrollTop + this.unpin <= position.top) ? false :
+                offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' :
+                offsetTop    != null && (scrollTop <= offsetTop) ? 'top' : false
+
+    if (this.affixed === affix) return
+    if (this.unpin) this.$element.css('top', '')
+
+    this.affixed = affix
+    this.unpin   = affix == 'bottom' ? position.top - scrollTop : null
+
+    this.$element.removeClass(Affix.RESET).addClass('affix' + (affix ? '-' + affix : ''))
+
+    if (affix == 'bottom') {
+      this.$element.offset({ top: document.body.offsetHeight - offsetBottom - this.$element.height() })
+    }
+  }
+
+
+  // AFFIX PLUGIN DEFINITION
+  // =======================
+
+  var old = $.fn.affix
+
+  $.fn.affix = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.affix')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.affix', (data = new Affix(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.affix.Constructor = Affix
+
+
+  // AFFIX NO CONFLICT
+  // =================
+
+  $.fn.affix.noConflict = function () {
+    $.fn.affix = old
+    return this
+  }
+
+
+  // AFFIX DATA-API
+  // ==============
+
+  $(window).on('load', function () {
+    $('[data-spy="affix"]').each(function () {
+      var $spy = $(this)
+      var data = $spy.data()
+
+      data.offset = data.offset || {}
+
+      if (data.offsetBottom) data.offset.bottom = data.offsetBottom
+      if (data.offsetTop)    data.offset.top    = data.offsetTop
+
+      $spy.affix(data)
+    })
+  })
+
+}(jQuery);
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/js/bootstrap.min.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/js/bootstrap.min.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/bootstrap/js/bootstrap.min.js	(revision 12677)
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2013 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i<h.length-1&&i++,~i||(i=0),h.eq(i).focus()}}}};var g=a.fn.dropdown;a.fn.dropdown=function(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new f(this)),"string"==typeof b&&d[b].call(c)})},a.fn.dropdown.Constructor=f,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=g,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",e,f.prototype.toggle).on("keydown.bs.dropdown.data-api",e+", [role=menu]",f.prototype.keydown)}(jQuery),+function(a){"use strict";var b=function(b,c){this.options=c,this.$element=a(b),this.$backdrop=this.isShown=null,this.options.remote&&this.$element.load(this.options.remote)};b.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},b.prototype.toggle=function(a){return this[this.isShown?"hide":"show"](a)},b.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.$element.on("click.dismiss.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(document.body),c.$element.show(),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one(a.support.transition.end,function(){c.$element.focus().trigger(e)}).emulateTransitionEnd(300):c.$element.focus().trigger(e)}))},b.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one(a.support.transition.end,a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},b.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.focus()},this))},b.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},b.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden.bs.modal")})},b.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},b.prototype.backdrop=function(b){var c=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var d=a.support.transition&&c;if(this.$backdrop=a('<div class="modal-backdrop '+c+'" />').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h<o?"right":d,c.removeClass(k).addClass(d)}var p=this.getCalculatedOffset(d,g,h,i);this.applyPlacement(p,d),this.$element.trigger("shown.bs."+this.type)}},b.prototype.applyPlacement=function(a,b){var c,d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),a.top=a.top+g,a.left=a.left+h,d.offset(a).addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;if("top"==b&&j!=f&&(c=!0,a.top=a.top+f-j),/bottom|top/.test(b)){var k=0;a.left<0&&(k=-2*a.left,a.left=0,d.offset(a),i=d[0].offsetWidth,j=d[0].offsetHeight),this.replaceArrow(k-e+i,i,"left")}else this.replaceArrow(j-f,j,"top");c&&d.offset(a)},b.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},b.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach()}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,b).emulateTransitionEnd(150):b(),this.$element.trigger("hidden.bs."+this.type),this)},b.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},b.prototype.hasContent=function(){return this.getTitle()},b.prototype.getPosition=function(){var b=this.$element[0];return a.extend({},"function"==typeof b.getBoundingClientRect?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},b.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},b.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},b.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},b.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},b.prototype.enable=function(){this.enabled=!0},b.prototype.disable=function(){this.enabled=!1},b.prototype.toggleEnabled=function(){this.enabled=!this.enabled},b.prototype.toggle=function(b){var c=b?a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type):this;c.tip().hasClass("in")?c.leave(c):c.enter(c)},b.prototype.destroy=function(){this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof c&&c;e||d.data("bs.tooltip",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(jQuery),+function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");b.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery);
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/css/IE8.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/css/IE8.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/css/IE8.css	(revision 12677)
@@ -0,0 +1,8 @@
+body {
+    padding-top: 60px;
+}
+
+ .navbar-brand {
+    padding-top:15px;
+    padding-bottom:15px;
+}
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/css/base.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/css/base.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/css/base.css	(revision 12677)
@@ -0,0 +1,361 @@
+/* Customized Bootstrap styles */
+
+body {
+    background-color: #f5f5f5;
+    padding-top: 52px;
+}
+
+.navbar-fixed-top, .navbar {
+    min-height:40px;
+}
+
+.navbar-nav > li > a, .navbar-brand {
+    padding-top:10px;
+    padding-bottom:10px;
+}
+
+.navbar-toggle {
+    padding: 4px 10px;
+}
+
+.navbar-nav.navbar-right:last-child {
+  margin-right: -5px;
+}
+
+.navbar-inverse {
+  border-color: #FFFFFF;
+}
+
+.ikoba-langselect img {
+    margin-top: -10px;
+    margin-bottom: -10px;
+}
+
+.breadcrumb , .wfstatus {
+    background-color: #fff;
+    margin-bottom: 0px;
+    margin-top: 0px;
+    padding: 2px 15px;
+}
+
+.breadcrumb > li {
+    display: inline;
+}
+
+.breadcrumb > li + li:before {
+    content: "/";
+    padding: 0px;
+}
+
+.well {
+    padding: 0px 5px 20px 5px;
+    margin-bottom: 10px;
+    background-image: None;
+    background-color: #fff;
+    filter: None;
+}
+
+.alert {
+  margin-bottom: 10px;
+  margin-top: 10px;
+}
+
+
+/* Ikoba styles */
+
+.ikoba-sidebar,
+.ikoba-content-narrow,
+.ikoba-content-wide,
+.ikoba-breadcrumbs-box-short,
+.ikoba-breadcrumbs-box-long,
+.ikoba-customer-status-box {
+    padding: 0px 5px 0px 5px;
+}
+
+.workflow {
+    border:1px solid #1fdf00;
+    background-color:#bbffb6;
+    font-family: courier;
+    font-size: 100%;
+    padding: 4px;
+    margin-bottom: 8px;
+    line-height: 16px;
+}
+
+.ikoba-breadcrumbs-box-short .well,
+.ikoba-breadcrumbs-box-long .well,
+.ikoba-customer-status-box .well {
+    padding: 5px 5px 5px 5px;
+}
+
+.ikoba-content-wide .well, .ikoba-content-narrow .well {
+    padding: 10px 20px 10px 20px;
+}
+
+.ikoba-content-desc {
+    color: #BFBFBF;
+    font-size: 18px;
+}
+
+.wfstatus, .wfstatus-sub {
+    text-align: center;
+    background-color: #909090;
+    color: white;
+}
+
+.ikoba-customer-status-box .well {
+    background-color: #909090;
+}
+
+.separator {
+    font-size: 16px;
+    color: #404040;
+    font-weight: bold;
+    line-height: 36px;
+}
+
+/* Sidebar */
+
+.ikoba-sidebar-title {
+    text-transform: none;
+    font-size: 14px;
+    color: #555;
+    font-weight: bold;
+    padding: 15px 0px 10px 8px;
+}
+
+.sidebar-nav .nav  > li > a {
+    padding: 3px 15px;
+}
+
+/* Buttons */
+
+input.btn {
+    padding: 6px 12px;
+    margin-right: 4px;
+}
+
+input.btn-sm {
+  padding: 5px 10px;
+}
+
+input.btn-xs {
+  padding: 1px 10px;
+}
+
+.table-condensed .btn {
+    margin: 0px;
+    font-size: 12px;
+}
+
+
+/* Content actionbar */
+
+div.actionbar {
+    margin-top: -6px;
+    margin-left: -10px;
+    margin-bottom: 0px;
+}
+
+.actionbar .btn-default {
+    margin-bottom: 4px;
+    padding: 4px 8px;
+    margin: 0px 0px 4px 0px;
+    display: inline-block;
+}
+
+/* Kofa forms */
+
+.form-table {
+    width: 100%;
+}
+
+.form-table td {
+    padding: 5px 0px 5px 4px;
+    border-top: none;
+}
+
+.value select, .phone-input, .ac-input,
+.textType, textarea,
+.dataTables_filter input,
+.dataTables_length select,
+.ordered-selection-field select {
+    background-color: #FFFFFF;
+    border: 1px solid #CCCCCC;
+    border-radius: 4px;
+    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
+    color: #555555;
+    padding: 6px 12px;
+    transition: border-color 0.15s ease-in-out 0s, box-shadow 0.15s ease-in-out 0s;
+    margin: 0px 4px 0px 0px;
+    width: 100%;
+}
+
+.dataTables_length select {
+    width: 90px;
+}
+
+.value select:focus, .phone-input:focus, .ac-input:focus,
+.textType:focus, textarea:focus,
+.dataTables_filter input:focus,
+.ordered-selection-field select:focus {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+
+.value select, .half {
+    width: 50%;
+}
+
+textarea {
+    font-family: Courier,"Courier New",monospace;
+}
+
+.fieldname {
+    width: 25%;
+}
+
+.form-table td {
+    vertical-align: bottom;
+}
+
+.form-table td.swcheckbox {
+  vertical-align: bottom;
+}
+
+.form-table td.fieldname {
+    vertical-align: top;
+}
+
+.form-inline select {
+    width: 100%;
+}
+
+.phone-input {
+    width: 30%;
+}
+
+.ac-series {
+    width: 50px;
+}
+
+.ac-number {
+    width: 110px;
+}
+
+.hint {
+    padding: 0px 0px 5px 0px;
+    color: #bfbfbf;
+}
+
+.error {
+    color: red;
+}
+
+.datepicker-le-year {
+    width: 50%;
+}
+
+.datepicker-short {
+    width: 15%;
+}
+
+.ui-widget-header .ui-icon {
+    background-image: url("../img/ui-icons_222222_256x240.png");
+}
+
+
+ /* dataTables stuff */
+
+.dataTables_wrapper {
+    min-height: 50px;
+    margin-bottom: 50px;
+}
+
+.dataTables_filter input {
+    width: 60%;
+}
+
+
+.sorting { background: url('../img/sort_both.png') no-repeat center right; }
+.sorting_asc { background: url('../img/sort_asc.png') no-repeat center right; }
+.sorting_desc { background: url('../img/sort_desc.png') no-repeat center right; }
+
+.sorting_asc_disabled { background: url('../img/sort_asc_disabled.png') no-repeat center right; }
+.sorting_desc_disabled { background: url('../img/sort_desc_disabled.png') no-repeat center right; }
+
+.paginate_enabled_previous { background: url('../img/back_enabled.png') no-repeat top left; }
+.paginate_enabled_previous:hover { background: url('../img/back_enabled_hover.png') no-repeat top left; }
+.paginate_disabled_previous { background: url('../img/back_disabled.png') no-repeat top left; }
+
+.paginate_enabled_next { background: url('../img/forward_enabled.png') no-repeat top right; }
+.paginate_enabled_next:hover { background: url('../img/forward_enabled_hover.png') no-repeat top right; }
+.paginate_disabled_next { background: url('../img/forward_disabled.png') no-repeat top right; }
+
+/* Scroll icons */
+
+#toTop-right {
+	display:none;
+	text-decoration:none;
+	position:fixed;
+	bottom:-4px;
+	right:-4px;
+	outline:none;
+}
+
+.scroll-up {
+  background: url("../img/scroll_up.png") no-repeat scroll left top rgba(0, 0, 0, 0);
+  padding: 25px;
+}
+
+#toBottom-right {
+	display:none;
+	text-decoration:none;
+	position:fixed;
+	top:47px;
+	right:-4px;
+	outline:none;
+}
+
+.scroll-down {
+  background: url("../img/scroll_down.png") no-repeat scroll left top rgba(0, 0, 0, 0);
+  padding: 25px;
+}
+
+ /* docutils stuff */
+
+.section h1 {
+    font-size: 30px}
+
+.section h2 {
+    font-size: 24px}
+
+.section h3 {
+    font-size: 18px}
+
+.section h4 {
+    font-size: 14px}
+
+/* Smartphone styles */
+
+@media (max-width: 767px) {
+
+  .ikoba-sidebar,
+  .ikoba-content-narrow ,
+  .ikoba-breadcrumbs-box-short,
+  .ikoba-breadcrumbs-box-long,
+  .ikoba-customer-status-box {
+      padding: 0px 0px 0px 0px;
+  }
+
+  .ikoba-content-wide {
+      padding: 0px 0px 0px 0px;
+  }
+
+  .value select, .half {
+      width: 100%;
+  }
+
+}
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/css/file-upload.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/css/file-upload.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/css/file-upload.css	(revision 12677)
@@ -0,0 +1,19 @@
+.btn-file {
+    position: relative;
+    overflow: hidden;
+}
+.btn-file input[type=file] {
+    position: absolute;
+    top: 0;
+    right: 0;
+    min-width: 100%;
+    min-height: 100%;
+    font-size: 999px;
+    text-align: right;
+    filter: alpha(opacity=0);
+    opacity: 0;
+    outline: none;
+    background: white;
+    cursor: inherit;
+    display: block;
+}
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/css/jquery-ui.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/css/jquery-ui.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/css/jquery-ui.css	(revision 12677)
@@ -0,0 +1,1229 @@
+/*! jQuery UI - v1.11.0pre - 2014-01-29
+* http://jqueryui.com
+* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, spinner.css, tabs.css, tooltip.css, theme.css
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden {
+	display: none;
+}
+.ui-helper-hidden-accessible {
+	border: 0;
+	clip: rect(0 0 0 0);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute;
+	width: 1px;
+}
+.ui-helper-reset {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	outline: 0;
+	line-height: 1.3;
+	text-decoration: none;
+	font-size: 100%;
+	list-style: none;
+}
+.ui-helper-clearfix:before,
+.ui-helper-clearfix:after {
+	content: "";
+	display: table;
+	border-collapse: collapse;
+}
+.ui-helper-clearfix:after {
+	clear: both;
+}
+.ui-helper-clearfix {
+	min-height: 0; /* support: IE7 */
+}
+.ui-helper-zfix {
+	width: 100%;
+	height: 100%;
+	top: 0;
+	left: 0;
+	position: absolute;
+	opacity: 0;
+	filter:Alpha(Opacity=0);
+}
+
+.ui-front {
+	z-index: 100;
+}
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled {
+	cursor: default !important;
+}
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	display: block;
+	text-indent: -99999px;
+	overflow: hidden;
+	background-repeat: no-repeat;
+}
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+}
+
+.ui-accordion .ui-accordion-header {
+	display: block;
+	cursor: pointer;
+	position: relative;
+	margin: 2px 0 0 0;
+	padding: .5em .5em .5em .7em;
+	min-height: 0; /* support: IE7 */
+	font-size: 100%;
+}
+.ui-accordion .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-icons .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-header .ui-accordion-header-icon {
+	position: absolute;
+	left: .5em;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-accordion .ui-accordion-content {
+	padding: 1em 2.2em;
+	border-top: 0;
+	overflow: auto;
+}
+
+.ui-autocomplete {
+	position: absolute;
+	top: 0;
+	left: 0;
+	cursor: default;
+}
+
+.ui-button {
+	display: inline-block;
+	position: relative;
+	padding: 0;
+	line-height: normal;
+	margin-right: .1em;
+	cursor: pointer;
+	vertical-align: middle;
+	text-align: center;
+	overflow: visible; /* removes extra width in IE */
+}
+.ui-button,
+.ui-button:link,
+.ui-button:visited,
+.ui-button:hover,
+.ui-button:active {
+	text-decoration: none;
+}
+/* to make room for the icon, a width needs to be set here */
+.ui-button-icon-only {
+	width: 2.2em;
+}
+/* button elements seem to need a little more width */
+button.ui-button-icon-only {
+	width: 2.4em;
+}
+.ui-button-icons-only {
+	width: 3.4em;
+}
+button.ui-button-icons-only {
+	width: 3.7em;
+}
+
+/* button text element */
+.ui-button .ui-button-text {
+	display: block;
+	line-height: normal;
+}
+.ui-button-text-only .ui-button-text {
+	padding: .4em 1em;
+}
+.ui-button-icon-only .ui-button-text,
+.ui-button-icons-only .ui-button-text {
+	padding: .4em;
+	text-indent: -9999999px;
+}
+.ui-button-text-icon-primary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 1em .4em 2.1em;
+}
+.ui-button-text-icon-secondary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 2.1em .4em 1em;
+}
+.ui-button-text-icons .ui-button-text {
+	padding-left: 2.1em;
+	padding-right: 2.1em;
+}
+/* no icon support for input elements, provide padding by default */
+input.ui-button {
+	padding: .4em 1em;
+}
+
+/* button icon element(s) */
+.ui-button-icon-only .ui-icon,
+.ui-button-text-icon-primary .ui-icon,
+.ui-button-text-icon-secondary .ui-icon,
+.ui-button-text-icons .ui-icon,
+.ui-button-icons-only .ui-icon {
+	position: absolute;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-button-icon-only .ui-icon {
+	left: 50%;
+	margin-left: -8px;
+}
+.ui-button-text-icon-primary .ui-button-icon-primary,
+.ui-button-text-icons .ui-button-icon-primary,
+.ui-button-icons-only .ui-button-icon-primary {
+	left: .5em;
+}
+.ui-button-text-icon-secondary .ui-button-icon-secondary,
+.ui-button-text-icons .ui-button-icon-secondary,
+.ui-button-icons-only .ui-button-icon-secondary {
+	right: .5em;
+}
+
+/* button sets */
+.ui-buttonset {
+	margin-right: 7px;
+}
+.ui-buttonset .ui-button {
+	margin-left: 0;
+	margin-right: -.3em;
+}
+
+/* workarounds */
+/* reset extra padding in Firefox, see h5bp.com/l */
+input.ui-button::-moz-focus-inner,
+button.ui-button::-moz-focus-inner {
+	border: 0;
+	padding: 0;
+}
+
+.ui-datepicker {
+	width: 17em;
+	padding: .2em .2em 0;
+	display: none;
+}
+.ui-datepicker .ui-datepicker-header {
+	position: relative;
+	padding: .2em 0;
+}
+.ui-datepicker .ui-datepicker-prev,
+.ui-datepicker .ui-datepicker-next {
+	position: absolute;
+	top: 2px;
+	width: 1.8em;
+	height: 1.8em;
+}
+.ui-datepicker .ui-datepicker-prev-hover,
+.ui-datepicker .ui-datepicker-next-hover {
+	top: 1px;
+}
+.ui-datepicker .ui-datepicker-prev {
+	left: 2px;
+}
+.ui-datepicker .ui-datepicker-next {
+	right: 2px;
+}
+.ui-datepicker .ui-datepicker-prev-hover {
+	left: 1px;
+}
+.ui-datepicker .ui-datepicker-next-hover {
+	right: 1px;
+}
+.ui-datepicker .ui-datepicker-prev span,
+.ui-datepicker .ui-datepicker-next span {
+	display: block;
+	position: absolute;
+	left: 50%;
+	margin-left: -8px;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-datepicker .ui-datepicker-title {
+	margin: 0 2.3em;
+	line-height: 1.8em;
+	text-align: center;
+}
+.ui-datepicker .ui-datepicker-title select {
+	font-size: 1em;
+	margin: 1px 0;
+}
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year {
+	width: 49%;
+}
+.ui-datepicker table {
+	width: 100%;
+	font-size: .9em;
+	border-collapse: collapse;
+	margin: 0 0 .4em;
+}
+.ui-datepicker th {
+	padding: .7em .3em;
+	text-align: center;
+	font-weight: bold;
+	border: 0;
+}
+.ui-datepicker td {
+	border: 0;
+	padding: 1px;
+}
+.ui-datepicker td span,
+.ui-datepicker td a {
+	display: block;
+	padding: .2em;
+	text-align: right;
+	text-decoration: none;
+}
+.ui-datepicker .ui-datepicker-buttonpane {
+	background-image: none;
+	margin: .7em 0 0 0;
+	padding: 0 .2em;
+	border-left: 0;
+	border-right: 0;
+	border-bottom: 0;
+}
+.ui-datepicker .ui-datepicker-buttonpane button {
+	float: right;
+	margin: .5em .2em .4em;
+	cursor: pointer;
+	padding: .2em .6em .3em .6em;
+	width: auto;
+	overflow: visible;
+}
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
+	float: left;
+}
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi {
+	width: auto;
+}
+.ui-datepicker-multi .ui-datepicker-group {
+	float: left;
+}
+.ui-datepicker-multi .ui-datepicker-group table {
+	width: 95%;
+	margin: 0 auto .4em;
+}
+.ui-datepicker-multi-2 .ui-datepicker-group {
+	width: 50%;
+}
+.ui-datepicker-multi-3 .ui-datepicker-group {
+	width: 33.3%;
+}
+.ui-datepicker-multi-4 .ui-datepicker-group {
+	width: 25%;
+}
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
+	border-left-width: 0;
+}
+.ui-datepicker-multi .ui-datepicker-buttonpane {
+	clear: left;
+}
+.ui-datepicker-row-break {
+	clear: both;
+	width: 100%;
+	font-size: 0;
+}
+
+/* RTL support */
+.ui-datepicker-rtl {
+	direction: rtl;
+}
+.ui-datepicker-rtl .ui-datepicker-prev {
+	right: 2px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next {
+	left: 2px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-prev:hover {
+	right: 1px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next:hover {
+	left: 1px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane {
+	clear: right;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button {
+	float: left;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
+.ui-datepicker-rtl .ui-datepicker-group {
+	float: right;
+}
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
+	border-right-width: 0;
+	border-left-width: 1px;
+}
+
+.ui-dialog {
+	overflow: hidden;
+	position: absolute;
+	top: 0;
+	left: 0;
+	padding: .2em;
+	outline: 0;
+}
+.ui-dialog .ui-dialog-titlebar {
+	padding: .4em 1em;
+	position: relative;
+}
+.ui-dialog .ui-dialog-title {
+	float: left;
+	margin: .1em 0;
+	white-space: nowrap;
+	width: 90%;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+.ui-dialog .ui-dialog-titlebar-close {
+	position: absolute;
+	right: .3em;
+	top: 50%;
+	width: 20px;
+	margin: -10px 0 0 0;
+	padding: 1px;
+	height: 20px;
+}
+.ui-dialog .ui-dialog-content {
+	position: relative;
+	border: 0;
+	padding: .5em 1em;
+	background: none;
+	overflow: auto;
+}
+.ui-dialog .ui-dialog-buttonpane {
+	text-align: left;
+	border-width: 1px 0 0 0;
+	background-image: none;
+	margin-top: .5em;
+	padding: .3em 1em .5em .4em;
+}
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
+	float: right;
+}
+.ui-dialog .ui-dialog-buttonpane button {
+	margin: .5em .4em .5em 0;
+	cursor: pointer;
+}
+.ui-dialog .ui-resizable-se {
+	width: 12px;
+	height: 12px;
+	right: -5px;
+	bottom: -5px;
+	background-position: 16px 16px;
+}
+.ui-draggable .ui-dialog-titlebar {
+	cursor: move;
+}
+
+.ui-menu {
+	list-style: none;
+	padding: 0;
+	margin: 0;
+	display: block;
+	outline: none;
+}
+.ui-menu .ui-menu {
+	position: absolute;
+}
+.ui-menu .ui-menu-item {
+	margin: 0;
+	display: block;
+	padding: 3px .4em;
+	cursor: pointer;
+	min-height: 0; /* support: IE7 */
+	/* support: IE10, see #8844 */
+	list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
+}
+.ui-menu .ui-menu-divider {
+	margin: 5px 0;
+	height: 0;
+	font-size: 0;
+	line-height: 0;
+	border-width: 1px 0 0 0;
+}
+.ui-menu .ui-state-focus,
+.ui-menu .ui-state-active {
+	margin: -1px;
+}
+
+/* icon support */
+.ui-menu-icons {
+	position: relative;
+}
+.ui-menu-icons .ui-menu-item {
+	position: relative;
+	padding-left: 2em;
+}
+
+/* left-aligned */
+.ui-menu .ui-icon {
+	position: absolute;
+	top: .2em;
+	left: .2em;
+}
+
+/* right-aligned */
+.ui-menu .ui-menu-icon {
+	position: relative;
+	top: -.2em;
+	float: right;
+}
+
+.ui-progressbar {
+	height: 2em;
+	text-align: left;
+	overflow: hidden;
+}
+.ui-progressbar .ui-progressbar-value {
+	margin: -1px;
+	height: 100%;
+}
+.ui-progressbar .ui-progressbar-overlay {
+	background: url("images/animated-overlay.gif");
+	height: 100%;
+	filter: alpha(opacity=25);
+	opacity: 0.25;
+}
+.ui-progressbar-indeterminate .ui-progressbar-value {
+	background-image: none;
+}
+
+.ui-resizable {
+	position: relative;
+}
+.ui-resizable-handle {
+	position: absolute;
+	font-size: 0.1px;
+	display: block;
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-resizable-disabled .ui-resizable-handle,
+.ui-resizable-autohide .ui-resizable-handle {
+	display: none;
+}
+.ui-resizable-n {
+	cursor: n-resize;
+	height: 7px;
+	width: 100%;
+	top: -5px;
+	left: 0;
+}
+.ui-resizable-s {
+	cursor: s-resize;
+	height: 7px;
+	width: 100%;
+	bottom: -5px;
+	left: 0;
+}
+.ui-resizable-e {
+	cursor: e-resize;
+	width: 7px;
+	right: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-w {
+	cursor: w-resize;
+	width: 7px;
+	left: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-se {
+	cursor: se-resize;
+	width: 12px;
+	height: 12px;
+	right: 1px;
+	bottom: 1px;
+}
+.ui-resizable-sw {
+	cursor: sw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	bottom: -5px;
+}
+.ui-resizable-nw {
+	cursor: nw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	top: -5px;
+}
+.ui-resizable-ne {
+	cursor: ne-resize;
+	width: 9px;
+	height: 9px;
+	right: -5px;
+	top: -5px;
+}
+
+.ui-selectable {
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-selectable-helper {
+	position: absolute;
+	z-index: 100;
+	border: 1px dotted black;
+}
+
+.ui-selectmenu-menu {
+	padding: 0;
+	margin: 0;
+	position: absolute;
+	top: 0;
+	left: 0;
+	display: none;
+}
+.ui-selectmenu-menu .ui-menu {
+	overflow: auto;
+	/* Support: IE7 */
+	overflow-x: hidden;
+	padding-bottom: 1px;
+}
+.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup {
+	font-size: 1em;
+	font-weight: bold;
+	line-height: 1.5;
+	padding: 2px 0.4em;
+	margin: 0.5em 0 0 0;
+	height: auto;
+	border: 0;
+}
+.ui-selectmenu-open {
+	display: block;
+}
+.ui-selectmenu-button {
+	display: inline-block;
+	overflow: hidden;
+	position: relative;
+	text-decoration: none;
+	cursor: pointer;
+}
+.ui-selectmenu-button span.ui-icon {
+	right: 0.5em;
+	left: auto;
+	margin-top: -8px;
+	position: absolute;
+	top: 50%;
+}
+.ui-selectmenu-button span.ui-selectmenu-text {
+	text-align: left;
+	padding: 0.4em 2.1em 0.4em 1em;
+	display: block;
+	line-height: 1.4;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.ui-slider {
+	position: relative;
+	text-align: left;
+}
+.ui-slider .ui-slider-handle {
+	position: absolute;
+	z-index: 2;
+	width: 1.2em;
+	height: 1.2em;
+	cursor: default;
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-slider .ui-slider-range {
+	position: absolute;
+	z-index: 1;
+	font-size: .7em;
+	display: block;
+	border: 0;
+	background-position: 0 0;
+}
+
+/* For IE8 - See #6727 */
+.ui-slider.ui-state-disabled .ui-slider-handle,
+.ui-slider.ui-state-disabled .ui-slider-range {
+	filter: inherit;
+}
+
+.ui-slider-horizontal {
+	height: .8em;
+}
+.ui-slider-horizontal .ui-slider-handle {
+	top: -.3em;
+	margin-left: -.6em;
+}
+.ui-slider-horizontal .ui-slider-range {
+	top: 0;
+	height: 100%;
+}
+.ui-slider-horizontal .ui-slider-range-min {
+	left: 0;
+}
+.ui-slider-horizontal .ui-slider-range-max {
+	right: 0;
+}
+
+.ui-slider-vertical {
+	width: .8em;
+	height: 100px;
+}
+.ui-slider-vertical .ui-slider-handle {
+	left: -.3em;
+	margin-left: 0;
+	margin-bottom: -.6em;
+}
+.ui-slider-vertical .ui-slider-range {
+	left: 0;
+	width: 100%;
+}
+.ui-slider-vertical .ui-slider-range-min {
+	bottom: 0;
+}
+.ui-slider-vertical .ui-slider-range-max {
+	top: 0;
+}
+
+.ui-spinner {
+	position: relative;
+	display: inline-block;
+	overflow: hidden;
+	padding: 0;
+	vertical-align: middle;
+}
+.ui-spinner-input {
+	border: none;
+	background: none;
+	color: inherit;
+	padding: 0;
+	margin: .2em 0;
+	vertical-align: middle;
+	margin-left: .4em;
+	margin-right: 22px;
+}
+.ui-spinner-button {
+	width: 16px;
+	height: 50%;
+	font-size: .5em;
+	padding: 0;
+	margin: 0;
+	text-align: center;
+	position: absolute;
+	cursor: default;
+	display: block;
+	overflow: hidden;
+	right: 0;
+}
+/* more specificity required here to override default borders */
+.ui-spinner a.ui-spinner-button {
+	border-top: none;
+	border-bottom: none;
+	border-right: none;
+}
+/* vertically center icon */
+.ui-spinner .ui-icon {
+	position: absolute;
+	margin-top: -8px;
+	top: 50%;
+	left: 0;
+}
+.ui-spinner-up {
+	top: 0;
+}
+.ui-spinner-down {
+	bottom: 0;
+}
+
+/* TR overrides */
+.ui-spinner .ui-icon-triangle-1-s {
+	/* need to fix icons sprite */
+	background-position: -65px -16px;
+}
+
+.ui-tabs {
+	position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+	padding: .2em;
+}
+.ui-tabs .ui-tabs-nav {
+	margin: 0;
+	padding: .2em .2em 0;
+}
+.ui-tabs .ui-tabs-nav li {
+	list-style: none;
+	float: left;
+	position: relative;
+	top: 0;
+	margin: 1px .2em 0 0;
+	border-bottom-width: 0;
+	padding: 0;
+	white-space: nowrap;
+}
+.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
+	float: left;
+	padding: .5em 1em;
+	text-decoration: none;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active {
+	margin-bottom: -1px;
+	padding-bottom: 1px;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
+	cursor: text;
+}
+.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
+	cursor: pointer;
+}
+.ui-tabs .ui-tabs-panel {
+	display: block;
+	border-width: 0;
+	padding: 1em 1.4em;
+	background: none;
+}
+
+.ui-tooltip {
+	padding: 8px;
+	position: absolute;
+	z-index: 9999;
+	max-width: 300px;
+	-webkit-box-shadow: 0 0 5px #aaa;
+	box-shadow: 0 0 5px #aaa;
+}
+body .ui-tooltip {
+	border-width: 2px;
+}
+
+/* Component containers
+----------------------------------*/
+.ui-widget {
+	font-family: Verdana,Arial,sans-serif/*{ffDefault}*/;
+	font-size: 1.1em/*{fsDefault}*/;
+}
+.ui-widget .ui-widget {
+	font-size: 1em;
+}
+.ui-widget input,
+.ui-widget select,
+.ui-widget textarea,
+.ui-widget button {
+	font-family: Verdana,Arial,sans-serif/*{ffDefault}*/;
+	font-size: 1em;
+}
+.ui-widget-content {
+	border: 1px solid #aaaaaa/*{borderColorContent}*/;
+	background: #ffffff/*{bgColorContent}*/ url("images/ui-bg_flat_75_ffffff_40x100.png")/*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/;
+	color: #222222/*{fcContent}*/;
+}
+.ui-widget-content a {
+	color: #222222/*{fcContent}*/;
+}
+.ui-widget-header {
+	border: 1px solid #aaaaaa/*{borderColorHeader}*/;
+	background: #cccccc/*{bgColorHeader}*/ url("images/ui-bg_highlight-soft_75_cccccc_1x100.png")/*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/;
+	color: #222222/*{fcHeader}*/;
+	font-weight: bold;
+}
+.ui-widget-header a {
+	color: #222222/*{fcHeader}*/;
+}
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default,
+.ui-widget-content .ui-state-default,
+.ui-widget-header .ui-state-default {
+	border: 1px solid #d3d3d3/*{borderColorDefault}*/;
+	background: #e6e6e6/*{bgColorDefault}*/ url("images/ui-bg_glass_75_e6e6e6_1x400.png")/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/;
+	font-weight: normal/*{fwDefault}*/;
+	color: #555555/*{fcDefault}*/;
+}
+.ui-state-default a,
+.ui-state-default a:link,
+.ui-state-default a:visited {
+	color: #555555/*{fcDefault}*/;
+	text-decoration: none;
+}
+.ui-state-hover,
+.ui-widget-content .ui-state-hover,
+.ui-widget-header .ui-state-hover,
+.ui-state-focus,
+.ui-widget-content .ui-state-focus,
+.ui-widget-header .ui-state-focus {
+	border: 1px solid #999999/*{borderColorHover}*/;
+	background: #dadada/*{bgColorHover}*/ url("images/ui-bg_glass_75_dadada_1x400.png")/*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/;
+	font-weight: normal/*{fwDefault}*/;
+	color: #212121/*{fcHover}*/;
+}
+.ui-state-hover a,
+.ui-state-hover a:hover,
+.ui-state-hover a:link,
+.ui-state-hover a:visited,
+.ui-state-focus a,
+.ui-state-focus a:hover,
+.ui-state-focus a:link,
+.ui-state-focus a:visited {
+	color: #212121/*{fcHover}*/;
+	text-decoration: none;
+}
+.ui-state-active,
+.ui-widget-content .ui-state-active,
+.ui-widget-header .ui-state-active {
+	border: 1px solid #aaaaaa/*{borderColorActive}*/;
+	background: #ffffff/*{bgColorActive}*/ url("images/ui-bg_glass_65_ffffff_1x400.png")/*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/;
+	font-weight: normal/*{fwDefault}*/;
+	color: #212121/*{fcActive}*/;
+}
+.ui-state-active a,
+.ui-state-active a:link,
+.ui-state-active a:visited {
+	color: #212121/*{fcActive}*/;
+	text-decoration: none;
+}
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight,
+.ui-widget-content .ui-state-highlight,
+.ui-widget-header .ui-state-highlight {
+	border: 1px solid #fcefa1/*{borderColorHighlight}*/;
+	background: #fbf9ee/*{bgColorHighlight}*/ url("images/ui-bg_glass_55_fbf9ee_1x400.png")/*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/;
+	color: #363636/*{fcHighlight}*/;
+}
+.ui-state-highlight a,
+.ui-widget-content .ui-state-highlight a,
+.ui-widget-header .ui-state-highlight a {
+	color: #363636/*{fcHighlight}*/;
+}
+.ui-state-error,
+.ui-widget-content .ui-state-error,
+.ui-widget-header .ui-state-error {
+	border: 1px solid #cd0a0a/*{borderColorError}*/;
+	background: #fef1ec/*{bgColorError}*/ url("images/ui-bg_glass_95_fef1ec_1x400.png")/*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/;
+	color: #cd0a0a/*{fcError}*/;
+}
+.ui-state-error a,
+.ui-widget-content .ui-state-error a,
+.ui-widget-header .ui-state-error a {
+	color: #cd0a0a/*{fcError}*/;
+}
+.ui-state-error-text,
+.ui-widget-content .ui-state-error-text,
+.ui-widget-header .ui-state-error-text {
+	color: #cd0a0a/*{fcError}*/;
+}
+.ui-priority-primary,
+.ui-widget-content .ui-priority-primary,
+.ui-widget-header .ui-priority-primary {
+	font-weight: bold;
+}
+.ui-priority-secondary,
+.ui-widget-content .ui-priority-secondary,
+.ui-widget-header .ui-priority-secondary {
+	opacity: .7;
+	filter:Alpha(Opacity=70);
+	font-weight: normal;
+}
+.ui-state-disabled,
+.ui-widget-content .ui-state-disabled,
+.ui-widget-header .ui-state-disabled {
+	opacity: .35;
+	filter:Alpha(Opacity=35);
+	background-image: none;
+}
+.ui-state-disabled .ui-icon {
+	filter:Alpha(Opacity=35); /* For IE8 - See #6059 */
+}
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	width: 16px;
+	height: 16px;
+}
+.ui-icon,
+.ui-widget-content .ui-icon {
+	background-image: url("images/ui-icons_222222_256x240.png")/*{iconsContent}*/;
+}
+.ui-widget-header .ui-icon {
+	background-image: url("images/ui-icons_222222_256x240.png")/*{iconsHeader}*/;
+}
+.ui-state-default .ui-icon {
+	background-image: url("images/ui-icons_888888_256x240.png")/*{iconsDefault}*/;
+}
+.ui-state-hover .ui-icon,
+.ui-state-focus .ui-icon {
+	background-image: url("images/ui-icons_454545_256x240.png")/*{iconsHover}*/;
+}
+.ui-state-active .ui-icon {
+	background-image: url("images/ui-icons_454545_256x240.png")/*{iconsActive}*/;
+}
+.ui-state-highlight .ui-icon {
+	background-image: url("images/ui-icons_2e83ff_256x240.png")/*{iconsHighlight}*/;
+}
+.ui-state-error .ui-icon,
+.ui-state-error-text .ui-icon {
+	background-image: url("images/ui-icons_cd0a0a_256x240.png")/*{iconsError}*/;
+}
+
+/* positioning */
+.ui-icon-blank { background-position: 16px 16px; }
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-left,
+.ui-corner-tl {
+	border-top-left-radius: 4px/*{cornerRadius}*/;
+}
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-right,
+.ui-corner-tr {
+	border-top-right-radius: 4px/*{cornerRadius}*/;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-left,
+.ui-corner-bl {
+	border-bottom-left-radius: 4px/*{cornerRadius}*/;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-right,
+.ui-corner-br {
+	border-bottom-right-radius: 4px/*{cornerRadius}*/;
+}
+
+/* Overlays */
+.ui-widget-overlay {
+	background: #aaaaaa/*{bgColorOverlay}*/ url("images/ui-bg_flat_0_aaaaaa_40x100.png")/*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/;
+	opacity: .3/*{opacityOverlay}*/;
+	filter: Alpha(Opacity=30)/*{opacityFilterOverlay}*/;
+}
+.ui-widget-shadow {
+	margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/;
+	padding: 8px/*{thicknessShadow}*/;
+	background: #aaaaaa/*{bgColorShadow}*/ url("images/ui-bg_flat_0_aaaaaa_40x100.png")/*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/;
+	opacity: .3/*{opacityShadow}*/;
+	filter: Alpha(Opacity=30)/*{opacityFilterShadow}*/;
+	border-radius: 8px/*{cornerRadiusShadow}*/;
+}
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/css/jquery.dataTables.css
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/css/jquery.dataTables.css	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/css/jquery.dataTables.css	(revision 12677)
@@ -0,0 +1,221 @@
+
+/*
+ * Table
+ */
+table.dataTable {
+	margin: 0 auto;
+	clear: both;
+	width: 100%;
+}
+
+table.dataTable thead th {
+	padding: 3px 18px 3px 10px;
+	border-bottom: 1px solid black;
+	font-weight: bold;
+	cursor: pointer;
+	*cursor: hand;
+}
+
+table.dataTable tfoot th {
+	padding: 3px 18px 3px 10px;
+	border-top: 1px solid black;
+	font-weight: bold;
+}
+
+table.dataTable td {
+	padding: 3px 10px;
+}
+
+table.dataTable td.center,
+table.dataTable td.dataTables_empty {
+	text-align: center;
+}
+
+table.dataTable tr.odd { background-color: #E2E4FF; }
+table.dataTable tr.even { background-color: white; }
+
+table.dataTable tr.odd td.sorting_1 { background-color: #D3D6FF; }
+table.dataTable tr.odd td.sorting_2 { background-color: #DADCFF; }
+table.dataTable tr.odd td.sorting_3 { background-color: #E0E2FF; }
+table.dataTable tr.even td.sorting_1 { background-color: #EAEBFF; }
+table.dataTable tr.even td.sorting_2 { background-color: #F2F3FF; }
+table.dataTable tr.even td.sorting_3 { background-color: #F9F9FF; }
+
+
+/*
+ * Table wrapper
+ */
+.dataTables_wrapper {
+	position: relative;
+	clear: both;
+	*zoom: 1;
+}
+
+
+/*
+ * Page length menu
+ */
+.dataTables_length {
+	float: left;
+}
+
+
+/*
+ * Filter
+ */
+.dataTables_filter {
+	float: right;
+	text-align: right;
+}
+
+
+/*
+ * Table information
+ */
+.dataTables_info {
+	clear: both;
+	float: left;
+}
+
+
+/*
+ * Pagination
+ */
+.dataTables_paginate {
+	float: right;
+	text-align: right;
+}
+
+/* Two button pagination - previous / next */
+.paginate_disabled_previous,
+.paginate_enabled_previous,
+.paginate_disabled_next,
+.paginate_enabled_next {
+	height: 19px;
+	float: left;
+	cursor: pointer;
+	*cursor: hand;
+	color: #111 !important;
+}
+.paginate_disabled_previous:hover,
+.paginate_enabled_previous:hover,
+.paginate_disabled_next:hover,
+.paginate_enabled_next:hover {
+	text-decoration: none !important;
+}
+.paginate_disabled_previous:active,
+.paginate_enabled_previous:active,
+.paginate_disabled_next:active,
+.paginate_enabled_next:active {
+	outline: none;
+}
+
+.paginate_disabled_previous,
+.paginate_disabled_next {
+	color: #666 !important;
+}
+.paginate_disabled_previous,
+.paginate_enabled_previous {
+	padding-left: 23px;
+}
+.paginate_disabled_next,
+.paginate_enabled_next {
+	padding-right: 23px;
+	margin-left: 10px;
+}
+
+.paginate_enabled_previous { background: url('../images/back_enabled.png') no-repeat top left; }
+.paginate_enabled_previous:hover { background: url('../images/back_enabled_hover.png') no-repeat top left; }
+.paginate_disabled_previous { background: url('../images/back_disabled.png') no-repeat top left; }
+
+.paginate_enabled_next { background: url('../images/forward_enabled.png') no-repeat top right; }
+.paginate_enabled_next:hover { background: url('../images/forward_enabled_hover.png') no-repeat top right; }
+.paginate_disabled_next { background: url('../images/forward_disabled.png') no-repeat top right; }
+
+/* Full number pagination */
+.paging_full_numbers {
+	height: 22px;
+	line-height: 22px;
+}
+.paging_full_numbers a:active {
+	outline: none
+}
+.paging_full_numbers a:hover {
+	text-decoration: none;
+}
+
+.paging_full_numbers a.paginate_button,
+.paging_full_numbers a.paginate_active {
+	border: 1px solid #aaa;
+	-webkit-border-radius: 5px;
+	-moz-border-radius: 5px;
+	border-radius: 5px;
+	padding: 2px 5px;
+	margin: 0 3px;
+	cursor: pointer;
+	*cursor: hand;
+	color: #333 !important;
+}
+
+.paging_full_numbers a.paginate_button {
+	background-color: #ddd;
+}
+
+.paging_full_numbers a.paginate_button:hover {
+	background-color: #ccc;
+	text-decoration: none !important;
+}
+
+.paging_full_numbers a.paginate_active {
+	background-color: #99B3FF;
+}
+
+
+/*
+ * Processing indicator
+ */
+.dataTables_processing {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	width: 250px;
+	height: 30px;
+	margin-left: -125px;
+	margin-top: -15px;
+	padding: 14px 0 2px 0;
+	border: 1px solid #ddd;
+	text-align: center;
+	color: #999;
+	font-size: 14px;
+	background-color: white;
+}
+
+
+/*
+ * Sorting
+ */
+.sorting { background: url('../images/sort_both.png') no-repeat center right; }
+.sorting_asc { background: url('../images/sort_asc.png') no-repeat center right; }
+.sorting_desc { background: url('../images/sort_desc.png') no-repeat center right; }
+
+.sorting_asc_disabled { background: url('../images/sort_asc_disabled.png') no-repeat center right; }
+.sorting_desc_disabled { background: url('../images/sort_desc_disabled.png') no-repeat center right; }
+ 
+table.dataTable thead th:active,
+table.dataTable thead td:active {
+	outline: none;
+}
+
+
+/*
+ * Scrolling
+ */
+.dataTables_scroll {
+	clear: both;
+}
+
+.dataTables_scrollBody {
+	*margin-top: -1px;
+	-webkit-overflow-scrolling: touch;
+}
+
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/file-upload.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/file-upload.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/file-upload.js	(revision 12677)
@@ -0,0 +1,21 @@
+$(document)
+	.on('change', '.btn-file :file', function() {
+		var input = $(this),
+		numFiles = input.get(0).files ? input.get(0).files.length : 1,
+		label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
+		input.trigger('fileselect', [numFiles, label]);
+});
+
+$(document).ready( function() {
+	$('.btn-file :file').on('fileselect', function(event, numFiles, label) {
+
+		var input = $(this).parents('.input-group').find(':text'),
+			log = numFiles > 1 ? numFiles + ' files selected' : label;
+
+		if( input.length ) {
+			input.val(log);
+		} else {
+			if( log ) alert(log);
+		}
+	});
+});
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-activate-tab.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-activate-tab.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-activate-tab.js	(revision 12677)
@@ -0,0 +1,10 @@
+$(document).ready(function(){
+
+/*
+Activate current tab, if any, but jump to top, if hash value contains '-top'
+*/
+
+  var anchor = window.location.hash.replace('-top','');
+  $('a[href="' + anchor + '"]').trigger('click');
+
+});
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-datatables.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-datatables.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-datatables.js	(revision 12677)
@@ -0,0 +1,32 @@
+$(document).ready(function(){
+
+  $('.dataTable').dataTable( {
+    "iDisplayLength": 1000,
+    "iDisplayEnd": 1000,
+    "aLengthMenu": [ 50, 100, 1000, 5000 ],
+  } );
+
+	$('.dataTableFiles').dataTable( {
+    "aaSorting": [[ 2, "desc" ]],
+    "iDisplayLength": 1000,
+    "iDisplayEnd": 1000,
+    "aLengthMenu": [ 50, 100, 1000, 5000 ],
+	} );
+
+	$('.dataTableManage').dataTable( {
+    "aaSorting": [[ 1, "asc" ]],
+    "aoColumnDefs":[{ "bSortable": false, "aTargets": [ 0 ] }],
+    "iDisplayLength": 1000,
+    "iDisplayEnd": 1000,
+    "aLengthMenu": [ 50, 100, 1000, 5000 ],
+	} );
+
+	$('.dataTableManageFiles').dataTable( {
+    "aaSorting": [[ 3, "desc" ]],
+    "aoColumnDefs":[{ "bSortable": false, "aTargets": [ 0 ] }],
+    "iDisplayLength": 1000,
+    "iDisplayEnd": 1000,
+    "aLengthMenu": [ 50, 100, 1000, 5000 ],
+	} );
+
+});
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-datepicker.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-datepicker.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-datepicker.js	(revision 12677)
@@ -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.ikoba/branches/uli-payments/layout/static/js/ikoba-functions.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-functions.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-functions.js	(revision 12677)
@@ -0,0 +1,22 @@
+var RecaptchaOptions = {
+    theme : 'white'
+ };
+
+function renderCurrentYear() {
+	var mydate=new Date()
+	var year=mydate.getYear()
+	if (year < 1000)
+	year+=1900
+	var day=mydate.getDay()
+	var month=mydate.getMonth()
+	var daym=mydate.getDate()
+	if (daym<10)
+	daym="0"+daym
+	document.write(year)
+}
+
+function toggle(source, name) {
+  checkboxes = document.getElementsByName(name);
+  for(var i in checkboxes)
+    checkboxes[i].checked = source.checked;
+}
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-scroll.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-scroll.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-scroll.js	(revision 12677)
@@ -0,0 +1,45 @@
+(function($){
+	$.fn.BottomTop = function(pos,cssClass,anchor) {
+
+		var easingType = 'easeInOutCubic';
+		var min = -1;
+		var inDelay = 500;
+		var outDelay = 500;
+		var scrollSpeed = 750;
+		var containerIDhash = '#' + pos;
+
+		// Auto Link
+		$('body').append('<a href="#'+anchor+'" id="'+pos+'" class="'+cssClass+'" />');
+    if (anchor == 'bottom') {
+  		$(containerIDhash).hide().click(function(){
+  			$('html, body').animate({scrollTop:$(bottom).offset().top}, scrollSpeed, easingType);
+  			return false;
+  		})
+      }
+    if (anchor == '') {
+  		$(containerIDhash).hide().click(function(){
+  			$('html, body').animate({scrollTop:0}, scrollSpeed, easingType);
+  			return false;
+  		})
+      }
+
+		$(window).scroll(function() {
+			var sd = $(window).scrollTop();
+
+			if ( sd > min )
+				$(containerIDhash).fadeIn(inDelay);
+			else
+				$(containerIDhash).fadeOut(Outdelay);
+		});
+
+};
+})(jQuery);
+
+
+$(document).ready(function(){
+	$('.top').BottomTop('toTop-right','scroll-up','');
+});
+
+$(document).ready(function(){
+	$('.bottom').BottomTop('toBottom-right','scroll-down','bottom');
+});
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-scrollto.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-scrollto.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-scrollto.js	(revision 12677)
@@ -0,0 +1,12 @@
+/* After loading the page, softly scroll to the #ikoba-scroll-target tag,
+   if it exists */
+$(document).ready(function (){
+    var scroll_target = $('#ikoba-scroll-target');
+    if (scroll_target.length) {
+	$('html,body').animate(
+            {
+		scrollTop: scroll_target.offset().top
+		    - $(window).height() + 2 * scroll_target.height()
+            })
+    }
+});
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-tooltip.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-tooltip.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/ikoba-tooltip.js	(revision 12677)
@@ -0,0 +1,3 @@
+$(function () {
+  $("[data-toggle=tooltip]").tooltip();
+});
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/jquery-ui.min.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/jquery-ui.min.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/jquery-ui.min.js	(revision 12677)
@@ -0,0 +1,7 @@
+/*! jQuery UI - v1.10.4 - 2014-01-17
+* http://jqueryui.com
+* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.position.js, jquery.ui.accordion.js, jquery.ui.autocomplete.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.effect.js, jquery.ui.effect-blind.js, jquery.ui.effect-bounce.js, jquery.ui.effect-clip.js, jquery.ui.effect-drop.js, jquery.ui.effect-explode.js, jquery.ui.effect-fade.js, jquery.ui.effect-fold.js, jquery.ui.effect-highlight.js, jquery.ui.effect-pulsate.js, jquery.ui.effect-scale.js, jquery.ui.effect-shake.js, jquery.ui.effect-slide.js, jquery.ui.effect-transfer.js, jquery.ui.menu.js, jquery.ui.progressbar.js, jquery.ui.resizable.js, jquery.ui.selectable.js, jquery.ui.slider.js, jquery.ui.sortable.js, jquery.ui.spinner.js, jquery.ui.tabs.js, jquery.ui.tooltip.js
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+(function(e,t){function i(t,i){var s,a,o,r=t.nodeName.toLowerCase();return"area"===r?(s=t.parentNode,a=s.name,t.href&&a&&"map"===s.nodeName.toLowerCase()?(o=e("img[usemap=#"+a+"]")[0],!!o&&n(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||i:i)&&n(t)}function n(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var s=0,a=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.4",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,n){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),n&&n.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var n,s,a=e(this[0]);a.length&&a[0]!==document;){if(n=a.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(s=parseInt(a.css("zIndex"),10),!isNaN(s)&&0!==s))return s;a=a.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++s)})},removeUniqueId:function(){return this.each(function(){a.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,n){return!!e.data(t,n[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),s=isNaN(n);return(s||n>=0)&&i(t,!s)}}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(i,n){function s(t,i,n,s){return e.each(a,function(){i-=parseFloat(e.css(t,"padding"+this))||0,n&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var a="Width"===n?["Left","Right"]:["Top","Bottom"],o=n.toLowerCase(),r={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+n]=function(i){return i===t?r["inner"+n].call(this):this.each(function(){e(this).css(o,s(this,i)+"px")})},e.fn["outer"+n]=function(t,i){return"number"!=typeof t?r["outer"+n].call(this,t):this.each(function(){e(this).css(o,s(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,n){var s,a=e.ui[t].prototype;for(s in n)a.plugins[s]=a.plugins[s]||[],a.plugins[s].push([i,n[s]])},call:function(e,t,i){var n,s=e.plugins[t];if(s&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(n=0;s.length>n;n++)e.options[s[n][0]]&&s[n][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var n=i&&"left"===i?"scrollLeft":"scrollTop",s=!1;return t[n]>0?!0:(t[n]=1,s=t[n]>0,t[n]=0,s)}})})(jQuery);(function(t,e){var i=0,s=Array.prototype.slice,n=t.cleanData;t.cleanData=function(e){for(var i,s=0;null!=(i=e[s]);s++)try{t(i).triggerHandler("remove")}catch(o){}n(e)},t.widget=function(i,s,n){var o,a,r,h,l={},c=i.split(".")[0];i=i.split(".")[1],o=c+"-"+i,n||(n=s,s=t.Widget),t.expr[":"][o.toLowerCase()]=function(e){return!!t.data(e,o)},t[c]=t[c]||{},a=t[c][i],r=t[c][i]=function(t,i){return this._createWidget?(arguments.length&&this._createWidget(t,i),e):new r(t,i)},t.extend(r,a,{version:n.version,_proto:t.extend({},n),_childConstructors:[]}),h=new s,h.options=t.widget.extend({},h.options),t.each(n,function(i,n){return t.isFunction(n)?(l[i]=function(){var t=function(){return s.prototype[i].apply(this,arguments)},e=function(t){return s.prototype[i].apply(this,t)};return function(){var i,s=this._super,o=this._superApply;return this._super=t,this._superApply=e,i=n.apply(this,arguments),this._super=s,this._superApply=o,i}}(),e):(l[i]=n,e)}),r.prototype=t.widget.extend(h,{widgetEventPrefix:a?h.widgetEventPrefix||i:i},l,{constructor:r,namespace:c,widgetName:i,widgetFullName:o}),a?(t.each(a._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,r,i._proto)}),delete a._childConstructors):s._childConstructors.push(r),t.widget.bridge(i,r)},t.widget.extend=function(i){for(var n,o,a=s.call(arguments,1),r=0,h=a.length;h>r;r++)for(n in a[r])o=a[r][n],a[r].hasOwnProperty(n)&&o!==e&&(i[n]=t.isPlainObject(o)?t.isPlainObject(i[n])?t.widget.extend({},i[n],o):t.widget.extend({},o):o);return i},t.widget.bridge=function(i,n){var o=n.prototype.widgetFullName||i;t.fn[i]=function(a){var r="string"==typeof a,h=s.call(arguments,1),l=this;return a=!r&&h.length?t.widget.extend.apply(null,[a].concat(h)):a,r?this.each(function(){var s,n=t.data(this,o);return n?t.isFunction(n[a])&&"_"!==a.charAt(0)?(s=n[a].apply(n,h),s!==n&&s!==e?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):e):t.error("no such method '"+a+"' for "+i+" widget instance"):t.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var e=t.data(this,o);e?e.option(a||{})._init():t.data(this,o,new n(a,this))}),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this.bindings=t(),this.hoverable=t(),this.focusable=t(),s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(i,s){var n,o,a,r=i;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof i)if(r={},n=i.split("."),i=n.shift(),n.length){for(o=r[i]=t.widget.extend({},this.options[i]),a=0;n.length-1>a;a++)o[n[a]]=o[n[a]]||{},o=o[n[a]];if(i=n.pop(),1===arguments.length)return o[i]===e?null:o[i];o[i]=s}else{if(1===arguments.length)return this.options[i]===e?null:this.options[i];r[i]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!e).attr("aria-disabled",e),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var o,a=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=o=t(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,o=this.widget()),t.each(n,function(n,r){function h(){return i||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?a[r]:r).apply(a,arguments):e}"string"!=typeof r&&(h.guid=r.guid=r.guid||h.guid||t.guid++);var l=n.match(/^(\w+)\s*(.*)$/),c=l[1]+a.eventNamespace,u=l[2];u?o.delegate(u,c,h):s.bind(c,h)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(e).undelegate(e)},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}})})(jQuery);(function(t){var e=!1;t(document).mouseup(function(){e=!1}),t.widget("ui.mouse",{version:"1.10.4",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.bind("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).bind("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):undefined}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(i){if(!e){this._mouseStarted&&this._mouseUp(i),this._mouseDownEvent=i;var s=this,n=1===i.which,a="string"==typeof this.options.cancel&&i.target.nodeName?t(i.target).closest(this.options.cancel).length:!1;return n&&!a&&this._mouseCapture(i)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){s.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(i)&&this._mouseDelayMet(i)&&(this._mouseStarted=this._mouseStart(i)!==!1,!this._mouseStarted)?(i.preventDefault(),!0):(!0===t.data(i.target,this.widgetName+".preventClickEvent")&&t.removeData(i.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return s._mouseMove(t)},this._mouseUpDelegate=function(t){return s._mouseUp(t)},t(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),i.preventDefault(),e=!0,!0)):!0}},_mouseMove:function(e){return t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button?this._mouseUp(e):this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){return t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),!1},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.width<e.element[0].scrollWidth,a="scroll"===s||"auto"===s&&e.height<e.element[0].scrollHeight;return{width:a?t.position.scrollbarWidth():0,height:n?t.position.scrollbarWidth():0}},getWithinInfo:function(e){var i=t(e||window),s=t.isWindow(i[0]),n=!!i[0]&&9===i[0].nodeType;return{element:i,isWindow:s,isDocument:n,offset:i.offset()||{left:0,top:0},scrollLeft:i.scrollLeft(),scrollTop:i.scrollTop(),width:s?i.width():i.outerWidth(),height:s?i.height():i.outerHeight()}}},t.fn.position=function(e){if(!e||!e.of)return f.apply(this,arguments);e=t.extend({},e);var a,p,g,m,v,_,b=t(e.of),y=t.position.getWithinInfo(e.within),k=t.position.getScrollInfo(y),w=(e.collision||"flip").split(" "),D={};return _=n(b),b[0].preventDefault&&(e.at="left top"),p=_.width,g=_.height,m=_.offset,v=t.extend({},m),t.each(["my","at"],function(){var t,i,s=(e[this]||"").split(" ");1===s.length&&(s=h.test(s[0])?s.concat(["center"]):c.test(s[0])?["center"].concat(s):["center","center"]),s[0]=h.test(s[0])?s[0]:"center",s[1]=c.test(s[1])?s[1]:"center",t=u.exec(s[0]),i=u.exec(s[1]),D[this]=[t?t[0]:0,i?i[0]:0],e[this]=[d.exec(s[0])[0],d.exec(s[1])[0]]}),1===w.length&&(w[1]=w[0]),"right"===e.at[0]?v.left+=p:"center"===e.at[0]&&(v.left+=p/2),"bottom"===e.at[1]?v.top+=g:"center"===e.at[1]&&(v.top+=g/2),a=i(D.at,p,g),v.left+=a[0],v.top+=a[1],this.each(function(){var n,h,c=t(this),u=c.outerWidth(),d=c.outerHeight(),f=s(this,"marginLeft"),_=s(this,"marginTop"),x=u+f+s(this,"marginRight")+k.width,C=d+_+s(this,"marginBottom")+k.height,M=t.extend({},v),T=i(D.my,c.outerWidth(),c.outerHeight());"right"===e.my[0]?M.left-=u:"center"===e.my[0]&&(M.left-=u/2),"bottom"===e.my[1]?M.top-=d:"center"===e.my[1]&&(M.top-=d/2),M.left+=T[0],M.top+=T[1],t.support.offsetFractions||(M.left=l(M.left),M.top=l(M.top)),n={marginLeft:f,marginTop:_},t.each(["left","top"],function(i,s){t.ui.position[w[i]]&&t.ui.position[w[i]][s](M,{targetWidth:p,targetHeight:g,elemWidth:u,elemHeight:d,collisionPosition:n,collisionWidth:x,collisionHeight:C,offset:[a[0]+T[0],a[1]+T[1]],my:e.my,at:e.at,within:y,elem:c})}),e.using&&(h=function(t){var i=m.left-M.left,s=i+p-u,n=m.top-M.top,a=n+g-d,l={target:{element:b,left:m.left,top:m.top,width:p,height:g},element:{element:c,left:M.left,top:M.top,width:u,height:d},horizontal:0>s?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(l.horizontal="center"),d>g&&g>r(n+a)&&(l.vertical="middle"),l.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,l)}),c.offset(t.extend(M,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-a-n;e.collisionWidth>a?l>0&&0>=h?(i=t.left+l+e.collisionWidth-a-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+a-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-a-n;e.collisionHeight>a?l>0&&0>=h?(i=t.top+l+e.collisionHeight-a-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+a-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-o-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-o-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-o-a,t.top+p+f+g>c&&(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,t.top+p+f+g>u&&(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(e){var t=0,i={},a={};i.height=i.paddingTop=i.paddingBottom=i.borderTopWidth=i.borderBottomWidth="hide",a.height=a.paddingTop=a.paddingBottom=a.borderTopWidth=a.borderBottomWidth="show",e.widget("ui.accordion",{version:"1.10.4",options:{active:0,animate:{},collapsible:!1,event:"click",header:"> li > :first-child,> :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},_create:function(){var t=this.options;this.prevShow=this.prevHide=e(),this.element.addClass("ui-accordion ui-widget ui-helper-reset").attr("role","tablist"),t.collapsible||t.active!==!1&&null!=t.active||(t.active=0),this._processPanels(),0>t.active&&(t.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():e(),content:this.active.length?this.active.next():e()}},_createIcons:function(){var t=this.options.icons;t&&(e("<span>").addClass("ui-accordion-header-icon ui-icon "+t.header).prependTo(this.headers),this.active.children(".ui-accordion-header-icon").removeClass(t.header).addClass(t.activeHeader),this.headers.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.removeClass("ui-accordion-icons").children(".ui-accordion-header-icon").remove()},_destroy:function(){var e;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.removeClass("ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("aria-controls").removeAttr("tabIndex").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),this._destroyIcons(),e=this.headers.next().css("display","").removeAttr("role").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),"content"!==this.options.heightStyle&&e.css("height","")},_setOption:function(e,t){return"active"===e?(this._activate(t),undefined):("event"===e&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(t)),this._super(e,t),"collapsible"!==e||t||this.options.active!==!1||this._activate(0),"icons"===e&&(this._destroyIcons(),t&&this._createIcons()),"disabled"===e&&this.headers.add(this.headers.next()).toggleClass("ui-state-disabled",!!t),undefined)},_keydown:function(t){if(!t.altKey&&!t.ctrlKey){var i=e.ui.keyCode,a=this.headers.length,s=this.headers.index(t.target),n=!1;switch(t.keyCode){case i.RIGHT:case i.DOWN:n=this.headers[(s+1)%a];break;case i.LEFT:case i.UP:n=this.headers[(s-1+a)%a];break;case i.SPACE:case i.ENTER:this._eventHandler(t);break;case i.HOME:n=this.headers[0];break;case i.END:n=this.headers[a-1]}n&&(e(t.target).attr("tabIndex",-1),e(n).attr("tabIndex",0),n.focus(),t.preventDefault())}},_panelKeyDown:function(t){t.keyCode===e.ui.keyCode.UP&&t.ctrlKey&&e(t.currentTarget).prev().focus()},refresh:function(){var t=this.options;this._processPanels(),t.active===!1&&t.collapsible===!0||!this.headers.length?(t.active=!1,this.active=e()):t.active===!1?this._activate(0):this.active.length&&!e.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(t.active=!1,this.active=e()):this._activate(Math.max(0,t.active-1)):t.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){this.headers=this.element.find(this.options.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all"),this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom").filter(":not(.ui-accordion-content-active)").hide()},_refresh:function(){var i,a=this.options,s=a.heightStyle,n=this.element.parent(),r=this.accordionId="ui-accordion-"+(this.element.attr("id")||++t);this.active=this._findActive(a.active).addClass("ui-accordion-header-active ui-state-active ui-corner-top").removeClass("ui-corner-all"),this.active.next().addClass("ui-accordion-content-active").show(),this.headers.attr("role","tab").each(function(t){var i=e(this),a=i.attr("id"),s=i.next(),n=s.attr("id");a||(a=r+"-header-"+t,i.attr("id",a)),n||(n=r+"-panel-"+t,s.attr("id",n)),i.attr("aria-controls",n),s.attr("aria-labelledby",a)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(a.event),"fill"===s?(i=n.height(),this.element.siblings(":visible").each(function(){var t=e(this),a=t.css("position");"absolute"!==a&&"fixed"!==a&&(i-=t.outerHeight(!0))}),this.headers.each(function(){i-=e(this).outerHeight(!0)}),this.headers.next().each(function(){e(this).height(Math.max(0,i-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):"auto"===s&&(i=0,this.headers.next().each(function(){i=Math.max(i,e(this).css("height","").height())}).height(i))},_activate:function(t){var i=this._findActive(t)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:e.noop}))},_findActive:function(t){return"number"==typeof t?this.headers.eq(t):e()},_setupEvents:function(t){var i={keydown:"_keydown"};t&&e.each(t.split(" "),function(e,t){i[t]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(t){var i=this.options,a=this.active,s=e(t.currentTarget),n=s[0]===a[0],r=n&&i.collapsible,o=r?e():s.next(),h=a.next(),d={oldHeader:a,oldPanel:h,newHeader:r?e():s,newPanel:o};t.preventDefault(),n&&!i.collapsible||this._trigger("beforeActivate",t,d)===!1||(i.active=r?!1:this.headers.index(s),this.active=n?e():s,this._toggle(d),a.removeClass("ui-accordion-header-active ui-state-active"),i.icons&&a.children(".ui-accordion-header-icon").removeClass(i.icons.activeHeader).addClass(i.icons.header),n||(s.removeClass("ui-corner-all").addClass("ui-accordion-header-active ui-state-active ui-corner-top"),i.icons&&s.children(".ui-accordion-header-icon").removeClass(i.icons.header).addClass(i.icons.activeHeader),s.next().addClass("ui-accordion-content-active")))},_toggle:function(t){var i=t.newPanel,a=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=a,this.options.animate?this._animate(i,a,t):(a.hide(),i.show(),this._toggleComplete(t)),a.attr({"aria-hidden":"true"}),a.prev().attr("aria-selected","false"),i.length&&a.length?a.prev().attr({tabIndex:-1,"aria-expanded":"false"}):i.length&&this.headers.filter(function(){return 0===e(this).attr("tabIndex")}).attr("tabIndex",-1),i.attr("aria-hidden","false").prev().attr({"aria-selected":"true",tabIndex:0,"aria-expanded":"true"})},_animate:function(e,t,s){var n,r,o,h=this,d=0,c=e.length&&(!t.length||e.index()<t.index()),l=this.options.animate||{},u=c&&l.down||l,v=function(){h._toggleComplete(s)};return"number"==typeof u&&(o=u),"string"==typeof u&&(r=u),r=r||u.easing||l.easing,o=o||u.duration||l.duration,t.length?e.length?(n=e.show().outerHeight(),t.animate(i,{duration:o,easing:r,step:function(e,t){t.now=Math.round(e)}}),e.hide().animate(a,{duration:o,easing:r,complete:v,step:function(e,i){i.now=Math.round(e),"height"!==i.prop?d+=i.now:"content"!==h.options.heightStyle&&(i.now=Math.round(n-t.outerHeight()-d),d=0)}}),undefined):t.animate(i,o,r,v):e.animate(a,o,r,v)},_toggleComplete:function(e){var t=e.oldPanel;t.removeClass("ui-accordion-content-active").prev().removeClass("ui-corner-top").addClass("ui-corner-all"),t.length&&(t.parent()[0].className=t.parent()[0].className),this._trigger("activate",null,e)}})})(jQuery);(function(e){e.widget("ui.autocomplete",{version:"1.10.4",defaultElement:"<input>",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,undefined;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),undefined):(this._searchTimeout(e),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(e),this._change(e),undefined)}}),this._initSource(),this.menu=e("<ul>").addClass("ui-autocomplete ui-front").appendTo(this._appendTo()).menu({role:null}).hide().data("ui-menu"),this._on(this.menu.element,{mousedown:function(t){t.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur});var i=this.menu.element[0];e(t.target).closest(".ui-menu-item").length||this._delay(function(){var t=this;this.document.one("mousedown",function(s){s.target===t.element[0]||s.target===i||e.contains(i,s.target)||t.close()})})},menufocus:function(t,i){if(this.isNewMenu&&(this.isNewMenu=!1,t.originalEvent&&/^mouse/.test(t.originalEvent.type)))return this.menu.blur(),this.document.one("mousemove",function(){e(t.target).trigger(t.originalEvent)}),undefined;var s=i.item.data("ui-autocomplete-item");!1!==this._trigger("focus",t,{item:s})?t.originalEvent&&/^key/.test(t.originalEvent.type)&&this._value(s.value):this.liveRegion.text(s.value)},menuselect:function(e,t){var i=t.item.data("ui-autocomplete-item"),s=this.previous;this.element[0]!==this.document[0].activeElement&&(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s,this.selectedItem=i})),!1!==this._trigger("select",e,{item:i})&&this._value(i.value),this.term=this._value(),this.close(e),this.selectedItem=i}}),this.liveRegion=e("<span>",{role:"status","aria-live":"polite"}).addClass("ui-helper-hidden-accessible").insertBefore(this.element),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(e,t){this._super(e,t),"source"===e&&this._initSource(),"appendTo"===e&&this.menu.element.appendTo(this._appendTo()),"disabled"===e&&t&&this.xhr&&this.xhr.abort()},_appendTo:function(){var t=this.options.appendTo;return t&&(t=t.jquery||t.nodeType?e(t):this.document.find(t).eq(0)),t||(t=this.element.closest(".ui-front")),t.length||(t=this.document[0].body),t},_initSource:function(){var t,i,s=this;e.isArray(this.options.source)?(t=this.options.source,this.source=function(i,s){s(e.ui.autocomplete.filter(t,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(t,n){s.xhr&&s.xhr.abort(),s.xhr=e.ajax({url:i,data:t,dataType:"json",success:function(e){n(e)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(e){clearTimeout(this.searching),this.searching=this._delay(function(){this.term!==this._value()&&(this.selectedItem=null,this.search(null,e))},this.options.delay)},search:function(e,t){return e=null!=e?e:this._value(),this.term=this._value(),e.length<this.options.minLength?this.close(t):this._trigger("search",t)!==!1?this._search(e):undefined},_search:function(e){this.pending++,this.element.addClass("ui-autocomplete-loading"),this.cancelSearch=!1,this.source({term:e},this._response())},_response:function(){var t=++this.requestIndex;return e.proxy(function(e){t===this.requestIndex&&this.__response(e),this.pending--,this.pending||this.element.removeClass("ui-autocomplete-loading")},this)},__response:function(e){e&&(e=this._normalize(e)),this._trigger("response",null,{content:e}),!this.options.disabled&&e&&e.length&&!this.cancelSearch?(this._suggest(e),this._trigger("open")):this._close()},close:function(e){this.cancelSearch=!0,this._close(e)},_close:function(e){this.menu.element.is(":visible")&&(this.menu.element.hide(),this.menu.blur(),this.isNewMenu=!0,this._trigger("close",e))},_change:function(e){this.previous!==this._value()&&this._trigger("change",e,{item:this.selectedItem})},_normalize:function(t){return t.length&&t[0].label&&t[0].value?t:e.map(t,function(t){return"string"==typeof t?{label:t,value:t}:e.extend({label:t.label||t.value,value:t.value||t.label},t)})},_suggest:function(t){var i=this.menu.element.empty();this._renderMenu(i,t),this.isNewMenu=!0,this.menu.refresh(),i.show(),this._resizeMenu(),i.position(e.extend({of:this.element},this.options.position)),this.options.autoFocus&&this.menu.next()},_resizeMenu:function(){var e=this.menu.element;e.outerWidth(Math.max(e.width("").outerWidth()+1,this.element.outerWidth()))},_renderMenu:function(t,i){var s=this;e.each(i,function(e,i){s._renderItemData(t,i)})},_renderItemData:function(e,t){return this._renderItem(e,t).data("ui-autocomplete-item",t)},_renderItem:function(t,i){return e("<li>").append(e("<a>").text(i.label)).appendTo(t)},_move:function(e,t){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(e)||this.menu.isLastItem()&&/^next/.test(e)?(this._value(this.term),this.menu.blur(),undefined):(this.menu[e](t),undefined):(this.search(null,t),undefined)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(e,t){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(e,t),t.preventDefault())}}),e.extend(e.ui.autocomplete,{escapeRegex:function(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(t,i){var s=RegExp(e.ui.autocomplete.escapeRegex(i),"i");return e.grep(t,function(e){return s.test(e.label||e.value||e)})}}),e.widget("ui.autocomplete",e.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(e){return e+(e>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var t;this._superApply(arguments),this.options.disabled||this.cancelSearch||(t=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.text(t))}})})(jQuery);(function(e){var t,i="ui-button ui-widget ui-state-default ui-corner-all",n="ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",s=function(){var t=e(this);setTimeout(function(){t.find(":ui-button").button("refresh")},1)},a=function(t){var i=t.name,n=t.form,s=e([]);return i&&(i=i.replace(/'/g,"\\'"),s=n?e(n).find("[name='"+i+"']"):e("[name='"+i+"']",t.ownerDocument).filter(function(){return!this.form})),s};e.widget("ui.button",{version:"1.10.4",defaultElement:"<button>",options:{disabled:null,text:!0,label:null,icons:{primary:null,secondary:null}},_create:function(){this.element.closest("form").unbind("reset"+this.eventNamespace).bind("reset"+this.eventNamespace,s),"boolean"!=typeof this.options.disabled?this.options.disabled=!!this.element.prop("disabled"):this.element.prop("disabled",this.options.disabled),this._determineButtonType(),this.hasTitle=!!this.buttonElement.attr("title");var n=this,o=this.options,r="checkbox"===this.type||"radio"===this.type,h=r?"":"ui-state-active";null===o.label&&(o.label="input"===this.type?this.buttonElement.val():this.buttonElement.html()),this._hoverable(this.buttonElement),this.buttonElement.addClass(i).attr("role","button").bind("mouseenter"+this.eventNamespace,function(){o.disabled||this===t&&e(this).addClass("ui-state-active")}).bind("mouseleave"+this.eventNamespace,function(){o.disabled||e(this).removeClass(h)}).bind("click"+this.eventNamespace,function(e){o.disabled&&(e.preventDefault(),e.stopImmediatePropagation())}),this._on({focus:function(){this.buttonElement.addClass("ui-state-focus")},blur:function(){this.buttonElement.removeClass("ui-state-focus")}}),r&&this.element.bind("change"+this.eventNamespace,function(){n.refresh()}),"checkbox"===this.type?this.buttonElement.bind("click"+this.eventNamespace,function(){return o.disabled?!1:undefined}):"radio"===this.type?this.buttonElement.bind("click"+this.eventNamespace,function(){if(o.disabled)return!1;e(this).addClass("ui-state-active"),n.buttonElement.attr("aria-pressed","true");var t=n.element[0];a(t).not(t).map(function(){return e(this).button("widget")[0]}).removeClass("ui-state-active").attr("aria-pressed","false")}):(this.buttonElement.bind("mousedown"+this.eventNamespace,function(){return o.disabled?!1:(e(this).addClass("ui-state-active"),t=this,n.document.one("mouseup",function(){t=null}),undefined)}).bind("mouseup"+this.eventNamespace,function(){return o.disabled?!1:(e(this).removeClass("ui-state-active"),undefined)}).bind("keydown"+this.eventNamespace,function(t){return o.disabled?!1:((t.keyCode===e.ui.keyCode.SPACE||t.keyCode===e.ui.keyCode.ENTER)&&e(this).addClass("ui-state-active"),undefined)}).bind("keyup"+this.eventNamespace+" blur"+this.eventNamespace,function(){e(this).removeClass("ui-state-active")}),this.buttonElement.is("a")&&this.buttonElement.keyup(function(t){t.keyCode===e.ui.keyCode.SPACE&&e(this).click()})),this._setOption("disabled",o.disabled),this._resetButton()},_determineButtonType:function(){var e,t,i;this.type=this.element.is("[type=checkbox]")?"checkbox":this.element.is("[type=radio]")?"radio":this.element.is("input")?"input":"button","checkbox"===this.type||"radio"===this.type?(e=this.element.parents().last(),t="label[for='"+this.element.attr("id")+"']",this.buttonElement=e.find(t),this.buttonElement.length||(e=e.length?e.siblings():this.element.siblings(),this.buttonElement=e.filter(t),this.buttonElement.length||(this.buttonElement=e.find(t))),this.element.addClass("ui-helper-hidden-accessible"),i=this.element.is(":checked"),i&&this.buttonElement.addClass("ui-state-active"),this.buttonElement.prop("aria-pressed",i)):this.buttonElement=this.element},widget:function(){return this.buttonElement},_destroy:function(){this.element.removeClass("ui-helper-hidden-accessible"),this.buttonElement.removeClass(i+" ui-state-active "+n).removeAttr("role").removeAttr("aria-pressed").html(this.buttonElement.find(".ui-button-text").html()),this.hasTitle||this.buttonElement.removeAttr("title")},_setOption:function(e,t){return this._super(e,t),"disabled"===e?(this.element.prop("disabled",!!t),t&&this.buttonElement.removeClass("ui-state-focus"),undefined):(this._resetButton(),undefined)},refresh:function(){var t=this.element.is("input, button")?this.element.is(":disabled"):this.element.hasClass("ui-button-disabled");t!==this.options.disabled&&this._setOption("disabled",t),"radio"===this.type?a(this.element[0]).each(function(){e(this).is(":checked")?e(this).button("widget").addClass("ui-state-active").attr("aria-pressed","true"):e(this).button("widget").removeClass("ui-state-active").attr("aria-pressed","false")}):"checkbox"===this.type&&(this.element.is(":checked")?this.buttonElement.addClass("ui-state-active").attr("aria-pressed","true"):this.buttonElement.removeClass("ui-state-active").attr("aria-pressed","false"))},_resetButton:function(){if("input"===this.type)return this.options.label&&this.element.val(this.options.label),undefined;var t=this.buttonElement.removeClass(n),i=e("<span></span>",this.document[0]).addClass("ui-button-text").html(this.options.label).appendTo(t.empty()).text(),s=this.options.icons,a=s.primary&&s.secondary,o=[];s.primary||s.secondary?(this.options.text&&o.push("ui-button-text-icon"+(a?"s":s.primary?"-primary":"-secondary")),s.primary&&t.prepend("<span class='ui-button-icon-primary ui-icon "+s.primary+"'></span>"),s.secondary&&t.append("<span class='ui-button-icon-secondary ui-icon "+s.secondary+"'></span>"),this.options.text||(o.push(a?"ui-button-icons-only":"ui-button-icon-only"),this.hasTitle||t.attr("title",e.trim(i)))):o.push("ui-button-text-only"),t.addClass(o.join(" "))}}),e.widget("ui.buttonset",{version:"1.10.4",options:{items:"button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(ui-button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(e,t){"disabled"===e&&this.buttons.button("option",e,t),this._super(e,t)},refresh:function(){var t="rtl"===this.element.css("direction");this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return e(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(t?"ui-corner-right":"ui-corner-left").end().filter(":last").addClass(t?"ui-corner-left":"ui-corner-right").end().end()},_destroy:function(){this.element.removeClass("ui-buttonset"),this.buttons.map(function(){return e(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy")}})})(jQuery);(function(e,t){function i(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},e.extend(this._defaults,this.regional[""]),this.dpDiv=a(e("<div id='"+this._mainDivId+"' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>"))}function a(t){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return t.delegate(i,"mouseout",function(){e(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).removeClass("ui-datepicker-next-hover")}).delegate(i,"mouseover",function(){e.datepicker._isDisabledDatepicker(n.inline?t.parent()[0]:n.input[0])||(e(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),e(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).addClass("ui-datepicker-next-hover"))})}function s(t,i){e.extend(t,i);for(var a in i)null==i[a]&&(t[a]=i[a]);return t}e.extend(e.ui,{datepicker:{version:"1.10.4"}});var n,r="datepicker";e.extend(i.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(e){return s(this._defaults,e||{}),this},_attachDatepicker:function(t,i){var a,s,n;a=t.nodeName.toLowerCase(),s="div"===a||"span"===a,t.id||(this.uuid+=1,t.id="dp"+this.uuid),n=this._newInst(e(t),s),n.settings=e.extend({},i||{}),"input"===a?this._connectDatepicker(t,n):s&&this._inlineDatepicker(t,n)},_newInst:function(t,i){var s=t[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1");return{id:s,input:t,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:i,dpDiv:i?a(e("<div class='"+this._inlineClass+" ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")):this.dpDiv}},_connectDatepicker:function(t,i){var a=e(t);i.append=e([]),i.trigger=e([]),a.hasClass(this.markerClassName)||(this._attachments(a,i),a.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp),this._autoSize(i),e.data(t,r,i),i.settings.disabled&&this._disableDatepicker(t))},_attachments:function(t,i){var a,s,n,r=this._get(i,"appendText"),o=this._get(i,"isRTL");i.append&&i.append.remove(),r&&(i.append=e("<span class='"+this._appendClass+"'>"+r+"</span>"),t[o?"before":"after"](i.append)),t.unbind("focus",this._showDatepicker),i.trigger&&i.trigger.remove(),a=this._get(i,"showOn"),("focus"===a||"both"===a)&&t.focus(this._showDatepicker),("button"===a||"both"===a)&&(s=this._get(i,"buttonText"),n=this._get(i,"buttonImage"),i.trigger=e(this._get(i,"buttonImageOnly")?e("<img/>").addClass(this._triggerClass).attr({src:n,alt:s,title:s}):e("<button type='button'></button>").addClass(this._triggerClass).html(n?e("<img/>").attr({src:n,alt:s,title:s}):s)),t[o?"before":"after"](i.trigger),i.trigger.click(function(){return e.datepicker._datepickerShowing&&e.datepicker._lastInput===t[0]?e.datepicker._hideDatepicker():e.datepicker._datepickerShowing&&e.datepicker._lastInput!==t[0]?(e.datepicker._hideDatepicker(),e.datepicker._showDatepicker(t[0])):e.datepicker._showDatepicker(t[0]),!1}))},_autoSize:function(e){if(this._get(e,"autoSize")&&!e.inline){var t,i,a,s,n=new Date(2009,11,20),r=this._get(e,"dateFormat");r.match(/[DM]/)&&(t=function(e){for(i=0,a=0,s=0;e.length>s;s++)e[s].length>i&&(i=e[s].length,a=s);return a},n.setMonth(t(this._get(e,r.match(/MM/)?"monthNames":"monthNamesShort"))),n.setDate(t(this._get(e,r.match(/DD/)?"dayNames":"dayNamesShort"))+20-n.getDay())),e.input.attr("size",this._formatDate(e,n).length)}},_inlineDatepicker:function(t,i){var a=e(t);a.hasClass(this.markerClassName)||(a.addClass(this.markerClassName).append(i.dpDiv),e.data(t,r,i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(t),i.dpDiv.css("display","block"))},_dialogDatepicker:function(t,i,a,n,o){var u,c,h,l,d,p=this._dialogInst;return p||(this.uuid+=1,u="dp"+this.uuid,this._dialogInput=e("<input type='text' id='"+u+"' style='position: absolute; top: -100px; width: 0px;'/>"),this._dialogInput.keydown(this._doKeyDown),e("body").append(this._dialogInput),p=this._dialogInst=this._newInst(this._dialogInput,!1),p.settings={},e.data(this._dialogInput[0],r,p)),s(p.settings,n||{}),i=i&&i.constructor===Date?this._formatDate(p,i):i,this._dialogInput.val(i),this._pos=o?o.length?o:[o.pageX,o.pageY]:null,this._pos||(c=document.documentElement.clientWidth,h=document.documentElement.clientHeight,l=document.documentElement.scrollLeft||document.body.scrollLeft,d=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[c/2-100+l,h/2-150+d]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),p.settings.onSelect=a,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),e.blockUI&&e.blockUI(this.dpDiv),e.data(this._dialogInput[0],r,p),this},_destroyDatepicker:function(t){var i,a=e(t),s=e.data(t,r);a.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),e.removeData(t,r),"input"===i?(s.append.remove(),s.trigger.remove(),a.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):("div"===i||"span"===i)&&a.removeClass(this.markerClassName).empty())},_enableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!1,n.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().removeClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}))},_disableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!0,n.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().addClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}),this._disabledInputs[this._disabledInputs.length]=t)},_isDisabledDatepicker:function(e){if(!e)return!1;for(var t=0;this._disabledInputs.length>t;t++)if(this._disabledInputs[t]===e)return!0;return!1},_getInst:function(t){try{return e.data(t,r)}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(i,a,n){var r,o,u,c,h=this._getInst(i);return 2===arguments.length&&"string"==typeof a?"defaults"===a?e.extend({},e.datepicker._defaults):h?"all"===a?e.extend({},h.settings):this._get(h,a):null:(r=a||{},"string"==typeof a&&(r={},r[a]=n),h&&(this._curInst===h&&this._hideDatepicker(),o=this._getDateDatepicker(i,!0),u=this._getMinMaxDate(h,"min"),c=this._getMinMaxDate(h,"max"),s(h.settings,r),null!==u&&r.dateFormat!==t&&r.minDate===t&&(h.settings.minDate=this._formatDate(h,u)),null!==c&&r.dateFormat!==t&&r.maxDate===t&&(h.settings.maxDate=this._formatDate(h,c)),"disabled"in r&&(r.disabled?this._disableDatepicker(i):this._enableDatepicker(i)),this._attachments(e(i),h),this._autoSize(h),this._setDate(h,o),this._updateAlternate(h),this._updateDatepicker(h)),t)},_changeDatepicker:function(e,t,i){this._optionDatepicker(e,t,i)},_refreshDatepicker:function(e){var t=this._getInst(e);t&&this._updateDatepicker(t)},_setDateDatepicker:function(e,t){var i=this._getInst(e);i&&(this._setDate(i,t),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(e,t){var i=this._getInst(e);return i&&!i.inline&&this._setDateFromField(i,t),i?this._getDate(i):null},_doKeyDown:function(t){var i,a,s,n=e.datepicker._getInst(t.target),r=!0,o=n.dpDiv.is(".ui-datepicker-rtl");if(n._keyEvent=!0,e.datepicker._datepickerShowing)switch(t.keyCode){case 9:e.datepicker._hideDatepicker(),r=!1;break;case 13:return s=e("td."+e.datepicker._dayOverClass+":not(."+e.datepicker._currentClass+")",n.dpDiv),s[0]&&e.datepicker._selectDay(t.target,n.selectedMonth,n.selectedYear,s[0]),i=e.datepicker._get(n,"onSelect"),i?(a=e.datepicker._formatDate(n),i.apply(n.input?n.input[0]:null,[a,n])):e.datepicker._hideDatepicker(),!1;case 27:e.datepicker._hideDatepicker();break;case 33:e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 34:e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 35:(t.ctrlKey||t.metaKey)&&e.datepicker._clearDate(t.target),r=t.ctrlKey||t.metaKey;break;case 36:(t.ctrlKey||t.metaKey)&&e.datepicker._gotoToday(t.target),r=t.ctrlKey||t.metaKey;break;case 37:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?1:-1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 38:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,-7,"D"),r=t.ctrlKey||t.metaKey;break;case 39:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?-1:1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 40:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,7,"D"),r=t.ctrlKey||t.metaKey;break;default:r=!1}else 36===t.keyCode&&t.ctrlKey?e.datepicker._showDatepicker(this):r=!1;r&&(t.preventDefault(),t.stopPropagation())},_doKeyPress:function(i){var a,s,n=e.datepicker._getInst(i.target);return e.datepicker._get(n,"constrainInput")?(a=e.datepicker._possibleChars(e.datepicker._get(n,"dateFormat")),s=String.fromCharCode(null==i.charCode?i.keyCode:i.charCode),i.ctrlKey||i.metaKey||" ">s||!a||a.indexOf(s)>-1):t},_doKeyUp:function(t){var i,a=e.datepicker._getInst(t.target);if(a.input.val()!==a.lastVal)try{i=e.datepicker.parseDate(e.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,e.datepicker._getFormatConfig(a)),i&&(e.datepicker._setDateFromField(a),e.datepicker._updateAlternate(a),e.datepicker._updateDatepicker(a))}catch(s){}return!0},_showDatepicker:function(t){if(t=t.target||t,"input"!==t.nodeName.toLowerCase()&&(t=e("input",t.parentNode)[0]),!e.datepicker._isDisabledDatepicker(t)&&e.datepicker._lastInput!==t){var i,a,n,r,o,u,c;i=e.datepicker._getInst(t),e.datepicker._curInst&&e.datepicker._curInst!==i&&(e.datepicker._curInst.dpDiv.stop(!0,!0),i&&e.datepicker._datepickerShowing&&e.datepicker._hideDatepicker(e.datepicker._curInst.input[0])),a=e.datepicker._get(i,"beforeShow"),n=a?a.apply(t,[t,i]):{},n!==!1&&(s(i.settings,n),i.lastVal=null,e.datepicker._lastInput=t,e.datepicker._setDateFromField(i),e.datepicker._inDialog&&(t.value=""),e.datepicker._pos||(e.datepicker._pos=e.datepicker._findPos(t),e.datepicker._pos[1]+=t.offsetHeight),r=!1,e(t).parents().each(function(){return r|="fixed"===e(this).css("position"),!r}),o={left:e.datepicker._pos[0],top:e.datepicker._pos[1]},e.datepicker._pos=null,i.dpDiv.empty(),i.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),e.datepicker._updateDatepicker(i),o=e.datepicker._checkOffset(i,o,r),i.dpDiv.css({position:e.datepicker._inDialog&&e.blockUI?"static":r?"fixed":"absolute",display:"none",left:o.left+"px",top:o.top+"px"}),i.inline||(u=e.datepicker._get(i,"showAnim"),c=e.datepicker._get(i,"duration"),i.dpDiv.zIndex(e(t).zIndex()+1),e.datepicker._datepickerShowing=!0,e.effects&&e.effects.effect[u]?i.dpDiv.show(u,e.datepicker._get(i,"showOptions"),c):i.dpDiv[u||"show"](u?c:null),e.datepicker._shouldFocusInput(i)&&i.input.focus(),e.datepicker._curInst=i))}},_updateDatepicker:function(t){this.maxRows=4,n=t,t.dpDiv.empty().append(this._generateHTML(t)),this._attachHandlers(t),t.dpDiv.find("."+this._dayOverClass+" a").mouseover();var i,a=this._getNumberOfMonths(t),s=a[1],r=17;t.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),s>1&&t.dpDiv.addClass("ui-datepicker-multi-"+s).css("width",r*s+"em"),t.dpDiv[(1!==a[0]||1!==a[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),t.dpDiv[(this._get(t,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),t===e.datepicker._curInst&&e.datepicker._datepickerShowing&&e.datepicker._shouldFocusInput(t)&&t.input.focus(),t.yearshtml&&(i=t.yearshtml,setTimeout(function(){i===t.yearshtml&&t.yearshtml&&t.dpDiv.find("select.ui-datepicker-year:first").replaceWith(t.yearshtml),i=t.yearshtml=null},0))},_shouldFocusInput:function(e){return e.input&&e.input.is(":visible")&&!e.input.is(":disabled")&&!e.input.is(":focus")},_checkOffset:function(t,i,a){var s=t.dpDiv.outerWidth(),n=t.dpDiv.outerHeight(),r=t.input?t.input.outerWidth():0,o=t.input?t.input.outerHeight():0,u=document.documentElement.clientWidth+(a?0:e(document).scrollLeft()),c=document.documentElement.clientHeight+(a?0:e(document).scrollTop());return i.left-=this._get(t,"isRTL")?s-r:0,i.left-=a&&i.left===t.input.offset().left?e(document).scrollLeft():0,i.top-=a&&i.top===t.input.offset().top+o?e(document).scrollTop():0,i.left-=Math.min(i.left,i.left+s>u&&u>s?Math.abs(i.left+s-u):0),i.top-=Math.min(i.top,i.top+n>c&&c>n?Math.abs(n+o):0),i},_findPos:function(t){for(var i,a=this._getInst(t),s=this._get(a,"isRTL");t&&("hidden"===t.type||1!==t.nodeType||e.expr.filters.hidden(t));)t=t[s?"previousSibling":"nextSibling"];return i=e(t).offset(),[i.left,i.top]},_hideDatepicker:function(t){var i,a,s,n,o=this._curInst;!o||t&&o!==e.data(t,r)||this._datepickerShowing&&(i=this._get(o,"showAnim"),a=this._get(o,"duration"),s=function(){e.datepicker._tidyDialog(o)},e.effects&&(e.effects.effect[i]||e.effects[i])?o.dpDiv.hide(i,e.datepicker._get(o,"showOptions"),a,s):o.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?a:null,s),i||s(),this._datepickerShowing=!1,n=this._get(o,"onClose"),n&&n.apply(o.input?o.input[0]:null,[o.input?o.input.val():"",o]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),e.blockUI&&(e.unblockUI(),e("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(e){e.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(t){if(e.datepicker._curInst){var i=e(t.target),a=e.datepicker._getInst(i[0]);(i[0].id!==e.datepicker._mainDivId&&0===i.parents("#"+e.datepicker._mainDivId).length&&!i.hasClass(e.datepicker.markerClassName)&&!i.closest("."+e.datepicker._triggerClass).length&&e.datepicker._datepickerShowing&&(!e.datepicker._inDialog||!e.blockUI)||i.hasClass(e.datepicker.markerClassName)&&e.datepicker._curInst!==a)&&e.datepicker._hideDatepicker()}},_adjustDate:function(t,i,a){var s=e(t),n=this._getInst(s[0]);this._isDisabledDatepicker(s[0])||(this._adjustInstDate(n,i+("M"===a?this._get(n,"showCurrentAtPos"):0),a),this._updateDatepicker(n))},_gotoToday:function(t){var i,a=e(t),s=this._getInst(a[0]);this._get(s,"gotoCurrent")&&s.currentDay?(s.selectedDay=s.currentDay,s.drawMonth=s.selectedMonth=s.currentMonth,s.drawYear=s.selectedYear=s.currentYear):(i=new Date,s.selectedDay=i.getDate(),s.drawMonth=s.selectedMonth=i.getMonth(),s.drawYear=s.selectedYear=i.getFullYear()),this._notifyChange(s),this._adjustDate(a)},_selectMonthYear:function(t,i,a){var s=e(t),n=this._getInst(s[0]);n["selected"+("M"===a?"Month":"Year")]=n["draw"+("M"===a?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(n),this._adjustDate(s)},_selectDay:function(t,i,a,s){var n,r=e(t);e(s).hasClass(this._unselectableClass)||this._isDisabledDatepicker(r[0])||(n=this._getInst(r[0]),n.selectedDay=n.currentDay=e("a",s).html(),n.selectedMonth=n.currentMonth=i,n.selectedYear=n.currentYear=a,this._selectDate(t,this._formatDate(n,n.currentDay,n.currentMonth,n.currentYear)))},_clearDate:function(t){var i=e(t);this._selectDate(i,"")},_selectDate:function(t,i){var a,s=e(t),n=this._getInst(s[0]);i=null!=i?i:this._formatDate(n),n.input&&n.input.val(i),this._updateAlternate(n),a=this._get(n,"onSelect"),a?a.apply(n.input?n.input[0]:null,[i,n]):n.input&&n.input.trigger("change"),n.inline?this._updateDatepicker(n):(this._hideDatepicker(),this._lastInput=n.input[0],"object"!=typeof n.input[0]&&n.input.focus(),this._lastInput=null)},_updateAlternate:function(t){var i,a,s,n=this._get(t,"altField");n&&(i=this._get(t,"altFormat")||this._get(t,"dateFormat"),a=this._getDate(t),s=this.formatDate(i,a,this._getFormatConfig(t)),e(n).each(function(){e(this).val(s)}))},noWeekends:function(e){var t=e.getDay();return[t>0&&6>t,""]},iso8601Week:function(e){var t,i=new Date(e.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),t=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((t-i)/864e5)/7)+1},parseDate:function(i,a,s){if(null==i||null==a)throw"Invalid arguments";if(a="object"==typeof a?""+a:a+"",""===a)return null;var n,r,o,u,c=0,h=(s?s.shortYearCutoff:null)||this._defaults.shortYearCutoff,l="string"!=typeof h?h:(new Date).getFullYear()%100+parseInt(h,10),d=(s?s.dayNamesShort:null)||this._defaults.dayNamesShort,p=(s?s.dayNames:null)||this._defaults.dayNames,g=(s?s.monthNamesShort:null)||this._defaults.monthNamesShort,m=(s?s.monthNames:null)||this._defaults.monthNames,f=-1,_=-1,v=-1,k=-1,y=!1,b=function(e){var t=i.length>n+1&&i.charAt(n+1)===e;return t&&n++,t},D=function(e){var t=b(e),i="@"===e?14:"!"===e?20:"y"===e&&t?4:"o"===e?3:2,s=RegExp("^\\d{1,"+i+"}"),n=a.substring(c).match(s);if(!n)throw"Missing number at position "+c;return c+=n[0].length,parseInt(n[0],10)},w=function(i,s,n){var r=-1,o=e.map(b(i)?n:s,function(e,t){return[[t,e]]}).sort(function(e,t){return-(e[1].length-t[1].length)});if(e.each(o,function(e,i){var s=i[1];return a.substr(c,s.length).toLowerCase()===s.toLowerCase()?(r=i[0],c+=s.length,!1):t}),-1!==r)return r+1;throw"Unknown name at position "+c},M=function(){if(a.charAt(c)!==i.charAt(n))throw"Unexpected literal at position "+c;c++};for(n=0;i.length>n;n++)if(y)"'"!==i.charAt(n)||b("'")?M():y=!1;else switch(i.charAt(n)){case"d":v=D("d");break;case"D":w("D",d,p);break;case"o":k=D("o");break;case"m":_=D("m");break;case"M":_=w("M",g,m);break;case"y":f=D("y");break;case"@":u=new Date(D("@")),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"!":u=new Date((D("!")-this._ticksTo1970)/1e4),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"'":b("'")?M():y=!0;break;default:M()}if(a.length>c&&(o=a.substr(c),!/^\s+/.test(o)))throw"Extra/unparsed characters found in date: "+o;if(-1===f?f=(new Date).getFullYear():100>f&&(f+=(new Date).getFullYear()-(new Date).getFullYear()%100+(l>=f?0:-100)),k>-1)for(_=1,v=k;;){if(r=this._getDaysInMonth(f,_-1),r>=v)break;_++,v-=r}if(u=this._daylightSavingAdjust(new Date(f,_-1,v)),u.getFullYear()!==f||u.getMonth()+1!==_||u.getDate()!==v)throw"Invalid date";return u},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(e,t,i){if(!t)return"";var a,s=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,n=(i?i.dayNames:null)||this._defaults.dayNames,r=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,o=(i?i.monthNames:null)||this._defaults.monthNames,u=function(t){var i=e.length>a+1&&e.charAt(a+1)===t;return i&&a++,i},c=function(e,t,i){var a=""+t;if(u(e))for(;i>a.length;)a="0"+a;return a},h=function(e,t,i,a){return u(e)?a[t]:i[t]},l="",d=!1;if(t)for(a=0;e.length>a;a++)if(d)"'"!==e.charAt(a)||u("'")?l+=e.charAt(a):d=!1;else switch(e.charAt(a)){case"d":l+=c("d",t.getDate(),2);break;case"D":l+=h("D",t.getDay(),s,n);break;case"o":l+=c("o",Math.round((new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime()-new Date(t.getFullYear(),0,0).getTime())/864e5),3);break;case"m":l+=c("m",t.getMonth()+1,2);break;case"M":l+=h("M",t.getMonth(),r,o);break;case"y":l+=u("y")?t.getFullYear():(10>t.getYear()%100?"0":"")+t.getYear()%100;break;case"@":l+=t.getTime();break;case"!":l+=1e4*t.getTime()+this._ticksTo1970;break;case"'":u("'")?l+="'":d=!0;break;default:l+=e.charAt(a)}return l},_possibleChars:function(e){var t,i="",a=!1,s=function(i){var a=e.length>t+1&&e.charAt(t+1)===i;return a&&t++,a};for(t=0;e.length>t;t++)if(a)"'"!==e.charAt(t)||s("'")?i+=e.charAt(t):a=!1;else switch(e.charAt(t)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":s("'")?i+="'":a=!0;break;default:i+=e.charAt(t)}return i},_get:function(e,i){return e.settings[i]!==t?e.settings[i]:this._defaults[i]},_setDateFromField:function(e,t){if(e.input.val()!==e.lastVal){var i=this._get(e,"dateFormat"),a=e.lastVal=e.input?e.input.val():null,s=this._getDefaultDate(e),n=s,r=this._getFormatConfig(e);try{n=this.parseDate(i,a,r)||s}catch(o){a=t?"":a}e.selectedDay=n.getDate(),e.drawMonth=e.selectedMonth=n.getMonth(),e.drawYear=e.selectedYear=n.getFullYear(),e.currentDay=a?n.getDate():0,e.currentMonth=a?n.getMonth():0,e.currentYear=a?n.getFullYear():0,this._adjustInstDate(e)}},_getDefaultDate:function(e){return this._restrictMinMax(e,this._determineDate(e,this._get(e,"defaultDate"),new Date))},_determineDate:function(t,i,a){var s=function(e){var t=new Date;return t.setDate(t.getDate()+e),t},n=function(i){try{return e.datepicker.parseDate(e.datepicker._get(t,"dateFormat"),i,e.datepicker._getFormatConfig(t))}catch(a){}for(var s=(i.toLowerCase().match(/^c/)?e.datepicker._getDate(t):null)||new Date,n=s.getFullYear(),r=s.getMonth(),o=s.getDate(),u=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,c=u.exec(i);c;){switch(c[2]||"d"){case"d":case"D":o+=parseInt(c[1],10);break;case"w":case"W":o+=7*parseInt(c[1],10);break;case"m":case"M":r+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r));break;case"y":case"Y":n+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r))}c=u.exec(i)}return new Date(n,r,o)},r=null==i||""===i?a:"string"==typeof i?n(i):"number"==typeof i?isNaN(i)?a:s(i):new Date(i.getTime());return r=r&&"Invalid Date"==""+r?a:r,r&&(r.setHours(0),r.setMinutes(0),r.setSeconds(0),r.setMilliseconds(0)),this._daylightSavingAdjust(r)},_daylightSavingAdjust:function(e){return e?(e.setHours(e.getHours()>12?e.getHours()+2:0),e):null},_setDate:function(e,t,i){var a=!t,s=e.selectedMonth,n=e.selectedYear,r=this._restrictMinMax(e,this._determineDate(e,t,new Date));e.selectedDay=e.currentDay=r.getDate(),e.drawMonth=e.selectedMonth=e.currentMonth=r.getMonth(),e.drawYear=e.selectedYear=e.currentYear=r.getFullYear(),s===e.selectedMonth&&n===e.selectedYear||i||this._notifyChange(e),this._adjustInstDate(e),e.input&&e.input.val(a?"":this._formatDate(e))},_getDate:function(e){var t=!e.currentYear||e.input&&""===e.input.val()?null:this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return t},_attachHandlers:function(t){var i=this._get(t,"stepMonths"),a="#"+t.id.replace(/\\\\/g,"\\");t.dpDiv.find("[data-handler]").map(function(){var t={prev:function(){e.datepicker._adjustDate(a,-i,"M")},next:function(){e.datepicker._adjustDate(a,+i,"M")},hide:function(){e.datepicker._hideDatepicker()},today:function(){e.datepicker._gotoToday(a)},selectDay:function(){return e.datepicker._selectDay(a,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return e.datepicker._selectMonthYear(a,this,"M"),!1},selectYear:function(){return e.datepicker._selectMonthYear(a,this,"Y"),!1}};e(this).bind(this.getAttribute("data-event"),t[this.getAttribute("data-handler")])})},_generateHTML:function(e){var t,i,a,s,n,r,o,u,c,h,l,d,p,g,m,f,_,v,k,y,b,D,w,M,C,x,I,N,T,A,E,S,Y,F,P,O,j,K,R,H=new Date,W=this._daylightSavingAdjust(new Date(H.getFullYear(),H.getMonth(),H.getDate())),L=this._get(e,"isRTL"),U=this._get(e,"showButtonPanel"),B=this._get(e,"hideIfNoPrevNext"),z=this._get(e,"navigationAsDateFormat"),q=this._getNumberOfMonths(e),G=this._get(e,"showCurrentAtPos"),J=this._get(e,"stepMonths"),Q=1!==q[0]||1!==q[1],V=this._daylightSavingAdjust(e.currentDay?new Date(e.currentYear,e.currentMonth,e.currentDay):new Date(9999,9,9)),$=this._getMinMaxDate(e,"min"),X=this._getMinMaxDate(e,"max"),Z=e.drawMonth-G,et=e.drawYear;if(0>Z&&(Z+=12,et--),X)for(t=this._daylightSavingAdjust(new Date(X.getFullYear(),X.getMonth()-q[0]*q[1]+1,X.getDate())),t=$&&$>t?$:t;this._daylightSavingAdjust(new Date(et,Z,1))>t;)Z--,0>Z&&(Z=11,et--);for(e.drawMonth=Z,e.drawYear=et,i=this._get(e,"prevText"),i=z?this.formatDate(i,this._daylightSavingAdjust(new Date(et,Z-J,1)),this._getFormatConfig(e)):i,a=this._canAdjustMonth(e,-1,et,Z)?"<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"e":"w")+"'>"+i+"</span></a>":B?"":"<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"e":"w")+"'>"+i+"</span></a>",s=this._get(e,"nextText"),s=z?this.formatDate(s,this._daylightSavingAdjust(new Date(et,Z+J,1)),this._getFormatConfig(e)):s,n=this._canAdjustMonth(e,1,et,Z)?"<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click' title='"+s+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"w":"e")+"'>"+s+"</span></a>":B?"":"<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+s+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"w":"e")+"'>"+s+"</span></a>",r=this._get(e,"currentText"),o=this._get(e,"gotoCurrent")&&e.currentDay?V:W,r=z?this.formatDate(r,o,this._getFormatConfig(e)):r,u=e.inline?"":"<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>"+this._get(e,"closeText")+"</button>",c=U?"<div class='ui-datepicker-buttonpane ui-widget-content'>"+(L?u:"")+(this._isInRange(e,o)?"<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'>"+r+"</button>":"")+(L?"":u)+"</div>":"",h=parseInt(this._get(e,"firstDay"),10),h=isNaN(h)?0:h,l=this._get(e,"showWeek"),d=this._get(e,"dayNames"),p=this._get(e,"dayNamesMin"),g=this._get(e,"monthNames"),m=this._get(e,"monthNamesShort"),f=this._get(e,"beforeShowDay"),_=this._get(e,"showOtherMonths"),v=this._get(e,"selectOtherMonths"),k=this._getDefaultDate(e),y="",D=0;q[0]>D;D++){for(w="",this.maxRows=4,M=0;q[1]>M;M++){if(C=this._daylightSavingAdjust(new Date(et,Z,e.selectedDay)),x=" ui-corner-all",I="",Q){if(I+="<div class='ui-datepicker-group",q[1]>1)switch(M){case 0:I+=" ui-datepicker-group-first",x=" ui-corner-"+(L?"right":"left");break;case q[1]-1:I+=" ui-datepicker-group-last",x=" ui-corner-"+(L?"left":"right");break;default:I+=" ui-datepicker-group-middle",x=""}I+="'>"}for(I+="<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix"+x+"'>"+(/all|left/.test(x)&&0===D?L?n:a:"")+(/all|right/.test(x)&&0===D?L?a:n:"")+this._generateMonthYearHeader(e,Z,et,$,X,D>0||M>0,g,m)+"</div><table class='ui-datepicker-calendar'><thead>"+"<tr>",N=l?"<th class='ui-datepicker-week-col'>"+this._get(e,"weekHeader")+"</th>":"",b=0;7>b;b++)T=(b+h)%7,N+="<th"+((b+h+6)%7>=5?" class='ui-datepicker-week-end'":"")+">"+"<span title='"+d[T]+"'>"+p[T]+"</span></th>";for(I+=N+"</tr></thead><tbody>",A=this._getDaysInMonth(et,Z),et===e.selectedYear&&Z===e.selectedMonth&&(e.selectedDay=Math.min(e.selectedDay,A)),E=(this._getFirstDayOfMonth(et,Z)-h+7)%7,S=Math.ceil((E+A)/7),Y=Q?this.maxRows>S?this.maxRows:S:S,this.maxRows=Y,F=this._daylightSavingAdjust(new Date(et,Z,1-E)),P=0;Y>P;P++){for(I+="<tr>",O=l?"<td class='ui-datepicker-week-col'>"+this._get(e,"calculateWeek")(F)+"</td>":"",b=0;7>b;b++)j=f?f.apply(e.input?e.input[0]:null,[F]):[!0,""],K=F.getMonth()!==Z,R=K&&!v||!j[0]||$&&$>F||X&&F>X,O+="<td class='"+((b+h+6)%7>=5?" ui-datepicker-week-end":"")+(K?" ui-datepicker-other-month":"")+(F.getTime()===C.getTime()&&Z===e.selectedMonth&&e._keyEvent||k.getTime()===F.getTime()&&k.getTime()===C.getTime()?" "+this._dayOverClass:"")+(R?" "+this._unselectableClass+" ui-state-disabled":"")+(K&&!_?"":" "+j[1]+(F.getTime()===V.getTime()?" "+this._currentClass:"")+(F.getTime()===W.getTime()?" ui-datepicker-today":""))+"'"+(K&&!_||!j[2]?"":" title='"+j[2].replace(/'/g,"&#39;")+"'")+(R?"":" data-handler='selectDay' data-event='click' data-month='"+F.getMonth()+"' data-year='"+F.getFullYear()+"'")+">"+(K&&!_?"&#xa0;":R?"<span class='ui-state-default'>"+F.getDate()+"</span>":"<a class='ui-state-default"+(F.getTime()===W.getTime()?" ui-state-highlight":"")+(F.getTime()===V.getTime()?" ui-state-active":"")+(K?" ui-priority-secondary":"")+"' href='#'>"+F.getDate()+"</a>")+"</td>",F.setDate(F.getDate()+1),F=this._daylightSavingAdjust(F);I+=O+"</tr>"}Z++,Z>11&&(Z=0,et++),I+="</tbody></table>"+(Q?"</div>"+(q[0]>0&&M===q[1]-1?"<div class='ui-datepicker-row-break'></div>":""):""),w+=I}y+=w}return y+=c,e._keyEvent=!1,y},_generateMonthYearHeader:function(e,t,i,a,s,n,r,o){var u,c,h,l,d,p,g,m,f=this._get(e,"changeMonth"),_=this._get(e,"changeYear"),v=this._get(e,"showMonthAfterYear"),k="<div class='ui-datepicker-title'>",y="";if(n||!f)y+="<span class='ui-datepicker-month'>"+r[t]+"</span>";else{for(u=a&&a.getFullYear()===i,c=s&&s.getFullYear()===i,y+="<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>",h=0;12>h;h++)(!u||h>=a.getMonth())&&(!c||s.getMonth()>=h)&&(y+="<option value='"+h+"'"+(h===t?" selected='selected'":"")+">"+o[h]+"</option>");y+="</select>"}if(v||(k+=y+(!n&&f&&_?"":"&#xa0;")),!e.yearshtml)if(e.yearshtml="",n||!_)k+="<span class='ui-datepicker-year'>"+i+"</span>";else{for(l=this._get(e,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(e){var t=e.match(/c[+\-].*/)?i+parseInt(e.substring(1),10):e.match(/[+\-].*/)?d+parseInt(e,10):parseInt(e,10);
+return isNaN(t)?d:t},g=p(l[0]),m=Math.max(g,p(l[1]||"")),g=a?Math.max(g,a.getFullYear()):g,m=s?Math.min(m,s.getFullYear()):m,e.yearshtml+="<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>";m>=g;g++)e.yearshtml+="<option value='"+g+"'"+(g===i?" selected='selected'":"")+">"+g+"</option>";e.yearshtml+="</select>",k+=e.yearshtml,e.yearshtml=null}return k+=this._get(e,"yearSuffix"),v&&(k+=(!n&&f&&_?"":"&#xa0;")+y),k+="</div>"},_adjustInstDate:function(e,t,i){var a=e.drawYear+("Y"===i?t:0),s=e.drawMonth+("M"===i?t:0),n=Math.min(e.selectedDay,this._getDaysInMonth(a,s))+("D"===i?t:0),r=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(a,s,n)));e.selectedDay=r.getDate(),e.drawMonth=e.selectedMonth=r.getMonth(),e.drawYear=e.selectedYear=r.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(e)},_restrictMinMax:function(e,t){var i=this._getMinMaxDate(e,"min"),a=this._getMinMaxDate(e,"max"),s=i&&i>t?i:t;return a&&s>a?a:s},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return null==t?[1,1]:"number"==typeof t?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return new Date(e,t,1).getDay()},_canAdjustMonth:function(e,t,i,a){var s=this._getNumberOfMonths(e),n=this._daylightSavingAdjust(new Date(i,a+(0>t?t:s[0]*s[1]),1));return 0>t&&n.setDate(this._getDaysInMonth(n.getFullYear(),n.getMonth())),this._isInRange(e,n)},_isInRange:function(e,t){var i,a,s=this._getMinMaxDate(e,"min"),n=this._getMinMaxDate(e,"max"),r=null,o=null,u=this._get(e,"yearRange");return u&&(i=u.split(":"),a=(new Date).getFullYear(),r=parseInt(i[0],10),o=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(r+=a),i[1].match(/[+\-].*/)&&(o+=a)),(!s||t.getTime()>=s.getTime())&&(!n||t.getTime()<=n.getTime())&&(!r||t.getFullYear()>=r)&&(!o||o>=t.getFullYear())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t="string"!=typeof t?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,i,a){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var s=t?"object"==typeof t?t:this._daylightSavingAdjust(new Date(a,i,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return this.formatDate(this._get(e,"dateFormat"),s,this._getFormatConfig(e))}}),e.fn.datepicker=function(t){if(!this.length)return this;e.datepicker.initialized||(e(document).mousedown(e.datepicker._checkExternalClick),e.datepicker.initialized=!0),0===e("#"+e.datepicker._mainDivId).length&&e("body").append(e.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof t||"isDisabled"!==t&&"getDate"!==t&&"widget"!==t?"option"===t&&2===arguments.length&&"string"==typeof arguments[1]?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof t?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this].concat(i)):e.datepicker._attachDatepicker(this,t)}):e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i))},e.datepicker=new i,e.datepicker.initialized=!1,e.datepicker.uuid=(new Date).getTime(),e.datepicker.version="1.10.4"})(jQuery);(function(e){var t={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},i={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};e.widget("ui.dialog",{version:"1.10.4",options:{appendTo:"body",autoOpen:!0,buttons:[],closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:null,maxWidth:null,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(t){var i=e(this).css(t).offset().top;0>i&&e(this).css("top",t.top-i)}},resizable:!0,show:null,title:null,width:300,beforeClose:null,close:null,drag:null,dragStart:null,dragStop:null,focus:null,open:null,resize:null,resizeStart:null,resizeStop:null},_create:function(){this.originalCss={display:this.element[0].style.display,width:this.element[0].style.width,minHeight:this.element[0].style.minHeight,maxHeight:this.element[0].style.maxHeight,height:this.element[0].style.height},this.originalPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.originalTitle=this.element.attr("title"),this.options.title=this.options.title||this.originalTitle,this._createWrapper(),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(this.uiDialog),this._createTitlebar(),this._createButtonPane(),this.options.draggable&&e.fn.draggable&&this._makeDraggable(),this.options.resizable&&e.fn.resizable&&this._makeResizable(),this._isOpen=!1},_init:function(){this.options.autoOpen&&this.open()},_appendTo:function(){var t=this.options.appendTo;return t&&(t.jquery||t.nodeType)?e(t):this.document.find(t||"body").eq(0)},_destroy:function(){var e,t=this.originalPosition;this._destroyOverlay(),this.element.removeUniqueId().removeClass("ui-dialog-content ui-widget-content").css(this.originalCss).detach(),this.uiDialog.stop(!0,!0).remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),e=t.parent.children().eq(t.index),e.length&&e[0]!==this.element[0]?e.before(this.element):t.parent.append(this.element)},widget:function(){return this.uiDialog},disable:e.noop,enable:e.noop,close:function(t){var i,a=this;if(this._isOpen&&this._trigger("beforeClose",t)!==!1){if(this._isOpen=!1,this._destroyOverlay(),!this.opener.filter(":focusable").focus().length)try{i=this.document[0].activeElement,i&&"body"!==i.nodeName.toLowerCase()&&e(i).blur()}catch(s){}this._hide(this.uiDialog,this.options.hide,function(){a._trigger("close",t)})}},isOpen:function(){return this._isOpen},moveToTop:function(){this._moveToTop()},_moveToTop:function(e,t){var i=!!this.uiDialog.nextAll(":visible").insertBefore(this.uiDialog).length;return i&&!t&&this._trigger("focus",e),i},open:function(){var t=this;return this._isOpen?(this._moveToTop()&&this._focusTabbable(),undefined):(this._isOpen=!0,this.opener=e(this.document[0].activeElement),this._size(),this._position(),this._createOverlay(),this._moveToTop(null,!0),this._show(this.uiDialog,this.options.show,function(){t._focusTabbable(),t._trigger("focus")}),this._trigger("open"),undefined)},_focusTabbable:function(){var e=this.element.find("[autofocus]");e.length||(e=this.element.find(":tabbable")),e.length||(e=this.uiDialogButtonPane.find(":tabbable")),e.length||(e=this.uiDialogTitlebarClose.filter(":tabbable")),e.length||(e=this.uiDialog),e.eq(0).focus()},_keepFocus:function(t){function i(){var t=this.document[0].activeElement,i=this.uiDialog[0]===t||e.contains(this.uiDialog[0],t);i||this._focusTabbable()}t.preventDefault(),i.call(this),this._delay(i)},_createWrapper:function(){this.uiDialog=e("<div>").addClass("ui-dialog ui-widget ui-widget-content ui-corner-all ui-front "+this.options.dialogClass).hide().attr({tabIndex:-1,role:"dialog"}).appendTo(this._appendTo()),this._on(this.uiDialog,{keydown:function(t){if(this.options.closeOnEscape&&!t.isDefaultPrevented()&&t.keyCode&&t.keyCode===e.ui.keyCode.ESCAPE)return t.preventDefault(),this.close(t),undefined;if(t.keyCode===e.ui.keyCode.TAB){var i=this.uiDialog.find(":tabbable"),a=i.filter(":first"),s=i.filter(":last");t.target!==s[0]&&t.target!==this.uiDialog[0]||t.shiftKey?t.target!==a[0]&&t.target!==this.uiDialog[0]||!t.shiftKey||(s.focus(1),t.preventDefault()):(a.focus(1),t.preventDefault())}},mousedown:function(e){this._moveToTop(e)&&this._focusTabbable()}}),this.element.find("[aria-describedby]").length||this.uiDialog.attr({"aria-describedby":this.element.uniqueId().attr("id")})},_createTitlebar:function(){var t;this.uiDialogTitlebar=e("<div>").addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(this.uiDialog),this._on(this.uiDialogTitlebar,{mousedown:function(t){e(t.target).closest(".ui-dialog-titlebar-close")||this.uiDialog.focus()}}),this.uiDialogTitlebarClose=e("<button type='button'></button>").button({label:this.options.closeText,icons:{primary:"ui-icon-closethick"},text:!1}).addClass("ui-dialog-titlebar-close").appendTo(this.uiDialogTitlebar),this._on(this.uiDialogTitlebarClose,{click:function(e){e.preventDefault(),this.close(e)}}),t=e("<span>").uniqueId().addClass("ui-dialog-title").prependTo(this.uiDialogTitlebar),this._title(t),this.uiDialog.attr({"aria-labelledby":t.attr("id")})},_title:function(e){this.options.title||e.html("&#160;"),e.text(this.options.title)},_createButtonPane:function(){this.uiDialogButtonPane=e("<div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),this.uiButtonSet=e("<div>").addClass("ui-dialog-buttonset").appendTo(this.uiDialogButtonPane),this._createButtons()},_createButtons:function(){var t=this,i=this.options.buttons;return this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),e.isEmptyObject(i)||e.isArray(i)&&!i.length?(this.uiDialog.removeClass("ui-dialog-buttons"),undefined):(e.each(i,function(i,a){var s,n;a=e.isFunction(a)?{click:a,text:i}:a,a=e.extend({type:"button"},a),s=a.click,a.click=function(){s.apply(t.element[0],arguments)},n={icons:a.icons,text:a.showText},delete a.icons,delete a.showText,e("<button></button>",a).button(n).appendTo(t.uiButtonSet)}),this.uiDialog.addClass("ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog),undefined)},_makeDraggable:function(){function t(e){return{position:e.position,offset:e.offset}}var i=this,a=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(a,s){e(this).addClass("ui-dialog-dragging"),i._blockFrames(),i._trigger("dragStart",a,t(s))},drag:function(e,a){i._trigger("drag",e,t(a))},stop:function(s,n){a.position=[n.position.left-i.document.scrollLeft(),n.position.top-i.document.scrollTop()],e(this).removeClass("ui-dialog-dragging"),i._unblockFrames(),i._trigger("dragStop",s,t(n))}})},_makeResizable:function(){function t(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}var i=this,a=this.options,s=a.resizable,n=this.uiDialog.css("position"),r="string"==typeof s?s:"n,e,s,w,se,sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:a.maxWidth,maxHeight:a.maxHeight,minWidth:a.minWidth,minHeight:this._minHeight(),handles:r,start:function(a,s){e(this).addClass("ui-dialog-resizing"),i._blockFrames(),i._trigger("resizeStart",a,t(s))},resize:function(e,a){i._trigger("resize",e,t(a))},stop:function(s,n){a.height=e(this).height(),a.width=e(this).width(),e(this).removeClass("ui-dialog-resizing"),i._unblockFrames(),i._trigger("resizeStop",s,t(n))}}).css("position",n)},_minHeight:function(){var e=this.options;return"auto"===e.height?e.minHeight:Math.min(e.minHeight,e.height)},_position:function(){var e=this.uiDialog.is(":visible");e||this.uiDialog.show(),this.uiDialog.position(this.options.position),e||this.uiDialog.hide()},_setOptions:function(a){var s=this,n=!1,r={};e.each(a,function(e,a){s._setOption(e,a),e in t&&(n=!0),e in i&&(r[e]=a)}),n&&(this._size(),this._position()),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option",r)},_setOption:function(e,t){var i,a,s=this.uiDialog;"dialogClass"===e&&s.removeClass(this.options.dialogClass).addClass(t),"disabled"!==e&&(this._super(e,t),"appendTo"===e&&this.uiDialog.appendTo(this._appendTo()),"buttons"===e&&this._createButtons(),"closeText"===e&&this.uiDialogTitlebarClose.button({label:""+t}),"draggable"===e&&(i=s.is(":data(ui-draggable)"),i&&!t&&s.draggable("destroy"),!i&&t&&this._makeDraggable()),"position"===e&&this._position(),"resizable"===e&&(a=s.is(":data(ui-resizable)"),a&&!t&&s.resizable("destroy"),a&&"string"==typeof t&&s.resizable("option","handles",t),a||t===!1||this._makeResizable()),"title"===e&&this._title(this.uiDialogTitlebar.find(".ui-dialog-title")))},_size:function(){var e,t,i,a=this.options;this.element.show().css({width:"auto",minHeight:0,maxHeight:"none",height:0}),a.minWidth>a.width&&(a.width=a.minWidth),e=this.uiDialog.css({height:"auto",width:a.width}).outerHeight(),t=Math.max(0,a.minHeight-e),i="number"==typeof a.maxHeight?Math.max(0,a.maxHeight-e):"none","auto"===a.height?this.element.css({minHeight:t,maxHeight:i,height:"auto"}):this.element.height(Math.max(0,a.height-e)),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())},_blockFrames:function(){this.iframeBlocks=this.document.find("iframe").map(function(){var t=e(this);return e("<div>").css({position:"absolute",width:t.outerWidth(),height:t.outerHeight()}).appendTo(t.parent()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_allowInteraction:function(t){return e(t.target).closest(".ui-dialog").length?!0:!!e(t.target).closest(".ui-datepicker").length},_createOverlay:function(){if(this.options.modal){var t=this,i=this.widgetFullName;e.ui.dialog.overlayInstances||this._delay(function(){e.ui.dialog.overlayInstances&&this.document.bind("focusin.dialog",function(a){t._allowInteraction(a)||(a.preventDefault(),e(".ui-dialog:visible:last .ui-dialog-content").data(i)._focusTabbable())})}),this.overlay=e("<div>").addClass("ui-widget-overlay ui-front").appendTo(this._appendTo()),this._on(this.overlay,{mousedown:"_keepFocus"}),e.ui.dialog.overlayInstances++}},_destroyOverlay:function(){this.options.modal&&this.overlay&&(e.ui.dialog.overlayInstances--,e.ui.dialog.overlayInstances||this.document.unbind("focusin.dialog"),this.overlay.remove(),this.overlay=null)}}),e.ui.dialog.overlayInstances=0,e.uiBackCompat!==!1&&e.widget("ui.dialog",e.ui.dialog,{_position:function(){var t,i=this.options.position,a=[],s=[0,0];i?(("string"==typeof i||"object"==typeof i&&"0"in i)&&(a=i.split?i.split(" "):[i[0],i[1]],1===a.length&&(a[1]=a[0]),e.each(["left","top"],function(e,t){+a[e]===a[e]&&(s[e]=a[e],a[e]=t)}),i={my:a[0]+(0>s[0]?s[0]:"+"+s[0])+" "+a[1]+(0>s[1]?s[1]:"+"+s[1]),at:a.join(" ")}),i=e.extend({},e.ui.dialog.prototype.options.position,i)):i=e.ui.dialog.prototype.options.position,t=this.uiDialog.is(":visible"),t||this.uiDialog.show(),this.uiDialog.position(i),t||this.uiDialog.hide()}})})(jQuery);(function(t){t.widget("ui.draggable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"!==this.options.helper||/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},_destroy:function(){this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy()},_mouseCapture:function(e){var i=this.options;return this.helper||i.disabled||t(e.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(e),this.handle?(t(i.iframeFix===!0?"iframe":i.iframeFix).each(function(){t("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>").css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(t(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(e){var i=this.options;return this.helper=this._createHelper(e),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),t.ui.ddmanager&&(t.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offsetParent=this.helper.offsetParent(),this.offsetParentCssPosition=this.offsetParent.css("position"),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},this.offset.scroll=!1,t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",e)===!1?(this._clear(),!1):(this._cacheHelperProportions(),t.ui.ddmanager&&!i.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this._mouseDrag(e,!0),t.ui.ddmanager&&t.ui.ddmanager.dragStart(this,e),!0)},_mouseDrag:function(e,i){if("fixed"===this.offsetParentCssPosition&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",e,s)===!1)return this._mouseUp({}),!1;this.position=s.position}return this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),!1},_mouseStop:function(e){var i=this,s=!1;return t.ui.ddmanager&&!this.options.dropBehaviour&&(s=t.ui.ddmanager.drop(this,e)),this.dropped&&(s=this.dropped,this.dropped=!1),"original"!==this.options.helper||t.contains(this.element[0].ownerDocument,this.element[0])?("invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||t.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?t(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",e)!==!1&&i._clear()}):this._trigger("stop",e)!==!1&&this._clear(),!1):!1},_mouseUp:function(e){return t("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),t.ui.ddmanager&&t.ui.ddmanager.dragStop(this,e),t.ui.mouse.prototype._mouseUp.call(this,e)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(e){return this.options.handle?!!t(e.target).closest(this.element.find(this.options.handle)).length:!0},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return s.parents("body").length||s.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s[0]===this.element[0]||/(fixed|absolute)/.test(s.css("position"))||s.css("position","absolute"),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.element.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;return n.containment?"window"===n.containment?(this.containment=[t(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,t(window).scrollLeft()+t(window).width()-this.helperProportions.width-this.margins.left,t(window).scrollTop()+(t(window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):"document"===n.containment?(this.containment=[0,0,t(document).width()-this.helperProportions.width-this.margins.left,(t(document).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):n.containment.constructor===Array?(this.containment=n.containment,undefined):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=t(n.containment),s=i[0],s&&(e="hidden"!==i.css("overflow"),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(e?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(e?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=i),undefined):(this.containment=null,undefined)},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent;return this.offset.scroll||(this.offset.scroll={top:n.scrollTop(),left:n.scrollLeft()}),{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top)*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)*s}},_generatePosition:function(e){var i,s,n,a,o=this.options,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,l=e.pageX,h=e.pageY;return this.offset.scroll||(this.offset.scroll={top:r.scrollTop(),left:r.scrollLeft()}),this.originalPosition&&(this.containment&&(this.relative_container?(s=this.relative_container.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,e.pageX-this.offset.click.left<i[0]&&(l=i[0]+this.offset.click.left),e.pageY-this.offset.click.top<i[1]&&(h=i[1]+this.offset.click.top),e.pageX-this.offset.click.left>i[2]&&(l=i[2]+this.offset.click.left),e.pageY-this.offset.click.top>i[3]&&(h=i[3]+this.offset.click.top)),o.grid&&(n=o.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/o.grid[1])*o.grid[1]:this.originalPageY,h=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-o.grid[1]:n+o.grid[1]:n,a=o.grid[0]?this.originalPageX+Math.round((l-this.originalPageX)/o.grid[0])*o.grid[0]:this.originalPageX,l=i?a-this.offset.click.left>=i[0]||a-this.offset.click.left>i[2]?a:a-this.offset.click.left>=i[0]?a-o.grid[0]:a+o.grid[0]:a)),{top:h-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top),left:l-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(e,i,s){return s=s||this._uiHash(),t.ui.plugin.call(this,e,[i,s]),"drag"===e&&(this.positionAbs=this._convertPositionTo("absolute")),t.Widget.prototype._trigger.call(this,e,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),t.ui.plugin.add("draggable","connectToSortable",{start:function(e,i){var s=t(this).data("ui-draggable"),n=s.options,a=t.extend({},i,{item:s.element});s.sortables=[],t(n.connectToSortable).each(function(){var i=t.data(this,"ui-sortable");i&&!i.options.disabled&&(s.sortables.push({instance:i,shouldRevert:i.options.revert}),i.refreshPositions(),i._trigger("activate",e,a))})},stop:function(e,i){var s=t(this).data("ui-draggable"),n=t.extend({},i,{item:s.element});t.each(s.sortables,function(){this.instance.isOver?(this.instance.isOver=0,s.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=this.shouldRevert),this.instance._mouseStop(e),this.instance.options.helper=this.instance.options._helper,"original"===s.options.helper&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",e,n))})},drag:function(e,i){var s=t(this).data("ui-draggable"),n=this;t.each(s.sortables,function(){var a=!1,o=this;this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this.instance._intersectsWith(this.instance.containerCache)&&(a=!0,t.each(s.sortables,function(){return this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this!==o&&this.instance._intersectsWith(this.instance.containerCache)&&t.contains(o.instance.element[0],this.instance.element[0])&&(a=!1),a})),a?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=t(n).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return i.helper[0]},e.target=this.instance.currentItem[0],this.instance._mouseCapture(e,!0),this.instance._mouseStart(e,!0,!0),this.instance.offset.click.top=s.offset.click.top,this.instance.offset.click.left=s.offset.click.left,this.instance.offset.parent.left-=s.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=s.offset.parent.top-this.instance.offset.parent.top,s._trigger("toSortable",e),s.dropped=this.instance.element,s.currentItem=s.element,this.instance.fromOutside=s),this.instance.currentItem&&this.instance._mouseDrag(e)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",e,this.instance._uiHash(this.instance)),this.instance._mouseStop(e,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),s._trigger("fromSortable",e),s.dropped=!1)})}}),t.ui.plugin.add("draggable","cursor",{start:function(){var e=t("body"),i=t(this).data("ui-draggable").options;e.css("cursor")&&(i._cursor=e.css("cursor")),e.css("cursor",i.cursor)},stop:function(){var e=t(this).data("ui-draggable").options;e._cursor&&t("body").css("cursor",e._cursor)}}),t.ui.plugin.add("draggable","opacity",{start:function(e,i){var s=t(i.helper),n=t(this).data("ui-draggable").options;s.css("opacity")&&(n._opacity=s.css("opacity")),s.css("opacity",n.opacity)},stop:function(e,i){var s=t(this).data("ui-draggable").options;s._opacity&&t(i.helper).css("opacity",s._opacity)}}),t.ui.plugin.add("draggable","scroll",{start:function(){var e=t(this).data("ui-draggable");e.scrollParent[0]!==document&&"HTML"!==e.scrollParent[0].tagName&&(e.overflowOffset=e.scrollParent.offset())},drag:function(e){var i=t(this).data("ui-draggable"),s=i.options,n=!1;i.scrollParent[0]!==document&&"HTML"!==i.scrollParent[0].tagName?(s.axis&&"x"===s.axis||(i.overflowOffset.top+i.scrollParent[0].offsetHeight-e.pageY<s.scrollSensitivity?i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop+s.scrollSpeed:e.pageY-i.overflowOffset.top<s.scrollSensitivity&&(i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop-s.scrollSpeed)),s.axis&&"y"===s.axis||(i.overflowOffset.left+i.scrollParent[0].offsetWidth-e.pageX<s.scrollSensitivity?i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft+s.scrollSpeed:e.pageX-i.overflowOffset.left<s.scrollSensitivity&&(i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft-s.scrollSpeed))):(s.axis&&"x"===s.axis||(e.pageY-t(document).scrollTop()<s.scrollSensitivity?n=t(document).scrollTop(t(document).scrollTop()-s.scrollSpeed):t(window).height()-(e.pageY-t(document).scrollTop())<s.scrollSensitivity&&(n=t(document).scrollTop(t(document).scrollTop()+s.scrollSpeed))),s.axis&&"y"===s.axis||(e.pageX-t(document).scrollLeft()<s.scrollSensitivity?n=t(document).scrollLeft(t(document).scrollLeft()-s.scrollSpeed):t(window).width()-(e.pageX-t(document).scrollLeft())<s.scrollSensitivity&&(n=t(document).scrollLeft(t(document).scrollLeft()+s.scrollSpeed)))),n!==!1&&t.ui.ddmanager&&!s.dropBehaviour&&t.ui.ddmanager.prepareOffsets(i,e)}}),t.ui.plugin.add("draggable","snap",{start:function(){var e=t(this).data("ui-draggable"),i=e.options;e.snapElements=[],t(i.snap.constructor!==String?i.snap.items||":data(ui-draggable)":i.snap).each(function(){var i=t(this),s=i.offset();this!==e.element[0]&&e.snapElements.push({item:this,width:i.outerWidth(),height:i.outerHeight(),top:s.top,left:s.left})})},drag:function(e,i){var s,n,a,o,r,l,h,c,u,d,p=t(this).data("ui-draggable"),g=p.options,f=g.snapTolerance,m=i.offset.left,_=m+p.helperProportions.width,v=i.offset.top,b=v+p.helperProportions.height;for(u=p.snapElements.length-1;u>=0;u--)r=p.snapElements[u].left,l=r+p.snapElements[u].width,h=p.snapElements[u].top,c=h+p.snapElements[u].height,r-f>_||m>l+f||h-f>b||v>c+f||!t.contains(p.snapElements[u].item.ownerDocument,p.snapElements[u].item)?(p.snapElements[u].snapping&&p.options.snap.release&&p.options.snap.release.call(p.element,e,t.extend(p._uiHash(),{snapItem:p.snapElements[u].item})),p.snapElements[u].snapping=!1):("inner"!==g.snapMode&&(s=f>=Math.abs(h-b),n=f>=Math.abs(c-v),a=f>=Math.abs(r-_),o=f>=Math.abs(l-m),s&&(i.position.top=p._convertPositionTo("relative",{top:h-p.helperProportions.height,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:c,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r-p.helperProportions.width}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:l}).left-p.margins.left)),d=s||n||a||o,"outer"!==g.snapMode&&(s=f>=Math.abs(h-v),n=f>=Math.abs(c-b),a=f>=Math.abs(r-m),o=f>=Math.abs(l-_),s&&(i.position.top=p._convertPositionTo("relative",{top:h,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:c-p.helperProportions.height,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:l-p.helperProportions.width}).left-p.margins.left)),!p.snapElements[u].snapping&&(s||n||a||o||d)&&p.options.snap.snap&&p.options.snap.snap.call(p.element,e,t.extend(p._uiHash(),{snapItem:p.snapElements[u].item})),p.snapElements[u].snapping=s||n||a||o||d)}}),t.ui.plugin.add("draggable","stack",{start:function(){var e,i=this.data("ui-draggable").options,s=t.makeArray(t(i.stack)).sort(function(e,i){return(parseInt(t(e).css("zIndex"),10)||0)-(parseInt(t(i).css("zIndex"),10)||0)});s.length&&(e=parseInt(t(s[0]).css("zIndex"),10)||0,t(s).each(function(i){t(this).css("zIndex",e+i)}),this.css("zIndex",e+s.length))}}),t.ui.plugin.add("draggable","zIndex",{start:function(e,i){var s=t(i.helper),n=t(this).data("ui-draggable").options;s.css("zIndex")&&(n._zIndex=s.css("zIndex")),s.css("zIndex",n.zIndex)},stop:function(e,i){var s=t(this).data("ui-draggable").options;s._zIndex&&t(i.helper).css("zIndex",s._zIndex)}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}t.widget("ui.droppable",{version:"1.10.4",widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var e,i=this.options,s=i.accept;this.isover=!1,this.isout=!0,this.accept=t.isFunction(s)?s:function(t){return t.is(s)},this.proportions=function(){return arguments.length?(e=arguments[0],undefined):e?e:e={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight}},t.ui.ddmanager.droppables[i.scope]=t.ui.ddmanager.droppables[i.scope]||[],t.ui.ddmanager.droppables[i.scope].push(this),i.addClasses&&this.element.addClass("ui-droppable")},_destroy:function(){for(var e=0,i=t.ui.ddmanager.droppables[this.options.scope];i.length>e;e++)i[e]===this&&i.splice(e,1);this.element.removeClass("ui-droppable ui-droppable-disabled")},_setOption:function(e,i){"accept"===e&&(this.accept=t.isFunction(i)?i:function(t){return t.is(i)}),t.Widget.prototype._setOption.apply(this,arguments)},_activate:function(e){var i=t.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),i&&this._trigger("activate",e,this.ui(i))},_deactivate:function(e){var i=t.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),i&&this._trigger("deactivate",e,this.ui(i))},_over:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",e,this.ui(i)))},_out:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",e,this.ui(i)))},_drop:function(e,i){var s=i||t.ui.ddmanager.current,n=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var e=t.data(this,"ui-droppable");return e.options.greedy&&!e.options.disabled&&e.options.scope===s.options.scope&&e.accept.call(e.element[0],s.currentItem||s.element)&&t.ui.intersect(s,t.extend(e,{offset:e.element.offset()}),e.options.tolerance)?(n=!0,!1):undefined}),n?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",e,this.ui(s)),this.element):!1):!1},ui:function(t){return{draggable:t.currentItem||t.element,helper:t.helper,position:t.position,offset:t.positionAbs}}}),t.ui.intersect=function(t,i,s){if(!i.offset)return!1;var n,a,o=(t.positionAbs||t.position.absolute).left,r=(t.positionAbs||t.position.absolute).top,l=o+t.helperProportions.width,h=r+t.helperProportions.height,c=i.offset.left,u=i.offset.top,d=c+i.proportions().width,p=u+i.proportions().height;switch(s){case"fit":return o>=c&&d>=l&&r>=u&&p>=h;case"intersect":return o+t.helperProportions.width/2>c&&d>l-t.helperProportions.width/2&&r+t.helperProportions.height/2>u&&p>h-t.helperProportions.height/2;case"pointer":return n=(t.positionAbs||t.position.absolute).left+(t.clickOffset||t.offset.click).left,a=(t.positionAbs||t.position.absolute).top+(t.clickOffset||t.offset.click).top,e(a,u,i.proportions().height)&&e(n,c,i.proportions().width);case"touch":return(r>=u&&p>=r||h>=u&&p>=h||u>r&&h>p)&&(o>=c&&d>=o||l>=c&&d>=l||c>o&&l>d);default:return!1}},t.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,i){var s,n,a=t.ui.ddmanager.droppables[e.options.scope]||[],o=i?i.type:null,r=(e.currentItem||e.element).find(":data(ui-droppable)").addBack();t:for(s=0;a.length>s;s++)if(!(a[s].options.disabled||e&&!a[s].accept.call(a[s].element[0],e.currentItem||e.element))){for(n=0;r.length>n;n++)if(r[n]===a[s].element[0]){a[s].proportions().height=0;continue t}a[s].visible="none"!==a[s].element.css("display"),a[s].visible&&("mousedown"===o&&a[s]._activate.call(a[s],i),a[s].offset=a[s].element.offset(),a[s].proportions({width:a[s].element[0].offsetWidth,height:a[s].element[0].offsetHeight}))}},drop:function(e,i){var s=!1;return t.each((t.ui.ddmanager.droppables[e.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&t.ui.intersect(e,this,this.options.tolerance)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],e.currentItem||e.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(e,i){e.element.parentsUntil("body").bind("scroll.droppable",function(){e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)})},drag:function(e,i){e.options.refreshPositions&&t.ui.ddmanager.prepareOffsets(e,i),t.each(t.ui.ddmanager.droppables[e.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,n,a,o=t.ui.intersect(e,this,this.options.tolerance),r=!o&&this.isover?"isout":o&&!this.isover?"isover":null;r&&(this.options.greedy&&(n=this.options.scope,a=this.element.parents(":data(ui-droppable)").filter(function(){return t.data(this,"ui-droppable").options.scope===n}),a.length&&(s=t.data(a[0],"ui-droppable"),s.greedyChild="isover"===r)),s&&"isover"===r&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[r]=!0,this["isout"===r?"isover":"isout"]=!1,this["isover"===r?"_over":"_out"].call(this,i),s&&"isout"===r&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(e,i){e.element.parentsUntil("body").unbind("scroll.droppable"),e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)}}})(jQuery);(function(t,e){var i="ui-effects-";t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=h(),n=s._rgba=[];return i=i.toLowerCase(),f(l,function(t,a){var o,r=a.re.exec(i),l=r&&a.parse(r),h=a.space||"rgba";return l?(o=s[h](l),s[c[h].cache]=o[c[h].cache],n=s._rgba=o._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,a.transparent),s):a[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var a,o="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,l=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],h=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=h.support={},p=t("<p>")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),h.fn=t.extend(h.prototype,{parse:function(n,o,r,l){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(o),o=e);var u=this,d=t.type(n),p=this._rgba=[];return o!==e&&(n=[n,o,r,l],d="array"),"string"===d?this.parse(s(n)||a._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof h?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var a=s.cache;f(s.props,function(t,e){if(!u[a]&&s.to){if("alpha"===t||null==n[t])return;u[a]=s.to(u._rgba)}u[a][e.idx]=i(n[t],e,!0)}),u[a]&&0>t.inArray(null,u[a].slice(0,3))&&(u[a][3]=1,s.from&&(u._rgba=s.from(u[a])))}),this):e},is:function(t){var i=h(t),s=!0,n=this;return f(c,function(t,a){var o,r=i[a.cache];return r&&(o=n[a.cache]||a.to&&a.to(n._rgba)||[],f(a.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===o[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=h(t),n=s._space(),a=c[n],o=0===this.alpha()?h("transparent"):this,r=o[a.cache]||a.to(o._rgba),l=r.slice();return s=s[a.cache],f(a.props,function(t,n){var a=n.idx,o=r[a],h=s[a],c=u[n.type]||{};null!==h&&(null===o?l[a]=h:(c.mod&&(h-o>c.mod/2?o+=c.mod:o-h>c.mod/2&&(o-=c.mod)),l[a]=i((h-o)*e+o,n)))}),this[n](l)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=h(e)._rgba;return h(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),h.fn.parse.prototype=h.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,a=t[2]/255,o=t[3],r=Math.max(s,n,a),l=Math.min(s,n,a),h=r-l,c=r+l,u=.5*c;return e=l===r?0:s===r?60*(n-a)/h+360:n===r?60*(a-s)/h+120:60*(s-n)/h+240,i=0===h?0:.5>=u?h/c:h/(2-c),[Math.round(e)%360,i,u,null==o?1:o]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],a=t[3],o=.5>=s?s*(1+i):s+i-s*i,r=2*s-o;return[Math.round(255*n(r,o,e+1/3)),Math.round(255*n(r,o,e)),Math.round(255*n(r,o,e-1/3)),a]},f(c,function(s,n){var a=n.props,o=n.cache,l=n.to,c=n.from;h.fn[s]=function(s){if(l&&!this[o]&&(this[o]=l(this._rgba)),s===e)return this[o].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[o].slice();return f(a,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=h(c(d)),n[o]=d,n):h(d)},f(a,function(e,i){h.fn[e]||(h.fn[e]=function(n){var a,o=t.type(n),l="alpha"===e?this._hsla?"hsla":"rgba":s,h=this[l](),c=h[i.idx];return"undefined"===o?c:("function"===o&&(n=n.call(this,c),o=t.type(n)),null==n&&i.empty?this:("string"===o&&(a=r.exec(n),a&&(n=c+parseFloat(a[2])*("+"===a[1]?1:-1))),h[i.idx]=n,this[l](h)))})})}),h.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var a,o,r="";if("transparent"!==n&&("string"!==t.type(n)||(a=s(n)))){if(n=h(a||n),!d.rgba&&1!==n._rgba[3]){for(o="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&o&&o.style;)try{r=t.css(o,"backgroundColor"),o=o.parentNode}catch(l){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(l){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=h(e.elem,i),e.end=h(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},h.hook(o),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},a=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(jQuery),function(){function i(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,a={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(a[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(a[i]=n[i]);return a}function s(e,i){var s,n,o={};for(s in i)n=i[s],e[s]!==n&&(a[s]||(t.fx.step[s]||!isNaN(parseFloat(n)))&&(o[s]=n));return o}var n=["add","remove","toggle"],a={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(jQuery.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(e,a,o,r){var l=t.speed(a,o,r);return this.queue(function(){var a,o=t(this),r=o.attr("class")||"",h=l.children?o.find("*").addBack():o;h=h.map(function(){var e=t(this);return{el:e,start:i(this)}}),a=function(){t.each(n,function(t,i){e[i]&&o[i+"Class"](e[i])})},a(),h=h.map(function(){return this.end=i(this.el[0]),this.diff=s(this.start,this.end),this}),o.attr("class",r),h=h.map(function(){var e=this,i=t.Deferred(),s=t.extend({},l,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,h.get()).done(function(){a(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),l.complete.call(o[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,a){return s?t.effects.animateClass.call(this,{add:i},s,n,a):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,a){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,a):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(i){return function(s,n,a,o,r){return"boolean"==typeof n||n===e?a?t.effects.animateClass.call(this,n?{add:s}:{remove:s},a,o,r):i.apply(this,arguments):t.effects.animateClass.call(this,{toggle:s},n,a,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,a){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,a)}})}(),function(){function s(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function n(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}t.extend(t.effects,{version:"1.10.4",save:function(t,e){for(var s=0;e.length>s;s++)null!==e[s]&&t.data(i+e[s],t[0].style[e[s]])},restore:function(t,s){var n,a;for(a=0;s.length>a;a++)null!==s[a]&&(n=t.data(i+s[a]),n===e&&(n=""),t.css(s[a],n))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("<div></div>").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},a=document.activeElement;try{a.id}catch(o){a=document.body}return e.wrap(s),(e[0]===a||t.contains(e[0],a))&&t(a).focus(),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).focus()),e},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var a=e.cssUnit(i);a[0]>0&&(n[i]=a[0]*s+a[1])}),n}}),t.fn.extend({effect:function(){function e(e){function s(){t.isFunction(a)&&a.call(n[0]),t.isFunction(e)&&e()}var n=t(this),a=i.complete,r=i.mode;(n.is(":hidden")?"hide"===r:"show"===r)?(n[r](),s()):o.call(n[0],i,s)}var i=s.apply(this,arguments),n=i.mode,a=i.queue,o=t.effects.effect[i.effect];return t.fx.off||!o?n?this[n](i.duration,i.complete):this.each(function(){i.complete&&i.complete.call(this)}):a===!1?this.each(e):this.queue(a||"fx",e)},show:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="show",this.effect.call(this,i)}}(t.fn.show),hide:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="hide",this.effect.call(this,i)}}(t.fn.hide),toggle:function(t){return function(e){if(n(e)||"boolean"==typeof e)return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="toggle",this.effect.call(this,i)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s}})}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}()})(jQuery);(function(t){var e=/up|down|vertical/,i=/up|left|vertical|horizontal/;t.effects.effect.blind=function(s,n){var a,o,r,l=t(this),h=["position","top","bottom","left","right","height","width"],c=t.effects.setMode(l,s.mode||"hide"),u=s.direction||"up",d=e.test(u),p=d?"height":"width",f=d?"top":"left",g=i.test(u),m={},v="show"===c;l.parent().is(".ui-effects-wrapper")?t.effects.save(l.parent(),h):t.effects.save(l,h),l.show(),a=t.effects.createWrapper(l).css({overflow:"hidden"}),o=a[p](),r=parseFloat(a.css(f))||0,m[p]=v?o:0,g||(l.css(d?"bottom":"right",0).css(d?"top":"left","auto").css({position:"absolute"}),m[f]=v?r:o+r),v&&(a.css(p,0),g||a.css(f,r+o)),a.animate(m,{duration:s.duration,easing:s.easing,queue:!1,complete:function(){"hide"===c&&l.hide(),t.effects.restore(l,h),t.effects.removeWrapper(l),n()}})}})(jQuery);(function(t){t.effects.effect.bounce=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","height","width"],l=t.effects.setMode(o,e.mode||"effect"),h="hide"===l,c="show"===l,u=e.direction||"up",d=e.distance,p=e.times||5,f=2*p+(c||h?1:0),g=e.duration/f,m=e.easing,v="up"===u||"down"===u?"top":"left",_="up"===u||"left"===u,b=o.queue(),y=b.length;for((c||h)&&r.push("opacity"),t.effects.save(o,r),o.show(),t.effects.createWrapper(o),d||(d=o["top"===v?"outerHeight":"outerWidth"]()/3),c&&(a={opacity:1},a[v]=0,o.css("opacity",0).css(v,_?2*-d:2*d).animate(a,g,m)),h&&(d/=Math.pow(2,p-1)),a={},a[v]=0,s=0;p>s;s++)n={},n[v]=(_?"-=":"+=")+d,o.animate(n,g,m).animate(a,g,m),d=h?2*d:d/2;h&&(n={opacity:0},n[v]=(_?"-=":"+=")+d,o.animate(n,g,m)),o.queue(function(){h&&o.hide(),t.effects.restore(o,r),t.effects.removeWrapper(o),i()}),y>1&&b.splice.apply(b,[1,0].concat(b.splice(y,f+1))),o.dequeue()}})(jQuery);(function(t){t.effects.effect.clip=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","height","width"],l=t.effects.setMode(o,e.mode||"hide"),h="show"===l,c=e.direction||"vertical",u="vertical"===c,d=u?"height":"width",p=u?"top":"left",f={};t.effects.save(o,r),o.show(),s=t.effects.createWrapper(o).css({overflow:"hidden"}),n="IMG"===o[0].tagName?s:o,a=n[d](),h&&(n.css(d,0),n.css(p,a/2)),f[d]=h?a:0,f[p]=h?0:a/2,n.animate(f,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){h||o.hide(),t.effects.restore(o,r),t.effects.removeWrapper(o),i()}})}})(jQuery);(function(t){t.effects.effect.drop=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","opacity","height","width"],o=t.effects.setMode(n,e.mode||"hide"),r="show"===o,l=e.direction||"left",h="up"===l||"down"===l?"top":"left",c="up"===l||"left"===l?"pos":"neg",u={opacity:r?1:0};t.effects.save(n,a),n.show(),t.effects.createWrapper(n),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0)/2,r&&n.css("opacity",0).css(h,"pos"===c?-s:s),u[h]=(r?"pos"===c?"+=":"-=":"pos"===c?"-=":"+=")+s,n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}})}})(jQuery);(function(t){t.effects.effect.explode=function(e,i){function s(){b.push(this),b.length===u*d&&n()}function n(){p.css({visibility:"visible"}),t(b).remove(),g||p.hide(),i()}var a,o,r,l,h,c,u=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=u,p=t(this),f=t.effects.setMode(p,e.mode||"hide"),g="show"===f,m=p.show().css("visibility","hidden").offset(),v=Math.ceil(p.outerWidth()/d),_=Math.ceil(p.outerHeight()/u),b=[];for(a=0;u>a;a++)for(l=m.top+a*_,c=a-(u-1)/2,o=0;d>o;o++)r=m.left+o*v,h=o-(d-1)/2,p.clone().appendTo("body").wrap("<div></div>").css({position:"absolute",visibility:"visible",left:-o*v,top:-a*_}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:v,height:_,left:r+(g?h*v:0),top:l+(g?c*_:0),opacity:g?0:1}).animate({left:r+(g?0:h*v),top:l+(g?0:c*_),opacity:g?1:0},e.duration||500,e.easing,s)}})(jQuery);(function(t){t.effects.effect.fade=function(e,i){var s=t(this),n=t.effects.setMode(s,e.mode||"toggle");s.animate({opacity:n},{queue:!1,duration:e.duration,easing:e.easing,complete:i})}})(jQuery);(function(t){t.effects.effect.fold=function(e,i){var s,n,a=t(this),o=["position","top","bottom","left","right","height","width"],r=t.effects.setMode(a,e.mode||"hide"),l="show"===r,h="hide"===r,c=e.size||15,u=/([0-9]+)%/.exec(c),d=!!e.horizFirst,p=l!==d,f=p?["width","height"]:["height","width"],g=e.duration/2,m={},v={};t.effects.save(a,o),a.show(),s=t.effects.createWrapper(a).css({overflow:"hidden"}),n=p?[s.width(),s.height()]:[s.height(),s.width()],u&&(c=parseInt(u[1],10)/100*n[h?0:1]),l&&s.css(d?{height:0,width:c}:{height:c,width:0}),m[f[0]]=l?n[0]:c,v[f[1]]=l?n[1]:0,s.animate(m,g,e.easing).animate(v,g,e.easing,function(){h&&a.hide(),t.effects.restore(a,o),t.effects.removeWrapper(a),i()})}})(jQuery);(function(t){t.effects.effect.highlight=function(e,i){var s=t(this),n=["backgroundImage","backgroundColor","opacity"],a=t.effects.setMode(s,e.mode||"show"),o={backgroundColor:s.css("backgroundColor")};"hide"===a&&(o.opacity=0),t.effects.save(s,n),s.show().css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(o,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===a&&s.hide(),t.effects.restore(s,n),i()}})}})(jQuery);(function(t){t.effects.effect.pulsate=function(e,i){var s,n=t(this),a=t.effects.setMode(n,e.mode||"show"),o="show"===a,r="hide"===a,l=o||"hide"===a,h=2*(e.times||5)+(l?1:0),c=e.duration/h,u=0,d=n.queue(),p=d.length;for((o||!n.is(":visible"))&&(n.css("opacity",0).show(),u=1),s=1;h>s;s++)n.animate({opacity:u},c,e.easing),u=1-u;n.animate({opacity:u},c,e.easing),n.queue(function(){r&&n.hide(),i()}),p>1&&d.splice.apply(d,[1,0].concat(d.splice(p,h+1))),n.dequeue()}})(jQuery);(function(t){t.effects.effect.puff=function(e,i){var s=t(this),n=t.effects.setMode(s,e.mode||"hide"),a="hide"===n,o=parseInt(e.percent,10)||150,r=o/100,l={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()};t.extend(e,{effect:"scale",queue:!1,fade:!0,mode:n,complete:i,percent:a?o:100,from:a?l:{height:l.height*r,width:l.width*r,outerHeight:l.outerHeight*r,outerWidth:l.outerWidth*r}}),s.effect(e)},t.effects.effect.scale=function(e,i){var s=t(this),n=t.extend(!0,{},e),a=t.effects.setMode(s,e.mode||"effect"),o=parseInt(e.percent,10)||(0===parseInt(e.percent,10)?0:"hide"===a?0:100),r=e.direction||"both",l=e.origin,h={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()},c={y:"horizontal"!==r?o/100:1,x:"vertical"!==r?o/100:1};n.effect="size",n.queue=!1,n.complete=i,"effect"!==a&&(n.origin=l||["middle","center"],n.restore=!0),n.from=e.from||("show"===a?{height:0,width:0,outerHeight:0,outerWidth:0}:h),n.to={height:h.height*c.y,width:h.width*c.x,outerHeight:h.outerHeight*c.y,outerWidth:h.outerWidth*c.x},n.fade&&("show"===a&&(n.from.opacity=0,n.to.opacity=1),"hide"===a&&(n.from.opacity=1,n.to.opacity=0)),s.effect(n)},t.effects.effect.size=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","width","height","overflow","opacity"],l=["position","top","bottom","left","right","overflow","opacity"],h=["width","height","overflow"],c=["fontSize"],u=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],d=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],p=t.effects.setMode(o,e.mode||"effect"),f=e.restore||"effect"!==p,g=e.scale||"both",m=e.origin||["middle","center"],v=o.css("position"),_=f?r:l,b={height:0,width:0,outerHeight:0,outerWidth:0};"show"===p&&o.show(),s={height:o.height(),width:o.width(),outerHeight:o.outerHeight(),outerWidth:o.outerWidth()},"toggle"===e.mode&&"show"===p?(o.from=e.to||b,o.to=e.from||s):(o.from=e.from||("show"===p?b:s),o.to=e.to||("hide"===p?b:s)),a={from:{y:o.from.height/s.height,x:o.from.width/s.width},to:{y:o.to.height/s.height,x:o.to.width/s.width}},("box"===g||"both"===g)&&(a.from.y!==a.to.y&&(_=_.concat(u),o.from=t.effects.setTransition(o,u,a.from.y,o.from),o.to=t.effects.setTransition(o,u,a.to.y,o.to)),a.from.x!==a.to.x&&(_=_.concat(d),o.from=t.effects.setTransition(o,d,a.from.x,o.from),o.to=t.effects.setTransition(o,d,a.to.x,o.to))),("content"===g||"both"===g)&&a.from.y!==a.to.y&&(_=_.concat(c).concat(h),o.from=t.effects.setTransition(o,c,a.from.y,o.from),o.to=t.effects.setTransition(o,c,a.to.y,o.to)),t.effects.save(o,_),o.show(),t.effects.createWrapper(o),o.css("overflow","hidden").css(o.from),m&&(n=t.effects.getBaseline(m,s),o.from.top=(s.outerHeight-o.outerHeight())*n.y,o.from.left=(s.outerWidth-o.outerWidth())*n.x,o.to.top=(s.outerHeight-o.to.outerHeight)*n.y,o.to.left=(s.outerWidth-o.to.outerWidth)*n.x),o.css(o.from),("content"===g||"both"===g)&&(u=u.concat(["marginTop","marginBottom"]).concat(c),d=d.concat(["marginLeft","marginRight"]),h=r.concat(u).concat(d),o.find("*[width]").each(function(){var i=t(this),s={height:i.height(),width:i.width(),outerHeight:i.outerHeight(),outerWidth:i.outerWidth()};f&&t.effects.save(i,h),i.from={height:s.height*a.from.y,width:s.width*a.from.x,outerHeight:s.outerHeight*a.from.y,outerWidth:s.outerWidth*a.from.x},i.to={height:s.height*a.to.y,width:s.width*a.to.x,outerHeight:s.height*a.to.y,outerWidth:s.width*a.to.x},a.from.y!==a.to.y&&(i.from=t.effects.setTransition(i,u,a.from.y,i.from),i.to=t.effects.setTransition(i,u,a.to.y,i.to)),a.from.x!==a.to.x&&(i.from=t.effects.setTransition(i,d,a.from.x,i.from),i.to=t.effects.setTransition(i,d,a.to.x,i.to)),i.css(i.from),i.animate(i.to,e.duration,e.easing,function(){f&&t.effects.restore(i,h)})})),o.animate(o.to,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){0===o.to.opacity&&o.css("opacity",o.from.opacity),"hide"===p&&o.hide(),t.effects.restore(o,_),f||("static"===v?o.css({position:"relative",top:o.to.top,left:o.to.left}):t.each(["top","left"],function(t,e){o.css(e,function(e,i){var s=parseInt(i,10),n=t?o.to.left:o.to.top;return"auto"===i?n+"px":s+n+"px"})})),t.effects.removeWrapper(o),i()}})}})(jQuery);(function(t){t.effects.effect.shake=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","height","width"],o=t.effects.setMode(n,e.mode||"effect"),r=e.direction||"left",l=e.distance||20,h=e.times||3,c=2*h+1,u=Math.round(e.duration/c),d="up"===r||"down"===r?"top":"left",p="up"===r||"left"===r,f={},g={},m={},v=n.queue(),_=v.length;for(t.effects.save(n,a),n.show(),t.effects.createWrapper(n),f[d]=(p?"-=":"+=")+l,g[d]=(p?"+=":"-=")+2*l,m[d]=(p?"-=":"+=")+2*l,n.animate(f,u,e.easing),s=1;h>s;s++)n.animate(g,u,e.easing).animate(m,u,e.easing);n.animate(g,u,e.easing).animate(f,u/2,e.easing).queue(function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}),_>1&&v.splice.apply(v,[1,0].concat(v.splice(_,c+1))),n.dequeue()}})(jQuery);(function(t){t.effects.effect.slide=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","width","height"],o=t.effects.setMode(n,e.mode||"show"),r="show"===o,l=e.direction||"left",h="up"===l||"down"===l?"top":"left",c="up"===l||"left"===l,u={};t.effects.save(n,a),n.show(),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0),t.effects.createWrapper(n).css({overflow:"hidden"}),r&&n.css(h,c?isNaN(s)?"-"+s:-s:s),u[h]=(r?c?"+=":"-=":c?"-=":"+=")+s,n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}})}})(jQuery);(function(t){t.effects.effect.transfer=function(e,i){var s=t(this),n=t(e.to),a="fixed"===n.css("position"),o=t("body"),r=a?o.scrollTop():0,l=a?o.scrollLeft():0,h=n.offset(),c={top:h.top-r,left:h.left-l,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("<div class='ui-effects-transfer'></div>").appendTo(document.body).addClass(e.className).css({top:u.top-r,left:u.left-l,height:s.innerHeight(),width:s.innerWidth(),position:a?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),i()})}})(jQuery);(function(t){t.widget("ui.menu",{version:"1.10.4",defaultElement:"<ul>",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,t.proxy(function(t){this.options.disabled&&t.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(t){t.preventDefault()},"click .ui-state-disabled > a":function(t){t.preventDefault()},"click .ui-menu-item:has(a)":function(e){var i=t(e.target).closest(".ui-menu-item");!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&t(this.document[0].activeElement).closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){var i=t(e.currentTarget);i.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(e,i)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.children(".ui-menu-item").eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){t.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(e){t(e.target).closest(".ui-menu").length||this.collapseAll(e),this.mouseHandled=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var e=t(this);e.data("ui-menu-submenu-carat")&&e.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(e){function i(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var s,n,a,o,r,l=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:l=!1,n=this.previousFilter||"",a=String.fromCharCode(e.keyCode),o=!1,clearTimeout(this.filterTimer),a===n?o=!0:a=n+a,r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())}),s=o&&-1!==s.index(this.active.next())?this.active.nextAll(".ui-menu-item"):s,s.length||(a=String.fromCharCode(e.keyCode),r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())})),s.length?(this.focus(e,s),s.length>1?(this.previousFilter=a,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}l&&e.preventDefault()},_activate:function(t){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i=this.options.icons.submenu,s=this.element.find(this.options.menus);this.element.toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length),s.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),s=e.prev("a"),n=t("<span>").addClass("ui-menu-icon ui-icon "+i).data("ui-menu-submenu-carat",!0);s.attr("aria-haspopup","true").prepend(n),e.attr("aria-labelledby",s.attr("id"))}),e=s.add(this.element),e.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),e.children(":not(.ui-menu-item)").each(function(){var e=t(this);/[^\-\u2014\u2013\s]/.test(e.text())||e.addClass("ui-widget-content ui-menu-divider")}),e.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){"icons"===t&&this.element.find(".ui-menu-icon").removeClass(this.options.icons.submenu).addClass(e.submenu),this._super(t,e)},focus:function(t,e){var i,s;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,a,o,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,a=this.activeMenu.scrollTop(),o=this.activeMenu.height(),r=e.height(),0>n?this.activeMenu.scrollTop(a+n):n+r>o&&this.activeMenu.scrollTop(a+n-o+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",t,{item:this.active}))},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.children(".ui-menu-item")[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())),undefined):(this.next(e),undefined)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item").first())),undefined):(this.next(e),undefined)},_hasScroll:function(){return this.element.outerHeight()<this.element.prop("scrollHeight")},select:function(e){this.active=this.active||t(e.target).closest(".ui-menu-item");var i={item:this.active};this.active.has(".ui-menu").length||this.collapseAll(e,!0),this._trigger("select",e,i)}})})(jQuery);(function(t,e){t.widget("ui.progressbar",{version:"1.10.4",options:{max:100,value:0,change:null,complete:null},min:0,_create:function(){this.oldValue=this.options.value=this._constrainedValue(),this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min}),this.valueDiv=t("<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>").appendTo(this.element),this._refreshValue()},_destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove()},value:function(t){return t===e?this.options.value:(this.options.value=this._constrainedValue(t),this._refreshValue(),e)},_constrainedValue:function(t){return t===e&&(t=this.options.value),this.indeterminate=t===!1,"number"!=typeof t&&(t=0),this.indeterminate?!1:Math.min(this.options.max,Math.max(this.min,t))},_setOptions:function(t){var e=t.value;delete t.value,this._super(t),this.options.value=this._constrainedValue(e),this._refreshValue()},_setOption:function(t,e){"max"===t&&(e=Math.max(this.min,e)),this._super(t,e)},_percentage:function(){return this.indeterminate?100:100*(this.options.value-this.min)/(this.options.max-this.min)},_refreshValue:function(){var e=this.options.value,i=this._percentage();this.valueDiv.toggle(this.indeterminate||e>this.min).toggleClass("ui-corner-right",e===this.options.max).width(i.toFixed(0)+"%"),this.element.toggleClass("ui-progressbar-indeterminate",this.indeterminate),this.indeterminate?(this.element.removeAttr("aria-valuenow"),this.overlayDiv||(this.overlayDiv=t("<div class='ui-progressbar-overlay'></div>").appendTo(this.valueDiv))):(this.element.attr({"aria-valuemax":this.options.max,"aria-valuenow":e}),this.overlayDiv&&(this.overlayDiv.remove(),this.overlayDiv=null)),this.oldValue!==e&&(this.oldValue=e,this._trigger("change")),e===this.options.max&&this._trigger("complete")}})})(jQuery);(function(t){function e(t){return parseInt(t,10)||0}function i(t){return!isNaN(parseInt(t,10))}t.widget("ui.resizable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_create:function(){var e,i,s,n,a,o=this,r=this.options;if(this.element.addClass("ui-resizable"),t.extend(this,{_aspectRatio:!!r.aspectRatio,aspectRatio:r.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:r.helper||r.ghost||r.animate?r.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(t("<div class='ui-wrapper' style='overflow: hidden;'></div>").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.data("ui-resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=r.handles||(t(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),e=this.handles.split(","),this.handles={},i=0;e.length>i;i++)s=t.trim(e[i]),a="ui-resizable-"+s,n=t("<div class='ui-resizable-handle "+a+"'></div>"),n.css({zIndex:r.zIndex}),"se"===s&&n.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[s]=".ui-resizable-"+s,this.element.append(n);this._renderAxis=function(e){var i,s,n,a;e=e||this.element;for(i in this.handles)this.handles[i].constructor===String&&(this.handles[i]=t(this.handles[i],this.element).show()),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)&&(s=t(this.handles[i],this.element),a=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),e.css(n,a),this._proportionallyResize()),t(this.handles[i]).length},this._renderAxis(this.element),this._handles=t(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){o.resizing||(this.className&&(n=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),o.axis=n&&n[1]?n[1]:"se")}),r.autoHide&&(this._handles.hide(),t(this.element).addClass("ui-resizable-autohide").mouseenter(function(){r.disabled||(t(this).removeClass("ui-resizable-autohide"),o._handles.show())}).mouseleave(function(){r.disabled||o.resizing||(t(this).addClass("ui-resizable-autohide"),o._handles.hide())})),this._mouseInit()},_destroy:function(){this._mouseDestroy();var e,i=function(e){t(e).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_mouseCapture:function(e){var i,s,n=!1;for(i in this.handles)s=t(this.handles[i])[0],(s===e.target||t.contains(s,e.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(i){var s,n,a,o=this.options,r=this.element.position(),h=this.element;return this.resizing=!0,/absolute/.test(h.css("position"))?h.css({position:"absolute",top:h.css("top"),left:h.css("left")}):h.is(".ui-draggable")&&h.css({position:"absolute",top:r.top,left:r.left}),this._renderProxy(),s=e(this.helper.css("left")),n=e(this.helper.css("top")),o.containment&&(s+=t(o.containment).scrollLeft()||0,n+=t(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:s,top:n},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:h.width(),height:h.height()},this.originalSize=this._helper?{width:h.outerWidth(),height:h.outerHeight()}:{width:h.width(),height:h.height()},this.originalPosition={left:s,top:n},this.sizeDiff={width:h.outerWidth()-h.width(),height:h.outerHeight()-h.height()},this.originalMousePosition={left:i.pageX,top:i.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,a=t(".ui-resizable-"+this.axis).css("cursor"),t("body").css("cursor","auto"===a?this.axis+"-resize":a),h.addClass("ui-resizable-resizing"),this._propagate("start",i),!0},_mouseDrag:function(e){var i,s=this.helper,n={},a=this.originalMousePosition,o=this.axis,r=this.position.top,h=this.position.left,l=this.size.width,c=this.size.height,u=e.pageX-a.left||0,d=e.pageY-a.top||0,p=this._change[o];return p?(i=p.apply(this,[e,u,d]),this._updateVirtualBoundaries(e.shiftKey),(this._aspectRatio||e.shiftKey)&&(i=this._updateRatio(i,e)),i=this._respectSize(i,e),this._updateCache(i),this._propagate("resize",e),this.position.top!==r&&(n.top=this.position.top+"px"),this.position.left!==h&&(n.left=this.position.left+"px"),this.size.width!==l&&(n.width=this.size.width+"px"),this.size.height!==c&&(n.height=this.size.height+"px"),s.css(n),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),t.isEmptyObject(n)||this._trigger("resize",e,this.ui()),!1):!1},_mouseStop:function(e){this.resizing=!1;var i,s,n,a,o,r,h,l=this.options,c=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&t.ui.hasScroll(i[0],"left")?0:c.sizeDiff.height,a=s?0:c.sizeDiff.width,o={width:c.helper.width()-a,height:c.helper.height()-n},r=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null,h=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null,l.animate||this.element.css(t.extend(o,{top:h,left:r})),c.helper.height(c.size.height),c.helper.width(c.size.width),this._helper&&!l.animate&&this._proportionallyResize()),t("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",e),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(t){var e,s,n,a,o,r=this.options;o={minWidth:i(r.minWidth)?r.minWidth:0,maxWidth:i(r.maxWidth)?r.maxWidth:1/0,minHeight:i(r.minHeight)?r.minHeight:0,maxHeight:i(r.maxHeight)?r.maxHeight:1/0},(this._aspectRatio||t)&&(e=o.minHeight*this.aspectRatio,n=o.minWidth/this.aspectRatio,s=o.maxHeight*this.aspectRatio,a=o.maxWidth/this.aspectRatio,e>o.minWidth&&(o.minWidth=e),n>o.minHeight&&(o.minHeight=n),o.maxWidth>s&&(o.maxWidth=s),o.maxHeight>a&&(o.maxHeight=a)),this._vBoundaries=o},_updateCache:function(t){this.offset=this.helper.offset(),i(t.left)&&(this.position.left=t.left),i(t.top)&&(this.position.top=t.top),i(t.height)&&(this.size.height=t.height),i(t.width)&&(this.size.width=t.width)},_updateRatio:function(t){var e=this.position,s=this.size,n=this.axis;return i(t.height)?t.width=t.height*this.aspectRatio:i(t.width)&&(t.height=t.width/this.aspectRatio),"sw"===n&&(t.left=e.left+(s.width-t.width),t.top=null),"nw"===n&&(t.top=e.top+(s.height-t.height),t.left=e.left+(s.width-t.width)),t},_respectSize:function(t){var e=this._vBoundaries,s=this.axis,n=i(t.width)&&e.maxWidth&&e.maxWidth<t.width,a=i(t.height)&&e.maxHeight&&e.maxHeight<t.height,o=i(t.width)&&e.minWidth&&e.minWidth>t.width,r=i(t.height)&&e.minHeight&&e.minHeight>t.height,h=this.originalPosition.left+this.originalSize.width,l=this.position.top+this.size.height,c=/sw|nw|w/.test(s),u=/nw|ne|n/.test(s);return o&&(t.width=e.minWidth),r&&(t.height=e.minHeight),n&&(t.width=e.maxWidth),a&&(t.height=e.maxHeight),o&&c&&(t.left=h-e.minWidth),n&&c&&(t.left=h-e.maxWidth),r&&u&&(t.top=l-e.minHeight),a&&u&&(t.top=l-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_proportionallyResize:function(){if(this._proportionallyResizeElements.length){var t,e,i,s,n,a=this.helper||this.element;for(t=0;this._proportionallyResizeElements.length>t;t++){if(n=this._proportionallyResizeElements[t],!this.borderDif)for(this.borderDif=[],i=[n.css("borderTopWidth"),n.css("borderRightWidth"),n.css("borderBottomWidth"),n.css("borderLeftWidth")],s=[n.css("paddingTop"),n.css("paddingRight"),n.css("paddingBottom"),n.css("paddingLeft")],e=0;i.length>e;e++)this.borderDif[e]=(parseInt(i[e],10)||0)+(parseInt(s[e],10)||0);n.css({height:a.height()-this.borderDif[0]-this.borderDif[2]||0,width:a.width()-this.borderDif[1]-this.borderDif[3]||0})}}},_renderProxy:function(){var e=this.element,i=this.options;this.elementOffset=e.offset(),this._helper?(this.helper=this.helper||t("<div style='overflow:hidden;'></div>"),this.helper.addClass(this._helper).css({width:this.element.outerWidth()-1,height:this.element.outerHeight()-1,position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize,s=this.originalPosition;return{left:s.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},sw:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[e,i,s]))},ne:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},nw:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[e,i,s]))}},_propagate:function(e,i){t.ui.plugin.call(this,e,[i,this.ui()]),"resize"!==e&&this._trigger(e,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),t.ui.plugin.add("resizable","animate",{stop:function(e){var i=t(this).data("ui-resizable"),s=i.options,n=i._proportionallyResizeElements,a=n.length&&/textarea/i.test(n[0].nodeName),o=a&&t.ui.hasScroll(n[0],"left")?0:i.sizeDiff.height,r=a?0:i.sizeDiff.width,h={width:i.size.width-r,height:i.size.height-o},l=parseInt(i.element.css("left"),10)+(i.position.left-i.originalPosition.left)||null,c=parseInt(i.element.css("top"),10)+(i.position.top-i.originalPosition.top)||null;i.element.animate(t.extend(h,c&&l?{top:c,left:l}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseInt(i.element.css("width"),10),height:parseInt(i.element.css("height"),10),top:parseInt(i.element.css("top"),10),left:parseInt(i.element.css("left"),10)};n&&n.length&&t(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",e)}})}}),t.ui.plugin.add("resizable","containment",{start:function(){var i,s,n,a,o,r,h,l=t(this).data("ui-resizable"),c=l.options,u=l.element,d=c.containment,p=d instanceof t?d.get(0):/parent/.test(d)?u.parent().get(0):d;p&&(l.containerElement=t(p),/document/.test(d)||d===document?(l.containerOffset={left:0,top:0},l.containerPosition={left:0,top:0},l.parentData={element:t(document),left:0,top:0,width:t(document).width(),height:t(document).height()||document.body.parentNode.scrollHeight}):(i=t(p),s=[],t(["Top","Right","Left","Bottom"]).each(function(t,n){s[t]=e(i.css("padding"+n))}),l.containerOffset=i.offset(),l.containerPosition=i.position(),l.containerSize={height:i.innerHeight()-s[3],width:i.innerWidth()-s[1]},n=l.containerOffset,a=l.containerSize.height,o=l.containerSize.width,r=t.ui.hasScroll(p,"left")?p.scrollWidth:o,h=t.ui.hasScroll(p)?p.scrollHeight:a,l.parentData={element:p,left:n.left,top:n.top,width:r,height:h}))},resize:function(e){var i,s,n,a,o=t(this).data("ui-resizable"),r=o.options,h=o.containerOffset,l=o.position,c=o._aspectRatio||e.shiftKey,u={top:0,left:0},d=o.containerElement;d[0]!==document&&/static/.test(d.css("position"))&&(u=h),l.left<(o._helper?h.left:0)&&(o.size.width=o.size.width+(o._helper?o.position.left-h.left:o.position.left-u.left),c&&(o.size.height=o.size.width/o.aspectRatio),o.position.left=r.helper?h.left:0),l.top<(o._helper?h.top:0)&&(o.size.height=o.size.height+(o._helper?o.position.top-h.top:o.position.top),c&&(o.size.width=o.size.height*o.aspectRatio),o.position.top=o._helper?h.top:0),o.offset.left=o.parentData.left+o.position.left,o.offset.top=o.parentData.top+o.position.top,i=Math.abs((o._helper?o.offset.left-u.left:o.offset.left-u.left)+o.sizeDiff.width),s=Math.abs((o._helper?o.offset.top-u.top:o.offset.top-h.top)+o.sizeDiff.height),n=o.containerElement.get(0)===o.element.parent().get(0),a=/relative|absolute/.test(o.containerElement.css("position")),n&&a&&(i-=Math.abs(o.parentData.left)),i+o.size.width>=o.parentData.width&&(o.size.width=o.parentData.width-i,c&&(o.size.height=o.size.width/o.aspectRatio)),s+o.size.height>=o.parentData.height&&(o.size.height=o.parentData.height-s,c&&(o.size.width=o.size.height*o.aspectRatio))},stop:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.containerOffset,n=e.containerPosition,a=e.containerElement,o=t(e.helper),r=o.offset(),h=o.outerWidth()-e.sizeDiff.width,l=o.outerHeight()-e.sizeDiff.height;e._helper&&!i.animate&&/relative/.test(a.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l}),e._helper&&!i.animate&&/static/.test(a.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l})}}),t.ui.plugin.add("resizable","alsoResize",{start:function(){var e=t(this).data("ui-resizable"),i=e.options,s=function(e){t(e).each(function(){var e=t(this);e.data("ui-resizable-alsoresize",{width:parseInt(e.width(),10),height:parseInt(e.height(),10),left:parseInt(e.css("left"),10),top:parseInt(e.css("top"),10)})})};"object"!=typeof i.alsoResize||i.alsoResize.parentNode?s(i.alsoResize):i.alsoResize.length?(i.alsoResize=i.alsoResize[0],s(i.alsoResize)):t.each(i.alsoResize,function(t){s(t)})},resize:function(e,i){var s=t(this).data("ui-resizable"),n=s.options,a=s.originalSize,o=s.originalPosition,r={height:s.size.height-a.height||0,width:s.size.width-a.width||0,top:s.position.top-o.top||0,left:s.position.left-o.left||0},h=function(e,s){t(e).each(function(){var e=t(this),n=t(this).data("ui-resizable-alsoresize"),a={},o=s&&s.length?s:e.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];t.each(o,function(t,e){var i=(n[e]||0)+(r[e]||0);i&&i>=0&&(a[e]=i||null)}),e.css(a)})};"object"!=typeof n.alsoResize||n.alsoResize.nodeType?h(n.alsoResize):t.each(n.alsoResize,function(t,e){h(t,e)})},stop:function(){t(this).removeData("resizable-alsoresize")}}),t.ui.plugin.add("resizable","ghost",{start:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.size;e.ghost=e.originalElement.clone(),e.ghost.css({opacity:.25,display:"block",position:"relative",height:s.height,width:s.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass("string"==typeof i.ghost?i.ghost:""),e.ghost.appendTo(e.helper)},resize:function(){var e=t(this).data("ui-resizable");e.ghost&&e.ghost.css({position:"relative",height:e.size.height,width:e.size.width})},stop:function(){var e=t(this).data("ui-resizable");e.ghost&&e.helper&&e.helper.get(0).removeChild(e.ghost.get(0))}}),t.ui.plugin.add("resizable","grid",{resize:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.size,n=e.originalSize,a=e.originalPosition,o=e.axis,r="number"==typeof i.grid?[i.grid,i.grid]:i.grid,h=r[0]||1,l=r[1]||1,c=Math.round((s.width-n.width)/h)*h,u=Math.round((s.height-n.height)/l)*l,d=n.width+c,p=n.height+u,f=i.maxWidth&&d>i.maxWidth,g=i.maxHeight&&p>i.maxHeight,m=i.minWidth&&i.minWidth>d,v=i.minHeight&&i.minHeight>p;i.grid=r,m&&(d+=h),v&&(p+=l),f&&(d-=h),g&&(p-=l),/^(se|s|e)$/.test(o)?(e.size.width=d,e.size.height=p):/^(ne)$/.test(o)?(e.size.width=d,e.size.height=p,e.position.top=a.top-u):/^(sw)$/.test(o)?(e.size.width=d,e.size.height=p,e.position.left=a.left-c):(p-l>0?(e.size.height=p,e.position.top=a.top-u):(e.size.height=l,e.position.top=a.top+n.height-l),d-h>0?(e.size.width=d,e.position.left=a.left-c):(e.size.width=h,e.position.left=a.left+n.width-h))}})})(jQuery);(function(t){t.widget("ui.selectable",t.ui.mouse,{version:"1.10.4",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var e,i=this;this.element.addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){e=t(i.options.filter,i.element[0]),e.addClass("ui-selectee"),e.each(function(){var e=t(this),i=e.offset();t.data(this,"selectable-item",{element:this,$element:e,left:i.left,top:i.top,right:i.left+e.outerWidth(),bottom:i.top+e.outerHeight(),startselected:!1,selected:e.hasClass("ui-selected"),selecting:e.hasClass("ui-selecting"),unselecting:e.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=e.addClass("ui-selectee"),this._mouseInit(),this.helper=t("<div class='ui-selectable-helper'></div>")},_destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item"),this.element.removeClass("ui-selectable ui-selectable-disabled"),this._mouseDestroy()},_mouseStart:function(e){var i=this,s=this.options;this.opos=[e.pageX,e.pageY],this.options.disabled||(this.selectees=t(s.filter,this.element[0]),this._trigger("start",e),t(s.appendTo).append(this.helper),this.helper.css({left:e.pageX,top:e.pageY,width:0,height:0}),s.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var s=t.data(this,"selectable-item");s.startselected=!0,e.metaKey||e.ctrlKey||(s.$element.removeClass("ui-selected"),s.selected=!1,s.$element.addClass("ui-unselecting"),s.unselecting=!0,i._trigger("unselecting",e,{unselecting:s.element}))}),t(e.target).parents().addBack().each(function(){var s,n=t.data(this,"selectable-item");return n?(s=!e.metaKey&&!e.ctrlKey||!n.$element.hasClass("ui-selected"),n.$element.removeClass(s?"ui-unselecting":"ui-selected").addClass(s?"ui-selecting":"ui-unselecting"),n.unselecting=!s,n.selecting=s,n.selected=s,s?i._trigger("selecting",e,{selecting:n.element}):i._trigger("unselecting",e,{unselecting:n.element}),!1):undefined}))},_mouseDrag:function(e){if(this.dragged=!0,!this.options.disabled){var i,s=this,n=this.options,a=this.opos[0],o=this.opos[1],r=e.pageX,l=e.pageY;return a>r&&(i=r,r=a,a=i),o>l&&(i=l,l=o,o=i),this.helper.css({left:a,top:o,width:r-a,height:l-o}),this.selectees.each(function(){var i=t.data(this,"selectable-item"),h=!1;i&&i.element!==s.element[0]&&("touch"===n.tolerance?h=!(i.left>r||a>i.right||i.top>l||o>i.bottom):"fit"===n.tolerance&&(h=i.left>a&&r>i.right&&i.top>o&&l>i.bottom),h?(i.selected&&(i.$element.removeClass("ui-selected"),i.selected=!1),i.unselecting&&(i.$element.removeClass("ui-unselecting"),i.unselecting=!1),i.selecting||(i.$element.addClass("ui-selecting"),i.selecting=!0,s._trigger("selecting",e,{selecting:i.element}))):(i.selecting&&((e.metaKey||e.ctrlKey)&&i.startselected?(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.$element.addClass("ui-selected"),i.selected=!0):(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.startselected&&(i.$element.addClass("ui-unselecting"),i.unselecting=!0),s._trigger("unselecting",e,{unselecting:i.element}))),i.selected&&(e.metaKey||e.ctrlKey||i.startselected||(i.$element.removeClass("ui-selected"),i.selected=!1,i.$element.addClass("ui-unselecting"),i.unselecting=!0,s._trigger("unselecting",e,{unselecting:i.element})))))}),!1}},_mouseStop:function(e){var i=this;return this.dragged=!1,t(".ui-unselecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");s.$element.removeClass("ui-unselecting"),s.unselecting=!1,s.startselected=!1,i._trigger("unselected",e,{unselected:s.element})}),t(".ui-selecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");s.$element.removeClass("ui-selecting").addClass("ui-selected"),s.selecting=!1,s.selected=!0,s.startselected=!0,i._trigger("selected",e,{selected:s.element})}),this._trigger("stop",e),this.helper.remove(),!1}})})(jQuery);(function(t){var e=5;t.widget("ui.slider",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null,change:null,slide:null,start:null,stop:null},_create:function(){this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"),this._refresh(),this._setOption("disabled",this.options.disabled),this._animateOff=!1},_refresh:function(){this._createRange(),this._createHandles(),this._setupEvents(),this._refreshValue()},_createHandles:function(){var e,i,s=this.options,n=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),a="<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>",o=[];for(i=s.values&&s.values.length||1,n.length>i&&(n.slice(i).remove(),n=n.slice(0,i)),e=n.length;i>e;e++)o.push(a);this.handles=n.add(t(o.join("")).appendTo(this.element)),this.handle=this.handles.eq(0),this.handles.each(function(e){t(this).data("ui-slider-handle-index",e)})},_createRange:function(){var e=this.options,i="";e.range?(e.range===!0&&(e.values?e.values.length&&2!==e.values.length?e.values=[e.values[0],e.values[0]]:t.isArray(e.values)&&(e.values=e.values.slice(0)):e.values=[this._valueMin(),this._valueMin()]),this.range&&this.range.length?this.range.removeClass("ui-slider-range-min ui-slider-range-max").css({left:"",bottom:""}):(this.range=t("<div></div>").appendTo(this.element),i="ui-slider-range ui-widget-header ui-corner-all"),this.range.addClass(i+("min"===e.range||"max"===e.range?" ui-slider-range-"+e.range:""))):(this.range&&this.range.remove(),this.range=null)},_setupEvents:function(){var t=this.handles.add(this.range).filter("a");this._off(t),this._on(t,this._handleEvents),this._hoverable(t),this._focusable(t)},_destroy:function(){this.handles.remove(),this.range&&this.range.remove(),this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-widget ui-widget-content ui-corner-all"),this._mouseDestroy()},_mouseCapture:function(e){var i,s,n,a,o,r,l,h,u=this,c=this.options;return c.disabled?!1:(this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),i={x:e.pageX,y:e.pageY},s=this._normValueFromMouse(i),n=this._valueMax()-this._valueMin()+1,this.handles.each(function(e){var i=Math.abs(s-u.values(e));(n>i||n===i&&(e===u._lastChangedValue||u.values(e)===c.min))&&(n=i,a=t(this),o=e)}),r=this._start(e,o),r===!1?!1:(this._mouseSliding=!0,this._handleIndex=o,a.addClass("ui-state-active").focus(),l=a.offset(),h=!t(e.target).parents().addBack().is(".ui-slider-handle"),this._clickOffset=h?{left:0,top:0}:{left:e.pageX-l.left-a.width()/2,top:e.pageY-l.top-a.height()/2-(parseInt(a.css("borderTopWidth"),10)||0)-(parseInt(a.css("borderBottomWidth"),10)||0)+(parseInt(a.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(e,o,s),this._animateOff=!0,!0))},_mouseStart:function(){return!0},_mouseDrag:function(t){var e={x:t.pageX,y:t.pageY},i=this._normValueFromMouse(e);return this._slide(t,this._handleIndex,i),!1},_mouseStop:function(t){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(t,this._handleIndex),this._change(t,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation="vertical"===this.options.orientation?"vertical":"horizontal"},_normValueFromMouse:function(t){var e,i,s,n,a;return"horizontal"===this.orientation?(e=this.elementSize.width,i=t.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(e=this.elementSize.height,i=t.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),s=i/e,s>1&&(s=1),0>s&&(s=0),"vertical"===this.orientation&&(s=1-s),n=this._valueMax()-this._valueMin(),a=this._valueMin()+s*n,this._trimAlignValue(a)},_start:function(t,e){var i={handle:this.handles[e],value:this.value()};return this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._trigger("start",t,i)},_slide:function(t,e,i){var s,n,a;this.options.values&&this.options.values.length?(s=this.values(e?0:1),2===this.options.values.length&&this.options.range===!0&&(0===e&&i>s||1===e&&s>i)&&(i=s),i!==this.values(e)&&(n=this.values(),n[e]=i,a=this._trigger("slide",t,{handle:this.handles[e],value:i,values:n}),s=this.values(e?0:1),a!==!1&&this.values(e,i))):i!==this.value()&&(a=this._trigger("slide",t,{handle:this.handles[e],value:i}),a!==!1&&this.value(i))},_stop:function(t,e){var i={handle:this.handles[e],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._trigger("stop",t,i)},_change:function(t,e){if(!this._keySliding&&!this._mouseSliding){var i={handle:this.handles[e],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._lastChangedValue=e,this._trigger("change",t,i)}},value:function(t){return arguments.length?(this.options.value=this._trimAlignValue(t),this._refreshValue(),this._change(null,0),undefined):this._value()},values:function(e,i){var s,n,a;if(arguments.length>1)return this.options.values[e]=this._trimAlignValue(i),this._refreshValue(),this._change(null,e),undefined;if(!arguments.length)return this._values();if(!t.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(e):this.value();for(s=this.options.values,n=arguments[0],a=0;s.length>a;a+=1)s[a]=this._trimAlignValue(n[a]),this._change(null,a);this._refreshValue()},_setOption:function(e,i){var s,n=0;switch("range"===e&&this.options.range===!0&&("min"===i?(this.options.value=this._values(0),this.options.values=null):"max"===i&&(this.options.value=this._values(this.options.values.length-1),this.options.values=null)),t.isArray(this.options.values)&&(n=this.options.values.length),t.Widget.prototype._setOption.apply(this,arguments),e){case"orientation":this._detectOrientation(),this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation),this._refreshValue();break;case"value":this._animateOff=!0,this._refreshValue(),this._change(null,0),this._animateOff=!1;break;case"values":for(this._animateOff=!0,this._refreshValue(),s=0;n>s;s+=1)this._change(null,s);this._animateOff=!1;break;case"min":case"max":this._animateOff=!0,this._refreshValue(),this._animateOff=!1;break;case"range":this._animateOff=!0,this._refresh(),this._animateOff=!1}},_value:function(){var t=this.options.value;return t=this._trimAlignValue(t)},_values:function(t){var e,i,s;if(arguments.length)return e=this.options.values[t],e=this._trimAlignValue(e);if(this.options.values&&this.options.values.length){for(i=this.options.values.slice(),s=0;i.length>s;s+=1)i[s]=this._trimAlignValue(i[s]);return i}return[]},_trimAlignValue:function(t){if(this._valueMin()>=t)return this._valueMin();if(t>=this._valueMax())return this._valueMax();var e=this.options.step>0?this.options.step:1,i=(t-this._valueMin())%e,s=t-i;return 2*Math.abs(i)>=e&&(s+=i>0?e:-e),parseFloat(s.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var e,i,s,n,a,o=this.options.range,r=this.options,l=this,h=this._animateOff?!1:r.animate,u={};this.options.values&&this.options.values.length?this.handles.each(function(s){i=100*((l.values(s)-l._valueMin())/(l._valueMax()-l._valueMin())),u["horizontal"===l.orientation?"left":"bottom"]=i+"%",t(this).stop(1,1)[h?"animate":"css"](u,r.animate),l.options.range===!0&&("horizontal"===l.orientation?(0===s&&l.range.stop(1,1)[h?"animate":"css"]({left:i+"%"},r.animate),1===s&&l.range[h?"animate":"css"]({width:i-e+"%"},{queue:!1,duration:r.animate})):(0===s&&l.range.stop(1,1)[h?"animate":"css"]({bottom:i+"%"},r.animate),1===s&&l.range[h?"animate":"css"]({height:i-e+"%"},{queue:!1,duration:r.animate}))),e=i}):(s=this.value(),n=this._valueMin(),a=this._valueMax(),i=a!==n?100*((s-n)/(a-n)):0,u["horizontal"===this.orientation?"left":"bottom"]=i+"%",this.handle.stop(1,1)[h?"animate":"css"](u,r.animate),"min"===o&&"horizontal"===this.orientation&&this.range.stop(1,1)[h?"animate":"css"]({width:i+"%"},r.animate),"max"===o&&"horizontal"===this.orientation&&this.range[h?"animate":"css"]({width:100-i+"%"},{queue:!1,duration:r.animate}),"min"===o&&"vertical"===this.orientation&&this.range.stop(1,1)[h?"animate":"css"]({height:i+"%"},r.animate),"max"===o&&"vertical"===this.orientation&&this.range[h?"animate":"css"]({height:100-i+"%"},{queue:!1,duration:r.animate}))},_handleEvents:{keydown:function(i){var s,n,a,o,r=t(i.target).data("ui-slider-handle-index");switch(i.keyCode){case t.ui.keyCode.HOME:case t.ui.keyCode.END:case t.ui.keyCode.PAGE_UP:case t.ui.keyCode.PAGE_DOWN:case t.ui.keyCode.UP:case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:case t.ui.keyCode.LEFT:if(i.preventDefault(),!this._keySliding&&(this._keySliding=!0,t(i.target).addClass("ui-state-active"),s=this._start(i,r),s===!1))return}switch(o=this.options.step,n=a=this.options.values&&this.options.values.length?this.values(r):this.value(),i.keyCode){case t.ui.keyCode.HOME:a=this._valueMin();break;case t.ui.keyCode.END:a=this._valueMax();break;case t.ui.keyCode.PAGE_UP:a=this._trimAlignValue(n+(this._valueMax()-this._valueMin())/e);break;case t.ui.keyCode.PAGE_DOWN:a=this._trimAlignValue(n-(this._valueMax()-this._valueMin())/e);break;case t.ui.keyCode.UP:case t.ui.keyCode.RIGHT:if(n===this._valueMax())return;a=this._trimAlignValue(n+o);break;case t.ui.keyCode.DOWN:case t.ui.keyCode.LEFT:if(n===this._valueMin())return;a=this._trimAlignValue(n-o)}this._slide(i,r,a)},click:function(t){t.preventDefault()},keyup:function(e){var i=t(e.target).data("ui-slider-handle-index");this._keySliding&&(this._keySliding=!1,this._stop(e,i),this._change(e,i),t(e.target).removeClass("ui-state-active"))}}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}function i(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))}t.widget("ui.sortable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_create:function(){var t=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?"x"===t.axis||i(this.items[0].item):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},_destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_setOption:function(e,i){"disabled"===e?(this.options[e]=i,this.widget().toggleClass("ui-sortable-disabled",!!i)):t.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(e,i){var s=null,n=!1,o=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,o.widgetName+"-item")===o?(s=t(this),!1):undefined}),t.data(e.target,o.widgetName+"-item")===o&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,o,a=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,a.cursorAt&&this._adjustOffsetFromHelper(a.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),a.containment&&this._setContainment(),a.cursor&&"auto"!==a.cursor&&(o=this.document.find("body"),this.storedCursor=o.css("cursor"),o.css("cursor",a.cursor),this.storedStylesheet=t("<style>*{ cursor: "+a.cursor+" !important; }</style>").appendTo(o)),a.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",a.opacity)),a.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",a.zIndex)),this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,o,a=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY<a.scrollSensitivity?this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop+a.scrollSpeed:e.pageY-this.overflowOffset.top<a.scrollSensitivity&&(this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop-a.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-e.pageX<a.scrollSensitivity?this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft+a.scrollSpeed:e.pageX-this.overflowOffset.left<a.scrollSensitivity&&(this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft-a.scrollSpeed)):(e.pageY-t(document).scrollTop()<a.scrollSensitivity?r=t(document).scrollTop(t(document).scrollTop()-a.scrollSpeed):t(window).height()-(e.pageY-t(document).scrollTop())<a.scrollSensitivity&&(r=t(document).scrollTop(t(document).scrollTop()+a.scrollSpeed)),e.pageX-t(document).scrollLeft()<a.scrollSensitivity?r=t(document).scrollLeft(t(document).scrollLeft()-a.scrollSpeed):t(window).width()-(e.pageX-t(document).scrollLeft())<a.scrollSensitivity&&(r=t(document).scrollLeft(t(document).scrollLeft()+a.scrollSpeed))),r!==!1&&t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e)),this.positionAbs=this._convertPositionTo("absolute"),this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),i=this.items.length-1;i>=0;i--)if(s=this.items[i],n=s.item[0],o=this._intersectsWithPointer(s),o&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===o?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===o?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),o=this.options.axis,a={};o&&"x"!==o||(a.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollLeft)),o&&"y"!==o||(a.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(a,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,o=t.left,a=o+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+c>o&&a>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>o&&a>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var i="x"===this.options.axis||e(this.positionAbs.top+this.offset.click.top,t.top,t.height),s="y"===this.options.axis||e(this.positionAbs.left+this.offset.click.left,t.left,t.width),n=i&&s,o=this._getDragVerticalDirection(),a=this._getDragHorizontalDirection();return n?this.floating?a&&"right"===a||"down"===o?2:1:o&&("down"===o?2:1):!1},_intersectsWithSides:function(t){var i=e(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),s=e(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),n=this._getDragVerticalDirection(),o=this._getDragHorizontalDirection();return this.floating&&o?"right"===o&&s||"left"===o&&!s:n&&("down"===n&&i||"up"===n&&!i)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,o,a,r=[],h=[],l=this._connectWith();if(l&&e)for(s=l.length-1;s>=0;s--)for(o=t(l[s]),n=o.length-1;n>=0;n--)a=t.data(o[n],this.widgetFullName),a&&a!==this&&!a.options.disabled&&h.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(h.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=h.length-1;s>=0;s--)h[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,o,a,r,h,l,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i]),s=n.length-1;s>=0;s--)o=t.data(n[s],this.widgetFullName),o&&o!==this&&!o.options.disabled&&(u.push([t.isFunction(o.options.items)?o.options.items.call(o.element[0],e,{item:this.currentItem}):t(o.options.items,o.element),o]),this.containers.push(o));for(i=u.length-1;i>=0;i--)for(a=u[i][1],r=u[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",a),c.push({item:h,instance:a,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,o;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),o=n.offset(),s.left=o.left,s.top=o.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)o=this.containers[i].element.offset(),this.containers[i].containerCache.left=o.left,this.containers[i].containerCache.top=o.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]).addClass(i||e.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper");return"tr"===s?e.currentItem.children().each(function(){t("<td>&#160;</td>",e.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(n)}):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_contactContainers:function(s){var n,o,a,r,h,l,c,u,d,p,f=null,g=null;for(n=this.containers.length-1;n>=0;n--)if(!t.contains(this.currentItem[0],this.containers[n].element[0]))if(this._intersectsWith(this.containers[n].containerCache)){if(f&&t.contains(this.containers[n].element[0],f.element[0]))continue;f=this.containers[n],g=n}else this.containers[n].containerCache.over&&(this.containers[n]._trigger("out",s,this._uiHash(this)),this.containers[n].containerCache.over=0);if(f)if(1===this.containers.length)this.containers[g].containerCache.over||(this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1);else{for(a=1e4,r=null,p=f.floating||i(this.currentItem),h=p?"left":"top",l=p?"width":"height",c=this.positionAbs[h]+this.offset.click[h],o=this.items.length-1;o>=0;o--)t.contains(this.containers[g].element[0],this.items[o].item[0])&&this.items[o].item[0]!==this.currentItem[0]&&(!p||e(this.positionAbs.top+this.offset.click.top,this.items[o].top,this.items[o].height))&&(u=this.items[o].item.offset()[h],d=!1,Math.abs(u-c)>Math.abs(u+this.items[o][l]-c)&&(d=!0,u+=this.items[o][l]),a>Math.abs(u-c)&&(a=Math.abs(u-c),r=this.items[o],this.direction=d?"up":"down"));if(!r&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[g])return;r?this._rearrange(s,r,null,!0):this._rearrange(s,null,this.containers[g].element,!0),this._trigger("change",s,this._uiHash()),this.containers[g]._trigger("change",s,this._uiHash(this)),this.currentContainer=this.containers[g],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,t("document"===n.containment?document:window).width()-this.helperProportions.width-this.margins.left,(t("document"===n.containment?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==document&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.left<this.containment[0]&&(o=this.containment[0]+this.offset.click.left),e.pageY-this.offset.click.top<this.containment[1]&&(a=this.containment[1]+this.offset.click.top),e.pageX-this.offset.click.left>this.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,this.cancelHelperRemoval){if(!e){for(this._trigger("beforeStop",t,this._uiHash()),s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!1}if(e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null,!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})})(jQuery);(function(t){function e(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.widget("ui.spinner",{version:"1.10.4",defaultElement:"<input>",widgetEventPrefix:"spin",options:{culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),""!==this.value()&&this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var e={},i=this.element;return t.each(["min","max","step"],function(t,s){var n=i.attr(s);void 0!==n&&n.length&&(e[s]=n)}),e},_events:{keydown:function(t){this._start(t)&&this._keydown(t)&&t.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(this._stop(),this._refresh(),this.previous!==this.element.val()&&this._trigger("change",t),void 0)},mousewheel:function(t,e){if(e){if(!this.spinning&&!this._start(t))return!1;this._spin((e>0?1:-1)*this.options.step,t),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(t)},100),t.preventDefault()}},"mousedown .ui-spinner-button":function(e){function i(){var t=this.element[0]===this.document[0].activeElement;t||(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s}))}var s;s=this.element[0]===this.document[0].activeElement?this.previous:this.element.val(),e.preventDefault(),i.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,i.call(this)}),this._start(e)!==!1&&this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(e){return t(e.currentTarget).hasClass("ui-state-active")?this._start(e)===!1?!1:(this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e),void 0):void 0},"mouseleave .ui-spinner-button":"_stop"},_draw:function(){var t=this.uiSpinner=this.element.addClass("ui-spinner-input").attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());this.element.attr("role","spinbutton"),this.buttons=t.find(".ui-spinner-button").attr("tabIndex",-1).button().removeClass("ui-corner-all"),this.buttons.height()>Math.ceil(.5*t.height())&&t.height()>0&&t.height(t.height()),this.options.disabled&&this.disable()},_keydown:function(e){var i=this.options,s=t.ui.keyCode;switch(e.keyCode){case s.UP:return this._repeat(null,1,e),!0;case s.DOWN:return this._repeat(null,-1,e),!0;case s.PAGE_UP:return this._repeat(null,i.page,e),!0;case s.PAGE_DOWN:return this._repeat(null,-i.page,e),!0}return!1},_uiSpinnerHtml:function(){return"<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>"},_buttonHtml:function(){return"<a class='ui-spinner-button ui-spinner-up ui-corner-tr'><span class='ui-icon "+this.options.icons.up+"'>&#9650;</span>"+"</a>"+"<a class='ui-spinner-button ui-spinner-down ui-corner-br'>"+"<span class='ui-icon "+this.options.icons.down+"'>&#9660;</span>"+"</a>"},_start:function(t){return this.spinning||this._trigger("start",t)!==!1?(this.counter||(this.counter=1),this.spinning=!0,!0):!1},_repeat:function(t,e,i){t=t||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,e,i)},t),this._spin(e*this.options.step,i)},_spin:function(t,e){var i=this.value()||0;this.counter||(this.counter=1),i=this._adjustValue(i+t*this._increment(this.counter)),this.spinning&&this._trigger("spin",e,{value:i})===!1||(this._value(i),this.counter++)},_increment:function(e){var i=this.options.incremental;return i?t.isFunction(i)?i(e):Math.floor(e*e*e/5e4-e*e/500+17*e/200+1):1},_precision:function(){var t=this._precisionOf(this.options.step);return null!==this.options.min&&(t=Math.max(t,this._precisionOf(this.options.min))),t},_precisionOf:function(t){var e=""+t,i=e.indexOf(".");return-1===i?0:e.length-i-1},_adjustValue:function(t){var e,i,s=this.options;return e=null!==s.min?s.min:0,i=t-e,i=Math.round(i/s.step)*s.step,t=e+i,t=parseFloat(t.toFixed(this._precision())),null!==s.max&&t>s.max?s.max:null!==s.min&&s.min>t?s.min:t},_stop:function(t){this.spinning&&(clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",t))},_setOption:function(t,e){if("culture"===t||"numberFormat"===t){var i=this._parse(this.element.val());return this.options[t]=e,this.element.val(this._format(i)),void 0}("max"===t||"min"===t||"step"===t)&&"string"==typeof e&&(e=this._parse(e)),"icons"===t&&(this.buttons.first().find(".ui-icon").removeClass(this.options.icons.up).addClass(e.up),this.buttons.last().find(".ui-icon").removeClass(this.options.icons.down).addClass(e.down)),this._super(t,e),"disabled"===t&&(e?(this.element.prop("disabled",!0),this.buttons.button("disable")):(this.element.prop("disabled",!1),this.buttons.button("enable")))},_setOptions:e(function(t){this._super(t),this._value(this.element.val())}),_parse:function(t){return"string"==typeof t&&""!==t&&(t=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(t,10,this.options.culture):+t),""===t||isNaN(t)?null:t},_format:function(t){return""===t?"":window.Globalize&&this.options.numberFormat?Globalize.format(t,this.options.numberFormat,this.options.culture):t},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},_value:function(t,e){var i;""!==t&&(i=this._parse(t),null!==i&&(e||(i=this._adjustValue(i)),t=this._format(i))),this.element.val(t),this._refresh()},_destroy:function(){this.element.removeClass("ui-spinner-input").prop("disabled",!1).removeAttr("autocomplete").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:e(function(t){this._stepUp(t)}),_stepUp:function(t){this._start()&&(this._spin((t||1)*this.options.step),this._stop())},stepDown:e(function(t){this._stepDown(t)}),_stepDown:function(t){this._start()&&(this._spin((t||1)*-this.options.step),this._stop())},pageUp:e(function(t){this._stepUp((t||1)*this.options.page)}),pageDown:e(function(t){this._stepDown((t||1)*this.options.page)}),value:function(t){return arguments.length?(e(this._value).call(this,t),void 0):this._parse(this.element.val())},widget:function(){return this.uiSpinner}})})(jQuery);(function(t,e){function i(){return++n}function s(t){return t=t.cloneNode(!1),t.hash.length>1&&decodeURIComponent(t.href.replace(a,""))===decodeURIComponent(location.href.replace(a,""))}var n=0,a=/#.*$/;t.widget("ui.tabs",{version:"1.10.4",delay:300,options:{active:null,collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_create:function(){var e=this,i=this.options;this.running=!1,this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all").toggleClass("ui-tabs-collapsible",i.collapsible).delegate(".ui-tabs-nav > li","mousedown"+this.eventNamespace,function(e){t(this).is(".ui-state-disabled")&&e.preventDefault()}).delegate(".ui-tabs-anchor","focus"+this.eventNamespace,function(){t(this).closest("li").is(".ui-state-disabled")&&this.blur()}),this._processTabs(),i.active=this._initialActive(),t.isArray(i.disabled)&&(i.disabled=t.unique(i.disabled.concat(t.map(this.tabs.filter(".ui-state-disabled"),function(t){return e.tabs.index(t)}))).sort()),this.active=this.options.active!==!1&&this.anchors.length?this._findActive(i.active):t(),this._refresh(),this.active.length&&this.load(i.active)},_initialActive:function(){var i=this.options.active,s=this.options.collapsible,n=location.hash.substring(1);return null===i&&(n&&this.tabs.each(function(s,a){return t(a).attr("aria-controls")===n?(i=s,!1):e}),null===i&&(i=this.tabs.index(this.tabs.filter(".ui-tabs-active"))),(null===i||-1===i)&&(i=this.tabs.length?0:!1)),i!==!1&&(i=this.tabs.index(this.tabs.eq(i)),-1===i&&(i=s?!1:0)),!s&&i===!1&&this.anchors.length&&(i=0),i},_getCreateEventData:function(){return{tab:this.active,panel:this.active.length?this._getPanelForTab(this.active):t()}},_tabKeydown:function(i){var s=t(this.document[0].activeElement).closest("li"),n=this.tabs.index(s),a=!0;if(!this._handlePageNav(i)){switch(i.keyCode){case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:n++;break;case t.ui.keyCode.UP:case t.ui.keyCode.LEFT:a=!1,n--;break;case t.ui.keyCode.END:n=this.anchors.length-1;break;case t.ui.keyCode.HOME:n=0;break;case t.ui.keyCode.SPACE:return i.preventDefault(),clearTimeout(this.activating),this._activate(n),e;case t.ui.keyCode.ENTER:return i.preventDefault(),clearTimeout(this.activating),this._activate(n===this.options.active?!1:n),e;default:return}i.preventDefault(),clearTimeout(this.activating),n=this._focusNextTab(n,a),i.ctrlKey||(s.attr("aria-selected","false"),this.tabs.eq(n).attr("aria-selected","true"),this.activating=this._delay(function(){this.option("active",n)},this.delay))}},_panelKeydown:function(e){this._handlePageNav(e)||e.ctrlKey&&e.keyCode===t.ui.keyCode.UP&&(e.preventDefault(),this.active.focus())},_handlePageNav:function(i){return i.altKey&&i.keyCode===t.ui.keyCode.PAGE_UP?(this._activate(this._focusNextTab(this.options.active-1,!1)),!0):i.altKey&&i.keyCode===t.ui.keyCode.PAGE_DOWN?(this._activate(this._focusNextTab(this.options.active+1,!0)),!0):e},_findNextTab:function(e,i){function s(){return e>n&&(e=0),0>e&&(e=n),e}for(var n=this.tabs.length-1;-1!==t.inArray(s(),this.options.disabled);)e=i?e+1:e-1;return e},_focusNextTab:function(t,e){return t=this._findNextTab(t,e),this.tabs.eq(t).focus(),t},_setOption:function(t,i){return"active"===t?(this._activate(i),e):"disabled"===t?(this._setupDisabled(i),e):(this._super(t,i),"collapsible"===t&&(this.element.toggleClass("ui-tabs-collapsible",i),i||this.options.active!==!1||this._activate(0)),"event"===t&&this._setupEvents(i),"heightStyle"===t&&this._setupHeightStyle(i),e)},_tabId:function(t){return t.attr("aria-controls")||"ui-tabs-"+i()},_sanitizeSelector:function(t){return t?t.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g,"\\$&"):""},refresh:function(){var e=this.options,i=this.tablist.children(":has(a[href])");e.disabled=t.map(i.filter(".ui-state-disabled"),function(t){return i.index(t)}),this._processTabs(),e.active!==!1&&this.anchors.length?this.active.length&&!t.contains(this.tablist[0],this.active[0])?this.tabs.length===e.disabled.length?(e.active=!1,this.active=t()):this._activate(this._findNextTab(Math.max(0,e.active-1),!1)):e.active=this.tabs.index(this.active):(e.active=!1,this.active=t()),this._refresh()},_refresh:function(){this._setupDisabled(this.options.disabled),this._setupEvents(this.options.event),this._setupHeightStyle(this.options.heightStyle),this.tabs.not(this.active).attr({"aria-selected":"false",tabIndex:-1}),this.panels.not(this._getPanelForTab(this.active)).hide().attr({"aria-expanded":"false","aria-hidden":"true"}),this.active.length?(this.active.addClass("ui-tabs-active ui-state-active").attr({"aria-selected":"true",tabIndex:0}),this._getPanelForTab(this.active).show().attr({"aria-expanded":"true","aria-hidden":"false"})):this.tabs.eq(0).attr("tabIndex",0)},_processTabs:function(){var e=this;this.tablist=this._getList().addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").attr("role","tablist"),this.tabs=this.tablist.find("> li:has(a[href])").addClass("ui-state-default ui-corner-top").attr({role:"tab",tabIndex:-1}),this.anchors=this.tabs.map(function(){return t("a",this)[0]}).addClass("ui-tabs-anchor").attr({role:"presentation",tabIndex:-1}),this.panels=t(),this.anchors.each(function(i,n){var a,o,r,h=t(n).uniqueId().attr("id"),l=t(n).closest("li"),c=l.attr("aria-controls");s(n)?(a=n.hash,o=e.element.find(e._sanitizeSelector(a))):(r=e._tabId(l),a="#"+r,o=e.element.find(a),o.length||(o=e._createPanel(r),o.insertAfter(e.panels[i-1]||e.tablist)),o.attr("aria-live","polite")),o.length&&(e.panels=e.panels.add(o)),c&&l.data("ui-tabs-aria-controls",c),l.attr({"aria-controls":a.substring(1),"aria-labelledby":h}),o.attr("aria-labelledby",h)}),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").attr("role","tabpanel")},_getList:function(){return this.tablist||this.element.find("ol,ul").eq(0)},_createPanel:function(e){return t("<div>").attr("id",e).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").data("ui-tabs-destroy",!0)},_setupDisabled:function(e){t.isArray(e)&&(e.length?e.length===this.anchors.length&&(e=!0):e=!1);for(var i,s=0;i=this.tabs[s];s++)e===!0||-1!==t.inArray(s,e)?t(i).addClass("ui-state-disabled").attr("aria-disabled","true"):t(i).removeClass("ui-state-disabled").removeAttr("aria-disabled");this.options.disabled=e},_setupEvents:function(e){var i={click:function(t){t.preventDefault()}};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(e){var i,s=this.element.parent();"fill"===e?(i=s.height(),i-=this.element.outerHeight()-this.element.height(),this.element.siblings(":visible").each(function(){var e=t(this),s=e.css("position");"absolute"!==s&&"fixed"!==s&&(i-=e.outerHeight(!0))}),this.element.children().not(this.panels).each(function(){i-=t(this).outerHeight(!0)}),this.panels.each(function(){t(this).height(Math.max(0,i-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===e&&(i=0,this.panels.each(function(){i=Math.max(i,t(this).height("").height())}).height(i))},_eventHandler:function(e){var i=this.options,s=this.active,n=t(e.currentTarget),a=n.closest("li"),o=a[0]===s[0],r=o&&i.collapsible,h=r?t():this._getPanelForTab(a),l=s.length?this._getPanelForTab(s):t(),c={oldTab:s,oldPanel:l,newTab:r?t():a,newPanel:h};e.preventDefault(),a.hasClass("ui-state-disabled")||a.hasClass("ui-tabs-loading")||this.running||o&&!i.collapsible||this._trigger("beforeActivate",e,c)===!1||(i.active=r?!1:this.tabs.index(a),this.active=o?t():a,this.xhr&&this.xhr.abort(),l.length||h.length||t.error("jQuery UI Tabs: Mismatching fragment identifier."),h.length&&this.load(this.tabs.index(a),e),this._toggle(e,c))},_toggle:function(e,i){function s(){a.running=!1,a._trigger("activate",e,i)}function n(){i.newTab.closest("li").addClass("ui-tabs-active ui-state-active"),o.length&&a.options.show?a._show(o,a.options.show,s):(o.show(),s())}var a=this,o=i.newPanel,r=i.oldPanel;this.running=!0,r.length&&this.options.hide?this._hide(r,this.options.hide,function(){i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),n()}):(i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),r.hide(),n()),r.attr({"aria-expanded":"false","aria-hidden":"true"}),i.oldTab.attr("aria-selected","false"),o.length&&r.length?i.oldTab.attr("tabIndex",-1):o.length&&this.tabs.filter(function(){return 0===t(this).attr("tabIndex")}).attr("tabIndex",-1),o.attr({"aria-expanded":"true","aria-hidden":"false"}),i.newTab.attr({"aria-selected":"true",tabIndex:0})},_activate:function(e){var i,s=this._findActive(e);s[0]!==this.active[0]&&(s.length||(s=this.active),i=s.find(".ui-tabs-anchor")[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return e===!1?t():this.tabs.eq(e)},_getIndex:function(t){return"string"==typeof t&&(t=this.anchors.index(this.anchors.filter("[href$='"+t+"']"))),t},_destroy:function(){this.xhr&&this.xhr.abort(),this.element.removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible"),this.tablist.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").removeAttr("role"),this.anchors.removeClass("ui-tabs-anchor").removeAttr("role").removeAttr("tabIndex").removeUniqueId(),this.tabs.add(this.panels).each(function(){t.data(this,"ui-tabs-destroy")?t(this).remove():t(this).removeClass("ui-state-default ui-state-active ui-state-disabled ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel").removeAttr("tabIndex").removeAttr("aria-live").removeAttr("aria-busy").removeAttr("aria-selected").removeAttr("aria-labelledby").removeAttr("aria-hidden").removeAttr("aria-expanded").removeAttr("role")}),this.tabs.each(function(){var e=t(this),i=e.data("ui-tabs-aria-controls");i?e.attr("aria-controls",i).removeData("ui-tabs-aria-controls"):e.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(i){var s=this.options.disabled;s!==!1&&(i===e?s=!1:(i=this._getIndex(i),s=t.isArray(s)?t.map(s,function(t){return t!==i?t:null}):t.map(this.tabs,function(t,e){return e!==i?e:null})),this._setupDisabled(s))},disable:function(i){var s=this.options.disabled;if(s!==!0){if(i===e)s=!0;else{if(i=this._getIndex(i),-1!==t.inArray(i,s))return;s=t.isArray(s)?t.merge([i],s).sort():[i]}this._setupDisabled(s)}},load:function(e,i){e=this._getIndex(e);var n=this,a=this.tabs.eq(e),o=a.find(".ui-tabs-anchor"),r=this._getPanelForTab(a),h={tab:a,panel:r};s(o[0])||(this.xhr=t.ajax(this._ajaxSettings(o,i,h)),this.xhr&&"canceled"!==this.xhr.statusText&&(a.addClass("ui-tabs-loading"),r.attr("aria-busy","true"),this.xhr.success(function(t){setTimeout(function(){r.html(t),n._trigger("load",i,h)},1)}).complete(function(t,e){setTimeout(function(){"abort"===e&&n.panels.stop(!1,!0),a.removeClass("ui-tabs-loading"),r.removeAttr("aria-busy"),t===n.xhr&&delete n.xhr},1)})))},_ajaxSettings:function(e,i,s){var n=this;return{url:e.attr("href"),beforeSend:function(e,a){return n._trigger("beforeLoad",i,t.extend({jqXHR:e,ajaxSettings:a},s))}}},_getPanelForTab:function(e){var i=t(e).attr("aria-controls");return this.element.find(this._sanitizeSelector("#"+i))}})})(jQuery);(function(t){function e(e,i){var s=(e.attr("aria-describedby")||"").split(/\s+/);s.push(i),e.data("ui-tooltip-id",i).attr("aria-describedby",t.trim(s.join(" ")))}function i(e){var i=e.data("ui-tooltip-id"),s=(e.attr("aria-describedby")||"").split(/\s+/),n=t.inArray(i,s);-1!==n&&s.splice(n,1),e.removeData("ui-tooltip-id"),s=t.trim(s.join(" ")),s?e.attr("aria-describedby",s):e.removeAttr("aria-describedby")}var s=0;t.widget("ui.tooltip",{version:"1.10.4",options:{content:function(){var e=t(this).attr("title")||"";return t("<a>").text(e).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,tooltipClass:null,track:!1,close:null,open:null},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.options.disabled&&this._disable()},_setOption:function(e,i){var s=this;return"disabled"===e?(this[i?"_disable":"_enable"](),this.options[e]=i,void 0):(this._super(e,i),"content"===e&&t.each(this.tooltips,function(t,e){s._updateContent(e)}),void 0)},_disable:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s[0],e.close(n,!0)}),this.element.find(this.options.items).addBack().each(function(){var e=t(this);e.is("[title]")&&e.data("ui-tooltip-title",e.attr("title")).attr("title","")})},_enable:function(){this.element.find(this.options.items).addBack().each(function(){var e=t(this);e.data("ui-tooltip-title")&&e.attr("title",e.data("ui-tooltip-title"))})},open:function(e){var i=this,s=t(e?e.target:this.element).closest(this.options.items);s.length&&!s.data("ui-tooltip-id")&&(s.attr("title")&&s.data("ui-tooltip-title",s.attr("title")),s.data("ui-tooltip-open",!0),e&&"mouseover"===e.type&&s.parents().each(function(){var e,s=t(this);s.data("ui-tooltip-open")&&(e=t.Event("blur"),e.target=e.currentTarget=this,i.close(e,!0)),s.attr("title")&&(s.uniqueId(),i.parents[this.id]={element:this,title:s.attr("title")},s.attr("title",""))}),this._updateContent(s,e))},_updateContent:function(t,e){var i,s=this.options.content,n=this,o=e?e.type:null;return"string"==typeof s?this._open(e,t,s):(i=s.call(t[0],function(i){t.data("ui-tooltip-open")&&n._delay(function(){e&&(e.type=o),this._open(e,t,i)})}),i&&this._open(e,t,i),void 0)},_open:function(i,s,n){function o(t){l.of=t,a.is(":hidden")||a.position(l)}var a,r,h,l=t.extend({},this.options.position);if(n){if(a=this._find(s),a.length)return a.find(".ui-tooltip-content").html(n),void 0;s.is("[title]")&&(i&&"mouseover"===i.type?s.attr("title",""):s.removeAttr("title")),a=this._tooltip(s),e(s,a.attr("id")),a.find(".ui-tooltip-content").html(n),this.options.track&&i&&/^mouse/.test(i.type)?(this._on(this.document,{mousemove:o}),o(i)):a.position(t.extend({of:s},this.options.position)),a.hide(),this._show(a,this.options.show),this.options.show&&this.options.show.delay&&(h=this.delayedShow=setInterval(function(){a.is(":visible")&&(o(l.of),clearInterval(h))},t.fx.interval)),this._trigger("open",i,{tooltip:a}),r={keyup:function(e){if(e.keyCode===t.ui.keyCode.ESCAPE){var i=t.Event(e);i.currentTarget=s[0],this.close(i,!0)}},remove:function(){this._removeTooltip(a)}},i&&"mouseover"!==i.type||(r.mouseleave="close"),i&&"focusin"!==i.type||(r.focusout="close"),this._on(!0,s,r)}},close:function(e){var s=this,n=t(e?e.currentTarget:this.element),o=this._find(n);this.closing||(clearInterval(this.delayedShow),n.data("ui-tooltip-title")&&n.attr("title",n.data("ui-tooltip-title")),i(n),o.stop(!0),this._hide(o,this.options.hide,function(){s._removeTooltip(t(this))}),n.removeData("ui-tooltip-open"),this._off(n,"mouseleave focusout keyup"),n[0]!==this.element[0]&&this._off(n,"remove"),this._off(this.document,"mousemove"),e&&"mouseleave"===e.type&&t.each(this.parents,function(e,i){t(i.element).attr("title",i.title),delete s.parents[e]}),this.closing=!0,this._trigger("close",e,{tooltip:o}),this.closing=!1)},_tooltip:function(e){var i="ui-tooltip-"+s++,n=t("<div>").attr({id:i,role:"tooltip"}).addClass("ui-tooltip ui-widget ui-corner-all ui-widget-content "+(this.options.tooltipClass||""));return t("<div>").addClass("ui-tooltip-content").appendTo(n),n.appendTo(this.document[0].body),this.tooltips[i]=e,n},_find:function(e){var i=e.data("ui-tooltip-id");return i?t("#"+i):t()},_removeTooltip:function(t){t.remove(),delete this.tooltips[t.attr("id")]},_destroy:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s[0],e.close(n,!0),t("#"+i).remove(),s.data("ui-tooltip-title")&&(s.attr("title",s.data("ui-tooltip-title")),s.removeData("ui-tooltip-title"))})}})})(jQuery);
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/jquery.dataTables.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/jquery.dataTables.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/jquery.dataTables.js	(revision 12677)
@@ -0,0 +1,12099 @@
+/**
+ * @summary     DataTables
+ * @description Paginate, search and sort HTML tables
+ * @version     1.9.4
+ * @file        jquery.dataTables.js
+ * @author      Allan Jardine (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ *
+ * @copyright Copyright 2008-2012 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ * 
+ * This source file is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
+ * 
+ * For details please refer to: http://www.datatables.net
+ */
+
+/*jslint evil: true, undef: true, browser: true */
+/*globals $, jQuery,define,_fnExternApiFunc,_fnInitialise,_fnInitComplete,_fnLanguageCompat,_fnAddColumn,_fnColumnOptions,_fnAddData,_fnCreateTr,_fnGatherData,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnServerParams,_fnAddOptionsHtml,_fnFeatureHtmlTable,_fnScrollDraw,_fnAdjustColumnSizing,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnBuildSearchArray,_fnBuildSearchRow,_fnFilterCreateSearch,_fnDataToSearch,_fnSort,_fnSortAttachListener,_fnSortingClasses,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnFeatureHtmlLength,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnNodeToDataIndex,_fnVisbleColumns,_fnCalculateEnd,_fnConvertToWidth,_fnCalculateColumnWidths,_fnScrollingWidthAdjust,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnDetectType,_fnSettingsFromNode,_fnGetDataMaster,_fnGetTrNodes,_fnGetTdNodes,_fnEscapeRegex,_fnDeleteIndex,_fnReOrderIndex,_fnColumnOrdering,_fnLog,_fnClearTable,_fnSaveState,_fnLoadState,_fnCreateCookie,_fnReadCookie,_fnDetectHeader,_fnGetUniqueThs,_fnScrollBarWidth,_fnApplyToChildren,_fnMap,_fnGetRowData,_fnGetCellData,_fnSetCellData,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnApplyColumnDefs,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnJsonString,_fnRender,_fnNodeToColumnIndex,_fnInfoMacros,_fnBrowserDetect,_fnGetColumns*/
+
+(/** @lends <global> */function( window, document, undefined ) {
+
+(function( factory ) {
+	"use strict";
+
+	// Define as an AMD module if possible
+	if ( typeof define === 'function' && define.amd )
+	{
+		define( ['jquery'], factory );
+	}
+	/* Define using browser globals otherwise
+	 * Prevent multiple instantiations if the script is loaded twice
+	 */
+	else if ( jQuery && !jQuery.fn.dataTable )
+	{
+		factory( jQuery );
+	}
+}
+(/** @lends <global> */function( $ ) {
+	"use strict";
+	/** 
+	 * DataTables is a plug-in for the jQuery Javascript library. It is a 
+	 * highly flexible tool, based upon the foundations of progressive 
+	 * enhancement, which will add advanced interaction controls to any 
+	 * HTML table. For a full list of features please refer to
+	 * <a href="http://datatables.net">DataTables.net</a>.
+	 *
+	 * Note that the <i>DataTable</i> object is not a global variable but is
+	 * aliased to <i>jQuery.fn.DataTable</i> and <i>jQuery.fn.dataTable</i> through which 
+	 * it may be  accessed.
+	 *
+	 *  @class
+	 *  @param {object} [oInit={}] Configuration object for DataTables. Options
+	 *    are defined by {@link DataTable.defaults}
+	 *  @requires jQuery 1.3+
+	 * 
+	 *  @example
+	 *    // Basic initialisation
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable();
+	 *    } );
+	 *  
+	 *  @example
+	 *    // Initialisation with configuration options - in this case, disable
+	 *    // pagination and sorting.
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable( {
+	 *        "bPaginate": false,
+	 *        "bSort": false 
+	 *      } );
+	 *    } );
+	 */
+	var DataTable = function( oInit )
+	{
+		
+		
+		/**
+		 * Add a column to the list used for the table with default values
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nTh The th element for this column
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddColumn( oSettings, nTh )
+		{
+			var oDefaults = DataTable.defaults.columns;
+			var iCol = oSettings.aoColumns.length;
+			var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {
+				"sSortingClass": oSettings.oClasses.sSortable,
+				"sSortingClassJUI": oSettings.oClasses.sSortJUI,
+				"nTh": nTh ? nTh : document.createElement('th'),
+				"sTitle":    oDefaults.sTitle    ? oDefaults.sTitle    : nTh ? nTh.innerHTML : '',
+				"aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],
+				"mData": oDefaults.mData ? oDefaults.oDefaults : iCol
+			} );
+			oSettings.aoColumns.push( oCol );
+			
+			/* Add a column specific filter */
+			if ( oSettings.aoPreSearchCols[ iCol ] === undefined || oSettings.aoPreSearchCols[ iCol ] === null )
+			{
+				oSettings.aoPreSearchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch );
+			}
+			else
+			{
+				var oPre = oSettings.aoPreSearchCols[ iCol ];
+				
+				/* Don't require that the user must specify bRegex, bSmart or bCaseInsensitive */
+				if ( oPre.bRegex === undefined )
+				{
+					oPre.bRegex = true;
+				}
+				
+				if ( oPre.bSmart === undefined )
+				{
+					oPre.bSmart = true;
+				}
+				
+				if ( oPre.bCaseInsensitive === undefined )
+				{
+					oPre.bCaseInsensitive = true;
+				}
+			}
+			
+			/* Use the column options function to initialise classes etc */
+			_fnColumnOptions( oSettings, iCol, null );
+		}
+		
+		
+		/**
+		 * Apply options for a column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column index to consider
+		 *  @param {object} oOptions object with sType, bVisible and bSearchable etc
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnOptions( oSettings, iCol, oOptions )
+		{
+			var oCol = oSettings.aoColumns[ iCol ];
+			
+			/* User specified column options */
+			if ( oOptions !== undefined && oOptions !== null )
+			{
+				/* Backwards compatibility for mDataProp */
+				if ( oOptions.mDataProp && !oOptions.mData )
+				{
+					oOptions.mData = oOptions.mDataProp;
+				}
+		
+				if ( oOptions.sType !== undefined )
+				{
+					oCol.sType = oOptions.sType;
+					oCol._bAutoType = false;
+				}
+				
+				$.extend( oCol, oOptions );
+				_fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );
+		
+				/* iDataSort to be applied (backwards compatibility), but aDataSort will take
+				 * priority if defined
+				 */
+				if ( oOptions.iDataSort !== undefined )
+				{
+					oCol.aDataSort = [ oOptions.iDataSort ];
+				}
+				_fnMap( oCol, oOptions, "aDataSort" );
+			}
+		
+			/* Cache the data get and set functions for speed */
+			var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null;
+			var mData = _fnGetObjectDataFn( oCol.mData );
+		
+			oCol.fnGetData = function (oData, sSpecific) {
+				var innerData = mData( oData, sSpecific );
+		
+				if ( oCol.mRender && (sSpecific && sSpecific !== '') )
+				{
+					return mRender( innerData, sSpecific, oData );
+				}
+				return innerData;
+			};
+			oCol.fnSetData = _fnSetObjectDataFn( oCol.mData );
+			
+			/* Feature sorting overrides column specific when off */
+			if ( !oSettings.oFeatures.bSort )
+			{
+				oCol.bSortable = false;
+			}
+			
+			/* Check that the class assignment is correct for sorting */
+			if ( !oCol.bSortable ||
+				 ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1) )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableNone;
+				oCol.sSortingClassJUI = "";
+			}
+			else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortable;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUI;
+			}
+			else if ( $.inArray('asc', oCol.asSorting) != -1 && $.inArray('desc', oCol.asSorting) == -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableAsc;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIAscAllowed;
+			}
+			else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) != -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableDesc;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIDescAllowed;
+			}
+		}
+		
+		
+		/**
+		 * Adjust the table column widths for new data. Note: you would probably want to 
+		 * do a redraw after calling this function!
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAdjustColumnSizing ( oSettings )
+		{
+			/* Not interested in doing column width calculation if auto-width is disabled */
+			if ( oSettings.oFeatures.bAutoWidth === false )
+			{
+				return false;
+			}
+			
+			_fnCalculateColumnWidths( oSettings );
+			for ( var i=0 , iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oSettings.aoColumns[i].nTh.style.width = oSettings.aoColumns[i].sWidth;
+			}
+		}
+		
+		
+		/**
+		 * Covert the index of a visible column to the index in the data array (take account
+		 * of hidden columns)
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iMatch Visible column index to lookup
+		 *  @returns {int} i the data index
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnVisibleToColumnIndex( oSettings, iMatch )
+		{
+			var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+		
+			return typeof aiVis[iMatch] === 'number' ?
+				aiVis[iMatch] :
+				null;
+		}
+		
+		
+		/**
+		 * Covert the index of an index in the data array and convert it to the visible
+		 *   column index (take account of hidden columns)
+		 *  @param {int} iMatch Column index to lookup
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {int} i the data index
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnIndexToVisible( oSettings, iMatch )
+		{
+			var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+			var iPos = $.inArray( iMatch, aiVis );
+		
+			return iPos !== -1 ? iPos : null;
+		}
+		
+		
+		/**
+		 * Get the number of visible columns
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {int} i the number of visible columns
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnVisbleColumns( oSettings )
+		{
+			return _fnGetColumns( oSettings, 'bVisible' ).length;
+		}
+		
+		
+		/**
+		 * Get an array of column indexes that match a given property
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sParam Parameter in aoColumns to look for - typically 
+		 *    bVisible or bSearchable
+		 *  @returns {array} Array of indexes with matched properties
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetColumns( oSettings, sParam )
+		{
+			var a = [];
+		
+			$.map( oSettings.aoColumns, function(val, i) {
+				if ( val[sParam] ) {
+					a.push( i );
+				}
+			} );
+		
+			return a;
+		}
+		
+		
+		/**
+		 * Get the sort type based on an input string
+		 *  @param {string} sData data we wish to know the type of
+		 *  @returns {string} type (defaults to 'string' if no type can be detected)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDetectType( sData )
+		{
+			var aTypes = DataTable.ext.aTypes;
+			var iLen = aTypes.length;
+			
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				var sType = aTypes[i]( sData );
+				if ( sType !== null )
+				{
+					return sType;
+				}
+			}
+			
+			return 'string';
+		}
+		
+		
+		/**
+		 * Figure out how to reorder a display list
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns array {int} aiReturn index list for reordering
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReOrderIndex ( oSettings, sColumns )
+		{
+			var aColumns = sColumns.split(',');
+			var aiReturn = [];
+			
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				for ( var j=0 ; j<iLen ; j++ )
+				{
+					if ( oSettings.aoColumns[i].sName == aColumns[j] )
+					{
+						aiReturn.push( j );
+						break;
+					}
+				}
+			}
+			
+			return aiReturn;
+		}
+		
+		
+		/**
+		 * Get the column ordering that DataTables expects
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {string} comma separated list of names
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnOrdering ( oSettings )
+		{
+			var sNames = '';
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				sNames += oSettings.aoColumns[i].sName+',';
+			}
+			if ( sNames.length == iLen )
+			{
+				return "";
+			}
+			return sNames.slice(0, -1);
+		}
+		
+		
+		/**
+		 * Take the column definitions and static columns arrays and calculate how
+		 * they relate to column indexes. The callback function will then apply the
+		 * definition found for a column to a suitable configuration object.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aoColDefs The aoColumnDefs array that is to be applied
+		 *  @param {array} aoCols The aoColumns array that defines columns individually
+		 *  @param {function} fn Callback function - takes two parameters, the calculated
+		 *    column index and the definition for that column.
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, fn )
+		{
+			var i, iLen, j, jLen, k, kLen;
+		
+			// Column definitions with aTargets
+			if ( aoColDefs )
+			{
+				/* Loop over the definitions array - loop in reverse so first instance has priority */
+				for ( i=aoColDefs.length-1 ; i>=0 ; i-- )
+				{
+					/* Each definition can target multiple columns, as it is an array */
+					var aTargets = aoColDefs[i].aTargets;
+					if ( !$.isArray( aTargets ) )
+					{
+						_fnLog( oSettings, 1, 'aTargets must be an array of targets, not a '+(typeof aTargets) );
+					}
+		
+					for ( j=0, jLen=aTargets.length ; j<jLen ; j++ )
+					{
+						if ( typeof aTargets[j] === 'number' && aTargets[j] >= 0 )
+						{
+							/* Add columns that we don't yet know about */
+							while( oSettings.aoColumns.length <= aTargets[j] )
+							{
+								_fnAddColumn( oSettings );
+							}
+		
+							/* Integer, basic index */
+							fn( aTargets[j], aoColDefs[i] );
+						}
+						else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 )
+						{
+							/* Negative integer, right to left column counting */
+							fn( oSettings.aoColumns.length+aTargets[j], aoColDefs[i] );
+						}
+						else if ( typeof aTargets[j] === 'string' )
+						{
+							/* Class name matching on TH element */
+							for ( k=0, kLen=oSettings.aoColumns.length ; k<kLen ; k++ )
+							{
+								if ( aTargets[j] == "_all" ||
+								     $(oSettings.aoColumns[k].nTh).hasClass( aTargets[j] ) )
+								{
+									fn( k, aoColDefs[i] );
+								}
+							}
+						}
+					}
+				}
+			}
+		
+			// Statically defined columns array
+			if ( aoCols )
+			{
+				for ( i=0, iLen=aoCols.length ; i<iLen ; i++ )
+				{
+					fn( i, aoCols[i] );
+				}
+			}
+		}
+		
+		/**
+		 * Add a data array to the table, creating DOM node etc. This is the parallel to 
+		 * _fnGatherData, but for adding rows from a Javascript source, rather than a
+		 * DOM source.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aData data array to be added
+		 *  @returns {int} >=0 if successful (index of new aoData entry), -1 if failed
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddData ( oSettings, aDataSupplied )
+		{
+			var oCol;
+			
+			/* Take an independent copy of the data source so we can bash it about as we wish */
+			var aDataIn = ($.isArray(aDataSupplied)) ?
+				aDataSupplied.slice() :
+				$.extend( true, {}, aDataSupplied );
+			
+			/* Create the object for storing information about this new row */
+			var iRow = oSettings.aoData.length;
+			var oData = $.extend( true, {}, DataTable.models.oRow );
+			oData._aData = aDataIn;
+			oSettings.aoData.push( oData );
+		
+			/* Create the cells */
+			var nTd, sThisType;
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oCol = oSettings.aoColumns[i];
+		
+				/* Use rendered data for filtering / sorting */
+				if ( typeof oCol.fnRender === 'function' && oCol.bUseRendered && oCol.mData !== null )
+				{
+					_fnSetCellData( oSettings, iRow, i, _fnRender(oSettings, iRow, i) );
+				}
+				else
+				{
+					_fnSetCellData( oSettings, iRow, i, _fnGetCellData( oSettings, iRow, i ) );
+				}
+				
+				/* See if we should auto-detect the column type */
+				if ( oCol._bAutoType && oCol.sType != 'string' )
+				{
+					/* Attempt to auto detect the type - same as _fnGatherData() */
+					var sVarType = _fnGetCellData( oSettings, iRow, i, 'type' );
+					if ( sVarType !== null && sVarType !== '' )
+					{
+						sThisType = _fnDetectType( sVarType );
+						if ( oCol.sType === null )
+						{
+							oCol.sType = sThisType;
+						}
+						else if ( oCol.sType != sThisType && oCol.sType != "html" )
+						{
+							/* String is always the 'fallback' option */
+							oCol.sType = 'string';
+						}
+					}
+				}
+			}
+			
+			/* Add to the display array */
+			oSettings.aiDisplayMaster.push( iRow );
+		
+			/* Create the DOM information */
+			if ( !oSettings.oFeatures.bDeferRender )
+			{
+				_fnCreateTr( oSettings, iRow );
+			}
+		
+			return iRow;
+		}
+		
+		
+		/**
+		 * Read in the data from the target table from the DOM
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGatherData( oSettings )
+		{
+			var iLoop, i, iLen, j, jLen, jInner,
+			 	nTds, nTrs, nTd, nTr, aLocalData, iThisIndex,
+				iRow, iRows, iColumn, iColumns, sNodeName,
+				oCol, oData;
+			
+			/*
+			 * Process by row first
+			 * Add the data object for the whole table - storing the tr node. Note - no point in getting
+			 * DOM based data if we are going to go and replace it with Ajax source data.
+			 */
+			if ( oSettings.bDeferLoading || oSettings.sAjaxSource === null )
+			{
+				nTr = oSettings.nTBody.firstChild;
+				while ( nTr )
+				{
+					if ( nTr.nodeName.toUpperCase() == "TR" )
+					{
+						iThisIndex = oSettings.aoData.length;
+						nTr._DT_RowIndex = iThisIndex;
+						oSettings.aoData.push( $.extend( true, {}, DataTable.models.oRow, {
+							"nTr": nTr
+						} ) );
+		
+						oSettings.aiDisplayMaster.push( iThisIndex );
+						nTd = nTr.firstChild;
+						jInner = 0;
+						while ( nTd )
+						{
+							sNodeName = nTd.nodeName.toUpperCase();
+							if ( sNodeName == "TD" || sNodeName == "TH" )
+							{
+								_fnSetCellData( oSettings, iThisIndex, jInner, $.trim(nTd.innerHTML) );
+								jInner++;
+							}
+							nTd = nTd.nextSibling;
+						}
+					}
+					nTr = nTr.nextSibling;
+				}
+			}
+			
+			/* Gather in the TD elements of the Table - note that this is basically the same as
+			 * fnGetTdNodes, but that function takes account of hidden columns, which we haven't yet
+			 * setup!
+			 */
+			nTrs = _fnGetTrNodes( oSettings );
+			nTds = [];
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				nTd = nTrs[i].firstChild;
+				while ( nTd )
+				{
+					sNodeName = nTd.nodeName.toUpperCase();
+					if ( sNodeName == "TD" || sNodeName == "TH" )
+					{
+						nTds.push( nTd );
+					}
+					nTd = nTd.nextSibling;
+				}
+			}
+			
+			/* Now process by column */
+			for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+			{
+				oCol = oSettings.aoColumns[iColumn];
+		
+				/* Get the title of the column - unless there is a user set one */
+				if ( oCol.sTitle === null )
+				{
+					oCol.sTitle = oCol.nTh.innerHTML;
+				}
+				
+				var
+					bAutoType = oCol._bAutoType,
+					bRender = typeof oCol.fnRender === 'function',
+					bClass = oCol.sClass !== null,
+					bVisible = oCol.bVisible,
+					nCell, sThisType, sRendered, sValType;
+				
+				/* A single loop to rule them all (and be more efficient) */
+				if ( bAutoType || bRender || bClass || !bVisible )
+				{
+					for ( iRow=0, iRows=oSettings.aoData.length ; iRow<iRows ; iRow++ )
+					{
+						oData = oSettings.aoData[iRow];
+						nCell = nTds[ (iRow*iColumns) + iColumn ];
+						
+						/* Type detection */
+						if ( bAutoType && oCol.sType != 'string' )
+						{
+							sValType = _fnGetCellData( oSettings, iRow, iColumn, 'type' );
+							if ( sValType !== '' )
+							{
+								sThisType = _fnDetectType( sValType );
+								if ( oCol.sType === null )
+								{
+									oCol.sType = sThisType;
+								}
+								else if ( oCol.sType != sThisType && 
+								          oCol.sType != "html" )
+								{
+									/* String is always the 'fallback' option */
+									oCol.sType = 'string';
+								}
+							}
+						}
+		
+						if ( oCol.mRender )
+						{
+							// mRender has been defined, so we need to get the value and set it
+							nCell.innerHTML = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+						}
+						else if ( oCol.mData !== iColumn )
+						{
+							// If mData is not the same as the column number, then we need to
+							// get the dev set value. If it is the column, no point in wasting
+							// time setting the value that is already there!
+							nCell.innerHTML = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+						}
+						
+						/* Rendering */
+						if ( bRender )
+						{
+							sRendered = _fnRender( oSettings, iRow, iColumn );
+							nCell.innerHTML = sRendered;
+							if ( oCol.bUseRendered )
+							{
+								/* Use the rendered data for filtering / sorting */
+								_fnSetCellData( oSettings, iRow, iColumn, sRendered );
+							}
+						}
+						
+						/* Classes */
+						if ( bClass )
+						{
+							nCell.className += ' '+oCol.sClass;
+						}
+						
+						/* Column visibility */
+						if ( !bVisible )
+						{
+							oData._anHidden[iColumn] = nCell;
+							nCell.parentNode.removeChild( nCell );
+						}
+						else
+						{
+							oData._anHidden[iColumn] = null;
+						}
+		
+						if ( oCol.fnCreatedCell )
+						{
+							oCol.fnCreatedCell.call( oSettings.oInstance,
+								nCell, _fnGetCellData( oSettings, iRow, iColumn, 'display' ), oData._aData, iRow, iColumn
+							);
+						}
+					}
+				}
+			}
+		
+			/* Row created callbacks */
+			if ( oSettings.aoRowCreatedCallback.length !== 0 )
+			{
+				for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+				{
+					oData = oSettings.aoData[i];
+					_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [oData.nTr, oData._aData, i] );
+				}
+			}
+		}
+		
+		
+		/**
+		 * Take a TR element and convert it to an index in aoData
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} n the TR element to find
+		 *  @returns {int} index if the node is found, null if not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnNodeToDataIndex( oSettings, n )
+		{
+			return (n._DT_RowIndex!==undefined) ? n._DT_RowIndex : null;
+		}
+		
+		
+		/**
+		 * Take a TD element and convert it into a column data index (not the visible index)
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow The row number the TD/TH can be found in
+		 *  @param {node} n The TD/TH element to find
+		 *  @returns {int} index if the node is found, -1 if not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnNodeToColumnIndex( oSettings, iRow, n )
+		{
+			var anCells = _fnGetTdNodes( oSettings, iRow );
+		
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( anCells[i] === n )
+				{
+					return i;
+				}
+			}
+			return -1;
+		}
+		
+		
+		/**
+		 * Get an array of data for a given row from the internal data cache
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {string} sSpecific data get type ('type' 'filter' 'sort')
+		 *  @param {array} aiColumns Array of column indexes to get data from
+		 *  @returns {array} Data array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetRowData( oSettings, iRow, sSpecific, aiColumns )
+		{
+			var out = [];
+			for ( var i=0, iLen=aiColumns.length ; i<iLen ; i++ )
+			{
+				out.push( _fnGetCellData( oSettings, iRow, aiColumns[i], sSpecific ) );
+			}
+			return out;
+		}
+		
+		
+		/**
+		 * Get the data for a given cell from the internal cache, taking into account data mapping
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {int} iCol Column index
+		 *  @param {string} sSpecific data get type ('display', 'type' 'filter' 'sort')
+		 *  @returns {*} Cell data
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetCellData( oSettings, iRow, iCol, sSpecific )
+		{
+			var sData;
+			var oCol = oSettings.aoColumns[iCol];
+			var oData = oSettings.aoData[iRow]._aData;
+		
+			if ( (sData=oCol.fnGetData( oData, sSpecific )) === undefined )
+			{
+				if ( oSettings.iDrawError != oSettings.iDraw && oCol.sDefaultContent === null )
+				{
+					_fnLog( oSettings, 0, "Requested unknown parameter "+
+						(typeof oCol.mData=='function' ? '{mData function}' : "'"+oCol.mData+"'")+
+						" from the data source for row "+iRow );
+					oSettings.iDrawError = oSettings.iDraw;
+				}
+				return oCol.sDefaultContent;
+			}
+		
+			/* When the data source is null, we can use default column data */
+			if ( sData === null && oCol.sDefaultContent !== null )
+			{
+				sData = oCol.sDefaultContent;
+			}
+			else if ( typeof sData === 'function' )
+			{
+				/* If the data source is a function, then we run it and use the return */
+				return sData();
+			}
+		
+			if ( sSpecific == 'display' && sData === null )
+			{
+				return '';
+			}
+			return sData;
+		}
+		
+		
+		/**
+		 * Set the value for a specific cell, into the internal data cache
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {int} iCol Column index
+		 *  @param {*} val Value to set
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSetCellData( oSettings, iRow, iCol, val )
+		{
+			var oCol = oSettings.aoColumns[iCol];
+			var oData = oSettings.aoData[iRow]._aData;
+		
+			oCol.fnSetData( oData, val );
+		}
+		
+		
+		// Private variable that is used to match array syntax in the data property object
+		var __reArray = /\[.*?\]$/;
+		
+		/**
+		 * Return a function that can be used to get data from a source object, taking
+		 * into account the ability to use nested objects as a source
+		 *  @param {string|int|function} mSource The data source for the object
+		 *  @returns {function} Data get function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetObjectDataFn( mSource )
+		{
+			if ( mSource === null )
+			{
+				/* Give an empty string for rendering / sorting etc */
+				return function (data, type) {
+					return null;
+				};
+			}
+			else if ( typeof mSource === 'function' )
+			{
+				return function (data, type, extra) {
+					return mSource( data, type, extra );
+				};
+			}
+			else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || mSource.indexOf('[') !== -1) )
+			{
+				/* If there is a . in the source string then the data source is in a 
+				 * nested object so we loop over the data for each level to get the next
+				 * level down. On each loop we test for undefined, and if found immediately
+				 * return. This allows entire objects to be missing and sDefaultContent to
+				 * be used if defined, rather than throwing an error
+				 */
+				var fetchData = function (data, type, src) {
+					var a = src.split('.');
+					var arrayNotation, out, innerSrc;
+		
+					if ( src !== "" )
+					{
+						for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+						{
+							// Check if we are dealing with an array notation request
+							arrayNotation = a[i].match(__reArray);
+		
+							if ( arrayNotation ) {
+								a[i] = a[i].replace(__reArray, '');
+		
+								// Condition allows simply [] to be passed in
+								if ( a[i] !== "" ) {
+									data = data[ a[i] ];
+								}
+								out = [];
+								
+								// Get the remainder of the nested object to get
+								a.splice( 0, i+1 );
+								innerSrc = a.join('.');
+		
+								// Traverse each entry in the array getting the properties requested
+								for ( var j=0, jLen=data.length ; j<jLen ; j++ ) {
+									out.push( fetchData( data[j], type, innerSrc ) );
+								}
+		
+								// If a string is given in between the array notation indicators, that
+								// is used to join the strings together, otherwise an array is returned
+								var join = arrayNotation[0].substring(1, arrayNotation[0].length-1);
+								data = (join==="") ? out : out.join(join);
+		
+								// The inner call to fetchData has already traversed through the remainder
+								// of the source requested, so we exit from the loop
+								break;
+							}
+		
+							if ( data === null || data[ a[i] ] === undefined )
+							{
+								return undefined;
+							}
+							data = data[ a[i] ];
+						}
+					}
+		
+					return data;
+				};
+		
+				return function (data, type) {
+					return fetchData( data, type, mSource );
+				};
+			}
+			else
+			{
+				/* Array or flat object mapping */
+				return function (data, type) {
+					return data[mSource];	
+				};
+			}
+		}
+		
+		
+		/**
+		 * Return a function that can be used to set data from a source object, taking
+		 * into account the ability to use nested objects as a source
+		 *  @param {string|int|function} mSource The data source for the object
+		 *  @returns {function} Data set function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSetObjectDataFn( mSource )
+		{
+			if ( mSource === null )
+			{
+				/* Nothing to do when the data source is null */
+				return function (data, val) {};
+			}
+			else if ( typeof mSource === 'function' )
+			{
+				return function (data, val) {
+					mSource( data, 'set', val );
+				};
+			}
+			else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || mSource.indexOf('[') !== -1) )
+			{
+				/* Like the get, we need to get data from a nested object */
+				var setData = function (data, val, src) {
+					var a = src.split('.'), b;
+					var arrayNotation, o, innerSrc;
+		
+					for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ )
+					{
+						// Check if we are dealing with an array notation request
+						arrayNotation = a[i].match(__reArray);
+		
+						if ( arrayNotation )
+						{
+							a[i] = a[i].replace(__reArray, '');
+							data[ a[i] ] = [];
+							
+							// Get the remainder of the nested object to set so we can recurse
+							b = a.slice();
+							b.splice( 0, i+1 );
+							innerSrc = b.join('.');
+		
+							// Traverse each entry in the array setting the properties requested
+							for ( var j=0, jLen=val.length ; j<jLen ; j++ )
+							{
+								o = {};
+								setData( o, val[j], innerSrc );
+								data[ a[i] ].push( o );
+							}
+		
+							// The inner call to setData has already traversed through the remainder
+							// of the source and has set the data, thus we can exit here
+							return;
+						}
+		
+						// If the nested object doesn't currently exist - since we are
+						// trying to set the value - create it
+						if ( data[ a[i] ] === null || data[ a[i] ] === undefined )
+						{
+							data[ a[i] ] = {};
+						}
+						data = data[ a[i] ];
+					}
+		
+					// If array notation is used, we just want to strip it and use the property name
+					// and assign the value. If it isn't used, then we get the result we want anyway
+					data[ a[a.length-1].replace(__reArray, '') ] = val;
+				};
+		
+				return function (data, val) {
+					return setData( data, val, mSource );
+				};
+			}
+			else
+			{
+				/* Array or flat object mapping */
+				return function (data, val) {
+					data[mSource] = val;	
+				};
+			}
+		}
+		
+		
+		/**
+		 * Return an array with the full table data
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns array {array} aData Master data array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetDataMaster ( oSettings )
+		{
+			var aData = [];
+			var iLen = oSettings.aoData.length;
+			for ( var i=0 ; i<iLen; i++ )
+			{
+				aData.push( oSettings.aoData[i]._aData );
+			}
+			return aData;
+		}
+		
+		
+		/**
+		 * Nuke the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnClearTable( oSettings )
+		{
+			oSettings.aoData.splice( 0, oSettings.aoData.length );
+			oSettings.aiDisplayMaster.splice( 0, oSettings.aiDisplayMaster.length );
+			oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length );
+			_fnCalculateEnd( oSettings );
+		}
+		
+		
+		 /**
+		 * Take an array of integers (index array) and remove a target integer (value - not 
+		 * the key!)
+		 *  @param {array} a Index array to target
+		 *  @param {int} iTarget value to find
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDeleteIndex( a, iTarget )
+		{
+			var iTargetIndex = -1;
+			
+			for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+			{
+				if ( a[i] == iTarget )
+				{
+					iTargetIndex = i;
+				}
+				else if ( a[i] > iTarget )
+				{
+					a[i]--;
+				}
+			}
+			
+			if ( iTargetIndex != -1 )
+			{
+				a.splice( iTargetIndex, 1 );
+			}
+		}
+		
+		
+		 /**
+		 * Call the developer defined fnRender function for a given cell (row/column) with
+		 * the required parameters and return the result.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData index for the row
+		 *  @param {int} iCol aoColumns index for the column
+		 *  @returns {*} Return of the developer's fnRender function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnRender( oSettings, iRow, iCol )
+		{
+			var oCol = oSettings.aoColumns[iCol];
+		
+			return oCol.fnRender( {
+				"iDataRow":    iRow,
+				"iDataColumn": iCol,
+				"oSettings":   oSettings,
+				"aData":       oSettings.aoData[iRow]._aData,
+				"mDataProp":   oCol.mData
+			}, _fnGetCellData(oSettings, iRow, iCol, 'display') );
+		}
+		/**
+		 * Create a new TR element (and it's TD children) for a row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow Row to consider
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCreateTr ( oSettings, iRow )
+		{
+			var oData = oSettings.aoData[iRow];
+			var nTd;
+		
+			if ( oData.nTr === null )
+			{
+				oData.nTr = document.createElement('tr');
+		
+				/* Use a private property on the node to allow reserve mapping from the node
+				 * to the aoData array for fast look up
+				 */
+				oData.nTr._DT_RowIndex = iRow;
+		
+				/* Special parameters can be given by the data source to be used on the row */
+				if ( oData._aData.DT_RowId )
+				{
+					oData.nTr.id = oData._aData.DT_RowId;
+				}
+		
+				if ( oData._aData.DT_RowClass )
+				{
+					oData.nTr.className = oData._aData.DT_RowClass;
+				}
+		
+				/* Process each column */
+				for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					var oCol = oSettings.aoColumns[i];
+					nTd = document.createElement( oCol.sCellType );
+		
+					/* Render if needed - if bUseRendered is true then we already have the rendered
+					 * value in the data source - so can just use that
+					 */
+					nTd.innerHTML = (typeof oCol.fnRender === 'function' && (!oCol.bUseRendered || oCol.mData === null)) ?
+						_fnRender( oSettings, iRow, i ) :
+						_fnGetCellData( oSettings, iRow, i, 'display' );
+				
+					/* Add user defined class */
+					if ( oCol.sClass !== null )
+					{
+						nTd.className = oCol.sClass;
+					}
+					
+					if ( oCol.bVisible )
+					{
+						oData.nTr.appendChild( nTd );
+						oData._anHidden[i] = null;
+					}
+					else
+					{
+						oData._anHidden[i] = nTd;
+					}
+		
+					if ( oCol.fnCreatedCell )
+					{
+						oCol.fnCreatedCell.call( oSettings.oInstance,
+							nTd, _fnGetCellData( oSettings, iRow, i, 'display' ), oData._aData, iRow, i
+						);
+					}
+				}
+		
+				_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [oData.nTr, oData._aData, iRow] );
+			}
+		}
+		
+		
+		/**
+		 * Create the HTML header for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildHead( oSettings )
+		{
+			var i, nTh, iLen, j, jLen;
+			var iThs = $('th, td', oSettings.nTHead).length;
+			var iCorrector = 0;
+			var jqChildren;
+			
+			/* If there is a header in place - then use it - otherwise it's going to get nuked... */
+			if ( iThs !== 0 )
+			{
+				/* We've got a thead from the DOM, so remove hidden columns and apply width to vis cols */
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					nTh.setAttribute('role', 'columnheader');
+					if ( oSettings.aoColumns[i].bSortable )
+					{
+						nTh.setAttribute('tabindex', oSettings.iTabIndex);
+						nTh.setAttribute('aria-controls', oSettings.sTableId);
+					}
+		
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					/* Set the title of the column if it is user defined (not what was auto detected) */
+					if ( oSettings.aoColumns[i].sTitle != nTh.innerHTML )
+					{
+						nTh.innerHTML = oSettings.aoColumns[i].sTitle;
+					}
+				}
+			}
+			else
+			{
+				/* We don't have a header in the DOM - so we are going to have to create one */
+				var nTr = document.createElement( "tr" );
+				
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					nTh.innerHTML = oSettings.aoColumns[i].sTitle;
+					nTh.setAttribute('tabindex', '0');
+					
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					nTr.appendChild( nTh );
+				}
+				$(oSettings.nTHead).html( '' )[0].appendChild( nTr );
+				_fnDetectHeader( oSettings.aoHeader, oSettings.nTHead );
+			}
+			
+			/* ARIA role for the rows */	
+			$(oSettings.nTHead).children('tr').attr('role', 'row');
+			
+			/* Add the extra markup needed by jQuery UI's themes */
+			if ( oSettings.bJUI )
+			{
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					
+					var nDiv = document.createElement('div');
+					nDiv.className = oSettings.oClasses.sSortJUIWrapper;
+					$(nTh).contents().appendTo(nDiv);
+					
+					var nSpan = document.createElement('span');
+					nSpan.className = oSettings.oClasses.sSortIcon;
+					nDiv.appendChild( nSpan );
+					nTh.appendChild( nDiv );
+				}
+			}
+			
+			if ( oSettings.oFeatures.bSort )
+			{
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bSortable !== false )
+					{
+						_fnSortAttachListener( oSettings, oSettings.aoColumns[i].nTh, i );
+					}
+					else
+					{
+						$(oSettings.aoColumns[i].nTh).addClass( oSettings.oClasses.sSortableNone );
+					}
+				}
+			}
+			
+			/* Deal with the footer - add classes if required */
+			if ( oSettings.oClasses.sFooterTH !== "" )
+			{
+				$(oSettings.nTFoot).children('tr').children('th').addClass( oSettings.oClasses.sFooterTH );
+			}
+			
+			/* Cache the footer elements */
+			if ( oSettings.nTFoot !== null )
+			{
+				var anCells = _fnGetUniqueThs( oSettings, null, oSettings.aoFooter );
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					if ( anCells[i] )
+					{
+						oSettings.aoColumns[i].nTf = anCells[i];
+						if ( oSettings.aoColumns[i].sClass )
+						{
+							$(anCells[i]).addClass( oSettings.aoColumns[i].sClass );
+						}
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Draw the header (or footer) element based on the column visibility states. The
+		 * methodology here is to use the layout array from _fnDetectHeader, modified for
+		 * the instantaneous column visibility, to construct the new layout. The grid is
+		 * traversed over cell at a time in a rows x columns grid fashion, although each 
+		 * cell insert can cover multiple elements in the grid - which is tracks using the
+		 * aApplied array. Cell inserts in the grid will only occur where there isn't
+		 * already a cell in that position.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param array {objects} aoSource Layout array from _fnDetectHeader
+		 *  @param {boolean} [bIncludeHidden=false] If true then include the hidden columns in the calc, 
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDrawHead( oSettings, aoSource, bIncludeHidden )
+		{
+			var i, iLen, j, jLen, k, kLen, n, nLocalTr;
+			var aoLocal = [];
+			var aApplied = [];
+			var iColumns = oSettings.aoColumns.length;
+			var iRowspan, iColspan;
+		
+			if (  bIncludeHidden === undefined )
+			{
+				bIncludeHidden = false;
+			}
+		
+			/* Make a copy of the master layout array, but without the visible columns in it */
+			for ( i=0, iLen=aoSource.length ; i<iLen ; i++ )
+			{
+				aoLocal[i] = aoSource[i].slice();
+				aoLocal[i].nTr = aoSource[i].nTr;
+		
+				/* Remove any columns which are currently hidden */
+				for ( j=iColumns-1 ; j>=0 ; j-- )
+				{
+					if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden )
+					{
+						aoLocal[i].splice( j, 1 );
+					}
+				}
+		
+				/* Prep the applied array - it needs an element for each row */
+				aApplied.push( [] );
+			}
+		
+			for ( i=0, iLen=aoLocal.length ; i<iLen ; i++ )
+			{
+				nLocalTr = aoLocal[i].nTr;
+				
+				/* All cells are going to be replaced, so empty out the row */
+				if ( nLocalTr )
+				{
+					while( (n = nLocalTr.firstChild) )
+					{
+						nLocalTr.removeChild( n );
+					}
+				}
+		
+				for ( j=0, jLen=aoLocal[i].length ; j<jLen ; j++ )
+				{
+					iRowspan = 1;
+					iColspan = 1;
+		
+					/* Check to see if there is already a cell (row/colspan) covering our target
+					 * insert point. If there is, then there is nothing to do.
+					 */
+					if ( aApplied[i][j] === undefined )
+					{
+						nLocalTr.appendChild( aoLocal[i][j].cell );
+						aApplied[i][j] = 1;
+		
+						/* Expand the cell to cover as many rows as needed */
+						while ( aoLocal[i+iRowspan] !== undefined &&
+						        aoLocal[i][j].cell == aoLocal[i+iRowspan][j].cell )
+						{
+							aApplied[i+iRowspan][j] = 1;
+							iRowspan++;
+						}
+		
+						/* Expand the cell to cover as many columns as needed */
+						while ( aoLocal[i][j+iColspan] !== undefined &&
+						        aoLocal[i][j].cell == aoLocal[i][j+iColspan].cell )
+						{
+							/* Must update the applied array over the rows for the columns */
+							for ( k=0 ; k<iRowspan ; k++ )
+							{
+								aApplied[i+k][j+iColspan] = 1;
+							}
+							iColspan++;
+						}
+		
+						/* Do the actual expansion in the DOM */
+						aoLocal[i][j].cell.rowSpan = iRowspan;
+						aoLocal[i][j].cell.colSpan = iColspan;
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Insert the required TR nodes into the table for display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDraw( oSettings )
+		{
+			/* Provide a pre-callback function which can be used to cancel the draw is false is returned */
+			var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );
+			if ( $.inArray( false, aPreDraw ) !== -1 )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				return;
+			}
+			
+			var i, iLen, n;
+			var anRows = [];
+			var iRowCount = 0;
+			var iStripes = oSettings.asStripeClasses.length;
+			var iOpenRows = oSettings.aoOpenRows.length;
+			
+			oSettings.bDrawing = true;
+			
+			/* Check and see if we have an initial draw position from state saving */
+			if ( oSettings.iInitDisplayStart !== undefined && oSettings.iInitDisplayStart != -1 )
+			{
+				if ( oSettings.oFeatures.bServerSide )
+				{
+					oSettings._iDisplayStart = oSettings.iInitDisplayStart;
+				}
+				else
+				{
+					oSettings._iDisplayStart = (oSettings.iInitDisplayStart >= oSettings.fnRecordsDisplay()) ?
+						0 : oSettings.iInitDisplayStart;
+				}
+				oSettings.iInitDisplayStart = -1;
+				_fnCalculateEnd( oSettings );
+			}
+			
+			/* Server-side processing draw intercept */
+			if ( oSettings.bDeferLoading )
+			{
+				oSettings.bDeferLoading = false;
+				oSettings.iDraw++;
+			}
+			else if ( !oSettings.oFeatures.bServerSide )
+			{
+				oSettings.iDraw++;
+			}
+			else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )
+			{
+				return;
+			}
+			
+			if ( oSettings.aiDisplay.length !== 0 )
+			{
+				var iStart = oSettings._iDisplayStart;
+				var iEnd = oSettings._iDisplayEnd;
+				
+				if ( oSettings.oFeatures.bServerSide )
+				{
+					iStart = 0;
+					iEnd = oSettings.aoData.length;
+				}
+				
+				for ( var j=iStart ; j<iEnd ; j++ )
+				{
+					var aoData = oSettings.aoData[ oSettings.aiDisplay[j] ];
+					if ( aoData.nTr === null )
+					{
+						_fnCreateTr( oSettings, oSettings.aiDisplay[j] );
+					}
+		
+					var nRow = aoData.nTr;
+					
+					/* Remove the old striping classes and then add the new one */
+					if ( iStripes !== 0 )
+					{
+						var sStripe = oSettings.asStripeClasses[ iRowCount % iStripes ];
+						if ( aoData._sRowStripe != sStripe )
+						{
+							$(nRow).removeClass( aoData._sRowStripe ).addClass( sStripe );
+							aoData._sRowStripe = sStripe;
+						}
+					}
+					
+					/* Row callback functions - might want to manipulate the row */
+					_fnCallbackFire( oSettings, 'aoRowCallback', null, 
+						[nRow, oSettings.aoData[ oSettings.aiDisplay[j] ]._aData, iRowCount, j] );
+					
+					anRows.push( nRow );
+					iRowCount++;
+					
+					/* If there is an open row - and it is attached to this parent - attach it on redraw */
+					if ( iOpenRows !== 0 )
+					{
+						for ( var k=0 ; k<iOpenRows ; k++ )
+						{
+							if ( nRow == oSettings.aoOpenRows[k].nParent )
+							{
+								anRows.push( oSettings.aoOpenRows[k].nTr );
+								break;
+							}
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Table is empty - create a row with an empty message in it */
+				anRows[ 0 ] = document.createElement( 'tr' );
+				
+				if ( oSettings.asStripeClasses[0] )
+				{
+					anRows[ 0 ].className = oSettings.asStripeClasses[0];
+				}
+		
+				var oLang = oSettings.oLanguage;
+				var sZero = oLang.sZeroRecords;
+				if ( oSettings.iDraw == 1 && oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )
+				{
+					sZero = oLang.sLoadingRecords;
+				}
+				else if ( oLang.sEmptyTable && oSettings.fnRecordsTotal() === 0 )
+				{
+					sZero = oLang.sEmptyTable;
+				}
+		
+				var nTd = document.createElement( 'td' );
+				nTd.setAttribute( 'valign', "top" );
+				nTd.colSpan = _fnVisbleColumns( oSettings );
+				nTd.className = oSettings.oClasses.sRowEmpty;
+				nTd.innerHTML = _fnInfoMacros( oSettings, sZero );
+				
+				anRows[ iRowCount ].appendChild( nTd );
+			}
+			
+			/* Header and footer callbacks */
+			_fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0], 
+				_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(), oSettings.aiDisplay ] );
+			
+			_fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0], 
+				_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(), oSettings.aiDisplay ] );
+			
+			/* 
+			 * Need to remove any old row from the display - note we can't just empty the tbody using
+			 * $().html('') since this will unbind the jQuery event handlers (even although the node 
+			 * still exists!) - equally we can't use innerHTML, since IE throws an exception.
+			 */
+			var
+				nAddFrag = document.createDocumentFragment(),
+				nRemoveFrag = document.createDocumentFragment(),
+				nBodyPar, nTrs;
+			
+			if ( oSettings.nTBody )
+			{
+				nBodyPar = oSettings.nTBody.parentNode;
+				nRemoveFrag.appendChild( oSettings.nTBody );
+				
+				/* When doing infinite scrolling, only remove child rows when sorting, filtering or start
+				 * up. When not infinite scroll, always do it.
+				 */
+				if ( !oSettings.oScroll.bInfinite || !oSettings._bInitComplete ||
+				 	oSettings.bSorted || oSettings.bFiltered )
+				{
+					while( (n = oSettings.nTBody.firstChild) )
+					{
+						oSettings.nTBody.removeChild( n );
+					}
+				}
+				
+				/* Put the draw table into the dom */
+				for ( i=0, iLen=anRows.length ; i<iLen ; i++ )
+				{
+					nAddFrag.appendChild( anRows[i] );
+				}
+				
+				oSettings.nTBody.appendChild( nAddFrag );
+				if ( nBodyPar !== null )
+				{
+					nBodyPar.appendChild( oSettings.nTBody );
+				}
+			}
+			
+			/* Call all required callback functions for the end of a draw */
+			_fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] );
+			
+			/* Draw is complete, sorting and filtering must be as well */
+			oSettings.bSorted = false;
+			oSettings.bFiltered = false;
+			oSettings.bDrawing = false;
+			
+			if ( oSettings.oFeatures.bServerSide )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				if ( !oSettings._bInitComplete )
+				{
+					_fnInitComplete( oSettings );
+				}
+			}
+		}
+		
+		
+		/**
+		 * Redraw the table - taking account of the various features which are enabled
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReDraw( oSettings )
+		{
+			if ( oSettings.oFeatures.bSort )
+			{
+				/* Sorting will refilter and draw for us */
+				_fnSort( oSettings, oSettings.oPreviousSearch );
+			}
+			else if ( oSettings.oFeatures.bFilter )
+			{
+				/* Filtering will redraw for us */
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch );
+			}
+			else
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Add the options to the page HTML for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddOptionsHtml ( oSettings )
+		{
+			/*
+			 * Create a temporary, empty, div which we can later on replace with what we have generated
+			 * we do it this way to rendering the 'options' html offline - speed :-)
+			 */
+			var nHolding = $('<div></div>')[0];
+			oSettings.nTable.parentNode.insertBefore( nHolding, oSettings.nTable );
+			
+			/* 
+			 * All DataTables are wrapped in a div
+			 */
+			oSettings.nTableWrapper = $('<div id="'+oSettings.sTableId+'_wrapper" class="'+oSettings.oClasses.sWrapper+'" role="grid"></div>')[0];
+			oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling;
+		
+			/* Track where we want to insert the option */
+			var nInsertNode = oSettings.nTableWrapper;
+			
+			/* Loop over the user set positioning and place the elements as needed */
+			var aDom = oSettings.sDom.split('');
+			var nTmp, iPushFeature, cOption, nNewNode, cNext, sAttr, j;
+			for ( var i=0 ; i<aDom.length ; i++ )
+			{
+				iPushFeature = 0;
+				cOption = aDom[i];
+				
+				if ( cOption == '<' )
+				{
+					/* New container div */
+					nNewNode = $('<div></div>')[0];
+					
+					/* Check to see if we should append an id and/or a class name to the container */
+					cNext = aDom[i+1];
+					if ( cNext == "'" || cNext == '"' )
+					{
+						sAttr = "";
+						j = 2;
+						while ( aDom[i+j] != cNext )
+						{
+							sAttr += aDom[i+j];
+							j++;
+						}
+						
+						/* Replace jQuery UI constants */
+						if ( sAttr == "H" )
+						{
+							sAttr = oSettings.oClasses.sJUIHeader;
+						}
+						else if ( sAttr == "F" )
+						{
+							sAttr = oSettings.oClasses.sJUIFooter;
+						}
+						
+						/* The attribute can be in the format of "#id.class", "#id" or "class" This logic
+						 * breaks the string into parts and applies them as needed
+						 */
+						if ( sAttr.indexOf('.') != -1 )
+						{
+							var aSplit = sAttr.split('.');
+							nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1);
+							nNewNode.className = aSplit[1];
+						}
+						else if ( sAttr.charAt(0) == "#" )
+						{
+							nNewNode.id = sAttr.substr(1, sAttr.length-1);
+						}
+						else
+						{
+							nNewNode.className = sAttr;
+						}
+						
+						i += j; /* Move along the position array */
+					}
+					
+					nInsertNode.appendChild( nNewNode );
+					nInsertNode = nNewNode;
+				}
+				else if ( cOption == '>' )
+				{
+					/* End container div */
+					nInsertNode = nInsertNode.parentNode;
+				}
+				else if ( cOption == 'l' && oSettings.oFeatures.bPaginate && oSettings.oFeatures.bLengthChange )
+				{
+					/* Length */
+					nTmp = _fnFeatureHtmlLength( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'f' && oSettings.oFeatures.bFilter )
+				{
+					/* Filter */
+					nTmp = _fnFeatureHtmlFilter( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'r' && oSettings.oFeatures.bProcessing )
+				{
+					/* pRocessing */
+					nTmp = _fnFeatureHtmlProcessing( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 't' )
+				{
+					/* Table */
+					nTmp = _fnFeatureHtmlTable( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption ==  'i' && oSettings.oFeatures.bInfo )
+				{
+					/* Info */
+					nTmp = _fnFeatureHtmlInfo( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'p' && oSettings.oFeatures.bPaginate )
+				{
+					/* Pagination */
+					nTmp = _fnFeatureHtmlPaginate( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( DataTable.ext.aoFeatures.length !== 0 )
+				{
+					/* Plug-in features */
+					var aoFeatures = DataTable.ext.aoFeatures;
+					for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ )
+					{
+						if ( cOption == aoFeatures[k].cFeature )
+						{
+							nTmp = aoFeatures[k].fnInit( oSettings );
+							if ( nTmp )
+							{
+								iPushFeature = 1;
+							}
+							break;
+						}
+					}
+				}
+				
+				/* Add to the 2D features array */
+				if ( iPushFeature == 1 && nTmp !== null )
+				{
+					if ( typeof oSettings.aanFeatures[cOption] !== 'object' )
+					{
+						oSettings.aanFeatures[cOption] = [];
+					}
+					oSettings.aanFeatures[cOption].push( nTmp );
+					nInsertNode.appendChild( nTmp );
+				}
+			}
+			
+			/* Built our DOM structure - replace the holding div with what we want */
+			nHolding.parentNode.replaceChild( oSettings.nTableWrapper, nHolding );
+		}
+		
+		
+		/**
+		 * Use the DOM source to create up an array of header cells. The idea here is to
+		 * create a layout grid (array) of rows x columns, which contains a reference
+		 * to the cell that that point in the grid (regardless of col/rowspan), such that
+		 * any column / row could be removed and the new grid constructed
+		 *  @param array {object} aLayout Array to store the calculated layout in
+		 *  @param {node} nThead The header/footer element for the table
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDetectHeader ( aLayout, nThead )
+		{
+			var nTrs = $(nThead).children('tr');
+			var nTr, nCell;
+			var i, k, l, iLen, jLen, iColShifted, iColumn, iColspan, iRowspan;
+			var bUnique;
+			var fnShiftCol = function ( a, i, j ) {
+				var k = a[i];
+		                while ( k[j] ) {
+					j++;
+				}
+				return j;
+			};
+		
+			aLayout.splice( 0, aLayout.length );
+			
+			/* We know how many rows there are in the layout - so prep it */
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				aLayout.push( [] );
+			}
+			
+			/* Calculate a layout array */
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				nTr = nTrs[i];
+				iColumn = 0;
+				
+				/* For every cell in the row... */
+				nCell = nTr.firstChild;
+				while ( nCell ) {
+					if ( nCell.nodeName.toUpperCase() == "TD" ||
+					     nCell.nodeName.toUpperCase() == "TH" )
+					{
+						/* Get the col and rowspan attributes from the DOM and sanitise them */
+						iColspan = nCell.getAttribute('colspan') * 1;
+						iRowspan = nCell.getAttribute('rowspan') * 1;
+						iColspan = (!iColspan || iColspan===0 || iColspan===1) ? 1 : iColspan;
+						iRowspan = (!iRowspan || iRowspan===0 || iRowspan===1) ? 1 : iRowspan;
+		
+						/* There might be colspan cells already in this row, so shift our target 
+						 * accordingly
+						 */
+						iColShifted = fnShiftCol( aLayout, i, iColumn );
+						
+						/* Cache calculation for unique columns */
+						bUnique = iColspan === 1 ? true : false;
+						
+						/* If there is col / rowspan, copy the information into the layout grid */
+						for ( l=0 ; l<iColspan ; l++ )
+						{
+							for ( k=0 ; k<iRowspan ; k++ )
+							{
+								aLayout[i+k][iColShifted+l] = {
+									"cell": nCell,
+									"unique": bUnique
+								};
+								aLayout[i+k].nTr = nTr;
+							}
+						}
+					}
+					nCell = nCell.nextSibling;
+				}
+			}
+		}
+		
+		
+		/**
+		 * Get an array of unique th elements, one for each column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nHeader automatically detect the layout from this node - optional
+		 *  @param {array} aLayout thead/tfoot layout from _fnDetectHeader - optional
+		 *  @returns array {node} aReturn list of unique th's
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetUniqueThs ( oSettings, nHeader, aLayout )
+		{
+			var aReturn = [];
+			if ( !aLayout )
+			{
+				aLayout = oSettings.aoHeader;
+				if ( nHeader )
+				{
+					aLayout = [];
+					_fnDetectHeader( aLayout, nHeader );
+				}
+			}
+		
+			for ( var i=0, iLen=aLayout.length ; i<iLen ; i++ )
+			{
+				for ( var j=0, jLen=aLayout[i].length ; j<jLen ; j++ )
+				{
+					if ( aLayout[i][j].unique && 
+						 (!aReturn[j] || !oSettings.bSortCellsTop) )
+					{
+						aReturn[j] = aLayout[i][j].cell;
+					}
+				}
+			}
+			
+			return aReturn;
+		}
+		
+		
+		
+		/**
+		 * Update the table using an Ajax call
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {boolean} Block the table drawing or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxUpdate( oSettings )
+		{
+			if ( oSettings.bAjaxDataGet )
+			{
+				oSettings.iDraw++;
+				_fnProcessingDisplay( oSettings, true );
+				var iColumns = oSettings.aoColumns.length;
+				var aoData = _fnAjaxParameters( oSettings );
+				_fnServerParams( oSettings, aoData );
+				
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData,
+					function(json) {
+						_fnAjaxUpdateDraw( oSettings, json );
+					}, oSettings );
+				return false;
+			}
+			else
+			{
+				return true;
+			}
+		}
+		
+		
+		/**
+		 * Build up the parameters in an object needed for a server-side processing request
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {bool} block the table drawing or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxParameters( oSettings )
+		{
+			var iColumns = oSettings.aoColumns.length;
+			var aoData = [], mDataProp, aaSort, aDataSort;
+			var i, j;
+			
+			aoData.push( { "name": "sEcho",          "value": oSettings.iDraw } );
+			aoData.push( { "name": "iColumns",       "value": iColumns } );
+			aoData.push( { "name": "sColumns",       "value": _fnColumnOrdering(oSettings) } );
+			aoData.push( { "name": "iDisplayStart",  "value": oSettings._iDisplayStart } );
+			aoData.push( { "name": "iDisplayLength", "value": oSettings.oFeatures.bPaginate !== false ?
+				oSettings._iDisplayLength : -1 } );
+				
+			for ( i=0 ; i<iColumns ; i++ )
+			{
+			  mDataProp = oSettings.aoColumns[i].mData;
+				aoData.push( { "name": "mDataProp_"+i, "value": typeof(mDataProp)==="function" ? 'function' : mDataProp } );
+			}
+			
+			/* Filtering */
+			if ( oSettings.oFeatures.bFilter !== false )
+			{
+				aoData.push( { "name": "sSearch", "value": oSettings.oPreviousSearch.sSearch } );
+				aoData.push( { "name": "bRegex",  "value": oSettings.oPreviousSearch.bRegex } );
+				for ( i=0 ; i<iColumns ; i++ )
+				{
+					aoData.push( { "name": "sSearch_"+i,     "value": oSettings.aoPreSearchCols[i].sSearch } );
+					aoData.push( { "name": "bRegex_"+i,      "value": oSettings.aoPreSearchCols[i].bRegex } );
+					aoData.push( { "name": "bSearchable_"+i, "value": oSettings.aoColumns[i].bSearchable } );
+				}
+			}
+			
+			/* Sorting */
+			if ( oSettings.oFeatures.bSort !== false )
+			{
+				var iCounter = 0;
+		
+				aaSort = ( oSettings.aaSortingFixed !== null ) ?
+					oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
+					oSettings.aaSorting.slice();
+				
+				for ( i=0 ; i<aaSort.length ; i++ )
+				{
+					aDataSort = oSettings.aoColumns[ aaSort[i][0] ].aDataSort;
+					
+					for ( j=0 ; j<aDataSort.length ; j++ )
+					{
+						aoData.push( { "name": "iSortCol_"+iCounter,  "value": aDataSort[j] } );
+						aoData.push( { "name": "sSortDir_"+iCounter,  "value": aaSort[i][1] } );
+						iCounter++;
+					}
+				}
+				aoData.push( { "name": "iSortingCols",   "value": iCounter } );
+				
+				for ( i=0 ; i<iColumns ; i++ )
+				{
+					aoData.push( { "name": "bSortable_"+i,  "value": oSettings.aoColumns[i].bSortable } );
+				}
+			}
+			
+			return aoData;
+		}
+		
+		
+		/**
+		 * Add Ajax parameters from plug-ins
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param array {objects} aoData name/value pairs to send to the server
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnServerParams( oSettings, aoData )
+		{
+			_fnCallbackFire( oSettings, 'aoServerParams', 'serverParams', [aoData] );
+		}
+		
+		
+		/**
+		 * Data the data from the server (nuking the old) and redraw the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} json json data return from the server.
+		 *  @param {string} json.sEcho Tracking flag for DataTables to match requests
+		 *  @param {int} json.iTotalRecords Number of records in the data set, not accounting for filtering
+		 *  @param {int} json.iTotalDisplayRecords Number of records in the data set, accounting for filtering
+		 *  @param {array} json.aaData The data to display on this page
+		 *  @param {string} [json.sColumns] Column ordering (sName, comma separated)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxUpdateDraw ( oSettings, json )
+		{
+			if ( json.sEcho !== undefined )
+			{
+				/* Protect against old returns over-writing a new one. Possible when you get
+				 * very fast interaction, and later queries are completed much faster
+				 */
+				if ( json.sEcho*1 < oSettings.iDraw )
+				{
+					return;
+				}
+				else
+				{
+					oSettings.iDraw = json.sEcho * 1;
+				}
+			}
+			
+			if ( !oSettings.oScroll.bInfinite ||
+				   (oSettings.oScroll.bInfinite && (oSettings.bSorted || oSettings.bFiltered)) )
+			{
+				_fnClearTable( oSettings );
+			}
+			oSettings._iRecordsTotal = parseInt(json.iTotalRecords, 10);
+			oSettings._iRecordsDisplay = parseInt(json.iTotalDisplayRecords, 10);
+			
+			/* Determine if reordering is required */
+			var sOrdering = _fnColumnOrdering(oSettings);
+			var bReOrder = (json.sColumns !== undefined && sOrdering !== "" && json.sColumns != sOrdering );
+			var aiIndex;
+			if ( bReOrder )
+			{
+				aiIndex = _fnReOrderIndex( oSettings, json.sColumns );
+			}
+			
+			var aData = _fnGetObjectDataFn( oSettings.sAjaxDataProp )( json );
+			for ( var i=0, iLen=aData.length ; i<iLen ; i++ )
+			{
+				if ( bReOrder )
+				{
+					/* If we need to re-order, then create a new array with the correct order and add it */
+					var aDataSorted = [];
+					for ( var j=0, jLen=oSettings.aoColumns.length ; j<jLen ; j++ )
+					{
+						aDataSorted.push( aData[i][ aiIndex[j] ] );
+					}
+					_fnAddData( oSettings, aDataSorted );
+				}
+				else
+				{
+					/* No re-order required, sever got it "right" - just straight add */
+					_fnAddData( oSettings, aData[i] );
+				}
+			}
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			oSettings.bAjaxDataGet = false;
+			_fnDraw( oSettings );
+			oSettings.bAjaxDataGet = true;
+			_fnProcessingDisplay( oSettings, false );
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for filtering text
+		 *  @returns {node} Filter control element
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlFilter ( oSettings )
+		{
+			var oPreviousSearch = oSettings.oPreviousSearch;
+			
+			var sSearchStr = oSettings.oLanguage.sSearch;
+			sSearchStr = (sSearchStr.indexOf('_INPUT_') !== -1) ?
+			  sSearchStr.replace('_INPUT_', '<input type="text" />') :
+			  sSearchStr==="" ? '<input type="text" />' : sSearchStr+' <input type="text" />';
+			
+			var nFilter = document.createElement( 'div' );
+			nFilter.className = oSettings.oClasses.sFilter;
+			nFilter.innerHTML = '<label>'+sSearchStr+'</label>';
+			if ( !oSettings.aanFeatures.f )
+			{
+				nFilter.id = oSettings.sTableId+'_filter';
+			}
+			
+			var jqFilter = $('input[type="text"]', nFilter);
+		
+			// Store a reference to the input element, so other input elements could be
+			// added to the filter wrapper if needed (submit button for example)
+			nFilter._DT_Input = jqFilter[0];
+		
+			jqFilter.val( oPreviousSearch.sSearch.replace('"','&quot;') );
+			jqFilter.bind( 'keyup.DT', function(e) {
+				/* Update all other filter input elements for the new display */
+				var n = oSettings.aanFeatures.f;
+				var val = this.value==="" ? "" : this.value; // mental IE8 fix :-(
+		
+				for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+				{
+					if ( n[i] != $(this).parents('div.dataTables_filter')[0] )
+					{
+						$(n[i]._DT_Input).val( val );
+					}
+				}
+				
+				/* Now do the filter */
+				if ( val != oPreviousSearch.sSearch )
+				{
+					_fnFilterComplete( oSettings, { 
+						"sSearch": val, 
+						"bRegex": oPreviousSearch.bRegex,
+						"bSmart": oPreviousSearch.bSmart ,
+						"bCaseInsensitive": oPreviousSearch.bCaseInsensitive 
+					} );
+				}
+			} );
+		
+			jqFilter
+				.attr('aria-controls', oSettings.sTableId)
+				.bind( 'keypress.DT', function(e) {
+					/* Prevent form submission */
+					if ( e.keyCode == 13 )
+					{
+						return false;
+					}
+				}
+			);
+			
+			return nFilter;
+		}
+		
+		
+		/**
+		 * Filter the table using both the global filter and column based filtering
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} oSearch search information
+		 *  @param {int} [iForce] force a research of the master array (1) or not (undefined or 0)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterComplete ( oSettings, oInput, iForce )
+		{
+			var oPrevSearch = oSettings.oPreviousSearch;
+			var aoPrevSearch = oSettings.aoPreSearchCols;
+			var fnSaveFilter = function ( oFilter ) {
+				/* Save the filtering values */
+				oPrevSearch.sSearch = oFilter.sSearch;
+				oPrevSearch.bRegex = oFilter.bRegex;
+				oPrevSearch.bSmart = oFilter.bSmart;
+				oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive;
+			};
+		
+			/* In server-side processing all filtering is done by the server, so no point hanging around here */
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				/* Global filter */
+				_fnFilter( oSettings, oInput.sSearch, iForce, oInput.bRegex, oInput.bSmart, oInput.bCaseInsensitive );
+				fnSaveFilter( oInput );
+		
+				/* Now do the individual column filter */
+				for ( var i=0 ; i<oSettings.aoPreSearchCols.length ; i++ )
+				{
+					_fnFilterColumn( oSettings, aoPrevSearch[i].sSearch, i, aoPrevSearch[i].bRegex, 
+						aoPrevSearch[i].bSmart, aoPrevSearch[i].bCaseInsensitive );
+				}
+				
+				/* Custom filtering */
+				_fnFilterCustom( oSettings );
+			}
+			else
+			{
+				fnSaveFilter( oInput );
+			}
+			
+			/* Tell the draw function we have been filtering */
+			oSettings.bFiltered = true;
+			$(oSettings.oInstance).trigger('filter', oSettings);
+			
+			/* Redraw the table */
+			oSettings._iDisplayStart = 0;
+			_fnCalculateEnd( oSettings );
+			_fnDraw( oSettings );
+			
+			/* Rebuild search array 'offline' */
+			_fnBuildSearchArray( oSettings, 0 );
+		}
+		
+		
+		/**
+		 * Apply custom filtering functions
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterCustom( oSettings )
+		{
+			var afnFilters = DataTable.ext.afnFiltering;
+			var aiFilterColumns = _fnGetColumns( oSettings, 'bSearchable' );
+		
+			for ( var i=0, iLen=afnFilters.length ; i<iLen ; i++ )
+			{
+				var iCorrector = 0;
+				for ( var j=0, jLen=oSettings.aiDisplay.length ; j<jLen ; j++ )
+				{
+					var iDisIndex = oSettings.aiDisplay[j-iCorrector];
+					var bTest = afnFilters[i](
+						oSettings,
+						_fnGetRowData( oSettings, iDisIndex, 'filter', aiFilterColumns ),
+						iDisIndex
+					);
+					
+					/* Check if we should use this row based on the filtering function */
+					if ( !bTest )
+					{
+						oSettings.aiDisplay.splice( j-iCorrector, 1 );
+						iCorrector++;
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Filter the table on a per-column basis
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sInput string to filter on
+		 *  @param {int} iColumn column to filter
+		 *  @param {bool} bRegex treat search string as a regular expression or not
+		 *  @param {bool} bSmart use smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterColumn ( oSettings, sInput, iColumn, bRegex, bSmart, bCaseInsensitive )
+		{
+			if ( sInput === "" )
+			{
+				return;
+			}
+			
+			var iIndexCorrector = 0;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive );
+			
+			for ( var i=oSettings.aiDisplay.length-1 ; i>=0 ; i-- )
+			{
+				var sData = _fnDataToSearch( _fnGetCellData( oSettings, oSettings.aiDisplay[i], iColumn, 'filter' ),
+					oSettings.aoColumns[iColumn].sType );
+				if ( ! rpSearch.test( sData ) )
+				{
+					oSettings.aiDisplay.splice( i, 1 );
+					iIndexCorrector++;
+				}
+			}
+		}
+		
+		
+		/**
+		 * Filter the data table based on user input and draw the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sInput string to filter on
+		 *  @param {int} iForce optional - force a research of the master array (1) or not (undefined or 0)
+		 *  @param {bool} bRegex treat as a regular expression or not
+		 *  @param {bool} bSmart perform smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilter( oSettings, sInput, iForce, bRegex, bSmart, bCaseInsensitive )
+		{
+			var i;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive );
+			var oPrevSearch = oSettings.oPreviousSearch;
+			
+			/* Check if we are forcing or not - optional parameter */
+			if ( !iForce )
+			{
+				iForce = 0;
+			}
+			
+			/* Need to take account of custom filtering functions - always filter */
+			if ( DataTable.ext.afnFiltering.length !== 0 )
+			{
+				iForce = 1;
+			}
+			
+			/*
+			 * If the input is blank - we want the full data set
+			 */
+			if ( sInput.length <= 0 )
+			{
+				oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			}
+			else
+			{
+				/*
+				 * We are starting a new search or the new search string is smaller 
+				 * then the old one (i.e. delete). Search from the master array
+			 	 */
+				if ( oSettings.aiDisplay.length == oSettings.aiDisplayMaster.length ||
+					   oPrevSearch.sSearch.length > sInput.length || iForce == 1 ||
+					   sInput.indexOf(oPrevSearch.sSearch) !== 0 )
+				{
+					/* Nuke the old display array - we are going to rebuild it */
+					oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
+					
+					/* Force a rebuild of the search array */
+					_fnBuildSearchArray( oSettings, 1 );
+					
+					/* Search through all records to populate the search array
+					 * The the oSettings.aiDisplayMaster and asDataSearch arrays have 1 to 1 
+					 * mapping
+					 */
+					for ( i=0 ; i<oSettings.aiDisplayMaster.length ; i++ )
+					{
+						if ( rpSearch.test(oSettings.asDataSearch[i]) )
+						{
+							oSettings.aiDisplay.push( oSettings.aiDisplayMaster[i] );
+						}
+					}
+			  }
+			  else
+				{
+			  	/* Using old search array - refine it - do it this way for speed
+			  	 * Don't have to search the whole master array again
+					 */
+			  	var iIndexCorrector = 0;
+			  	
+			  	/* Search the current results */
+			  	for ( i=0 ; i<oSettings.asDataSearch.length ; i++ )
+					{
+			  		if ( ! rpSearch.test(oSettings.asDataSearch[i]) )
+						{
+			  			oSettings.aiDisplay.splice( i-iIndexCorrector, 1 );
+			  			iIndexCorrector++;
+			  		}
+			  	}
+			  }
+			}
+		}
+		
+		
+		/**
+		 * Create an array which can be quickly search through
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iMaster use the master data array - optional
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildSearchArray ( oSettings, iMaster )
+		{
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				/* Clear out the old data */
+				oSettings.asDataSearch = [];
+		
+				var aiFilterColumns = _fnGetColumns( oSettings, 'bSearchable' );
+				var aiIndex = (iMaster===1) ?
+				 	oSettings.aiDisplayMaster :
+				 	oSettings.aiDisplay;
+				
+				for ( var i=0, iLen=aiIndex.length ; i<iLen ; i++ )
+				{
+					oSettings.asDataSearch[i] = _fnBuildSearchRow(
+						oSettings,
+						_fnGetRowData( oSettings, aiIndex[i], 'filter', aiFilterColumns )
+					);
+				}
+			}
+		}
+		
+		
+		/**
+		 * Create a searchable string from a single data row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aData Row data array to use for the data to search
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildSearchRow( oSettings, aData )
+		{
+			var sSearch = aData.join('  ');
+			
+			/* If it looks like there is an HTML entity in the string, attempt to decode it */
+			if ( sSearch.indexOf('&') !== -1 )
+			{
+				sSearch = $('<div>').html(sSearch).text();
+			}
+			
+			// Strip newline characters
+			return sSearch.replace( /[\n\r]/g, " " );
+		}
+		
+		/**
+		 * Build a regular expression object suitable for searching a table
+		 *  @param {string} sSearch string to search for
+		 *  @param {bool} bRegex treat as a regular expression or not
+		 *  @param {bool} bSmart perform smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insensitive matching or not
+		 *  @returns {RegExp} constructed object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterCreateSearch( sSearch, bRegex, bSmart, bCaseInsensitive )
+		{
+			var asSearch, sRegExpString;
+			
+			if ( bSmart )
+			{
+				/* Generate the regular expression to use. Something along the lines of:
+				 * ^(?=.*?\bone\b)(?=.*?\btwo\b)(?=.*?\bthree\b).*$
+				 */
+				asSearch = bRegex ? sSearch.split( ' ' ) : _fnEscapeRegex( sSearch ).split( ' ' );
+				sRegExpString = '^(?=.*?'+asSearch.join( ')(?=.*?' )+').*$';
+				return new RegExp( sRegExpString, bCaseInsensitive ? "i" : "" );
+			}
+			else
+			{
+				sSearch = bRegex ? sSearch : _fnEscapeRegex( sSearch );
+				return new RegExp( sSearch, bCaseInsensitive ? "i" : "" );
+			}
+		}
+		
+		
+		/**
+		 * Convert raw data into something that the user can search on
+		 *  @param {string} sData data to be modified
+		 *  @param {string} sType data type
+		 *  @returns {string} search string
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDataToSearch ( sData, sType )
+		{
+			if ( typeof DataTable.ext.ofnSearch[sType] === "function" )
+			{
+				return DataTable.ext.ofnSearch[sType]( sData );
+			}
+			else if ( sData === null )
+			{
+				return '';
+			}
+			else if ( sType == "html" )
+			{
+				return sData.replace(/[\r\n]/g," ").replace( /<.*?>/g, "" );
+			}
+			else if ( typeof sData === "string" )
+			{
+				return sData.replace(/[\r\n]/g," ");
+			}
+			return sData;
+		}
+		
+		
+		/**
+		 * scape a string such that it can be used in a regular expression
+		 *  @param {string} sVal string to escape
+		 *  @returns {string} escaped string
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnEscapeRegex ( sVal )
+		{
+			var acEscape = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ];
+			var reReplace = new RegExp( '(\\' + acEscape.join('|\\') + ')', 'g' );
+			return sVal.replace(reReplace, '\\$1');
+		}
+		
+		
+		/**
+		 * Generate the node required for the info display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Information element
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlInfo ( oSettings )
+		{
+			var nInfo = document.createElement( 'div' );
+			nInfo.className = oSettings.oClasses.sInfo;
+			
+			/* Actions that are to be taken once only for this feature */
+			if ( !oSettings.aanFeatures.i )
+			{
+				/* Add draw callback */
+				oSettings.aoDrawCallback.push( {
+					"fn": _fnUpdateInfo,
+					"sName": "information"
+				} );
+				
+				/* Add id */
+				nInfo.id = oSettings.sTableId+'_info';
+			}
+			oSettings.nTable.setAttribute( 'aria-describedby', oSettings.sTableId+'_info' );
+			
+			return nInfo;
+		}
+		
+		
+		/**
+		 * Update the information elements in the display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnUpdateInfo ( oSettings )
+		{
+			/* Show information about the table */
+			if ( !oSettings.oFeatures.bInfo || oSettings.aanFeatures.i.length === 0 )
+			{
+				return;
+			}
+			
+			var
+				oLang = oSettings.oLanguage,
+				iStart = oSettings._iDisplayStart+1,
+				iEnd = oSettings.fnDisplayEnd(),
+				iMax = oSettings.fnRecordsTotal(),
+				iTotal = oSettings.fnRecordsDisplay(),
+				sOut;
+			
+			if ( iTotal === 0 )
+			{
+				/* Empty record set */
+				sOut = oLang.sInfoEmpty;
+			}
+			else {
+				/* Normal record set */
+				sOut = oLang.sInfo;
+			}
+		
+			if ( iTotal != iMax )
+			{
+				/* Record set after filtering */
+				sOut += ' ' + oLang.sInfoFiltered;
+			}
+		
+			// Convert the macros
+			sOut += oLang.sInfoPostFix;
+			sOut = _fnInfoMacros( oSettings, sOut );
+			
+			if ( oLang.fnInfoCallback !== null )
+			{
+				sOut = oLang.fnInfoCallback.call( oSettings.oInstance, 
+					oSettings, iStart, iEnd, iMax, iTotal, sOut );
+			}
+			
+			var n = oSettings.aanFeatures.i;
+			for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+			{
+				$(n[i]).html( sOut );
+			}
+		}
+		
+		
+		function _fnInfoMacros ( oSettings, str )
+		{
+			var
+				iStart = oSettings._iDisplayStart+1,
+				sStart = oSettings.fnFormatNumber( iStart ),
+				iEnd = oSettings.fnDisplayEnd(),
+				sEnd = oSettings.fnFormatNumber( iEnd ),
+				iTotal = oSettings.fnRecordsDisplay(),
+				sTotal = oSettings.fnFormatNumber( iTotal ),
+				iMax = oSettings.fnRecordsTotal(),
+				sMax = oSettings.fnFormatNumber( iMax );
+		
+			// When infinite scrolling, we are always starting at 1. _iDisplayStart is used only
+			// internally
+			if ( oSettings.oScroll.bInfinite )
+			{
+				sStart = oSettings.fnFormatNumber( 1 );
+			}
+		
+			return str.
+				replace(/_START_/g, sStart).
+				replace(/_END_/g,   sEnd).
+				replace(/_TOTAL_/g, sTotal).
+				replace(/_MAX_/g,   sMax);
+		}
+		
+		
+		
+		/**
+		 * Draw the table for the first time, adding all required features
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnInitialise ( oSettings )
+		{
+			var i, iLen, iAjaxStart=oSettings.iInitDisplayStart;
+			
+			/* Ensure that the table data is fully initialised */
+			if ( oSettings.bInitialised === false )
+			{
+				setTimeout( function(){ _fnInitialise( oSettings ); }, 200 );
+				return;
+			}
+			
+			/* Show the display HTML options */
+			_fnAddOptionsHtml( oSettings );
+			
+			/* Build and draw the header / footer for the table */
+			_fnBuildHead( oSettings );
+			_fnDrawHead( oSettings, oSettings.aoHeader );
+			if ( oSettings.nTFoot )
+			{
+				_fnDrawHead( oSettings, oSettings.aoFooter );
+			}
+		
+			/* Okay to show that something is going on now */
+			_fnProcessingDisplay( oSettings, true );
+			
+			/* Calculate sizes for columns */
+			if ( oSettings.oFeatures.bAutoWidth )
+			{
+				_fnCalculateColumnWidths( oSettings );
+			}
+			
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoColumns[i].sWidth !== null )
+				{
+					oSettings.aoColumns[i].nTh.style.width = _fnStringToCss( oSettings.aoColumns[i].sWidth );
+				}
+			}
+			
+			/* If there is default sorting required - let's do it. The sort function will do the
+			 * drawing for us. Otherwise we draw the table regardless of the Ajax source - this allows
+			 * the table to look initialised for Ajax sourcing data (show 'loading' message possibly)
+			 */
+			if ( oSettings.oFeatures.bSort )
+			{
+				_fnSort( oSettings );
+			}
+			else if ( oSettings.oFeatures.bFilter )
+			{
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch );
+			}
+			else
+			{
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			/* if there is an ajax source load the data */
+			if ( oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )
+			{
+				var aoData = [];
+				_fnServerParams( oSettings, aoData );
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData, function(json) {
+					var aData = (oSettings.sAjaxDataProp !== "") ?
+					 	_fnGetObjectDataFn( oSettings.sAjaxDataProp )(json) : json;
+		
+					/* Got the data - add it to the table */
+					for ( i=0 ; i<aData.length ; i++ )
+					{
+						_fnAddData( oSettings, aData[i] );
+					}
+					
+					/* Reset the init display for cookie saving. We've already done a filter, and
+					 * therefore cleared it before. So we need to make it appear 'fresh'
+					 */
+					oSettings.iInitDisplayStart = iAjaxStart;
+					
+					if ( oSettings.oFeatures.bSort )
+					{
+						_fnSort( oSettings );
+					}
+					else
+					{
+						oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+						_fnCalculateEnd( oSettings );
+						_fnDraw( oSettings );
+					}
+					
+					_fnProcessingDisplay( oSettings, false );
+					_fnInitComplete( oSettings, json );
+				}, oSettings );
+				return;
+			}
+			
+			/* Server-side processing initialisation complete is done at the end of _fnDraw */
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				_fnInitComplete( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Draw the table for the first time, adding all required features
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} [json] JSON from the server that completed the table, if using Ajax source
+		 *    with client-side processing (optional)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnInitComplete ( oSettings, json )
+		{
+			oSettings._bInitComplete = true;
+			_fnCallbackFire( oSettings, 'aoInitComplete', 'init', [oSettings, json] );
+		}
+		
+		
+		/**
+		 * Language compatibility - when certain options are given, and others aren't, we
+		 * need to duplicate the values over, in order to provide backwards compatibility
+		 * with older language files.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLanguageCompat( oLanguage )
+		{
+			var oDefaults = DataTable.defaults.oLanguage;
+		
+			/* Backwards compatibility - if there is no sEmptyTable given, then use the same as
+			 * sZeroRecords - assuming that is given.
+			 */
+			if ( !oLanguage.sEmptyTable && oLanguage.sZeroRecords &&
+				oDefaults.sEmptyTable === "No data available in table" )
+			{
+				_fnMap( oLanguage, oLanguage, 'sZeroRecords', 'sEmptyTable' );
+			}
+		
+			/* Likewise with loading records */
+			if ( !oLanguage.sLoadingRecords && oLanguage.sZeroRecords &&
+				oDefaults.sLoadingRecords === "Loading..." )
+			{
+				_fnMap( oLanguage, oLanguage, 'sZeroRecords', 'sLoadingRecords' );
+			}
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for user display length changing
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Display length feature node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlLength ( oSettings )
+		{
+			if ( oSettings.oScroll.bInfinite )
+			{
+				return null;
+			}
+			
+			/* This can be overruled by not using the _MENU_ var/macro in the language variable */
+			var sName = 'name="'+oSettings.sTableId+'_length"';
+			var sStdMenu = '<select size="1" '+sName+'>';
+			var i, iLen;
+			var aLengthMenu = oSettings.aLengthMenu;
+			
+			if ( aLengthMenu.length == 2 && typeof aLengthMenu[0] === 'object' && 
+					typeof aLengthMenu[1] === 'object' )
+			{
+				for ( i=0, iLen=aLengthMenu[0].length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+aLengthMenu[0][i]+'">'+aLengthMenu[1][i]+'</option>';
+				}
+			}
+			else
+			{
+				for ( i=0, iLen=aLengthMenu.length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+aLengthMenu[i]+'">'+aLengthMenu[i]+'</option>';
+				}
+			}
+			sStdMenu += '</select>';
+			
+			var nLength = document.createElement( 'div' );
+			if ( !oSettings.aanFeatures.l )
+			{
+				nLength.id = oSettings.sTableId+'_length';
+			}
+			nLength.className = oSettings.oClasses.sLength;
+			nLength.innerHTML = '<label>'+oSettings.oLanguage.sLengthMenu.replace( '_MENU_', sStdMenu )+'</label>';
+			
+			/*
+			 * Set the length to the current display length - thanks to Andrea Pavlovic for this fix,
+			 * and Stefan Skopnik for fixing the fix!
+			 */
+			$('select option[value="'+oSettings._iDisplayLength+'"]', nLength).attr("selected", true);
+			
+			$('select', nLength).bind( 'change.DT', function(e) {
+				var iVal = $(this).val();
+				
+				/* Update all other length options for the new display */
+				var n = oSettings.aanFeatures.l;
+				for ( i=0, iLen=n.length ; i<iLen ; i++ )
+				{
+					if ( n[i] != this.parentNode )
+					{
+						$('select', n[i]).val( iVal );
+					}
+				}
+				
+				/* Redraw the table */
+				oSettings._iDisplayLength = parseInt(iVal, 10);
+				_fnCalculateEnd( oSettings );
+				
+				/* If we have space to show extra rows (backing up from the end point - then do so */
+				if ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() )
+				{
+					oSettings._iDisplayStart = oSettings.fnDisplayEnd() - oSettings._iDisplayLength;
+					if ( oSettings._iDisplayStart < 0 )
+					{
+						oSettings._iDisplayStart = 0;
+					}
+				}
+				
+				if ( oSettings._iDisplayLength == -1 )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+				
+				_fnDraw( oSettings );
+			} );
+		
+		
+			$('select', nLength).attr('aria-controls', oSettings.sTableId);
+			
+			return nLength;
+		}
+		
+		
+		/**
+		 * Recalculate the end point based on the start point
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCalculateEnd( oSettings )
+		{
+			if ( oSettings.oFeatures.bPaginate === false )
+			{
+				oSettings._iDisplayEnd = oSettings.aiDisplay.length;
+			}
+			else
+			{
+				/* Set the end point of the display - based on how many elements there are
+				 * still to display
+				 */
+				if ( oSettings._iDisplayStart + oSettings._iDisplayLength > oSettings.aiDisplay.length ||
+					   oSettings._iDisplayLength == -1 )
+				{
+					oSettings._iDisplayEnd = oSettings.aiDisplay.length;
+				}
+				else
+				{
+					oSettings._iDisplayEnd = oSettings._iDisplayStart + oSettings._iDisplayLength;
+				}
+			}
+		}
+		
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Note that most of the paging logic is done in 
+		 * DataTable.ext.oPagination
+		 */
+		
+		/**
+		 * Generate the node required for default pagination
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Pagination feature node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlPaginate ( oSettings )
+		{
+			if ( oSettings.oScroll.bInfinite )
+			{
+				return null;
+			}
+			
+			var nPaginate = document.createElement( 'div' );
+			nPaginate.className = oSettings.oClasses.sPaging+oSettings.sPaginationType;
+			
+			DataTable.ext.oPagination[ oSettings.sPaginationType ].fnInit( oSettings, nPaginate, 
+				function( oSettings ) {
+					_fnCalculateEnd( oSettings );
+					_fnDraw( oSettings );
+				}
+			);
+			
+			/* Add a draw callback for the pagination on first instance, to update the paging display */
+			if ( !oSettings.aanFeatures.p )
+			{
+				oSettings.aoDrawCallback.push( {
+					"fn": function( oSettings ) {
+						DataTable.ext.oPagination[ oSettings.sPaginationType ].fnUpdate( oSettings, function( oSettings ) {
+							_fnCalculateEnd( oSettings );
+							_fnDraw( oSettings );
+						} );
+					},
+					"sName": "pagination"
+				} );
+			}
+			return nPaginate;
+		}
+		
+		
+		/**
+		 * Alter the display settings to change the page
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer)
+		 *  @returns {bool} true page has changed, false - no change (no effect) eg 'first' on page 1
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnPageChange ( oSettings, mAction )
+		{
+			var iOldStart = oSettings._iDisplayStart;
+			
+			if ( typeof mAction === "number" )
+			{
+				oSettings._iDisplayStart = mAction * oSettings._iDisplayLength;
+				if ( oSettings._iDisplayStart > oSettings.fnRecordsDisplay() )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "first" )
+			{
+				oSettings._iDisplayStart = 0;
+			}
+			else if ( mAction == "previous" )
+			{
+				oSettings._iDisplayStart = oSettings._iDisplayLength>=0 ?
+					oSettings._iDisplayStart - oSettings._iDisplayLength :
+					0;
+				
+				/* Correct for under-run */
+				if ( oSettings._iDisplayStart < 0 )
+				{
+				  oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "next" )
+			{
+				if ( oSettings._iDisplayLength >= 0 )
+				{
+					/* Make sure we are not over running the display array */
+					if ( oSettings._iDisplayStart + oSettings._iDisplayLength < oSettings.fnRecordsDisplay() )
+					{
+						oSettings._iDisplayStart += oSettings._iDisplayLength;
+					}
+				}
+				else
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "last" )
+			{
+				if ( oSettings._iDisplayLength >= 0 )
+				{
+					var iPages = parseInt( (oSettings.fnRecordsDisplay()-1) / oSettings._iDisplayLength, 10 ) + 1;
+					oSettings._iDisplayStart = (iPages-1) * oSettings._iDisplayLength;
+				}
+				else
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else
+			{
+				_fnLog( oSettings, 0, "Unknown paging action: "+mAction );
+			}
+			$(oSettings.oInstance).trigger('page', oSettings);
+			
+			return iOldStart != oSettings._iDisplayStart;
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for the processing node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Processing element
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlProcessing ( oSettings )
+		{
+			var nProcessing = document.createElement( 'div' );
+			
+			if ( !oSettings.aanFeatures.r )
+			{
+				nProcessing.id = oSettings.sTableId+'_processing';
+			}
+			nProcessing.innerHTML = oSettings.oLanguage.sProcessing;
+			nProcessing.className = oSettings.oClasses.sProcessing;
+			oSettings.nTable.parentNode.insertBefore( nProcessing, oSettings.nTable );
+			
+			return nProcessing;
+		}
+		
+		
+		/**
+		 * Display or hide the processing indicator
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {bool} bShow Show the processing indicator (true) or not (false)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnProcessingDisplay ( oSettings, bShow )
+		{
+			if ( oSettings.oFeatures.bProcessing )
+			{
+				var an = oSettings.aanFeatures.r;
+				for ( var i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					an[i].style.visibility = bShow ? "visible" : "hidden";
+				}
+			}
+		
+			$(oSettings.oInstance).trigger('processing', [oSettings, bShow]);
+		}
+		
+		/**
+		 * Add any control elements for the table - specifically scrolling
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Node to add to the DOM
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlTable ( oSettings )
+		{
+			/* Check if scrolling is enabled or not - if not then leave the DOM unaltered */
+			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )
+			{
+				return oSettings.nTable;
+			}
+			
+			/*
+			 * The HTML structure that we want to generate in this function is:
+			 *  div - nScroller
+			 *    div - nScrollHead
+			 *      div - nScrollHeadInner
+			 *        table - nScrollHeadTable
+			 *          thead - nThead
+			 *    div - nScrollBody
+			 *      table - oSettings.nTable
+			 *        thead - nTheadSize
+			 *        tbody - nTbody
+			 *    div - nScrollFoot
+			 *      div - nScrollFootInner
+			 *        table - nScrollFootTable
+			 *          tfoot - nTfoot
+			 */
+			var
+			 	nScroller = document.createElement('div'),
+			 	nScrollHead = document.createElement('div'),
+			 	nScrollHeadInner = document.createElement('div'),
+			 	nScrollBody = document.createElement('div'),
+			 	nScrollFoot = document.createElement('div'),
+			 	nScrollFootInner = document.createElement('div'),
+			 	nScrollHeadTable = oSettings.nTable.cloneNode(false),
+			 	nScrollFootTable = oSettings.nTable.cloneNode(false),
+				nThead = oSettings.nTable.getElementsByTagName('thead')[0],
+			 	nTfoot = oSettings.nTable.getElementsByTagName('tfoot').length === 0 ? null : 
+					oSettings.nTable.getElementsByTagName('tfoot')[0],
+				oClasses = oSettings.oClasses;
+			
+			nScrollHead.appendChild( nScrollHeadInner );
+			nScrollFoot.appendChild( nScrollFootInner );
+			nScrollBody.appendChild( oSettings.nTable );
+			nScroller.appendChild( nScrollHead );
+			nScroller.appendChild( nScrollBody );
+			nScrollHeadInner.appendChild( nScrollHeadTable );
+			nScrollHeadTable.appendChild( nThead );
+			if ( nTfoot !== null )
+			{
+				nScroller.appendChild( nScrollFoot );
+				nScrollFootInner.appendChild( nScrollFootTable );
+				nScrollFootTable.appendChild( nTfoot );
+			}
+			
+			nScroller.className = oClasses.sScrollWrapper;
+			nScrollHead.className = oClasses.sScrollHead;
+			nScrollHeadInner.className = oClasses.sScrollHeadInner;
+			nScrollBody.className = oClasses.sScrollBody;
+			nScrollFoot.className = oClasses.sScrollFoot;
+			nScrollFootInner.className = oClasses.sScrollFootInner;
+			
+			if ( oSettings.oScroll.bAutoCss )
+			{
+				nScrollHead.style.overflow = "hidden";
+				nScrollHead.style.position = "relative";
+				nScrollFoot.style.overflow = "hidden";
+				nScrollBody.style.overflow = "auto";
+			}
+			
+			nScrollHead.style.border = "0";
+			nScrollHead.style.width = "100%";
+			nScrollFoot.style.border = "0";
+			nScrollHeadInner.style.width = oSettings.oScroll.sXInner !== "" ?
+				oSettings.oScroll.sXInner : "100%"; /* will be overwritten */
+			
+			/* Modify attributes to respect the clones */
+			nScrollHeadTable.removeAttribute('id');
+			nScrollHeadTable.style.marginLeft = "0";
+			oSettings.nTable.style.marginLeft = "0";
+			if ( nTfoot !== null )
+			{
+				nScrollFootTable.removeAttribute('id');
+				nScrollFootTable.style.marginLeft = "0";
+			}
+			
+			/* Move caption elements from the body to the header, footer or leave where it is
+			 * depending on the configuration. Note that the DTD says there can be only one caption */
+			var nCaption = $(oSettings.nTable).children('caption');
+			if ( nCaption.length > 0 )
+			{
+				nCaption = nCaption[0];
+				if ( nCaption._captionSide === "top" )
+				{
+					nScrollHeadTable.appendChild( nCaption );
+				}
+				else if ( nCaption._captionSide === "bottom" && nTfoot )
+				{
+					nScrollFootTable.appendChild( nCaption );
+				}
+			}
+			
+			/*
+			 * Sizing
+			 */
+			/* When x-scrolling add the width and a scroller to move the header with the body */
+			if ( oSettings.oScroll.sX !== "" )
+			{
+				nScrollHead.style.width = _fnStringToCss( oSettings.oScroll.sX );
+				nScrollBody.style.width = _fnStringToCss( oSettings.oScroll.sX );
+				
+				if ( nTfoot !== null )
+				{
+					nScrollFoot.style.width = _fnStringToCss( oSettings.oScroll.sX );	
+				}
+				
+				/* When the body is scrolled, then we also want to scroll the headers */
+				$(nScrollBody).scroll( function (e) {
+					nScrollHead.scrollLeft = this.scrollLeft;
+					
+					if ( nTfoot !== null )
+					{
+						nScrollFoot.scrollLeft = this.scrollLeft;
+					}
+				} );
+			}
+			
+			/* When yscrolling, add the height */
+			if ( oSettings.oScroll.sY !== "" )
+			{
+				nScrollBody.style.height = _fnStringToCss( oSettings.oScroll.sY );
+			}
+			
+			/* Redraw - align columns across the tables */
+			oSettings.aoDrawCallback.push( {
+				"fn": _fnScrollDraw,
+				"sName": "scrolling"
+			} );
+			
+			/* Infinite scrolling event handlers */
+			if ( oSettings.oScroll.bInfinite )
+			{
+				$(nScrollBody).scroll( function() {
+					/* Use a blocker to stop scrolling from loading more data while other data is still loading */
+					if ( !oSettings.bDrawing && $(this).scrollTop() !== 0 )
+					{
+						/* Check if we should load the next data set */
+						if ( $(this).scrollTop() + $(this).height() > 
+							$(oSettings.nTable).height() - oSettings.oScroll.iLoadGap )
+						{
+							/* Only do the redraw if we have to - we might be at the end of the data */
+							if ( oSettings.fnDisplayEnd() < oSettings.fnRecordsDisplay() )
+							{
+								_fnPageChange( oSettings, 'next' );
+								_fnCalculateEnd( oSettings );
+								_fnDraw( oSettings );
+							}
+						}
+					}
+				} );
+			}
+			
+			oSettings.nScrollHead = nScrollHead;
+			oSettings.nScrollFoot = nScrollFoot;
+			
+			return nScroller;
+		}
+		
+		
+		/**
+		 * Update the various tables for resizing. It's a bit of a pig this function, but
+		 * basically the idea to:
+		 *   1. Re-create the table inside the scrolling div
+		 *   2. Take live measurements from the DOM
+		 *   3. Apply the measurements
+		 *   4. Clean up
+		 *  @param {object} o dataTables settings object
+		 *  @returns {node} Node to add to the DOM
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollDraw ( o )
+		{
+			var
+				nScrollHeadInner = o.nScrollHead.getElementsByTagName('div')[0],
+				nScrollHeadTable = nScrollHeadInner.getElementsByTagName('table')[0],
+				nScrollBody = o.nTable.parentNode,
+				i, iLen, j, jLen, anHeadToSize, anHeadSizers, anFootSizers, anFootToSize, oStyle, iVis,
+				nTheadSize, nTfootSize,
+				iWidth, aApplied=[], aAppliedFooter=[], iSanityWidth,
+				nScrollFootInner = (o.nTFoot !== null) ? o.nScrollFoot.getElementsByTagName('div')[0] : null,
+				nScrollFootTable = (o.nTFoot !== null) ? nScrollFootInner.getElementsByTagName('table')[0] : null,
+				ie67 = o.oBrowser.bScrollOversize,
+				zeroOut = function(nSizer) {
+					oStyle = nSizer.style;
+					oStyle.paddingTop = "0";
+					oStyle.paddingBottom = "0";
+					oStyle.borderTopWidth = "0";
+					oStyle.borderBottomWidth = "0";
+					oStyle.height = 0;
+				};
+			
+			/*
+			 * 1. Re-create the table inside the scrolling div
+			 */
+			
+			/* Remove the old minimised thead and tfoot elements in the inner table */
+			$(o.nTable).children('thead, tfoot').remove();
+		
+			/* Clone the current header and footer elements and then place it into the inner table */
+			nTheadSize = $(o.nTHead).clone()[0];
+			o.nTable.insertBefore( nTheadSize, o.nTable.childNodes[0] );
+			anHeadToSize = o.nTHead.getElementsByTagName('tr');
+			anHeadSizers = nTheadSize.getElementsByTagName('tr');
+			
+			if ( o.nTFoot !== null )
+			{
+				nTfootSize = $(o.nTFoot).clone()[0];
+				o.nTable.insertBefore( nTfootSize, o.nTable.childNodes[1] );
+				anFootToSize = o.nTFoot.getElementsByTagName('tr');
+				anFootSizers = nTfootSize.getElementsByTagName('tr');
+			}
+			
+			/*
+			 * 2. Take live measurements from the DOM - do not alter the DOM itself!
+			 */
+			
+			/* Remove old sizing and apply the calculated column widths
+			 * Get the unique column headers in the newly created (cloned) header. We want to apply the
+			 * calculated sizes to this header
+			 */
+			if ( o.oScroll.sX === "" )
+			{
+				nScrollBody.style.width = '100%';
+				nScrollHeadInner.parentNode.style.width = '100%';
+			}
+			
+			var nThs = _fnGetUniqueThs( o, nTheadSize );
+			for ( i=0, iLen=nThs.length ; i<iLen ; i++ )
+			{
+				iVis = _fnVisibleToColumnIndex( o, i );
+				nThs[i].style.width = o.aoColumns[iVis].sWidth;
+			}
+			
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( function(n) {
+					n.style.width = "";
+				}, anFootSizers );
+			}
+		
+			// If scroll collapse is enabled, when we put the headers back into the body for sizing, we
+			// will end up forcing the scrollbar to appear, making our measurements wrong for when we
+			// then hide it (end of this function), so add the header height to the body scroller.
+			if ( o.oScroll.bCollapse && o.oScroll.sY !== "" )
+			{
+				nScrollBody.style.height = (nScrollBody.offsetHeight + o.nTHead.offsetHeight)+"px";
+			}
+			
+			/* Size the table as a whole */
+			iSanityWidth = $(o.nTable).outerWidth();
+			if ( o.oScroll.sX === "" )
+			{
+				/* No x scrolling */
+				o.nTable.style.width = "100%";
+				
+				/* I know this is rubbish - but IE7 will make the width of the table when 100% include
+				 * the scrollbar - which is shouldn't. When there is a scrollbar we need to take this
+				 * into account.
+				 */
+				if ( ie67 && ($('tbody', nScrollBody).height() > nScrollBody.offsetHeight || 
+					$(nScrollBody).css('overflow-y') == "scroll")  )
+				{
+					o.nTable.style.width = _fnStringToCss( $(o.nTable).outerWidth() - o.oScroll.iBarWidth);
+				}
+			}
+			else
+			{
+				if ( o.oScroll.sXInner !== "" )
+				{
+					/* x scroll inner has been given - use it */
+					o.nTable.style.width = _fnStringToCss(o.oScroll.sXInner);
+				}
+				else if ( iSanityWidth == $(nScrollBody).width() &&
+				   $(nScrollBody).height() < $(o.nTable).height() )
+				{
+					/* There is y-scrolling - try to take account of the y scroll bar */
+					o.nTable.style.width = _fnStringToCss( iSanityWidth-o.oScroll.iBarWidth );
+					if ( $(o.nTable).outerWidth() > iSanityWidth-o.oScroll.iBarWidth )
+					{
+						/* Not possible to take account of it */
+						o.nTable.style.width = _fnStringToCss( iSanityWidth );
+					}
+				}
+				else
+				{
+					/* All else fails */
+					o.nTable.style.width = _fnStringToCss( iSanityWidth );
+				}
+			}
+			
+			/* Recalculate the sanity width - now that we've applied the required width, before it was
+			 * a temporary variable. This is required because the column width calculation is done
+			 * before this table DOM is created.
+			 */
+			iSanityWidth = $(o.nTable).outerWidth();
+			
+			/* We want the hidden header to have zero height, so remove padding and borders. Then
+			 * set the width based on the real headers
+			 */
+			
+			// Apply all styles in one pass. Invalidates layout only once because we don't read any 
+			// DOM properties.
+			_fnApplyToChildren( zeroOut, anHeadSizers );
+			 
+			// Read all widths in next pass. Forces layout only once because we do not change 
+			// any DOM properties.
+			_fnApplyToChildren( function(nSizer) {
+				aApplied.push( _fnStringToCss( $(nSizer).width() ) );
+			}, anHeadSizers );
+			 
+			// Apply all widths in final pass. Invalidates layout only once because we do not
+			// read any DOM properties.
+			_fnApplyToChildren( function(nToSize, i) {
+				nToSize.style.width = aApplied[i];
+			}, anHeadToSize );
+		
+			$(anHeadSizers).height(0);
+			
+			/* Same again with the footer if we have one */
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( zeroOut, anFootSizers );
+				 
+				_fnApplyToChildren( function(nSizer) {
+					aAppliedFooter.push( _fnStringToCss( $(nSizer).width() ) );
+				}, anFootSizers );
+				 
+				_fnApplyToChildren( function(nToSize, i) {
+					nToSize.style.width = aAppliedFooter[i];
+				}, anFootToSize );
+		
+				$(anFootSizers).height(0);
+			}
+			
+			/*
+			 * 3. Apply the measurements
+			 */
+			
+			/* "Hide" the header and footer that we used for the sizing. We want to also fix their width
+			 * to what they currently are
+			 */
+			_fnApplyToChildren( function(nSizer, i) {
+				nSizer.innerHTML = "";
+				nSizer.style.width = aApplied[i];
+			}, anHeadSizers );
+			
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( function(nSizer, i) {
+					nSizer.innerHTML = "";
+					nSizer.style.width = aAppliedFooter[i];
+				}, anFootSizers );
+			}
+			
+			/* Sanity check that the table is of a sensible width. If not then we are going to get
+			 * misalignment - try to prevent this by not allowing the table to shrink below its min width
+			 */
+			if ( $(o.nTable).outerWidth() < iSanityWidth )
+			{
+				/* The min width depends upon if we have a vertical scrollbar visible or not */
+				var iCorrection = ((nScrollBody.scrollHeight > nScrollBody.offsetHeight || 
+					$(nScrollBody).css('overflow-y') == "scroll")) ?
+						iSanityWidth+o.oScroll.iBarWidth : iSanityWidth;
+				
+				/* IE6/7 are a law unto themselves... */
+				if ( ie67 && (nScrollBody.scrollHeight > 
+					nScrollBody.offsetHeight || $(nScrollBody).css('overflow-y') == "scroll")  )
+				{
+					o.nTable.style.width = _fnStringToCss( iCorrection-o.oScroll.iBarWidth );
+				}
+				
+				/* Apply the calculated minimum width to the table wrappers */
+				nScrollBody.style.width = _fnStringToCss( iCorrection );
+				o.nScrollHead.style.width = _fnStringToCss( iCorrection );
+				
+				if ( o.nTFoot !== null )
+				{
+					o.nScrollFoot.style.width = _fnStringToCss( iCorrection );
+				}
+				
+				/* And give the user a warning that we've stopped the table getting too small */
+				if ( o.oScroll.sX === "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. The table has been drawn at its minimum possible width." );
+				}
+				else if ( o.oScroll.sXInner !== "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. Increase the sScrollXInner value or remove it to allow automatic"+
+						" calculation" );
+				}
+			}
+			else
+			{
+				nScrollBody.style.width = _fnStringToCss( '100%' );
+				o.nScrollHead.style.width = _fnStringToCss( '100%' );
+				
+				if ( o.nTFoot !== null )
+				{
+					o.nScrollFoot.style.width = _fnStringToCss( '100%' );
+				}
+			}
+			
+			
+			/*
+			 * 4. Clean up
+			 */
+			if ( o.oScroll.sY === "" )
+			{
+				/* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting
+				 * the scrollbar height from the visible display, rather than adding it on. We need to
+				 * set the height in order to sort this. Don't want to do it in any other browsers.
+				 */
+				if ( ie67 )
+				{
+					nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+o.oScroll.iBarWidth );
+				}
+			}
+			
+			if ( o.oScroll.sY !== "" && o.oScroll.bCollapse )
+			{
+				nScrollBody.style.height = _fnStringToCss( o.oScroll.sY );
+				
+				var iExtra = (o.oScroll.sX !== "" && o.nTable.offsetWidth > nScrollBody.offsetWidth) ?
+				 	o.oScroll.iBarWidth : 0;
+				if ( o.nTable.offsetHeight < nScrollBody.offsetHeight )
+				{
+					nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+iExtra );
+				}
+			}
+			
+			/* Finally set the width's of the header and footer tables */
+			var iOuterWidth = $(o.nTable).outerWidth();
+			nScrollHeadTable.style.width = _fnStringToCss( iOuterWidth );
+			nScrollHeadInner.style.width = _fnStringToCss( iOuterWidth );
+		
+			// Figure out if there are scrollbar present - if so then we need a the header and footer to
+			// provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar)
+			var bScrolling = $(o.nTable).height() > nScrollBody.clientHeight || $(nScrollBody).css('overflow-y') == "scroll";
+			nScrollHeadInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
+			
+			if ( o.nTFoot !== null )
+			{
+				nScrollFootTable.style.width = _fnStringToCss( iOuterWidth );
+				nScrollFootInner.style.width = _fnStringToCss( iOuterWidth );
+				nScrollFootInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
+			}
+		
+			/* Adjust the position of the header in case we loose the y-scrollbar */
+			$(nScrollBody).scroll();
+			
+			/* If sorting or filtering has occurred, jump the scrolling back to the top */
+			if ( o.bSorted || o.bFiltered )
+			{
+				nScrollBody.scrollTop = 0;
+			}
+		}
+		
+		
+		/**
+		 * Apply a given function to the display child nodes of an element array (typically
+		 * TD children of TR rows
+		 *  @param {function} fn Method to apply to the objects
+		 *  @param array {nodes} an1 List of elements to look through for display children
+		 *  @param array {nodes} an2 Another list (identical structure to the first) - optional
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnApplyToChildren( fn, an1, an2 )
+		{
+			var index=0, i=0, iLen=an1.length;
+			var nNode1, nNode2;
+		
+			while ( i < iLen )
+			{
+				nNode1 = an1[i].firstChild;
+				nNode2 = an2 ? an2[i].firstChild : null;
+				while ( nNode1 )
+				{
+					if ( nNode1.nodeType === 1 )
+					{
+						if ( an2 )
+						{
+							fn( nNode1, nNode2, index );
+						}
+						else
+						{
+							fn( nNode1, index );
+						}
+						index++;
+					}
+					nNode1 = nNode1.nextSibling;
+					nNode2 = an2 ? nNode2.nextSibling : null;
+				}
+				i++;
+			}
+		}
+		
+		/**
+		 * Convert a CSS unit width to pixels (e.g. 2em)
+		 *  @param {string} sWidth width to be converted
+		 *  @param {node} nParent parent to get the with for (required for relative widths) - optional
+		 *  @returns {int} iWidth width in pixels
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnConvertToWidth ( sWidth, nParent )
+		{
+			if ( !sWidth || sWidth === null || sWidth === '' )
+			{
+				return 0;
+			}
+			
+			if ( !nParent )
+			{
+				nParent = document.body;
+			}
+			
+			var iWidth;
+			var nTmp = document.createElement( "div" );
+			nTmp.style.width = _fnStringToCss( sWidth );
+			
+			nParent.appendChild( nTmp );
+			iWidth = nTmp.offsetWidth;
+			nParent.removeChild( nTmp );
+			
+			return ( iWidth );
+		}
+		
+		
+		/**
+		 * Calculate the width of columns for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCalculateColumnWidths ( oSettings )
+		{
+			var iTableWidth = oSettings.nTable.offsetWidth;
+			var iUserInputs = 0;
+			var iTmpWidth;
+			var iVisibleColumns = 0;
+			var iColums = oSettings.aoColumns.length;
+			var i, iIndex, iCorrector, iWidth;
+			var oHeaders = $('th', oSettings.nTHead);
+			var widthAttr = oSettings.nTable.getAttribute('width');
+			var nWrapper = oSettings.nTable.parentNode;
+			
+			/* Convert any user input sizes into pixel sizes */
+			for ( i=0 ; i<iColums ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bVisible )
+				{
+					iVisibleColumns++;
+					
+					if ( oSettings.aoColumns[i].sWidth !== null )
+					{
+						iTmpWidth = _fnConvertToWidth( oSettings.aoColumns[i].sWidthOrig, 
+							nWrapper );
+						if ( iTmpWidth !== null )
+						{
+							oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );
+						}
+							
+						iUserInputs++;
+					}
+				}
+			}
+			
+			/* If the number of columns in the DOM equals the number that we have to process in 
+			 * DataTables, then we can use the offsets that are created by the web-browser. No custom 
+			 * sizes can be set in order for this to happen, nor scrolling used
+			 */
+			if ( iColums == oHeaders.length && iUserInputs === 0 && iVisibleColumns == iColums &&
+				oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )
+			{
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					iTmpWidth = $(oHeaders[i]).width();
+					if ( iTmpWidth !== null )
+					{
+						oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );
+					}
+				}
+			}
+			else
+			{
+				/* Otherwise we are going to have to do some calculations to get the width of each column.
+				 * Construct a 1 row table with the widest node in the data, and any user defined widths,
+				 * then insert it into the DOM and allow the browser to do all the hard work of
+				 * calculating table widths.
+				 */
+				var
+					nCalcTmp = oSettings.nTable.cloneNode( false ),
+					nTheadClone = oSettings.nTHead.cloneNode(true),
+					nBody = document.createElement( 'tbody' ),
+					nTr = document.createElement( 'tr' ),
+					nDivSizing;
+				
+				nCalcTmp.removeAttribute( "id" );
+				nCalcTmp.appendChild( nTheadClone );
+				if ( oSettings.nTFoot !== null )
+				{
+					nCalcTmp.appendChild( oSettings.nTFoot.cloneNode(true) );
+					_fnApplyToChildren( function(n) {
+						n.style.width = "";
+					}, nCalcTmp.getElementsByTagName('tr') );
+				}
+				
+				nCalcTmp.appendChild( nBody );
+				nBody.appendChild( nTr );
+				
+				/* Remove any sizing that was previously applied by the styles */
+				var jqColSizing = $('thead th', nCalcTmp);
+				if ( jqColSizing.length === 0 )
+				{
+					jqColSizing = $('tbody tr:eq(0)>td', nCalcTmp);
+				}
+		
+				/* Apply custom sizing to the cloned header */
+				var nThs = _fnGetUniqueThs( oSettings, nTheadClone );
+				iCorrector = 0;
+				for ( i=0 ; i<iColums ; i++ )
+				{
+					var oColumn = oSettings.aoColumns[i];
+					if ( oColumn.bVisible && oColumn.sWidthOrig !== null && oColumn.sWidthOrig !== "" )
+					{
+						nThs[i-iCorrector].style.width = _fnStringToCss( oColumn.sWidthOrig );
+					}
+					else if ( oColumn.bVisible )
+					{
+						nThs[i-iCorrector].style.width = "";
+					}
+					else
+					{
+						iCorrector++;
+					}
+				}
+		
+				/* Find the biggest td for each column and put it into the table */
+				for ( i=0 ; i<iColums ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						var nTd = _fnGetWidestNode( oSettings, i );
+						if ( nTd !== null )
+						{
+							nTd = nTd.cloneNode(true);
+							if ( oSettings.aoColumns[i].sContentPadding !== "" )
+							{
+								nTd.innerHTML += oSettings.aoColumns[i].sContentPadding;
+							}
+							nTr.appendChild( nTd );
+						}
+					}
+				}
+				
+				/* Build the table and 'display' it */
+				nWrapper.appendChild( nCalcTmp );
+				
+				/* When scrolling (X or Y) we want to set the width of the table as appropriate. However,
+				 * when not scrolling leave the table width as it is. This results in slightly different,
+				 * but I think correct behaviour
+				 */
+				if ( oSettings.oScroll.sX !== "" && oSettings.oScroll.sXInner !== "" )
+				{
+					nCalcTmp.style.width = _fnStringToCss(oSettings.oScroll.sXInner);
+				}
+				else if ( oSettings.oScroll.sX !== "" )
+				{
+					nCalcTmp.style.width = "";
+					if ( $(nCalcTmp).width() < nWrapper.offsetWidth )
+					{
+						nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );
+					}
+				}
+				else if ( oSettings.oScroll.sY !== "" )
+				{
+					nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );
+				}
+				else if ( widthAttr )
+				{
+					nCalcTmp.style.width = _fnStringToCss( widthAttr );
+				}
+				nCalcTmp.style.visibility = "hidden";
+				
+				/* Scrolling considerations */
+				_fnScrollingWidthAdjust( oSettings, nCalcTmp );
+				
+				/* Read the width's calculated by the browser and store them for use by the caller. We
+				 * first of all try to use the elements in the body, but it is possible that there are
+				 * no elements there, under which circumstances we use the header elements
+				 */
+				var oNodes = $("tbody tr:eq(0)", nCalcTmp).children();
+				if ( oNodes.length === 0 )
+				{
+					oNodes = _fnGetUniqueThs( oSettings, $('thead', nCalcTmp)[0] );
+				}
+		
+				/* Browsers need a bit of a hand when a width is assigned to any columns when 
+				 * x-scrolling as they tend to collapse the table to the min-width, even if
+				 * we sent the column widths. So we need to keep track of what the table width
+				 * should be by summing the user given values, and the automatic values
+				 */
+				if ( oSettings.oScroll.sX !== "" )
+				{
+					var iTotal = 0;
+					iCorrector = 0;
+					for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+					{
+						if ( oSettings.aoColumns[i].bVisible )
+						{
+							if ( oSettings.aoColumns[i].sWidthOrig === null )
+							{
+								iTotal += $(oNodes[iCorrector]).outerWidth();
+							}
+							else
+							{
+								iTotal += parseInt(oSettings.aoColumns[i].sWidth.replace('px',''), 10) +
+									($(oNodes[iCorrector]).outerWidth() - $(oNodes[iCorrector]).width());
+							}
+							iCorrector++;
+						}
+					}
+					
+					nCalcTmp.style.width = _fnStringToCss( iTotal );
+					oSettings.nTable.style.width = _fnStringToCss( iTotal );
+				}
+		
+				iCorrector = 0;
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						iWidth = $(oNodes[iCorrector]).width();
+						if ( iWidth !== null && iWidth > 0 )
+						{
+							oSettings.aoColumns[i].sWidth = _fnStringToCss( iWidth );
+						}
+						iCorrector++;
+					}
+				}
+		
+				var cssWidth = $(nCalcTmp).css('width');
+				oSettings.nTable.style.width = (cssWidth.indexOf('%') !== -1) ?
+				    cssWidth : _fnStringToCss( $(nCalcTmp).outerWidth() );
+				nCalcTmp.parentNode.removeChild( nCalcTmp );
+			}
+		
+			if ( widthAttr )
+			{
+				oSettings.nTable.style.width = _fnStringToCss( widthAttr );
+			}
+		}
+		
+		
+		/**
+		 * Adjust a table's width to take account of scrolling
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} n table node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollingWidthAdjust ( oSettings, n )
+		{
+			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY !== "" )
+			{
+				/* When y-scrolling only, we want to remove the width of the scroll bar so the table
+				 * + scroll bar will fit into the area avaialble.
+				 */
+				var iOrigWidth = $(n).width();
+				n.style.width = _fnStringToCss( $(n).outerWidth()-oSettings.oScroll.iBarWidth );
+			}
+			else if ( oSettings.oScroll.sX !== "" )
+			{
+				/* When x-scrolling both ways, fix the table at it's current size, without adjusting */
+				n.style.width = _fnStringToCss( $(n).outerWidth() );
+			}
+		}
+		
+		
+		/**
+		 * Get the widest node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column of interest
+		 *  @returns {node} widest table node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetWidestNode( oSettings, iCol )
+		{
+			var iMaxIndex = _fnGetMaxLenString( oSettings, iCol );
+			if ( iMaxIndex < 0 )
+			{
+				return null;
+			}
+		
+			if ( oSettings.aoData[iMaxIndex].nTr === null )
+			{
+				var n = document.createElement('td');
+				n.innerHTML = _fnGetCellData( oSettings, iMaxIndex, iCol, '' );
+				return n;
+			}
+			return _fnGetTdNodes(oSettings, iMaxIndex)[iCol];
+		}
+		
+		
+		/**
+		 * Get the maximum strlen for each data column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column of interest
+		 *  @returns {string} max string length for each column
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetMaxLenString( oSettings, iCol )
+		{
+			var iMax = -1;
+			var iMaxIndex = -1;
+			
+			for ( var i=0 ; i<oSettings.aoData.length ; i++ )
+			{
+				var s = _fnGetCellData( oSettings, i, iCol, 'display' )+"";
+				s = s.replace( /<.*?>/g, "" );
+				if ( s.length > iMax )
+				{
+					iMax = s.length;
+					iMaxIndex = i;
+				}
+			}
+			
+			return iMaxIndex;
+		}
+		
+		
+		/**
+		 * Append a CSS unit (only if required) to a string
+		 *  @param {array} aArray1 first array
+		 *  @param {array} aArray2 second array
+		 *  @returns {int} 0 if match, 1 if length is different, 2 if no match
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnStringToCss( s )
+		{
+			if ( s === null )
+			{
+				return "0px";
+			}
+			
+			if ( typeof s == 'number' )
+			{
+				if ( s < 0 )
+				{
+					return "0px";
+				}
+				return s+"px";
+			}
+			
+			/* Check if the last character is not 0-9 */
+			var c = s.charCodeAt( s.length-1 );
+			if (c < 0x30 || c > 0x39)
+			{
+				return s;
+			}
+			return s+"px";
+		}
+		
+		
+		/**
+		 * Get the width of a scroll bar in this browser being used
+		 *  @returns {int} width in pixels
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollBarWidth ()
+		{  
+			var inner = document.createElement('p');
+			var style = inner.style;
+			style.width = "100%";
+			style.height = "200px";
+			style.padding = "0px";
+			
+			var outer = document.createElement('div');
+			style = outer.style;
+			style.position = "absolute";
+			style.top = "0px";
+			style.left = "0px";
+			style.visibility = "hidden";
+			style.width = "200px";
+			style.height = "150px";
+			style.padding = "0px";
+			style.overflow = "hidden";
+			outer.appendChild(inner);
+			
+			document.body.appendChild(outer);
+			var w1 = inner.offsetWidth;
+			outer.style.overflow = 'scroll';
+			var w2 = inner.offsetWidth;
+			if ( w1 == w2 )
+			{
+				w2 = outer.clientWidth;
+			}
+			
+			document.body.removeChild(outer);
+			return (w1 - w2);  
+		}
+		
+		/**
+		 * Change the order of the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {bool} bApplyClasses optional - should we apply classes or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSort ( oSettings, bApplyClasses )
+		{
+			var
+				i, iLen, j, jLen, k, kLen,
+				sDataType, nTh,
+				aaSort = [],
+			 	aiOrig = [],
+				oSort = DataTable.ext.oSort,
+				aoData = oSettings.aoData,
+				aoColumns = oSettings.aoColumns,
+				oAria = oSettings.oLanguage.oAria;
+			
+			/* No sorting required if server-side or no sorting array */
+			if ( !oSettings.oFeatures.bServerSide && 
+				(oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) )
+			{
+				aaSort = ( oSettings.aaSortingFixed !== null ) ?
+					oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
+					oSettings.aaSorting.slice();
+				
+				/* If there is a sorting data type, and a function belonging to it, then we need to
+				 * get the data from the developer's function and apply it for this column
+				 */
+				for ( i=0 ; i<aaSort.length ; i++ )
+				{
+					var iColumn = aaSort[i][0];
+					var iVisColumn = _fnColumnIndexToVisible( oSettings, iColumn );
+					sDataType = oSettings.aoColumns[ iColumn ].sSortDataType;
+					if ( DataTable.ext.afnSortData[sDataType] )
+					{
+						var aData = DataTable.ext.afnSortData[sDataType].call( 
+							oSettings.oInstance, oSettings, iColumn, iVisColumn
+						);
+						if ( aData.length === aoData.length )
+						{
+							for ( j=0, jLen=aoData.length ; j<jLen ; j++ )
+							{
+								_fnSetCellData( oSettings, j, iColumn, aData[j] );
+							}
+						}
+						else
+						{
+							_fnLog( oSettings, 0, "Returned data sort array (col "+iColumn+") is the wrong length" );
+						}
+					}
+				}
+				
+				/* Create a value - key array of the current row positions such that we can use their
+				 * current position during the sort, if values match, in order to perform stable sorting
+				 */
+				for ( i=0, iLen=oSettings.aiDisplayMaster.length ; i<iLen ; i++ )
+				{
+					aiOrig[ oSettings.aiDisplayMaster[i] ] = i;
+				}
+		
+				/* Build an internal data array which is specific to the sort, so we can get and prep
+				 * the data to be sorted only once, rather than needing to do it every time the sorting
+				 * function runs. This make the sorting function a very simple comparison
+				 */
+				var iSortLen = aaSort.length;
+				var fnSortFormat, aDataSort;
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					for ( j=0 ; j<iSortLen ; j++ )
+					{
+						aDataSort = aoColumns[ aaSort[j][0] ].aDataSort;
+		
+						for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ )
+						{
+							sDataType = aoColumns[ aDataSort[k] ].sType;
+							fnSortFormat = oSort[ (sDataType ? sDataType : 'string')+"-pre" ];
+							
+							aoData[i]._aSortData[ aDataSort[k] ] = fnSortFormat ?
+								fnSortFormat( _fnGetCellData( oSettings, i, aDataSort[k], 'sort' ) ) :
+								_fnGetCellData( oSettings, i, aDataSort[k], 'sort' );
+						}
+					}
+				}
+				
+				/* Do the sort - here we want multi-column sorting based on a given data source (column)
+				 * and sorting function (from oSort) in a certain direction. It's reasonably complex to
+				 * follow on it's own, but this is what we want (example two column sorting):
+				 *  fnLocalSorting = function(a,b){
+				 *  	var iTest;
+				 *  	iTest = oSort['string-asc']('data11', 'data12');
+				 *  	if (iTest !== 0)
+				 *  		return iTest;
+				 *    iTest = oSort['numeric-desc']('data21', 'data22');
+				 *    if (iTest !== 0)
+				 *  		return iTest;
+				 *  	return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+				 *  }
+				 * Basically we have a test for each sorting column, if the data in that column is equal,
+				 * test the next column. If all columns match, then we use a numeric sort on the row 
+				 * positions in the original data array to provide a stable sort.
+				 */
+				oSettings.aiDisplayMaster.sort( function ( a, b ) {
+					var k, l, lLen, iTest, aDataSort, sDataType;
+					for ( k=0 ; k<iSortLen ; k++ )
+					{
+						aDataSort = aoColumns[ aaSort[k][0] ].aDataSort;
+		
+						for ( l=0, lLen=aDataSort.length ; l<lLen ; l++ )
+						{
+							sDataType = aoColumns[ aDataSort[l] ].sType;
+							
+							iTest = oSort[ (sDataType ? sDataType : 'string')+"-"+aaSort[k][1] ](
+								aoData[a]._aSortData[ aDataSort[l] ],
+								aoData[b]._aSortData[ aDataSort[l] ]
+							);
+						
+							if ( iTest !== 0 )
+							{
+								return iTest;
+							}
+						}
+					}
+					
+					return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+				} );
+			}
+			
+			/* Alter the sorting classes to take account of the changes */
+			if ( (bApplyClasses === undefined || bApplyClasses) && !oSettings.oFeatures.bDeferRender )
+			{
+				_fnSortingClasses( oSettings );
+			}
+		
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				var sTitle = aoColumns[i].sTitle.replace( /<.*?>/g, "" );
+				nTh = aoColumns[i].nTh;
+				nTh.removeAttribute('aria-sort');
+				nTh.removeAttribute('aria-label');
+				
+				/* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */
+				if ( aoColumns[i].bSortable )
+				{
+					if ( aaSort.length > 0 && aaSort[0][0] == i )
+					{
+						nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" );
+						
+						var nextSort = (aoColumns[i].asSorting[ aaSort[0][2]+1 ]) ? 
+							aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0];
+						nTh.setAttribute('aria-label', sTitle+
+							(nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
+					}
+					else
+					{
+						nTh.setAttribute('aria-label', sTitle+
+							(aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
+					}
+				}
+				else
+				{
+					nTh.setAttribute('aria-label', sTitle);
+				}
+			}
+			
+			/* Tell the draw function that we have sorted the data */
+			oSettings.bSorted = true;
+			$(oSettings.oInstance).trigger('sort', oSettings);
+			
+			/* Copy the master data into the draw array and re-draw */
+			if ( oSettings.oFeatures.bFilter )
+			{
+				/* _fnFilter() will redraw the table for us */
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
+			}
+			else
+			{
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+				oSettings._iDisplayStart = 0; /* reset display back to page 0 */
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Attach a sort handler (click) to a node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nNode node to attach the handler to
+		 *  @param {int} iDataIndex column sorting index
+		 *  @param {function} [fnCallback] callback function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback )
+		{
+			_fnBindAction( nNode, {}, function (e) {
+				/* If the column is not sortable - don't to anything */
+				if ( oSettings.aoColumns[iDataIndex].bSortable === false )
+				{
+					return;
+				}
+				
+				/*
+				 * This is a little bit odd I admit... I declare a temporary function inside the scope of
+				 * _fnBuildHead and the click handler in order that the code presented here can be used 
+				 * twice - once for when bProcessing is enabled, and another time for when it is 
+				 * disabled, as we need to perform slightly different actions.
+				 *   Basically the issue here is that the Javascript engine in modern browsers don't 
+				 * appear to allow the rendering engine to update the display while it is still executing
+				 * it's thread (well - it does but only after long intervals). This means that the 
+				 * 'processing' display doesn't appear for a table sort. To break the js thread up a bit
+				 * I force an execution break by using setTimeout - but this breaks the expected 
+				 * thread continuation for the end-developer's point of view (their code would execute
+				 * too early), so we only do it when we absolutely have to.
+				 */
+				var fnInnerSorting = function () {
+					var iColumn, iNextSort;
+					
+					/* If the shift key is pressed then we are multiple column sorting */
+					if ( e.shiftKey )
+					{
+						/* Are we already doing some kind of sort on this column? */
+						var bFound = false;
+						for ( var i=0 ; i<oSettings.aaSorting.length ; i++ )
+						{
+							if ( oSettings.aaSorting[i][0] == iDataIndex )
+							{
+								bFound = true;
+								iColumn = oSettings.aaSorting[i][0];
+								iNextSort = oSettings.aaSorting[i][2]+1;
+								
+								if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] )
+								{
+									/* Reached the end of the sorting options, remove from multi-col sort */
+									oSettings.aaSorting.splice( i, 1 );
+								}
+								else
+								{
+									/* Move onto next sorting direction */
+									oSettings.aaSorting[i][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
+									oSettings.aaSorting[i][2] = iNextSort;
+								}
+								break;
+							}
+						}
+						
+						/* No sort yet - add it in */
+						if ( bFound === false )
+						{
+							oSettings.aaSorting.push( [ iDataIndex, 
+								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );
+						}
+					}
+					else
+					{
+						/* If no shift key then single column sort */
+						if ( oSettings.aaSorting.length == 1 && oSettings.aaSorting[0][0] == iDataIndex )
+						{
+							iColumn = oSettings.aaSorting[0][0];
+							iNextSort = oSettings.aaSorting[0][2]+1;
+							if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] )
+							{
+								iNextSort = 0;
+							}
+							oSettings.aaSorting[0][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
+							oSettings.aaSorting[0][2] = iNextSort;
+						}
+						else
+						{
+							oSettings.aaSorting.splice( 0, oSettings.aaSorting.length );
+							oSettings.aaSorting.push( [ iDataIndex, 
+								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );
+						}
+					}
+					
+					/* Run the sort */
+					_fnSort( oSettings );
+				}; /* /fnInnerSorting */
+				
+				if ( !oSettings.oFeatures.bProcessing )
+				{
+					fnInnerSorting();
+				}
+				else
+				{
+					_fnProcessingDisplay( oSettings, true );
+					setTimeout( function() {
+						fnInnerSorting();
+						if ( !oSettings.oFeatures.bServerSide )
+						{
+							_fnProcessingDisplay( oSettings, false );
+						}
+					}, 0 );
+				}
+				
+				/* Call the user specified callback function - used for async user interaction */
+				if ( typeof fnCallback == 'function' )
+				{
+					fnCallback( oSettings );
+				}
+			} );
+		}
+		
+		
+		/**
+		 * Set the sorting classes on the header, Note: it is safe to call this function 
+		 * when bSort and bSortClasses are false
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSortingClasses( oSettings )
+		{
+			var i, iLen, j, jLen, iFound;
+			var aaSort, sClass;
+			var iColumns = oSettings.aoColumns.length;
+			var oClasses = oSettings.oClasses;
+			
+			for ( i=0 ; i<iColumns ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bSortable )
+				{
+					$(oSettings.aoColumns[i].nTh).removeClass( oClasses.sSortAsc +" "+ oClasses.sSortDesc +
+						" "+ oSettings.aoColumns[i].sSortingClass );
+				}
+			}
+			
+			if ( oSettings.aaSortingFixed !== null )
+			{
+				aaSort = oSettings.aaSortingFixed.concat( oSettings.aaSorting );
+			}
+			else
+			{
+				aaSort = oSettings.aaSorting.slice();
+			}
+			
+			/* Apply the required classes to the header */
+			for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bSortable )
+				{
+					sClass = oSettings.aoColumns[i].sSortingClass;
+					iFound = -1;
+					for ( j=0 ; j<aaSort.length ; j++ )
+					{
+						if ( aaSort[j][0] == i )
+						{
+							sClass = ( aaSort[j][1] == "asc" ) ?
+								oClasses.sSortAsc : oClasses.sSortDesc;
+							iFound = j;
+							break;
+						}
+					}
+					$(oSettings.aoColumns[i].nTh).addClass( sClass );
+					
+					if ( oSettings.bJUI )
+					{
+						/* jQuery UI uses extra markup */
+						var jqSpan = $("span."+oClasses.sSortIcon,  oSettings.aoColumns[i].nTh);
+						jqSpan.removeClass(oClasses.sSortJUIAsc +" "+ oClasses.sSortJUIDesc +" "+ 
+							oClasses.sSortJUI +" "+ oClasses.sSortJUIAscAllowed +" "+ oClasses.sSortJUIDescAllowed );
+						
+						var sSpanClass;
+						if ( iFound == -1 )
+						{
+						 	sSpanClass = oSettings.aoColumns[i].sSortingClassJUI;
+						}
+						else if ( aaSort[iFound][1] == "asc" )
+						{
+							sSpanClass = oClasses.sSortJUIAsc;
+						}
+						else
+						{
+							sSpanClass = oClasses.sSortJUIDesc;
+						}
+						
+						jqSpan.addClass( sSpanClass );
+					}
+				}
+				else
+				{
+					/* No sorting on this column, so add the base class. This will have been assigned by
+					 * _fnAddColumn
+					 */
+					$(oSettings.aoColumns[i].nTh).addClass( oSettings.aoColumns[i].sSortingClass );
+				}
+			}
+			
+			/* 
+			 * Apply the required classes to the table body
+			 * Note that this is given as a feature switch since it can significantly slow down a sort
+			 * on large data sets (adding and removing of classes is always slow at the best of times..)
+			 * Further to this, note that this code is admittedly fairly ugly. It could be made a lot 
+			 * simpler using jQuery selectors and add/removeClass, but that is significantly slower
+			 * (on the order of 5 times slower) - hence the direct DOM manipulation here.
+			 * Note that for deferred drawing we do use jQuery - the reason being that taking the first
+			 * row found to see if the whole column needs processed can miss classes since the first
+			 * column might be new.
+			 */
+			sClass = oClasses.sSortColumn;
+			
+			if ( oSettings.oFeatures.bSort && oSettings.oFeatures.bSortClasses )
+			{
+				var nTds = _fnGetTdNodes( oSettings );
+				
+				/* Determine what the sorting class for each column should be */
+				var iClass, iTargetCol;
+				var asClasses = [];
+				for (i = 0; i < iColumns; i++)
+				{
+					asClasses.push("");
+				}
+				for (i = 0, iClass = 1; i < aaSort.length; i++)
+				{
+					iTargetCol = parseInt( aaSort[i][0], 10 );
+					asClasses[iTargetCol] = sClass + iClass;
+					
+					if ( iClass < 3 )
+					{
+						iClass++;
+					}
+				}
+				
+				/* Make changes to the classes for each cell as needed */
+				var reClass = new RegExp(sClass + "[123]");
+				var sTmpClass, sCurrentClass, sNewClass;
+				for ( i=0, iLen=nTds.length; i<iLen; i++ )
+				{
+					/* Determine which column we're looking at */
+					iTargetCol = i % iColumns;
+					
+					/* What is the full list of classes now */
+					sCurrentClass = nTds[i].className;
+					/* What sorting class should be applied? */
+					sNewClass = asClasses[iTargetCol];
+					/* What would the new full list be if we did a replacement? */
+					sTmpClass = sCurrentClass.replace(reClass, sNewClass);
+					
+					if ( sTmpClass != sCurrentClass )
+					{
+						/* We changed something */
+						nTds[i].className = $.trim( sTmpClass );
+					}
+					else if ( sNewClass.length > 0 && sCurrentClass.indexOf(sNewClass) == -1 )
+					{
+						/* We need to add a class */
+						nTds[i].className = sCurrentClass + " " + sNewClass;
+					}
+				}
+			}
+		}
+		
+		
+		
+		/**
+		 * Save the state of a table in a cookie such that the page can be reloaded
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSaveState ( oSettings )
+		{
+			if ( !oSettings.oFeatures.bStateSave || oSettings.bDestroying )
+			{
+				return;
+			}
+		
+			/* Store the interesting variables */
+			var i, iLen, bInfinite=oSettings.oScroll.bInfinite;
+			var oState = {
+				"iCreate":      new Date().getTime(),
+				"iStart":       (bInfinite ? 0 : oSettings._iDisplayStart),
+				"iEnd":         (bInfinite ? oSettings._iDisplayLength : oSettings._iDisplayEnd),
+				"iLength":      oSettings._iDisplayLength,
+				"aaSorting":    $.extend( true, [], oSettings.aaSorting ),
+				"oSearch":      $.extend( true, {}, oSettings.oPreviousSearch ),
+				"aoSearchCols": $.extend( true, [], oSettings.aoPreSearchCols ),
+				"abVisCols":    []
+			};
+		
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oState.abVisCols.push( oSettings.aoColumns[i].bVisible );
+			}
+		
+			_fnCallbackFire( oSettings, "aoStateSaveParams", 'stateSaveParams', [oSettings, oState] );
+			
+			oSettings.fnStateSave.call( oSettings.oInstance, oSettings, oState );
+		}
+		
+		
+		/**
+		 * Attempt to load a saved table state from a cookie
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} oInit DataTables init object so we can override settings
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLoadState ( oSettings, oInit )
+		{
+			if ( !oSettings.oFeatures.bStateSave )
+			{
+				return;
+			}
+		
+			var oData = oSettings.fnStateLoad.call( oSettings.oInstance, oSettings );
+			if ( !oData )
+			{
+				return;
+			}
+			
+			/* Allow custom and plug-in manipulation functions to alter the saved data set and
+			 * cancelling of loading by returning false
+			 */
+			var abStateLoad = _fnCallbackFire( oSettings, 'aoStateLoadParams', 'stateLoadParams', [oSettings, oData] );
+			if ( $.inArray( false, abStateLoad ) !== -1 )
+			{
+				return;
+			}
+			
+			/* Store the saved state so it might be accessed at any time */
+			oSettings.oLoadedState = $.extend( true, {}, oData );
+			
+			/* Restore key features */
+			oSettings._iDisplayStart    = oData.iStart;
+			oSettings.iInitDisplayStart = oData.iStart;
+			oSettings._iDisplayEnd      = oData.iEnd;
+			oSettings._iDisplayLength   = oData.iLength;
+			oSettings.aaSorting         = oData.aaSorting.slice();
+			oSettings.saved_aaSorting   = oData.aaSorting.slice();
+			
+			/* Search filtering  */
+			$.extend( oSettings.oPreviousSearch, oData.oSearch );
+			$.extend( true, oSettings.aoPreSearchCols, oData.aoSearchCols );
+			
+			/* Column visibility state
+			 * Pass back visibility settings to the init handler, but to do not here override
+			 * the init object that the user might have passed in
+			 */
+			oInit.saved_aoColumns = [];
+			for ( var i=0 ; i<oData.abVisCols.length ; i++ )
+			{
+				oInit.saved_aoColumns[i] = {};
+				oInit.saved_aoColumns[i].bVisible = oData.abVisCols[i];
+			}
+		
+			_fnCallbackFire( oSettings, 'aoStateLoaded', 'stateLoaded', [oSettings, oData] );
+		}
+		
+		
+		/**
+		 * Create a new cookie with a value to store the state of a table
+		 *  @param {string} sName name of the cookie to create
+		 *  @param {string} sValue the value the cookie should take
+		 *  @param {int} iSecs duration of the cookie
+		 *  @param {string} sBaseName sName is made up of the base + file name - this is the base
+		 *  @param {function} fnCallback User definable function to modify the cookie
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCreateCookie ( sName, sValue, iSecs, sBaseName, fnCallback )
+		{
+			var date = new Date();
+			date.setTime( date.getTime()+(iSecs*1000) );
+			
+			/* 
+			 * Shocking but true - it would appear IE has major issues with having the path not having
+			 * a trailing slash on it. We need the cookie to be available based on the path, so we
+			 * have to append the file name to the cookie name. Appalling. Thanks to vex for adding the
+			 * patch to use at least some of the path
+			 */
+			var aParts = window.location.pathname.split('/');
+			var sNameFile = sName + '_' + aParts.pop().replace(/[\/:]/g,"").toLowerCase();
+			var sFullCookie, oData;
+			
+			if ( fnCallback !== null )
+			{
+				oData = (typeof $.parseJSON === 'function') ? 
+					$.parseJSON( sValue ) : eval( '('+sValue+')' );
+				sFullCookie = fnCallback( sNameFile, oData, date.toGMTString(),
+					aParts.join('/')+"/" );
+			}
+			else
+			{
+				sFullCookie = sNameFile + "=" + encodeURIComponent(sValue) +
+					"; expires=" + date.toGMTString() +"; path=" + aParts.join('/')+"/";
+			}
+			
+			/* Are we going to go over the cookie limit of 4KiB? If so, try to delete a cookies
+			 * belonging to DataTables.
+			 */
+			var
+				aCookies =document.cookie.split(';'),
+				iNewCookieLen = sFullCookie.split(';')[0].length,
+				aOldCookies = [];
+			
+			if ( iNewCookieLen+document.cookie.length+10 > 4096 ) /* Magic 10 for padding */
+			{
+				for ( var i=0, iLen=aCookies.length ; i<iLen ; i++ )
+				{
+					if ( aCookies[i].indexOf( sBaseName ) != -1 )
+					{
+						/* It's a DataTables cookie, so eval it and check the time stamp */
+						var aSplitCookie = aCookies[i].split('=');
+						try {
+							oData = eval( '('+decodeURIComponent(aSplitCookie[1])+')' );
+		
+							if ( oData && oData.iCreate )
+							{
+								aOldCookies.push( {
+									"name": aSplitCookie[0],
+									"time": oData.iCreate
+								} );
+							}
+						}
+						catch( e ) {}
+					}
+				}
+		
+				// Make sure we delete the oldest ones first
+				aOldCookies.sort( function (a, b) {
+					return b.time - a.time;
+				} );
+		
+				// Eliminate as many old DataTables cookies as we need to
+				while ( iNewCookieLen + document.cookie.length + 10 > 4096 ) {
+					if ( aOldCookies.length === 0 ) {
+						// Deleted all DT cookies and still not enough space. Can't state save
+						return;
+					}
+					
+					var old = aOldCookies.pop();
+					document.cookie = old.name+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+
+						aParts.join('/') + "/";
+				}
+			}
+			
+			document.cookie = sFullCookie;
+		}
+		
+		
+		/**
+		 * Read an old cookie to get a cookie with an old table state
+		 *  @param {string} sName name of the cookie to read
+		 *  @returns {string} contents of the cookie - or null if no cookie with that name found
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReadCookie ( sName )
+		{
+			var
+				aParts = window.location.pathname.split('/'),
+				sNameEQ = sName + '_' + aParts[aParts.length-1].replace(/[\/:]/g,"").toLowerCase() + '=',
+			 	sCookieContents = document.cookie.split(';');
+			
+			for( var i=0 ; i<sCookieContents.length ; i++ )
+			{
+				var c = sCookieContents[i];
+				
+				while (c.charAt(0)==' ')
+				{
+					c = c.substring(1,c.length);
+				}
+				
+				if (c.indexOf(sNameEQ) === 0)
+				{
+					return decodeURIComponent( c.substring(sNameEQ.length,c.length) );
+				}
+			}
+			return null;
+		}
+		
+		
+		/**
+		 * Return the settings object for a particular table
+		 *  @param {node} nTable table we are using as a dataTable
+		 *  @returns {object} Settings object - or null if not found
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSettingsFromNode ( nTable )
+		{
+			for ( var i=0 ; i<DataTable.settings.length ; i++ )
+			{
+				if ( DataTable.settings[i].nTable === nTable )
+				{
+					return DataTable.settings[i];
+				}
+			}
+			
+			return null;
+		}
+		
+		
+		/**
+		 * Return an array with the TR nodes for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {array} TR array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetTrNodes ( oSettings )
+		{
+			var aNodes = [];
+			var aoData = oSettings.aoData;
+			for ( var i=0, iLen=aoData.length ; i<iLen ; i++ )
+			{
+				if ( aoData[i].nTr !== null )
+				{
+					aNodes.push( aoData[i].nTr );
+				}
+			}
+			return aNodes;
+		}
+		
+		
+		/**
+		 * Return an flat array with all TD nodes for the table, or row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} [iIndividualRow] aoData index to get the nodes for - optional 
+		 *    if not given then the return array will contain all nodes for the table
+		 *  @returns {array} TD array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetTdNodes ( oSettings, iIndividualRow )
+		{
+			var anReturn = [];
+			var iCorrector;
+			var anTds, nTd;
+			var iRow, iRows=oSettings.aoData.length,
+				iColumn, iColumns, oData, sNodeName, iStart=0, iEnd=iRows;
+			
+			/* Allow the collection to be limited to just one row */
+			if ( iIndividualRow !== undefined )
+			{
+				iStart = iIndividualRow;
+				iEnd = iIndividualRow+1;
+			}
+		
+			for ( iRow=iStart ; iRow<iEnd ; iRow++ )
+			{
+				oData = oSettings.aoData[iRow];
+				if ( oData.nTr !== null )
+				{
+					/* get the TD child nodes - taking into account text etc nodes */
+					anTds = [];
+					nTd = oData.nTr.firstChild;
+					while ( nTd )
+					{
+						sNodeName = nTd.nodeName.toLowerCase();
+						if ( sNodeName == 'td' || sNodeName == 'th' )
+						{
+							anTds.push( nTd );
+						}
+						nTd = nTd.nextSibling;
+					}
+		
+					iCorrector = 0;
+					for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+					{
+						if ( oSettings.aoColumns[iColumn].bVisible )
+						{
+							anReturn.push( anTds[iColumn-iCorrector] );
+						}
+						else
+						{
+							anReturn.push( oData._anHidden[iColumn] );
+							iCorrector++;
+						}
+					}
+				}
+			}
+		
+			return anReturn;
+		}
+		
+		
+		/**
+		 * Log an error message
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iLevel log error messages, or display them to the user
+		 *  @param {string} sMesg error message
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLog( oSettings, iLevel, sMesg )
+		{
+			var sAlert = (oSettings===null) ?
+				"DataTables warning: "+sMesg :
+				"DataTables warning (table id = '"+oSettings.sTableId+"'): "+sMesg;
+			
+			if ( iLevel === 0 )
+			{
+				if ( DataTable.ext.sErrMode == 'alert' )
+				{
+					alert( sAlert );
+				}
+				else
+				{
+					throw new Error(sAlert);
+				}
+				return;
+			}
+			else if ( window.console && console.log )
+			{
+				console.log( sAlert );
+			}
+		}
+		
+		
+		/**
+		 * See if a property is defined on one object, if so assign it to the other object
+		 *  @param {object} oRet target object
+		 *  @param {object} oSrc source object
+		 *  @param {string} sName property
+		 *  @param {string} [sMappedName] name to map too - optional, sName used if not given
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnMap( oRet, oSrc, sName, sMappedName )
+		{
+			if ( sMappedName === undefined )
+			{
+				sMappedName = sName;
+			}
+			if ( oSrc[sName] !== undefined )
+			{
+				oRet[sMappedName] = oSrc[sName];
+			}
+		}
+		
+		
+		/**
+		 * Extend objects - very similar to jQuery.extend, but deep copy objects, and shallow
+		 * copy arrays. The reason we need to do this, is that we don't want to deep copy array
+		 * init values (such as aaSorting) since the dev wouldn't be able to override them, but
+		 * we do want to deep copy arrays.
+		 *  @param {object} oOut Object to extend
+		 *  @param {object} oExtender Object from which the properties will be applied to oOut
+		 *  @returns {object} oOut Reference, just for convenience - oOut === the return.
+		 *  @memberof DataTable#oApi
+		 *  @todo This doesn't take account of arrays inside the deep copied objects.
+		 */
+		function _fnExtend( oOut, oExtender )
+		{
+			var val;
+			
+			for ( var prop in oExtender )
+			{
+				if ( oExtender.hasOwnProperty(prop) )
+				{
+					val = oExtender[prop];
+		
+					if ( typeof oInit[prop] === 'object' && val !== null && $.isArray(val) === false )
+					{
+						$.extend( true, oOut[prop], val );
+					}
+					else
+					{
+						oOut[prop] = val;
+					}
+				}
+			}
+		
+			return oOut;
+		}
+		
+		
+		/**
+		 * Bind an event handers to allow a click or return key to activate the callback.
+		 * This is good for accessibility since a return on the keyboard will have the
+		 * same effect as a click, if the element has focus.
+		 *  @param {element} n Element to bind the action to
+		 *  @param {object} oData Data object to pass to the triggered function
+		 *  @param {function} fn Callback function for when the event is triggered
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBindAction( n, oData, fn )
+		{
+			$(n)
+				.bind( 'click.DT', oData, function (e) {
+						n.blur(); // Remove focus outline for mouse users
+						fn(e);
+					} )
+				.bind( 'keypress.DT', oData, function (e){
+					if ( e.which === 13 ) {
+						fn(e);
+					} } )
+				.bind( 'selectstart.DT', function () {
+					/* Take the brutal approach to cancelling text selection */
+					return false;
+					} );
+		}
+		
+		
+		/**
+		 * Register a callback function. Easily allows a callback function to be added to
+		 * an array store of callback functions that can then all be called together.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+		 *  @param {function} fn Function to be called back
+		 *  @param {string} sName Identifying name for the callback (i.e. a label)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCallbackReg( oSettings, sStore, fn, sName )
+		{
+			if ( fn )
+			{
+				oSettings[sStore].push( {
+					"fn": fn,
+					"sName": sName
+				} );
+			}
+		}
+		
+		
+		/**
+		 * Fire callback functions and trigger events. Note that the loop over the callback
+		 * array store is done backwards! Further note that you do not want to fire off triggers
+		 * in time sensitive applications (for example cell creation) as its slow.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+		 *  @param {string} sTrigger Name of the jQuery custom event to trigger. If null no trigger
+		 *    is fired
+		 *  @param {array} aArgs Array of arguments to pass to the callback function / trigger
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCallbackFire( oSettings, sStore, sTrigger, aArgs )
+		{
+			var aoStore = oSettings[sStore];
+			var aRet =[];
+		
+			for ( var i=aoStore.length-1 ; i>=0 ; i-- )
+			{
+				aRet.push( aoStore[i].fn.apply( oSettings.oInstance, aArgs ) );
+			}
+		
+			if ( sTrigger !== null )
+			{
+				$(oSettings.oInstance).trigger(sTrigger, aArgs);
+			}
+		
+			return aRet;
+		}
+		
+		
+		/**
+		 * JSON stringify. If JSON.stringify it provided by the browser, json2.js or any other
+		 * library, then we use that as it is fast, safe and accurate. If the function isn't 
+		 * available then we need to built it ourselves - the inspiration for this function comes
+		 * from Craig Buckler ( http://www.sitepoint.com/javascript-json-serialization/ ). It is
+		 * not perfect and absolutely should not be used as a replacement to json2.js - but it does
+		 * do what we need, without requiring a dependency for DataTables.
+		 *  @param {object} o JSON object to be converted
+		 *  @returns {string} JSON string
+		 *  @memberof DataTable#oApi
+		 */
+		var _fnJsonString = (window.JSON) ? JSON.stringify : function( o )
+		{
+			/* Not an object or array */
+			var sType = typeof o;
+			if (sType !== "object" || o === null)
+			{
+				// simple data type
+				if (sType === "string")
+				{
+					o = '"'+o+'"';
+				}
+				return o+"";
+			}
+		
+			/* If object or array, need to recurse over it */
+			var
+				sProp, mValue,
+				json = [],
+				bArr = $.isArray(o);
+			
+			for (sProp in o)
+			{
+				mValue = o[sProp];
+				sType = typeof mValue;
+		
+				if (sType === "string")
+				{
+					mValue = '"'+mValue+'"';
+				}
+				else if (sType === "object" && mValue !== null)
+				{
+					mValue = _fnJsonString(mValue);
+				}
+		
+				json.push((bArr ? "" : '"'+sProp+'":') + mValue);
+			}
+		
+			return (bArr ? "[" : "{") + json + (bArr ? "]" : "}");
+		};
+		
+		
+		/**
+		 * From some browsers (specifically IE6/7) we need special handling to work around browser
+		 * bugs - this function is used to detect when these workarounds are needed.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBrowserDetect( oSettings )
+		{
+			/* IE6/7 will oversize a width 100% element inside a scrolling element, to include the
+			 * width of the scrollbar, while other browsers ensure the inner element is contained
+			 * without forcing scrolling
+			 */
+			var n = $(
+				'<div style="position:absolute; top:0; left:0; height:1px; width:1px; overflow:hidden">'+
+					'<div style="position:absolute; top:1px; left:1px; width:100px; overflow:scroll;">'+
+						'<div id="DT_BrowserTest" style="width:100%; height:10px;"></div>'+
+					'</div>'+
+				'</div>')[0];
+		
+			document.body.appendChild( n );
+			oSettings.oBrowser.bScrollOversize = $('#DT_BrowserTest', n)[0].offsetWidth === 100 ? true : false;
+			document.body.removeChild( n );
+		}
+		
+
+		/**
+		 * Perform a jQuery selector action on the table's TR elements (from the tbody) and
+		 * return the resulting jQuery object.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select TR elements that meet the current filter
+		 *    criterion ("applied") or all TR elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the TR elements in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be 
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {object} jQuery object, filtered by the given selector.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Highlight every second row
+		 *      oTable.$('tr:odd').css('backgroundColor', 'blue');
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to rows with 'Webkit' in them, add a background colour and then
+		 *      // remove the filter, thus highlighting the 'Webkit' rows only.
+		 *      oTable.fnFilter('Webkit');
+		 *      oTable.$('tr', {"filter": "applied"}).css('backgroundColor', 'blue');
+		 *      oTable.fnFilter('');
+		 *    } );
+		 */
+		this.$ = function ( sSelector, oOpts )
+		{
+			var i, iLen, a = [], tr;
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var aoData = oSettings.aoData;
+			var aiDisplay = oSettings.aiDisplay;
+			var aiDisplayMaster = oSettings.aiDisplayMaster;
+		
+			if ( !oOpts )
+			{
+				oOpts = {};
+			}
+		
+			oOpts = $.extend( {}, {
+				"filter": "none", // applied
+				"order": "current", // "original"
+				"page": "all" // current
+			}, oOpts );
+		
+			// Current page implies that order=current and fitler=applied, since it is fairly
+			// senseless otherwise
+			if ( oOpts.page == 'current' )
+			{
+				for ( i=oSettings._iDisplayStart, iLen=oSettings.fnDisplayEnd() ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplay[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "current" && oOpts.filter == "none" )
+			{
+				for ( i=0, iLen=aiDisplayMaster.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplayMaster[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "current" && oOpts.filter == "applied" )
+			{
+				for ( i=0, iLen=aiDisplay.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplay[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "original" && oOpts.filter == "none" )
+			{
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ i ].nTr ;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "original" && oOpts.filter == "applied" )
+			{
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ i ].nTr;
+					if ( $.inArray( i, aiDisplay ) !== -1 && tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else
+			{
+				_fnLog( oSettings, 1, "Unknown selection options" );
+			}
+		
+			/* We need to filter on the TR elements and also 'find' in their descendants
+			 * to make the selector act like it would in a full table - so we need
+			 * to build both results and then combine them together
+			 */
+			var jqA = $(a);
+			var jqTRs = jqA.filter( sSelector );
+			var jqDescendants = jqA.find( sSelector );
+		
+			return $( [].concat($.makeArray(jqTRs), $.makeArray(jqDescendants)) );
+		};
+		
+		
+		/**
+		 * Almost identical to $ in operation, but in this case returns the data for the matched
+		 * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes
+		 * rather than any descendants, so the data can be obtained for the row/cell. If matching
+		 * rows are found, the data returned is the original data array/object that was used to  
+		 * create the row (or a generated array if from a DOM source).
+		 *
+		 * This method is often useful in-combination with $ where both functions are given the
+		 * same parameters and the array indexes will match identically.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select elements that meet the current filter
+		 *    criterion ("applied") or all elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the data in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be 
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {array} Data for the matched elements. If any elements, as a result of the
+		 *    selector, were not TR, TD or TH elements in the DataTable, they will have a null 
+		 *    entry in the array.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Get the data from the first row in the table
+		 *      var data = oTable._('tr:first');
+		 *
+		 *      // Do something useful with the data
+		 *      alert( "First cell is: "+data[0] );
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to 'Webkit' and get all data for 
+		 *      oTable.fnFilter('Webkit');
+		 *      var data = oTable._('tr', {"filter": "applied"});
+		 *      
+		 *      // Do something with the data
+		 *      alert( data.length+" rows matched the filter" );
+		 *    } );
+		 */
+		this._ = function ( sSelector, oOpts )
+		{
+			var aOut = [];
+			var i, iLen, iIndex;
+			var aTrs = this.$( sSelector, oOpts );
+		
+			for ( i=0, iLen=aTrs.length ; i<iLen ; i++ )
+			{
+				aOut.push( this.fnGetData(aTrs[i]) );
+			}
+		
+			return aOut;
+		};
+		
+		
+		/**
+		 * Add a single new row or multiple rows of data to the table. Please note
+		 * that this is suitable for client-side processing only - if you are using 
+		 * server-side processing (i.e. "bServerSide": true), then to add data, you
+		 * must add it to the data source, i.e. the server-side, through an Ajax call.
+		 *  @param {array|object} mData The data to be added to the table. This can be:
+		 *    <ul>
+		 *      <li>1D array of data - add a single row with the data provided</li>
+		 *      <li>2D array of arrays - add multiple rows in a single call</li>
+		 *      <li>object - data object when using <i>mData</i></li>
+		 *      <li>array of objects - multiple data objects when using <i>mData</i></li>
+		 *    </ul>
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @returns {array} An array of integers, representing the list of indexes in 
+		 *    <i>aoData</i> ({@link DataTable.models.oSettings}) that have been added to 
+		 *    the table.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    // Global var for counter
+		 *    var giCount = 2;
+		 *    
+		 *    $(document).ready(function() {
+		 *      $('#example').dataTable();
+		 *    } );
+		 *    
+		 *    function fnClickAddRow() {
+		 *      $('#example').dataTable().fnAddData( [
+		 *        giCount+".1",
+		 *        giCount+".2",
+		 *        giCount+".3",
+		 *        giCount+".4" ]
+		 *      );
+		 *        
+		 *      giCount++;
+		 *    }
+		 */
+		this.fnAddData = function( mData, bRedraw )
+		{
+			if ( mData.length === 0 )
+			{
+				return [];
+			}
+			
+			var aiReturn = [];
+			var iTest;
+			
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			/* Check if we want to add multiple rows or not */
+			if ( typeof mData[0] === "object" && mData[0] !== null )
+			{
+				for ( var i=0 ; i<mData.length ; i++ )
+				{
+					iTest = _fnAddData( oSettings, mData[i] );
+					if ( iTest == -1 )
+					{
+						return aiReturn;
+					}
+					aiReturn.push( iTest );
+				}
+			}
+			else
+			{
+				iTest = _fnAddData( oSettings, mData );
+				if ( iTest == -1 )
+				{
+					return aiReturn;
+				}
+				aiReturn.push( iTest );
+			}
+			
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return aiReturn;
+		};
+		
+		
+		/**
+		 * This function will make DataTables recalculate the column sizes, based on the data 
+		 * contained in the table and the sizes applied to the columns (in the DOM, CSS or 
+		 * through the sWidth parameter). This can be useful when the width of the table's 
+		 * parent element changes (for example a window resize).
+		 *  @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *      
+		 *      $(window).bind('resize', function () {
+		 *        oTable.fnAdjustColumnSizing();
+		 *      } );
+		 *    } );
+		 */
+		this.fnAdjustColumnSizing = function ( bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode(this[DataTable.ext.iApiIndex]);
+			_fnAdjustColumnSizing( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				this.fnDraw( false );
+			}
+			else if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )
+			{
+				/* If not redrawing, but scrolling, we want to apply the new column sizes anyway */
+				this.oApi._fnScrollDraw(oSettings);
+			}
+		};
+		
+		
+		/**
+		 * Quickly and simply clear a table
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...)
+		 *      oTable.fnClearTable();
+		 *    } );
+		 */
+		this.fnClearTable = function( bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			_fnClearTable( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * The exact opposite of 'opening' a row, this function will close any rows which 
+		 * are currently 'open'.
+		 *  @param {node} nTr the table row to 'close'
+		 *  @returns {int} 0 on success, or 1 if failed (can't find the row)
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnClose = function( nTr )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )
+			{
+				if ( oSettings.aoOpenRows[i].nParent == nTr )
+				{
+					var nTrParent = oSettings.aoOpenRows[i].nTr.parentNode;
+					if ( nTrParent )
+					{
+						/* Remove it if it is currently on display */
+						nTrParent.removeChild( oSettings.aoOpenRows[i].nTr );
+					}
+					oSettings.aoOpenRows.splice( i, 1 );
+					return 0;
+				}
+			}
+			return 1;
+		};
+		
+		
+		/**
+		 * Remove a row for the table
+		 *  @param {mixed} mTarget The index of the row from aoData to be deleted, or
+		 *    the TR element you want to delete
+		 *  @param {function|null} [fnCallBack] Callback function
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @returns {array} The row that was deleted
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Immediately remove the first row
+		 *      oTable.fnDeleteRow( 0 );
+		 *    } );
+		 */
+		this.fnDeleteRow = function( mTarget, fnCallBack, bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen, iAODataIndex;
+			
+			iAODataIndex = (typeof mTarget === 'object') ? 
+				_fnNodeToDataIndex(oSettings, mTarget) : mTarget;
+			
+			/* Return the data array from this row */
+			var oData = oSettings.aoData.splice( iAODataIndex, 1 );
+		
+			/* Update the _DT_RowIndex parameter */
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoData[i].nTr !== null )
+				{
+					oSettings.aoData[i].nTr._DT_RowIndex = i;
+				}
+			}
+			
+			/* Remove the target row from the search array */
+			var iDisplayIndex = $.inArray( iAODataIndex, oSettings.aiDisplay );
+			oSettings.asDataSearch.splice( iDisplayIndex, 1 );
+			
+			/* Delete from the display arrays */
+			_fnDeleteIndex( oSettings.aiDisplayMaster, iAODataIndex );
+			_fnDeleteIndex( oSettings.aiDisplay, iAODataIndex );
+			
+			/* If there is a user callback function - call it */
+			if ( typeof fnCallBack === "function" )
+			{
+				fnCallBack.call( this, oSettings, oData );
+			}
+			
+			/* Check for an 'overflow' they case for displaying the table */
+			if ( oSettings._iDisplayStart >= oSettings.fnRecordsDisplay() )
+			{
+				oSettings._iDisplayStart -= oSettings._iDisplayLength;
+				if ( oSettings._iDisplayStart < 0 )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			return oData;
+		};
+		
+		
+		/**
+		 * Restore the table to it's original state in the DOM by removing all of DataTables 
+		 * enhancements, alterations to the DOM structure of the table and event listeners.
+		 *  @param {boolean} [bRemove=false] Completely remove the table from the DOM
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      // This example is fairly pointless in reality, but shows how fnDestroy can be used
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnDestroy();
+		 *    } );
+		 */
+		this.fnDestroy = function ( bRemove )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var nOrig = oSettings.nTableWrapper.parentNode;
+			var nBody = oSettings.nTBody;
+			var i, iLen;
+		
+			bRemove = (bRemove===undefined) ? false : bRemove;
+			
+			/* Flag to note that the table is currently being destroyed - no action should be taken */
+			oSettings.bDestroying = true;
+			
+			/* Fire off the destroy callbacks for plug-ins etc */
+			_fnCallbackFire( oSettings, "aoDestroyCallback", "destroy", [oSettings] );
+		
+			/* If the table is not being removed, restore the hidden columns */
+			if ( !bRemove )
+			{
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible === false )
+					{
+						this.fnSetColumnVis( i, true );
+					}
+				}
+			}
+			
+			/* Blitz all DT events */
+			$(oSettings.nTableWrapper).find('*').andSelf().unbind('.DT');
+			
+			/* If there is an 'empty' indicator row, remove it */
+			$('tbody>tr>td.'+oSettings.oClasses.sRowEmpty, oSettings.nTable).parent().remove();
+			
+			/* When scrolling we had to break the table up - restore it */
+			if ( oSettings.nTable != oSettings.nTHead.parentNode )
+			{
+				$(oSettings.nTable).children('thead').remove();
+				oSettings.nTable.appendChild( oSettings.nTHead );
+			}
+			
+			if ( oSettings.nTFoot && oSettings.nTable != oSettings.nTFoot.parentNode )
+			{
+				$(oSettings.nTable).children('tfoot').remove();
+				oSettings.nTable.appendChild( oSettings.nTFoot );
+			}
+			
+			/* Remove the DataTables generated nodes, events and classes */
+			oSettings.nTable.parentNode.removeChild( oSettings.nTable );
+			$(oSettings.nTableWrapper).remove();
+			
+			oSettings.aaSorting = [];
+			oSettings.aaSortingFixed = [];
+			_fnSortingClasses( oSettings );
+			
+			$(_fnGetTrNodes( oSettings )).removeClass( oSettings.asStripeClasses.join(' ') );
+			
+			$('th, td', oSettings.nTHead).removeClass( [
+				oSettings.oClasses.sSortable,
+				oSettings.oClasses.sSortableAsc,
+				oSettings.oClasses.sSortableDesc,
+				oSettings.oClasses.sSortableNone ].join(' ')
+			);
+			if ( oSettings.bJUI )
+			{
+				$('th span.'+oSettings.oClasses.sSortIcon
+					+ ', td span.'+oSettings.oClasses.sSortIcon, oSettings.nTHead).remove();
+		
+				$('th, td', oSettings.nTHead).each( function () {
+					var jqWrapper = $('div.'+oSettings.oClasses.sSortJUIWrapper, this);
+					var kids = jqWrapper.contents();
+					$(this).append( kids );
+					jqWrapper.remove();
+				} );
+			}
+			
+			/* Add the TR elements back into the table in their original order */
+			if ( !bRemove && oSettings.nTableReinsertBefore )
+			{
+				nOrig.insertBefore( oSettings.nTable, oSettings.nTableReinsertBefore );
+			}
+			else if ( !bRemove )
+			{
+				nOrig.appendChild( oSettings.nTable );
+			}
+		
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoData[i].nTr !== null )
+				{
+					nBody.appendChild( oSettings.aoData[i].nTr );
+				}
+			}
+			
+			/* Restore the width of the original table */
+			if ( oSettings.oFeatures.bAutoWidth === true )
+			{
+			  oSettings.nTable.style.width = _fnStringToCss(oSettings.sDestroyWidth);
+			}
+			
+			/* If the were originally stripe classes - then we add them back here. Note
+			 * this is not fool proof (for example if not all rows had stripe classes - but
+			 * it's a good effort without getting carried away
+			 */
+			iLen = oSettings.asDestroyStripes.length;
+			if (iLen)
+			{
+				var anRows = $(nBody).children('tr');
+				for ( i=0 ; i<iLen ; i++ )
+				{
+					anRows.filter(':nth-child(' + iLen + 'n + ' + i + ')').addClass( oSettings.asDestroyStripes[i] );
+				}
+			}
+			
+			/* Remove the settings object from the settings array */
+			for ( i=0, iLen=DataTable.settings.length ; i<iLen ; i++ )
+			{
+				if ( DataTable.settings[i] == oSettings )
+				{
+					DataTable.settings.splice( i, 1 );
+				}
+			}
+			
+			/* End it all */
+			oSettings = null;
+			oInit = null;
+		};
+		
+		
+		/**
+		 * Redraw the table
+		 *  @param {bool} [bComplete=true] Re-filter and resort (if enabled) the table before the draw.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Re-draw the table - you wouldn't want to do it here, but it's an example :-)
+		 *      oTable.fnDraw();
+		 *    } );
+		 */
+		this.fnDraw = function( bComplete )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			if ( bComplete === false )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			else
+			{
+				_fnReDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * Filter the input based on data
+		 *  @param {string} sInput String to filter the table on
+		 *  @param {int|null} [iColumn] Column to limit filtering to
+		 *  @param {bool} [bRegex=false] Treat as regular expression or not
+		 *  @param {bool} [bSmart=true] Perform smart filtering or not
+		 *  @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es)
+		 *  @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false)
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sometime later - filter...
+		 *      oTable.fnFilter( 'test string' );
+		 *    } );
+		 */
+		this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( !oSettings.oFeatures.bFilter )
+			{
+				return;
+			}
+			
+			if ( bRegex === undefined || bRegex === null )
+			{
+				bRegex = false;
+			}
+			
+			if ( bSmart === undefined || bSmart === null )
+			{
+				bSmart = true;
+			}
+			
+			if ( bShowGlobal === undefined || bShowGlobal === null )
+			{
+				bShowGlobal = true;
+			}
+			
+			if ( bCaseInsensitive === undefined || bCaseInsensitive === null )
+			{
+				bCaseInsensitive = true;
+			}
+			
+			if ( iColumn === undefined || iColumn === null )
+			{
+				/* Global filter */
+				_fnFilterComplete( oSettings, {
+					"sSearch":sInput+"",
+					"bRegex": bRegex,
+					"bSmart": bSmart,
+					"bCaseInsensitive": bCaseInsensitive
+				}, 1 );
+				
+				if ( bShowGlobal && oSettings.aanFeatures.f )
+				{
+					var n = oSettings.aanFeatures.f;
+					for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+					{
+						// IE9 throws an 'unknown error' if document.activeElement is used
+						// inside an iframe or frame...
+						try {
+							if ( n[i]._DT_Input != document.activeElement )
+							{
+								$(n[i]._DT_Input).val( sInput );
+							}
+						}
+						catch ( e ) {
+							$(n[i]._DT_Input).val( sInput );
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Single column filter */
+				$.extend( oSettings.aoPreSearchCols[ iColumn ], {
+					"sSearch": sInput+"",
+					"bRegex": bRegex,
+					"bSmart": bSmart,
+					"bCaseInsensitive": bCaseInsensitive
+				} );
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
+			}
+		};
+		
+		
+		/**
+		 * Get the data for the whole table, an individual row or an individual cell based on the 
+		 * provided parameters.
+		 *  @param {int|node} [mRow] A TR row node, TD/TH cell node or an integer. If given as
+		 *    a TR node then the data source for the whole row will be returned. If given as a
+		 *    TD/TH cell node then iCol will be automatically calculated and the data for the
+		 *    cell returned. If given as an integer, then this is treated as the aoData internal
+		 *    data index for the row (see fnGetPosition) and the data for that row used.
+		 *  @param {int} [iCol] Optional column index that you want the data of.
+		 *  @returns {array|object|string} If mRow is undefined, then the data for all rows is
+		 *    returned. If mRow is defined, just data for that row, and is iCol is
+		 *    defined, only data for the designated cell is returned.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    // Row data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('tr').click( function () {
+		 *        var data = oTable.fnGetData( this );
+		 *        // ... do something with the array / object of data for the row
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Individual cell data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('td').click( function () {
+		 *        var sData = oTable.fnGetData( this );
+		 *        alert( 'The cell clicked on had the value of '+sData );
+		 *      } );
+		 *    } );
+		 */
+		this.fnGetData = function( mRow, iCol )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( mRow !== undefined )
+			{
+				var iRow = mRow;
+				if ( typeof mRow === 'object' )
+				{
+					var sNode = mRow.nodeName.toLowerCase();
+					if (sNode === "tr" )
+					{
+						iRow = _fnNodeToDataIndex(oSettings, mRow);
+					}
+					else if ( sNode === "td" )
+					{
+						iRow = _fnNodeToDataIndex(oSettings, mRow.parentNode);
+						iCol = _fnNodeToColumnIndex( oSettings, iRow, mRow );
+					}
+				}
+		
+				if ( iCol !== undefined )
+				{
+					return _fnGetCellData( oSettings, iRow, iCol, '' );
+				}
+				return (oSettings.aoData[iRow]!==undefined) ?
+					oSettings.aoData[iRow]._aData : null;
+			}
+			return _fnGetDataMaster( oSettings );
+		};
+		
+		
+		/**
+		 * Get an array of the TR nodes that are used in the table's body. Note that you will 
+		 * typically want to use the '$' API method in preference to this as it is more 
+		 * flexible.
+		 *  @param {int} [iRow] Optional row index for the TR element you want
+		 *  @returns {array|node} If iRow is undefined, returns an array of all TR elements
+		 *    in the table's body, or iRow is defined, just the TR element requested.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Get the nodes from the table
+		 *      var nNodes = oTable.fnGetNodes( );
+		 *    } );
+		 */
+		this.fnGetNodes = function( iRow )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( iRow !== undefined ) {
+				return (oSettings.aoData[iRow]!==undefined) ?
+					oSettings.aoData[iRow].nTr : null;
+			}
+			return _fnGetTrNodes( oSettings );
+		};
+		
+		
+		/**
+		 * Get the array indexes of a particular cell from it's DOM element
+		 * and column index including hidden columns
+		 *  @param {node} nNode this can either be a TR, TD or TH in the table's body
+		 *  @returns {int} If nNode is given as a TR, then a single index is returned, or
+		 *    if given as a cell, an array of [row index, column index (visible), 
+		 *    column index (all)] is given.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      $('#example tbody td').click( function () {
+		 *        // Get the position of the current data from the node
+		 *        var aPos = oTable.fnGetPosition( this );
+		 *        
+		 *        // Get the data array for this row
+		 *        var aData = oTable.fnGetData( aPos[0] );
+		 *        
+		 *        // Update the data array and return the value
+		 *        aData[ aPos[1] ] = 'clicked';
+		 *        this.innerHTML = 'clicked';
+		 *      } );
+		 *      
+		 *      // Init DataTables
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnGetPosition = function( nNode )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var sNodeName = nNode.nodeName.toUpperCase();
+			
+			if ( sNodeName == "TR" )
+			{
+				return _fnNodeToDataIndex(oSettings, nNode);
+			}
+			else if ( sNodeName == "TD" || sNodeName == "TH" )
+			{
+				var iDataIndex = _fnNodeToDataIndex( oSettings, nNode.parentNode );
+				var iColumnIndex = _fnNodeToColumnIndex( oSettings, iDataIndex, nNode );
+				return [ iDataIndex, _fnColumnIndexToVisible(oSettings, iColumnIndex ), iColumnIndex ];
+			}
+			return null;
+		};
+		
+		
+		/**
+		 * Check to see if a row is 'open' or not.
+		 *  @param {node} nTr the table row to check
+		 *  @returns {boolean} true if the row is currently open, false otherwise
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnIsOpen = function( nTr )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var aoOpenRows = oSettings.aoOpenRows;
+			
+			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )
+			{
+				if ( oSettings.aoOpenRows[i].nParent == nTr )
+				{
+					return true;
+				}
+			}
+			return false;
+		};
+		
+		
+		/**
+		 * This function will place a new row directly after a row which is currently 
+		 * on display on the page, with the HTML contents that is passed into the 
+		 * function. This can be used, for example, to ask for confirmation that a 
+		 * particular record should be deleted.
+		 *  @param {node} nTr The table row to 'open'
+		 *  @param {string|node|jQuery} mHtml The HTML to put into the row
+		 *  @param {string} sClass Class to give the new TD cell
+		 *  @returns {node} The row opened. Note that if the table row passed in as the
+		 *    first parameter, is not found in the table, this method will silently
+		 *    return.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnOpen = function( nTr, mHtml, sClass )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+		
+			/* Check that the row given is in the table */
+			var nTableRows = _fnGetTrNodes( oSettings );
+			if ( $.inArray(nTr, nTableRows) === -1 )
+			{
+				return;
+			}
+			
+			/* the old open one if there is one */
+			this.fnClose( nTr );
+			
+			var nNewRow = document.createElement("tr");
+			var nNewCell = document.createElement("td");
+			nNewRow.appendChild( nNewCell );
+			nNewCell.className = sClass;
+			nNewCell.colSpan = _fnVisbleColumns( oSettings );
+		
+			if (typeof mHtml === "string")
+			{
+				nNewCell.innerHTML = mHtml;
+			}
+			else
+			{
+				$(nNewCell).html( mHtml );
+			}
+		
+			/* If the nTr isn't on the page at the moment - then we don't insert at the moment */
+			var nTrs = $('tr', oSettings.nTBody);
+			if ( $.inArray(nTr, nTrs) != -1  )
+			{
+				$(nNewRow).insertAfter(nTr);
+			}
+			
+			oSettings.aoOpenRows.push( {
+				"nTr": nNewRow,
+				"nParent": nTr
+			} );
+			
+			return nNewRow;
+		};
+		
+		
+		/**
+		 * Change the pagination - provides the internal logic for pagination in a simple API 
+		 * function. With this function you can have a DataTables table go to the next, 
+		 * previous, first or last pages.
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer), note that page 0 is the first page.
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnPageChange( 'next' );
+		 *    } );
+		 */
+		this.fnPageChange = function ( mAction, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			_fnPageChange( oSettings, mAction );
+			_fnCalculateEnd( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * Show a particular column
+		 *  @param {int} iCol The column whose display should be changed
+		 *  @param {bool} bShow Show (true) or hide (false) the column
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Hide the second column after initialisation
+		 *      oTable.fnSetColumnVis( 1, false );
+		 *    } );
+		 */
+		this.fnSetColumnVis = function ( iCol, bShow, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen;
+			var aoColumns = oSettings.aoColumns;
+			var aoData = oSettings.aoData;
+			var nTd, bAppend, iBefore;
+			
+			/* No point in doing anything if we are requesting what is already true */
+			if ( aoColumns[iCol].bVisible == bShow )
+			{
+				return;
+			}
+			
+			/* Show the column */
+			if ( bShow )
+			{
+				var iInsert = 0;
+				for ( i=0 ; i<iCol ; i++ )
+				{
+					if ( aoColumns[i].bVisible )
+					{
+						iInsert++;
+					}
+				}
+				
+				/* Need to decide if we should use appendChild or insertBefore */
+				bAppend = (iInsert >= _fnVisbleColumns( oSettings ));
+		
+				/* Which coloumn should we be inserting before? */
+				if ( !bAppend )
+				{
+					for ( i=iCol ; i<aoColumns.length ; i++ )
+					{
+						if ( aoColumns[i].bVisible )
+						{
+							iBefore = i;
+							break;
+						}
+					}
+				}
+		
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					if ( aoData[i].nTr !== null )
+					{
+						if ( bAppend )
+						{
+							aoData[i].nTr.appendChild( 
+								aoData[i]._anHidden[iCol]
+							);
+						}
+						else
+						{
+							aoData[i].nTr.insertBefore(
+								aoData[i]._anHidden[iCol], 
+								_fnGetTdNodes( oSettings, i )[iBefore] );
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Remove a column from display */
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					if ( aoData[i].nTr !== null )
+					{
+						nTd = _fnGetTdNodes( oSettings, i )[iCol];
+						aoData[i]._anHidden[iCol] = nTd;
+						nTd.parentNode.removeChild( nTd );
+					}
+				}
+			}
+		
+			/* Clear to set the visible flag */
+			aoColumns[iCol].bVisible = bShow;
+		
+			/* Redraw the header and footer based on the new column visibility */
+			_fnDrawHead( oSettings, oSettings.aoHeader );
+			if ( oSettings.nTFoot )
+			{
+				_fnDrawHead( oSettings, oSettings.aoFooter );
+			}
+			
+			/* If there are any 'open' rows, then we need to alter the colspan for this col change */
+			for ( i=0, iLen=oSettings.aoOpenRows.length ; i<iLen ; i++ )
+			{
+				oSettings.aoOpenRows[i].nTr.colSpan = _fnVisbleColumns( oSettings );
+			}
+			
+			/* Do a redraw incase anything depending on the table columns needs it 
+			 * (built-in: scrolling) 
+			 */
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnAdjustColumnSizing( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			_fnSaveState( oSettings );
+		};
+		
+		
+		/**
+		 * Get the settings for a particular table for external manipulation
+		 *  @returns {object} DataTables settings object. See 
+		 *    {@link DataTable.models.oSettings}
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      var oSettings = oTable.fnSettings();
+		 *      
+		 *      // Show an example parameter from the settings
+		 *      alert( oSettings._iDisplayStart );
+		 *    } );
+		 */
+		this.fnSettings = function()
+		{
+			return _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+		};
+		
+		
+		/**
+		 * Sort the table by a particular column
+		 *  @param {int} iCol the data index to sort on. Note that this will not match the 
+		 *    'display index' if you have hidden data entries
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sort immediately with columns 0 and 1
+		 *      oTable.fnSort( [ [0,'asc'], [1,'asc'] ] );
+		 *    } );
+		 */
+		this.fnSort = function( aaSort )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			oSettings.aaSorting = aaSort;
+			_fnSort( oSettings );
+		};
+		
+		
+		/**
+		 * Attach a sort listener to an element for a given column
+		 *  @param {node} nNode the element to attach the sort listener to
+		 *  @param {int} iColumn the column that a click on this node will sort on
+		 *  @param {function} [fnCallback] callback function when sort is run
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sort on column 1, when 'sorter' is clicked on
+		 *      oTable.fnSortListener( document.getElementById('sorter'), 1 );
+		 *    } );
+		 */
+		this.fnSortListener = function( nNode, iColumn, fnCallback )
+		{
+			_fnSortAttachListener( _fnSettingsFromNode( this[DataTable.ext.iApiIndex] ), nNode, iColumn,
+			 	fnCallback );
+		};
+		
+		
+		/**
+		 * Update a table cell or row - this method will accept either a single value to
+		 * update the cell with, an array of values with one element for each column or
+		 * an object in the same format as the original data source. The function is
+		 * self-referencing in order to make the multi column updates easier.
+		 *  @param {object|array|string} mData Data to update the cell/row with
+		 *  @param {node|int} mRow TR element you want to update or the aoData index
+		 *  @param {int} [iColumn] The column to update (not used of mData is an array or object)
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @param {bool} [bAction=true] Perform pre-draw actions or not
+		 *  @returns {int} 0 on success, 1 on error
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell
+		 *      oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], 1, 0 ); // Row
+		 *    } );
+		 */
+		this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen, sDisplay;
+			var iRow = (typeof mRow === 'object') ? 
+				_fnNodeToDataIndex(oSettings, mRow) : mRow;
+			
+			if ( $.isArray(mData) && iColumn === undefined )
+			{
+				/* Array update - update the whole row */
+				oSettings.aoData[iRow]._aData = mData.slice();
+				
+				/* Flag to the function that we are recursing */
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					this.fnUpdate( _fnGetCellData( oSettings, iRow, i ), iRow, i, false, false );
+				}
+			}
+			else if ( $.isPlainObject(mData) && iColumn === undefined )
+			{
+				/* Object update - update the whole row - assume the developer gets the object right */
+				oSettings.aoData[iRow]._aData = $.extend( true, {}, mData );
+		
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					this.fnUpdate( _fnGetCellData( oSettings, iRow, i ), iRow, i, false, false );
+				}
+			}
+			else
+			{
+				/* Individual cell update */
+				_fnSetCellData( oSettings, iRow, iColumn, mData );
+				sDisplay = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+				
+				var oCol = oSettings.aoColumns[iColumn];
+				if ( oCol.fnRender !== null )
+				{
+					sDisplay = _fnRender( oSettings, iRow, iColumn );
+					if ( oCol.bUseRendered )
+					{
+						_fnSetCellData( oSettings, iRow, iColumn, sDisplay );
+					}
+				}
+				
+				if ( oSettings.aoData[iRow].nTr !== null )
+				{
+					/* Do the actual HTML update */
+					_fnGetTdNodes( oSettings, iRow )[iColumn].innerHTML = sDisplay;
+				}
+			}
+			
+			/* Modify the search index for this row (strictly this is likely not needed, since fnReDraw
+			 * will rebuild the search array - however, the redraw might be disabled by the user)
+			 */
+			var iDisplayIndex = $.inArray( iRow, oSettings.aiDisplay );
+			oSettings.asDataSearch[iDisplayIndex] = _fnBuildSearchRow(
+				oSettings, 
+				_fnGetRowData( oSettings, iRow, 'filter', _fnGetColumns( oSettings, 'bSearchable' ) )
+			);
+			
+			/* Perform pre-draw actions */
+			if ( bAction === undefined || bAction )
+			{
+				_fnAdjustColumnSizing( oSettings );
+			}
+			
+			/* Redraw the table */
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return 0;
+		};
+		
+		
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+		 * to ensure compatibility.
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+		 *    formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+		 *    version, or false if this version of DataTales is not suitable
+		 *  @method
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		this.fnVersionCheck = DataTable.ext.fnVersionCheck;
+		
+		
+		/*
+		 * This is really a good bit rubbish this method of exposing the internal methods
+		 * publicly... - To be fixed in 2.0 using methods on the prototype
+		 */
+		
+		
+		/**
+		 * Create a wrapper function for exporting an internal functions to an external API.
+		 *  @param {string} sFunc API function name
+		 *  @returns {function} wrapped function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnExternApiFunc (sFunc)
+		{
+			return function() {
+				var aArgs = [_fnSettingsFromNode(this[DataTable.ext.iApiIndex])].concat( 
+					Array.prototype.slice.call(arguments) );
+				return DataTable.ext.oApi[sFunc].apply( this, aArgs );
+			};
+		}
+		
+		
+		/**
+		 * Reference to internal functions for use by plug-in developers. Note that these
+		 * methods are references to internal functions and are considered to be private.
+		 * If you use these methods, be aware that they are liable to change between versions
+		 * (check the upgrade notes).
+		 *  @namespace
+		 */
+		this.oApi = {
+			"_fnExternApiFunc": _fnExternApiFunc,
+			"_fnInitialise": _fnInitialise,
+			"_fnInitComplete": _fnInitComplete,
+			"_fnLanguageCompat": _fnLanguageCompat,
+			"_fnAddColumn": _fnAddColumn,
+			"_fnColumnOptions": _fnColumnOptions,
+			"_fnAddData": _fnAddData,
+			"_fnCreateTr": _fnCreateTr,
+			"_fnGatherData": _fnGatherData,
+			"_fnBuildHead": _fnBuildHead,
+			"_fnDrawHead": _fnDrawHead,
+			"_fnDraw": _fnDraw,
+			"_fnReDraw": _fnReDraw,
+			"_fnAjaxUpdate": _fnAjaxUpdate,
+			"_fnAjaxParameters": _fnAjaxParameters,
+			"_fnAjaxUpdateDraw": _fnAjaxUpdateDraw,
+			"_fnServerParams": _fnServerParams,
+			"_fnAddOptionsHtml": _fnAddOptionsHtml,
+			"_fnFeatureHtmlTable": _fnFeatureHtmlTable,
+			"_fnScrollDraw": _fnScrollDraw,
+			"_fnAdjustColumnSizing": _fnAdjustColumnSizing,
+			"_fnFeatureHtmlFilter": _fnFeatureHtmlFilter,
+			"_fnFilterComplete": _fnFilterComplete,
+			"_fnFilterCustom": _fnFilterCustom,
+			"_fnFilterColumn": _fnFilterColumn,
+			"_fnFilter": _fnFilter,
+			"_fnBuildSearchArray": _fnBuildSearchArray,
+			"_fnBuildSearchRow": _fnBuildSearchRow,
+			"_fnFilterCreateSearch": _fnFilterCreateSearch,
+			"_fnDataToSearch": _fnDataToSearch,
+			"_fnSort": _fnSort,
+			"_fnSortAttachListener": _fnSortAttachListener,
+			"_fnSortingClasses": _fnSortingClasses,
+			"_fnFeatureHtmlPaginate": _fnFeatureHtmlPaginate,
+			"_fnPageChange": _fnPageChange,
+			"_fnFeatureHtmlInfo": _fnFeatureHtmlInfo,
+			"_fnUpdateInfo": _fnUpdateInfo,
+			"_fnFeatureHtmlLength": _fnFeatureHtmlLength,
+			"_fnFeatureHtmlProcessing": _fnFeatureHtmlProcessing,
+			"_fnProcessingDisplay": _fnProcessingDisplay,
+			"_fnVisibleToColumnIndex": _fnVisibleToColumnIndex,
+			"_fnColumnIndexToVisible": _fnColumnIndexToVisible,
+			"_fnNodeToDataIndex": _fnNodeToDataIndex,
+			"_fnVisbleColumns": _fnVisbleColumns,
+			"_fnCalculateEnd": _fnCalculateEnd,
+			"_fnConvertToWidth": _fnConvertToWidth,
+			"_fnCalculateColumnWidths": _fnCalculateColumnWidths,
+			"_fnScrollingWidthAdjust": _fnScrollingWidthAdjust,
+			"_fnGetWidestNode": _fnGetWidestNode,
+			"_fnGetMaxLenString": _fnGetMaxLenString,
+			"_fnStringToCss": _fnStringToCss,
+			"_fnDetectType": _fnDetectType,
+			"_fnSettingsFromNode": _fnSettingsFromNode,
+			"_fnGetDataMaster": _fnGetDataMaster,
+			"_fnGetTrNodes": _fnGetTrNodes,
+			"_fnGetTdNodes": _fnGetTdNodes,
+			"_fnEscapeRegex": _fnEscapeRegex,
+			"_fnDeleteIndex": _fnDeleteIndex,
+			"_fnReOrderIndex": _fnReOrderIndex,
+			"_fnColumnOrdering": _fnColumnOrdering,
+			"_fnLog": _fnLog,
+			"_fnClearTable": _fnClearTable,
+			"_fnSaveState": _fnSaveState,
+			"_fnLoadState": _fnLoadState,
+			"_fnCreateCookie": _fnCreateCookie,
+			"_fnReadCookie": _fnReadCookie,
+			"_fnDetectHeader": _fnDetectHeader,
+			"_fnGetUniqueThs": _fnGetUniqueThs,
+			"_fnScrollBarWidth": _fnScrollBarWidth,
+			"_fnApplyToChildren": _fnApplyToChildren,
+			"_fnMap": _fnMap,
+			"_fnGetRowData": _fnGetRowData,
+			"_fnGetCellData": _fnGetCellData,
+			"_fnSetCellData": _fnSetCellData,
+			"_fnGetObjectDataFn": _fnGetObjectDataFn,
+			"_fnSetObjectDataFn": _fnSetObjectDataFn,
+			"_fnApplyColumnDefs": _fnApplyColumnDefs,
+			"_fnBindAction": _fnBindAction,
+			"_fnExtend": _fnExtend,
+			"_fnCallbackReg": _fnCallbackReg,
+			"_fnCallbackFire": _fnCallbackFire,
+			"_fnJsonString": _fnJsonString,
+			"_fnRender": _fnRender,
+			"_fnNodeToColumnIndex": _fnNodeToColumnIndex,
+			"_fnInfoMacros": _fnInfoMacros,
+			"_fnBrowserDetect": _fnBrowserDetect,
+			"_fnGetColumns": _fnGetColumns
+		};
+		
+		$.extend( DataTable.ext.oApi, this.oApi );
+		
+		for ( var sFunc in DataTable.ext.oApi )
+		{
+			if ( sFunc )
+			{
+				this[sFunc] = _fnExternApiFunc(sFunc);
+			}
+		}
+		
+		
+		var _that = this;
+		this.each(function() {
+			var i=0, iLen, j, jLen, k, kLen;
+			var sId = this.getAttribute( 'id' );
+			var bInitHandedOff = false;
+			var bUsePassedData = false;
+			
+			
+			/* Sanity check */
+			if ( this.nodeName.toLowerCase() != 'table' )
+			{
+				_fnLog( null, 0, "Attempted to initialise DataTables on a node which is not a "+
+					"table: "+this.nodeName );
+				return;
+			}
+			
+			/* Check to see if we are re-initialising a table */
+			for ( i=0, iLen=DataTable.settings.length ; i<iLen ; i++ )
+			{
+				/* Base check on table node */
+				if ( DataTable.settings[i].nTable == this )
+				{
+					if ( oInit === undefined || oInit.bRetrieve )
+					{
+						return DataTable.settings[i].oInstance;
+					}
+					else if ( oInit.bDestroy )
+					{
+						DataTable.settings[i].oInstance.fnDestroy();
+						break;
+					}
+					else
+					{
+						_fnLog( DataTable.settings[i], 0, "Cannot reinitialise DataTable.\n\n"+
+							"To retrieve the DataTables object for this table, pass no arguments or see "+
+							"the docs for bRetrieve and bDestroy" );
+						return;
+					}
+				}
+				
+				/* If the element we are initialising has the same ID as a table which was previously
+				 * initialised, but the table nodes don't match (from before) then we destroy the old
+				 * instance by simply deleting it. This is under the assumption that the table has been
+				 * destroyed by other methods. Anyone using non-id selectors will need to do this manually
+				 */
+				if ( DataTable.settings[i].sTableId == this.id )
+				{
+					DataTable.settings.splice( i, 1 );
+					break;
+				}
+			}
+			
+			/* Ensure the table has an ID - required for accessibility */
+			if ( sId === null || sId === "" )
+			{
+				sId = "DataTables_Table_"+(DataTable.ext._oExternConfig.iNextUnique++);
+				this.id = sId;
+			}
+			
+			/* Create the settings object for this table and set some of the default parameters */
+			var oSettings = $.extend( true, {}, DataTable.models.oSettings, {
+				"nTable":        this,
+				"oApi":          _that.oApi,
+				"oInit":         oInit,
+				"sDestroyWidth": $(this).width(),
+				"sInstance":     sId,
+				"sTableId":      sId
+			} );
+			DataTable.settings.push( oSettings );
+			
+			// Need to add the instance after the instance after the settings object has been added
+			// to the settings array, so we can self reference the table instance if more than one
+			oSettings.oInstance = (_that.length===1) ? _that : $(this).dataTable();
+			
+			/* Setting up the initialisation object */
+			if ( !oInit )
+			{
+				oInit = {};
+			}
+			
+			// Backwards compatibility, before we apply all the defaults
+			if ( oInit.oLanguage )
+			{
+				_fnLanguageCompat( oInit.oLanguage );
+			}
+			
+			oInit = _fnExtend( $.extend(true, {}, DataTable.defaults), oInit );
+			
+			// Map the initialisation options onto the settings object
+			_fnMap( oSettings.oFeatures, oInit, "bPaginate" );
+			_fnMap( oSettings.oFeatures, oInit, "bLengthChange" );
+			_fnMap( oSettings.oFeatures, oInit, "bFilter" );
+			_fnMap( oSettings.oFeatures, oInit, "bSort" );
+			_fnMap( oSettings.oFeatures, oInit, "bInfo" );
+			_fnMap( oSettings.oFeatures, oInit, "bProcessing" );
+			_fnMap( oSettings.oFeatures, oInit, "bAutoWidth" );
+			_fnMap( oSettings.oFeatures, oInit, "bSortClasses" );
+			_fnMap( oSettings.oFeatures, oInit, "bServerSide" );
+			_fnMap( oSettings.oFeatures, oInit, "bDeferRender" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollX", "sX" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollXInner", "sXInner" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollY", "sY" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollCollapse", "bCollapse" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollInfinite", "bInfinite" );
+			_fnMap( oSettings.oScroll, oInit, "iScrollLoadGap", "iLoadGap" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollAutoCss", "bAutoCss" );
+			_fnMap( oSettings, oInit, "asStripeClasses" );
+			_fnMap( oSettings, oInit, "asStripClasses", "asStripeClasses" ); // legacy
+			_fnMap( oSettings, oInit, "fnServerData" );
+			_fnMap( oSettings, oInit, "fnFormatNumber" );
+			_fnMap( oSettings, oInit, "sServerMethod" );
+			_fnMap( oSettings, oInit, "aaSorting" );
+			_fnMap( oSettings, oInit, "aaSortingFixed" );
+			_fnMap( oSettings, oInit, "aLengthMenu" );
+			_fnMap( oSettings, oInit, "sPaginationType" );
+			_fnMap( oSettings, oInit, "sAjaxSource" );
+			_fnMap( oSettings, oInit, "sAjaxDataProp" );
+			_fnMap( oSettings, oInit, "iCookieDuration" );
+			_fnMap( oSettings, oInit, "sCookiePrefix" );
+			_fnMap( oSettings, oInit, "sDom" );
+			_fnMap( oSettings, oInit, "bSortCellsTop" );
+			_fnMap( oSettings, oInit, "iTabIndex" );
+			_fnMap( oSettings, oInit, "oSearch", "oPreviousSearch" );
+			_fnMap( oSettings, oInit, "aoSearchCols", "aoPreSearchCols" );
+			_fnMap( oSettings, oInit, "iDisplayLength", "_iDisplayLength" );
+			_fnMap( oSettings, oInit, "bJQueryUI", "bJUI" );
+			_fnMap( oSettings, oInit, "fnCookieCallback" );
+			_fnMap( oSettings, oInit, "fnStateLoad" );
+			_fnMap( oSettings, oInit, "fnStateSave" );
+			_fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" );
+			
+			/* Callback functions which are array driven */
+			_fnCallbackReg( oSettings, 'aoDrawCallback',       oInit.fnDrawCallback,      'user' );
+			_fnCallbackReg( oSettings, 'aoServerParams',       oInit.fnServerParams,      'user' );
+			_fnCallbackReg( oSettings, 'aoStateSaveParams',    oInit.fnStateSaveParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoadParams',    oInit.fnStateLoadParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoaded',        oInit.fnStateLoaded,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCallback',        oInit.fnRowCallback,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow,        'user' );
+			_fnCallbackReg( oSettings, 'aoHeaderCallback',     oInit.fnHeaderCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoFooterCallback',     oInit.fnFooterCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoInitComplete',       oInit.fnInitComplete,      'user' );
+			_fnCallbackReg( oSettings, 'aoPreDrawCallback',    oInit.fnPreDrawCallback,   'user' );
+			
+			if ( oSettings.oFeatures.bServerSide && oSettings.oFeatures.bSort &&
+				   oSettings.oFeatures.bSortClasses )
+			{
+				/* Enable sort classes for server-side processing. Safe to do it here, since server-side
+				 * processing must be enabled by the developer
+				 */
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSortingClasses, 'server_side_sort_classes' );
+			}
+			else if ( oSettings.oFeatures.bDeferRender )
+			{
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSortingClasses, 'defer_sort_classes' );
+			}
+			
+			if ( oInit.bJQueryUI )
+			{
+				/* Use the JUI classes object for display. You could clone the oStdClasses object if 
+				 * you want to have multiple tables with multiple independent classes 
+				 */
+				$.extend( oSettings.oClasses, DataTable.ext.oJUIClasses );
+				
+				if ( oInit.sDom === DataTable.defaults.sDom && DataTable.defaults.sDom === "lfrtip" )
+				{
+					/* Set the DOM to use a layout suitable for jQuery UI's theming */
+					oSettings.sDom = '<"H"lfr>t<"F"ip>';
+				}
+			}
+			else
+			{
+				$.extend( oSettings.oClasses, DataTable.ext.oStdClasses );
+			}
+			$(this).addClass( oSettings.oClasses.sTable );
+			
+			/* Calculate the scroll bar width and cache it for use later on */
+			if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )
+			{
+				oSettings.oScroll.iBarWidth = _fnScrollBarWidth();
+			}
+			
+			if ( oSettings.iInitDisplayStart === undefined )
+			{
+				/* Display start point, taking into account the save saving */
+				oSettings.iInitDisplayStart = oInit.iDisplayStart;
+				oSettings._iDisplayStart = oInit.iDisplayStart;
+			}
+			
+			/* Must be done after everything which can be overridden by a cookie! */
+			if ( oInit.bStateSave )
+			{
+				oSettings.oFeatures.bStateSave = true;
+				_fnLoadState( oSettings, oInit );
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' );
+			}
+			
+			if ( oInit.iDeferLoading !== null )
+			{
+				oSettings.bDeferLoading = true;
+				var tmp = $.isArray( oInit.iDeferLoading );
+				oSettings._iRecordsDisplay = tmp ? oInit.iDeferLoading[0] : oInit.iDeferLoading;
+				oSettings._iRecordsTotal = tmp ? oInit.iDeferLoading[1] : oInit.iDeferLoading;
+			}
+			
+			if ( oInit.aaData !== null )
+			{
+				bUsePassedData = true;
+			}
+			
+			/* Language definitions */
+			if ( oInit.oLanguage.sUrl !== "" )
+			{
+				/* Get the language definitions from a file - because this Ajax call makes the language
+				 * get async to the remainder of this function we use bInitHandedOff to indicate that 
+				 * _fnInitialise will be fired by the returned Ajax handler, rather than the constructor
+				 */
+				oSettings.oLanguage.sUrl = oInit.oLanguage.sUrl;
+				$.getJSON( oSettings.oLanguage.sUrl, null, function( json ) {
+					_fnLanguageCompat( json );
+					$.extend( true, oSettings.oLanguage, oInit.oLanguage, json );
+					_fnInitialise( oSettings );
+				} );
+				bInitHandedOff = true;
+			}
+			else
+			{
+				$.extend( true, oSettings.oLanguage, oInit.oLanguage );
+			}
+			
+			
+			/*
+			 * Stripes
+			 */
+			if ( oInit.asStripeClasses === null )
+			{
+				oSettings.asStripeClasses =[
+					oSettings.oClasses.sStripeOdd,
+					oSettings.oClasses.sStripeEven
+				];
+			}
+			
+			/* Remove row stripe classes if they are already on the table row */
+			iLen=oSettings.asStripeClasses.length;
+			oSettings.asDestroyStripes = [];
+			if (iLen)
+			{
+				var bStripeRemove = false;
+				var anRows = $(this).children('tbody').children('tr:lt(' + iLen + ')');
+				for ( i=0 ; i<iLen ; i++ )
+				{
+					if ( anRows.hasClass( oSettings.asStripeClasses[i] ) )
+					{
+						bStripeRemove = true;
+						
+						/* Store the classes which we are about to remove so they can be re-added on destroy */
+						oSettings.asDestroyStripes.push( oSettings.asStripeClasses[i] );
+					}
+				}
+				
+				if ( bStripeRemove )
+				{
+					anRows.removeClass( oSettings.asStripeClasses.join(' ') );
+				}
+			}
+			
+			/*
+			 * Columns
+			 * See if we should load columns automatically or use defined ones
+			 */
+			var anThs = [];
+			var aoColumnsInit;
+			var nThead = this.getElementsByTagName('thead');
+			if ( nThead.length !== 0 )
+			{
+				_fnDetectHeader( oSettings.aoHeader, nThead[0] );
+				anThs = _fnGetUniqueThs( oSettings );
+			}
+			
+			/* If not given a column array, generate one with nulls */
+			if ( oInit.aoColumns === null )
+			{
+				aoColumnsInit = [];
+				for ( i=0, iLen=anThs.length ; i<iLen ; i++ )
+				{
+					aoColumnsInit.push( null );
+				}
+			}
+			else
+			{
+				aoColumnsInit = oInit.aoColumns;
+			}
+			
+			/* Add the columns */
+			for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )
+			{
+				/* Short cut - use the loop to check if we have column visibility state to restore */
+				if ( oInit.saved_aoColumns !== undefined && oInit.saved_aoColumns.length == iLen )
+				{
+					if ( aoColumnsInit[i] === null )
+					{
+						aoColumnsInit[i] = {};
+					}
+					aoColumnsInit[i].bVisible = oInit.saved_aoColumns[i].bVisible;
+				}
+				
+				_fnAddColumn( oSettings, anThs ? anThs[i] : null );
+			}
+			
+			/* Apply the column definitions */
+			_fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, aoColumnsInit, function (iCol, oDef) {
+				_fnColumnOptions( oSettings, iCol, oDef );
+			} );
+			
+			
+			/*
+			 * Sorting
+			 * Check the aaSorting array
+			 */
+			for ( i=0, iLen=oSettings.aaSorting.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aaSorting[i][0] >= oSettings.aoColumns.length )
+				{
+					oSettings.aaSorting[i][0] = 0;
+				}
+				var oColumn = oSettings.aoColumns[ oSettings.aaSorting[i][0] ];
+				
+				/* Add a default sorting index */
+				if ( oSettings.aaSorting[i][2] === undefined )
+				{
+					oSettings.aaSorting[i][2] = 0;
+				}
+				
+				/* If aaSorting is not defined, then we use the first indicator in asSorting */
+				if ( oInit.aaSorting === undefined && oSettings.saved_aaSorting === undefined )
+				{
+					oSettings.aaSorting[i][1] = oColumn.asSorting[0];
+				}
+				
+				/* Set the current sorting index based on aoColumns.asSorting */
+				for ( j=0, jLen=oColumn.asSorting.length ; j<jLen ; j++ )
+				{
+					if ( oSettings.aaSorting[i][1] == oColumn.asSorting[j] )
+					{
+						oSettings.aaSorting[i][2] = j;
+						break;
+					}
+				}
+			}
+				
+			/* Do a first pass on the sorting classes (allows any size changes to be taken into
+			 * account, and also will apply sorting disabled classes if disabled
+			 */
+			_fnSortingClasses( oSettings );
+			
+			
+			/*
+			 * Final init
+			 * Cache the header, body and footer as required, creating them if needed
+			 */
+			
+			/* Browser support detection */
+			_fnBrowserDetect( oSettings );
+			
+			// Work around for Webkit bug 83867 - store the caption-side before removing from doc
+			var captions = $(this).children('caption').each( function () {
+				this._captionSide = $(this).css('caption-side');
+			} );
+			
+			var thead = $(this).children('thead');
+			if ( thead.length === 0 )
+			{
+				thead = [ document.createElement( 'thead' ) ];
+				this.appendChild( thead[0] );
+			}
+			oSettings.nTHead = thead[0];
+			
+			var tbody = $(this).children('tbody');
+			if ( tbody.length === 0 )
+			{
+				tbody = [ document.createElement( 'tbody' ) ];
+				this.appendChild( tbody[0] );
+			}
+			oSettings.nTBody = tbody[0];
+			oSettings.nTBody.setAttribute( "role", "alert" );
+			oSettings.nTBody.setAttribute( "aria-live", "polite" );
+			oSettings.nTBody.setAttribute( "aria-relevant", "all" );
+			
+			var tfoot = $(this).children('tfoot');
+			if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") )
+			{
+				// If we are a scrolling table, and no footer has been given, then we need to create
+				// a tfoot element for the caption element to be appended to
+				tfoot = [ document.createElement( 'tfoot' ) ];
+				this.appendChild( tfoot[0] );
+			}
+			
+			if ( tfoot.length > 0 )
+			{
+				oSettings.nTFoot = tfoot[0];
+				_fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot );
+			}
+			
+			/* Check if there is data passing into the constructor */
+			if ( bUsePassedData )
+			{
+				for ( i=0 ; i<oInit.aaData.length ; i++ )
+				{
+					_fnAddData( oSettings, oInit.aaData[ i ] );
+				}
+			}
+			else
+			{
+				/* Grab the data from the page */
+				_fnGatherData( oSettings );
+			}
+			
+			/* Copy the data index array */
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			/* Initialisation complete - table can be drawn */
+			oSettings.bInitialised = true;
+			
+			/* Check if we need to initialise the table (it might not have been handed off to the
+			 * language processor)
+			 */
+			if ( bInitHandedOff === false )
+			{
+				_fnInitialise( oSettings );
+			}
+		} );
+		_that = null;
+		return this;
+	};
+
+	
+	
+	/**
+	 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+	 * to ensure compatibility.
+	 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+	 *    formats "X" and "X.Y" are also acceptable.
+	 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+	 *    version, or false if this version of DataTales is not suitable
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    alert( $.fn.dataTable.fnVersionCheck( '1.9.0' ) );
+	 */
+	DataTable.fnVersionCheck = function( sVersion )
+	{
+		/* This is cheap, but effective */
+		var fnZPad = function (Zpad, count)
+		{
+			while(Zpad.length < count) {
+				Zpad += '0';
+			}
+			return Zpad;
+		};
+		var aThis = DataTable.ext.sVersion.split('.');
+		var aThat = sVersion.split('.');
+		var sThis = '', sThat = '';
+		
+		for ( var i=0, iLen=aThat.length ; i<iLen ; i++ )
+		{
+			sThis += fnZPad( aThis[i], 3 );
+			sThat += fnZPad( aThat[i], 3 );
+		}
+		
+		return parseInt(sThis, 10) >= parseInt(sThat, 10);
+	};
+	
+	
+	/**
+	 * Check if a TABLE node is a DataTable table already or not.
+	 *  @param {node} nTable The TABLE node to check if it is a DataTable or not (note that other
+	 *    node types can be passed in, but will always return false).
+	 *  @returns {boolean} true the table given is a DataTable, or false otherwise
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    var ex = document.getElementById('example');
+	 *    if ( ! $.fn.DataTable.fnIsDataTable( ex ) ) {
+	 *      $(ex).dataTable();
+	 *    }
+	 */
+	DataTable.fnIsDataTable = function ( nTable )
+	{
+		var o = DataTable.settings;
+	
+		for ( var i=0 ; i<o.length ; i++ )
+		{
+			if ( o[i].nTable === nTable || o[i].nScrollHead === nTable || o[i].nScrollFoot === nTable )
+			{
+				return true;
+			}
+		}
+	
+		return false;
+	};
+	
+	
+	/**
+	 * Get all DataTable tables that have been initialised - optionally you can select to
+	 * get only currently visible tables.
+	 *  @param {boolean} [bVisible=false] Flag to indicate if you want all (default) or 
+	 *    visible tables only.
+	 *  @returns {array} Array of TABLE nodes (not DataTable instances) which are DataTables
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    var table = $.fn.dataTable.fnTables(true);
+	 *    if ( table.length > 0 ) {
+	 *      $(table).dataTable().fnAdjustColumnSizing();
+	 *    }
+	 */
+	DataTable.fnTables = function ( bVisible )
+	{
+		var out = [];
+	
+		jQuery.each( DataTable.settings, function (i, o) {
+			if ( !bVisible || (bVisible === true && $(o.nTable).is(':visible')) )
+			{
+				out.push( o.nTable );
+			}
+		} );
+	
+		return out;
+	};
+	
+
+	/**
+	 * Version string for plug-ins to check compatibility. Allowed format is
+	 * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
+	 * e are optional
+	 *  @member
+	 *  @type string
+	 *  @default Version number
+	 */
+	DataTable.version = "1.9.4";
+
+	/**
+	 * Private data store, containing all of the settings objects that are created for the
+	 * tables on a given page.
+	 * 
+	 * Note that the <i>DataTable.settings</i> object is aliased to <i>jQuery.fn.dataTableExt</i> 
+	 * through which it may be accessed and manipulated, or <i>jQuery.fn.dataTable.settings</i>.
+	 *  @member
+	 *  @type array
+	 *  @default []
+	 *  @private
+	 */
+	DataTable.settings = [];
+
+	/**
+	 * Object models container, for the various models that DataTables has available
+	 * to it. These models define the objects that are used to hold the active state 
+	 * and configuration of the table.
+	 *  @namespace
+	 */
+	DataTable.models = {};
+	
+	
+	/**
+	 * DataTables extension options and plug-ins. This namespace acts as a collection "area"
+	 * for plug-ins that can be used to extend the default DataTables behaviour - indeed many
+	 * of the build in methods use this method to provide their own capabilities (sorting methods
+	 * for example).
+	 * 
+	 * Note that this namespace is aliased to jQuery.fn.dataTableExt so it can be readily accessed
+	 * and modified by plug-ins.
+	 *  @namespace
+	 */
+	DataTable.models.ext = {
+		/**
+		 * Plug-in filtering functions - this method of filtering is complimentary to the default
+		 * type based filtering, and a lot more comprehensive as it allows you complete control
+		 * over the filtering logic. Each element in this array is a function (parameters
+		 * described below) that is called for every row in the table, and your logic decides if
+		 * it should be included in the filtered data set or not.
+		 *   <ul>
+		 *     <li>
+		 *       Function input parameters:
+		 *       <ul>
+		 *         <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *         <li>{array|object} Data for the row to be processed (same as the original format
+		 *           that was passed in as the data source, or an array from a DOM data source</li>
+		 *         <li>{int} Row index in aoData ({@link DataTable.models.oSettings.aoData}), which can
+		 *           be useful to retrieve the TR element if you need DOM interaction.</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{boolean} Include the row in the filtered result set (true) or not (false)</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 *
+		 *  @example
+		 *    // The following example shows custom filtering being applied to the fourth column (i.e.
+		 *    // the aData[3] index) based on two input values from the end-user, matching the data in 
+		 *    // a certain range.
+		 *    $.fn.dataTableExt.afnFiltering.push(
+		 *      function( oSettings, aData, iDataIndex ) {
+		 *        var iMin = document.getElementById('min').value * 1;
+		 *        var iMax = document.getElementById('max').value * 1;
+		 *        var iVersion = aData[3] == "-" ? 0 : aData[3]*1;
+		 *        if ( iMin == "" && iMax == "" ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin == "" && iVersion < iMax ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin < iVersion && "" == iMax ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin < iVersion && iVersion < iMax ) {
+		 *          return true;
+		 *        }
+		 *        return false;
+		 *      }
+		 *    );
+		 */
+		"afnFiltering": [],
+	
+	
+		/**
+		 * Plug-in sorting functions - this method of sorting is complimentary to the default type
+		 * based sorting that DataTables does automatically, allowing much greater control over the
+		 * the data that is being used to sort a column. This is useful if you want to do sorting
+		 * based on live data (for example the contents of an 'input' element) rather than just the
+		 * static string that DataTables knows of. The way these plug-ins work is that you create
+		 * an array of the values you wish to be sorted for the column in question and then return
+		 * that array. Which pre-sorting function is run here depends on the sSortDataType parameter
+		 * that is used for the column (if any). This is the corollary of <i>ofnSearch</i> for sort 
+		 * data.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+	     *         <li>{int} Target column index</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{array} Data for the column to be sorted upon</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  
+		 * Note that as of v1.9, it is typically preferable to use <i>mData</i> to prepare data for
+		 * the different uses that DataTables can put the data to. Specifically <i>mData</i> when
+		 * used as a function will give you a 'type' (sorting, filtering etc) that you can use to 
+		 * prepare the data as required for the different types. As such, this method is deprecated.
+		 *  @type array
+		 *  @default []
+		 *  @deprecated
+		 *
+		 *  @example
+		 *    // Updating the cached sorting information with user entered values in HTML input elements
+		 *    jQuery.fn.dataTableExt.afnSortData['dom-text'] = function ( oSettings, iColumn )
+		 *    {
+		 *      var aData = [];
+		 *      $( 'td:eq('+iColumn+') input', oSettings.oApi._fnGetTrNodes(oSettings) ).each( function () {
+		 *        aData.push( this.value );
+		 *      } );
+		 *      return aData;
+		 *    }
+		 */
+		"afnSortData": [],
+	
+	
+		/**
+		 * Feature plug-ins - This is an array of objects which describe the feature plug-ins that are
+		 * available to DataTables. These feature plug-ins are accessible through the sDom initialisation
+		 * option. As such, each feature plug-in must describe a function that is used to initialise
+		 * itself (fnInit), a character so the feature can be enabled by sDom (cFeature) and the name
+		 * of the feature (sFeature). Thus the objects attached to this method must provide:
+		 *   <ul>
+		 *     <li>{function} fnInit Initialisation of the plug-in
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>{node|null} The element which contains your feature. Note that the return
+		 *                may also be void if your plug-in does not require to inject any DOM elements 
+		 *                into DataTables control (sDom) - for example this might be useful when 
+		 *                developing a plug-in which allows table control via keyboard entry.</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </li>
+		 *     <li>{character} cFeature Character that will be matched in sDom - case sensitive</li>
+		 *     <li>{string} sFeature Feature name</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 * 
+		 *  @example
+		 *    // How TableTools initialises itself.
+		 *    $.fn.dataTableExt.aoFeatures.push( {
+		 *      "fnInit": function( oSettings ) {
+		 *        return new TableTools( { "oDTSettings": oSettings } );
+		 *      },
+		 *      "cFeature": "T",
+		 *      "sFeature": "TableTools"
+		 *    } );
+		 */
+		"aoFeatures": [],
+	
+	
+		/**
+		 * Type detection plug-in functions - DataTables utilises types to define how sorting and
+		 * filtering behave, and types can be either  be defined by the developer (sType for the
+		 * column) or they can be automatically detected by the methods in this array. The functions
+		 * defined in the array are quite simple, taking a single parameter (the data to analyse) 
+		 * and returning the type if it is a known type, or null otherwise.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data from the column cell to be analysed</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{string|null} Data type detected, or null if unknown (and thus pass it
+		 *           on to the other type detection functions.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 *  
+		 *  @example
+		 *    // Currency type detection plug-in:
+		 *    jQuery.fn.dataTableExt.aTypes.push(
+		 *      function ( sData ) {
+		 *        var sValidChars = "0123456789.-";
+		 *        var Char;
+		 *        
+		 *        // Check the numeric part
+		 *        for ( i=1 ; i<sData.length ; i++ ) {
+		 *          Char = sData.charAt(i); 
+		 *          if (sValidChars.indexOf(Char) == -1) {
+		 *            return null;
+		 *          }
+		 *        }
+		 *        
+		 *        // Check prefixed by currency
+		 *        if ( sData.charAt(0) == '$' || sData.charAt(0) == '&pound;' ) {
+		 *          return 'currency';
+		 *        }
+		 *        return null;
+		 *      }
+		 *    );
+		 */
+		"aTypes": [],
+	
+	
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, 
+		 * in order to ensure compatibility.
+		 *  @type function
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note 
+		 *    that the formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the 
+		 *    required version, or false if this version of DataTales is not suitable
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		"fnVersionCheck": DataTable.fnVersionCheck,
+	
+	
+		/**
+		 * Index for what 'this' index API functions should use
+		 *  @type int
+		 *  @default 0
+		 */
+		"iApiIndex": 0,
+	
+	
+		/**
+		 * Pre-processing of filtering data plug-ins - When you assign the sType for a column
+		 * (or have it automatically detected for you by DataTables or a type detection plug-in), 
+		 * you will typically be using this for custom sorting, but it can also be used to provide 
+		 * custom filtering by allowing you to pre-processing the data and returning the data in
+		 * the format that should be filtered upon. This is done by adding functions this object 
+		 * with a parameter name which matches the sType for that target column. This is the
+		 * corollary of <i>afnSortData</i> for filtering data.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data from the column cell to be prepared for filtering</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{string|null} Formatted string that will be used for the filtering.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 * 
+		 * Note that as of v1.9, it is typically preferable to use <i>mData</i> to prepare data for
+		 * the different uses that DataTables can put the data to. Specifically <i>mData</i> when
+		 * used as a function will give you a 'type' (sorting, filtering etc) that you can use to 
+		 * prepare the data as required for the different types. As such, this method is deprecated.
+		 *  @type object
+		 *  @default {}
+		 *  @deprecated
+		 *
+		 *  @example
+		 *    $.fn.dataTableExt.ofnSearch['title-numeric'] = function ( sData ) {
+		 *      return sData.replace(/\n/g," ").replace( /<.*?>/g, "" );
+		 *    }
+		 */
+		"ofnSearch": {},
+	
+	
+		/**
+		 * Container for all private functions in DataTables so they can be exposed externally
+		 *  @type object
+		 *  @default {}
+		 */
+		"oApi": {},
+	
+	
+		/**
+		 * Storage for the various classes that DataTables uses
+		 *  @type object
+		 *  @default {}
+		 */
+		"oStdClasses": {},
+		
+	
+		/**
+		 * Storage for the various classes that DataTables uses - jQuery UI suitable
+		 *  @type object
+		 *  @default {}
+		 */
+		"oJUIClasses": {},
+	
+	
+		/**
+		 * Pagination plug-in methods - The style and controls of the pagination can significantly 
+		 * impact on how the end user interacts with the data in your table, and DataTables allows 
+		 * the addition of pagination controls by extending this object, which can then be enabled
+		 * through the <i>sPaginationType</i> initialisation parameter. Each pagination type that
+		 * is added is an object (the property name of which is what <i>sPaginationType</i> refers
+		 * to) that has two properties, both methods that are used by DataTables to update the
+		 * control's state.
+		 *   <ul>
+		 *     <li>
+		 *       fnInit -  Initialisation of the paging controls. Called only during initialisation 
+		 *         of the table. It is expected that this function will add the required DOM elements 
+		 *         to the page for the paging controls to work. The element pointer 
+		 *         'oSettings.aanFeatures.p' array is provided by DataTables to contain the paging 
+		 *         controls (note that this is a 2D array to allow for multiple instances of each 
+		 *         DataTables DOM element). It is suggested that you add the controls to this element 
+		 *         as children
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *             <li>{node} Container into which the pagination controls must be inserted</li>
+		 *             <li>{function} Draw callback function - whenever the controls cause a page
+		 *               change, this method must be called to redraw the table.</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>No return required</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </il>
+		 *     <li>
+		 *       fnInit -  This function is called whenever the paging status of the table changes and is
+		 *         typically used to update classes and/or text of the paging controls to reflex the new 
+		 *         status.
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *             <li>{function} Draw callback function - in case you need to redraw the table again
+		 *               or attach new event listeners</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>No return required</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    $.fn.dataTableExt.oPagination.four_button = {
+		 *      "fnInit": function ( oSettings, nPaging, fnCallbackDraw ) {
+		 *        nFirst = document.createElement( 'span' );
+		 *        nPrevious = document.createElement( 'span' );
+		 *        nNext = document.createElement( 'span' );
+		 *        nLast = document.createElement( 'span' );
+		 *        
+		 *        nFirst.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sFirst ) );
+		 *        nPrevious.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sPrevious ) );
+		 *        nNext.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sNext ) );
+		 *        nLast.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sLast ) );
+		 *        
+		 *        nFirst.className = "paginate_button first";
+		 *        nPrevious.className = "paginate_button previous";
+		 *        nNext.className="paginate_button next";
+		 *        nLast.className = "paginate_button last";
+		 *        
+		 *        nPaging.appendChild( nFirst );
+		 *        nPaging.appendChild( nPrevious );
+		 *        nPaging.appendChild( nNext );
+		 *        nPaging.appendChild( nLast );
+		 *        
+		 *        $(nFirst).click( function () {
+		 *          oSettings.oApi._fnPageChange( oSettings, "first" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nPrevious).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "previous" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nNext).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "next" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nLast).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "last" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nFirst).bind( 'selectstart', function () { return false; } );
+		 *        $(nPrevious).bind( 'selectstart', function () { return false; } );
+		 *        $(nNext).bind( 'selectstart', function () { return false; } );
+		 *        $(nLast).bind( 'selectstart', function () { return false; } );
+		 *      },
+		 *      
+		 *      "fnUpdate": function ( oSettings, fnCallbackDraw ) {
+		 *        if ( !oSettings.aanFeatures.p ) {
+		 *          return;
+		 *        }
+		 *        
+		 *        // Loop over each instance of the pager
+		 *        var an = oSettings.aanFeatures.p;
+		 *        for ( var i=0, iLen=an.length ; i<iLen ; i++ ) {
+		 *          var buttons = an[i].getElementsByTagName('span');
+		 *          if ( oSettings._iDisplayStart === 0 ) {
+		 *            buttons[0].className = "paginate_disabled_previous";
+		 *            buttons[1].className = "paginate_disabled_previous";
+		 *          }
+		 *          else {
+		 *            buttons[0].className = "paginate_enabled_previous";
+		 *            buttons[1].className = "paginate_enabled_previous";
+		 *          }
+		 *          
+		 *          if ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) {
+		 *            buttons[2].className = "paginate_disabled_next";
+		 *            buttons[3].className = "paginate_disabled_next";
+		 *          }
+		 *          else {
+		 *            buttons[2].className = "paginate_enabled_next";
+		 *            buttons[3].className = "paginate_enabled_next";
+		 *          }
+		 *        }
+		 *      }
+		 *    };
+		 */
+		"oPagination": {},
+	
+	
+		/**
+		 * Sorting plug-in methods - Sorting in DataTables is based on the detected type of the
+		 * data column (you can add your own type detection functions, or override automatic 
+		 * detection using sType). With this specific type given to the column, DataTables will 
+		 * apply the required sort from the functions in the object. Each sort type must provide
+		 * two mandatory methods, one each for ascending and descending sorting, and can optionally
+		 * provide a pre-formatting method that will help speed up sorting by allowing DataTables
+		 * to pre-format the sort data only once (rather than every time the actual sort functions
+		 * are run). The two sorting functions are typical Javascript sort methods:
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data to compare to the second parameter</li>
+		 *         <li>{*} Data to compare to the first parameter</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{int} Sorting match: <0 if first parameter should be sorted lower than
+		 *           the second parameter, ===0 if the two parameters are equal and >0 if
+		 *           the first parameter should be sorted height than the second parameter.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    // Case-sensitive string sorting, with no pre-formatting method
+		 *    $.extend( $.fn.dataTableExt.oSort, {
+		 *      "string-case-asc": function(x,y) {
+		 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		 *      },
+		 *      "string-case-desc": function(x,y) {
+		 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		 *      }
+		 *    } );
+		 *
+		 *  @example
+		 *    // Case-insensitive string sorting, with pre-formatting
+		 *    $.extend( $.fn.dataTableExt.oSort, {
+		 *      "string-pre": function(x) {
+		 *        return x.toLowerCase();
+		 *      },
+		 *      "string-asc": function(x,y) {
+		 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		 *      },
+		 *      "string-desc": function(x,y) {
+		 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		 *      }
+		 *    } );
+		 */
+		"oSort": {},
+	
+	
+		/**
+		 * Version string for plug-ins to check compatibility. Allowed format is
+		 * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
+		 * e are optional
+		 *  @type string
+		 *  @default Version number
+		 */
+		"sVersion": DataTable.version,
+	
+	
+		/**
+		 * How should DataTables report an error. Can take the value 'alert' or 'throw'
+		 *  @type string
+		 *  @default alert
+		 */
+		"sErrMode": "alert",
+	
+	
+		/**
+		 * Store information for DataTables to access globally about other instances
+		 *  @namespace
+		 *  @private
+		 */
+		"_oExternConfig": {
+			/* int:iNextUnique - next unique number for an instance */
+			"iNextUnique": 0
+		}
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * search information for the global filter and individual column filters.
+	 *  @namespace
+	 */
+	DataTable.models.oSearch = {
+		/**
+		 * Flag to indicate if the filtering should be case insensitive or not
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bCaseInsensitive": true,
+	
+		/**
+		 * Applied search term
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sSearch": "",
+	
+		/**
+		 * Flag to indicate if the search term should be interpreted as a
+		 * regular expression (true) or not (false) and therefore and special
+		 * regex characters escaped.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bRegex": false,
+	
+		/**
+		 * Flag to indicate if DataTables is to use its smart filtering or not.
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bSmart": true
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * each individual row. This is the object format used for the settings 
+	 * aoData array.
+	 *  @namespace
+	 */
+	DataTable.models.oRow = {
+		/**
+		 * TR element for the row
+		 *  @type node
+		 *  @default null
+		 */
+		"nTr": null,
+	
+		/**
+		 * Data object from the original data source for the row. This is either
+		 * an array if using the traditional form of DataTables, or an object if
+		 * using mData options. The exact type will depend on the passed in
+		 * data from the data source, or will be an array if using DOM a data 
+		 * source.
+		 *  @type array|object
+		 *  @default []
+		 */
+		"_aData": [],
+	
+		/**
+		 * Sorting data cache - this array is ostensibly the same length as the
+		 * number of columns (although each index is generated only as it is 
+		 * needed), and holds the data that is used for sorting each column in the
+		 * row. We do this cache generation at the start of the sort in order that
+		 * the formatting of the sort data need be done only once for each cell
+		 * per sort. This array should not be read from or written to by anything
+		 * other than the master sorting methods.
+		 *  @type array
+		 *  @default []
+		 *  @private
+		 */
+		"_aSortData": [],
+	
+		/**
+		 * Array of TD elements that are cached for hidden rows, so they can be
+		 * reinserted into the table if a column is made visible again (or to act
+		 * as a store if a column is made hidden). Only hidden columns have a 
+		 * reference in the array. For non-hidden columns the value is either
+		 * undefined or null.
+		 *  @type array nodes
+		 *  @default []
+		 *  @private
+		 */
+		"_anHidden": [],
+	
+		/**
+		 * Cache of the class name that DataTables has applied to the row, so we
+		 * can quickly look at this variable rather than needing to do a DOM check
+		 * on className for the nTr property.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @private
+		 */
+		"_sRowStripe": ""
+	};
+	
+	
+	
+	/**
+	 * Template object for the column information object in DataTables. This object
+	 * is held in the settings aoColumns array and contains all the information that
+	 * DataTables needs about each individual column.
+	 * 
+	 * Note that this object is related to {@link DataTable.defaults.columns} 
+	 * but this one is the internal data store for DataTables's cache of columns.
+	 * It should NOT be manipulated outside of DataTables. Any configuration should
+	 * be done through the initialisation options.
+	 *  @namespace
+	 */
+	DataTable.models.oColumn = {
+		/**
+		 * A list of the columns that sorting should occur on when this column
+		 * is sorted. That this property is an array allows multi-column sorting
+		 * to be defined for a column (for example first name / last name columns
+		 * would benefit from this). The values are integers pointing to the
+		 * columns to be sorted on (typically it will be a single integer pointing
+		 * at itself, but that doesn't need to be the case).
+		 *  @type array
+		 */
+		"aDataSort": null,
+	
+		/**
+		 * Define the sorting directions that are applied to the column, in sequence
+		 * as the column is repeatedly sorted upon - i.e. the first value is used
+		 * as the sorting direction when the column if first sorted (clicked on).
+		 * Sort it again (click again) and it will move on to the next index.
+		 * Repeat until loop.
+		 *  @type array
+		 */
+		"asSorting": null,
+		
+		/**
+		 * Flag to indicate if the column is searchable, and thus should be included
+		 * in the filtering or not.
+		 *  @type boolean
+		 */
+		"bSearchable": null,
+		
+		/**
+		 * Flag to indicate if the column is sortable or not.
+		 *  @type boolean
+		 */
+		"bSortable": null,
+		
+		/**
+		 * <code>Deprecated</code> When using fnRender, you have two options for what 
+		 * to do with the data, and this property serves as the switch. Firstly, you 
+		 * can have the sorting and filtering use the rendered value (true - default), 
+		 * or you can have the sorting and filtering us the original value (false).
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type boolean
+		 *  @deprecated
+		 */
+		"bUseRendered": null,
+		
+		/**
+		 * Flag to indicate if the column is currently visible in the table or not
+		 *  @type boolean
+		 */
+		"bVisible": null,
+		
+		/**
+		 * Flag to indicate to the type detection method if the automatic type
+		 * detection should be used, or if a column type (sType) has been specified
+		 *  @type boolean
+		 *  @default true
+		 *  @private
+		 */
+		"_bAutoType": true,
+		
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @default null
+		 */
+		"fnCreatedCell": null,
+		
+		/**
+		 * Function to get data from a cell in a column. You should <b>never</b>
+		 * access data directly through _aData internally in DataTables - always use
+		 * the method attached to this property. It allows mData to function as
+		 * required. This function is automatically assigned by the column 
+		 * initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array 
+		 *    (i.e. aoData[]._aData)
+		 *  @param {string} sSpecific The specific data type you want to get - 
+		 *    'display', 'type' 'filter' 'sort'
+		 *  @returns {*} The data for the cell from the given row's data
+		 *  @default null
+		 */
+		"fnGetData": null,
+		
+		/**
+		 * <code>Deprecated</code> Custom display function that will be called for the 
+		 * display of each cell in this column.
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type function
+		 *  @param {object} o Object with the following parameters:
+		 *  @param {int}    o.iDataRow The row in aoData
+		 *  @param {int}    o.iDataColumn The column in question
+		 *  @param {array}  o.aData The data for the row in question
+		 *  @param {object} o.oSettings The settings object for this DataTables instance
+		 *  @returns {string} The string you which to use in the display
+		 *  @default null
+		 *  @deprecated
+		 */
+		"fnRender": null,
+		
+		/**
+		 * Function to set data for a cell in the column. You should <b>never</b> 
+		 * set the data directly to _aData internally in DataTables - always use
+		 * this method. It allows mData to function as required. This function
+		 * is automatically assigned by the column initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array 
+		 *    (i.e. aoData[]._aData)
+		 *  @param {*} sValue Value to set
+		 *  @default null
+		 */
+		"fnSetData": null,
+		
+		/**
+		 * Property to read the value for the cells in the column from the data 
+		 * source array / object. If null, then the default content is used, if a
+		 * function is given then the return from the function is used.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mData": null,
+		
+		/**
+		 * Partner property to mData which is used (only when defined) to get
+		 * the data - i.e. it is basically the same as mData, but without the
+		 * 'set' option, and also the data fed to it is the result from mData.
+		 * This is the rendering method to match the data method of mData.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mRender": null,
+		
+		/**
+		 * Unique header TH/TD element for this column - this is what the sorting
+		 * listener is attached to (if sorting is enabled.)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTh": null,
+		
+		/**
+		 * Unique footer TH/TD element for this column (if there is one). Not used 
+		 * in DataTables as such, but can be used for plug-ins to reference the 
+		 * footer for each column.
+		 *  @type node
+		 *  @default null
+		 */
+		"nTf": null,
+		
+		/**
+		 * The class to apply to all TD elements in the table's TBODY for the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sClass": null,
+		
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer 
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 *  @type string
+		 */
+		"sContentPadding": null,
+		
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 */
+		"sDefaultContent": null,
+		
+		/**
+		 * Name for the column, allowing reference to the column by name as well as
+		 * by index (needs a lookup to work by name).
+		 *  @type string
+		 */
+		"sName": null,
+		
+		/**
+		 * Custom sorting data type - defines which of the available plug-ins in
+		 * afnSortData the custom sorting will use - if any is defined.
+		 *  @type string
+		 *  @default std
+		 */
+		"sSortDataType": 'std',
+		
+		/**
+		 * Class to be applied to the header element when sorting on this column
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClass": null,
+		
+		/**
+		 * Class to be applied to the header element when sorting on this column -
+		 * when jQuery UI theming is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClassJUI": null,
+		
+		/**
+		 * Title of the column - what is seen in the TH element (nTh).
+		 *  @type string
+		 */
+		"sTitle": null,
+		
+		/**
+		 * Column sorting and filtering type
+		 *  @type string
+		 *  @default null
+		 */
+		"sType": null,
+		
+		/**
+		 * Width of the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidth": null,
+		
+		/**
+		 * Width of the column when it was first "encountered"
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidthOrig": null
+	};
+	
+	
+	
+	/**
+	 * Initialisation options that can be given to DataTables at initialisation 
+	 * time.
+	 *  @namespace
+	 */
+	DataTable.defaults = {
+		/**
+		 * An array of data to use for the table, passed in at initialisation which 
+		 * will be used in preference to any data which is already in the DOM. This is
+		 * particularly useful for constructing tables purely in Javascript, for
+		 * example with a custom Ajax call.
+		 *  @type array
+		 *  @default null
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    // Using a 2D array data source
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "aaData": [
+		 *          ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'],
+		 *          ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'],
+		 *        ],
+		 *        "aoColumns": [
+		 *          { "sTitle": "Engine" },
+		 *          { "sTitle": "Browser" },
+		 *          { "sTitle": "Platform" },
+		 *          { "sTitle": "Version" },
+		 *          { "sTitle": "Grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using an array of objects as a data source (mData)
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "aaData": [
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 4.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  4,
+		 *            "grade":    "X"
+		 *          },
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 5.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  5,
+		 *            "grade":    "C"
+		 *          }
+		 *        ],
+		 *        "aoColumns": [
+		 *          { "sTitle": "Engine",   "mData": "engine" },
+		 *          { "sTitle": "Browser",  "mData": "browser" },
+		 *          { "sTitle": "Platform", "mData": "platform" },
+		 *          { "sTitle": "Version",  "mData": "version" },
+		 *          { "sTitle": "Grade",    "mData": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aaData": null,
+	
+	
+		/**
+		 * If sorting is enabled, then DataTables will perform a first pass sort on 
+		 * initialisation. You can define which column(s) the sort is performed upon, 
+		 * and the sorting direction, with this variable. The aaSorting array should 
+		 * contain an array for each column to be sorted initially containing the 
+		 * column's index and a direction string ('asc' or 'desc').
+		 *  @type array
+		 *  @default [[0,'asc']]
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    // Sort by 3rd column first, and then 4th column
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSorting": [[2,'asc'], [3,'desc']]
+		 *      } );
+		 *    } );
+		 *    
+		 *    // No initial sorting
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSorting": []
+		 *      } );
+		 *    } );
+		 */
+		"aaSorting": [[0,'asc']],
+	
+	
+		/**
+		 * This parameter is basically identical to the aaSorting parameter, but 
+		 * cannot be overridden by user interaction with the table. What this means 
+		 * is that you could have a column (visible or hidden) which the sorting will 
+		 * always be forced on first - any sorting after that (from the user) will 
+		 * then be performed as required. This can be useful for grouping rows 
+		 * together.
+		 *  @type array
+		 *  @default null
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSortingFixed": [[0,'asc']]
+		 *      } );
+		 *    } )
+		 */
+		"aaSortingFixed": null,
+	
+	
+		/**
+		 * This parameter allows you to readily specify the entries in the length drop
+		 * down menu that DataTables shows when pagination is enabled. It can be 
+		 * either a 1D array of options which will be used for both the displayed 
+		 * option and the value, or a 2D array which will use the array in the first 
+		 * position as the value, and the array in the second position as the 
+		 * displayed options (useful for language strings such as 'All').
+		 *  @type array
+		 *  @default [ 10, 25, 50, 100 ]
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aLengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]]
+		 *      } );
+		 *    } );
+		 *  
+		 *  @example
+		 *    // Setting the default display length as well as length menu
+		 *    // This is likely to be wanted if you remove the '10' option which
+		 *    // is the iDisplayLength default.
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayLength": 25,
+		 *        "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]]
+		 *      } );
+		 *    } );
+		 */
+		"aLengthMenu": [ 10, 25, 50, 100 ],
+	
+	
+		/**
+		 * The aoColumns option in the initialisation parameter allows you to define
+		 * details about the way individual columns behave. For a full list of
+		 * column options that can be set, please see 
+		 * {@link DataTable.defaults.columns}. Note that if you use aoColumns to
+		 * define your columns, you must have an entry in the array for every single
+		 * column that you have in your table (these can be null if you don't which
+		 * to specify any options).
+		 *  @member
+		 */
+		"aoColumns": null,
+	
+		/**
+		 * Very similar to aoColumns, aoColumnDefs allows you to target a specific 
+		 * column, multiple columns, or all columns, using the aTargets property of 
+		 * each object in the array. This allows great flexibility when creating 
+		 * tables, as the aoColumnDefs arrays can be of any length, targeting the 
+		 * columns you specifically want. aoColumnDefs may use any of the column 
+		 * options available: {@link DataTable.defaults.columns}, but it _must_
+		 * have aTargets defined in each object in the array. Values in the aTargets
+		 * array may be:
+		 *   <ul>
+		 *     <li>a string - class name will be matched on the TH for the column</li>
+		 *     <li>0 or a positive integer - column index counting from the left</li>
+		 *     <li>a negative integer - column index counting from the right</li>
+		 *     <li>the string "_all" - all columns (i.e. assign a default)</li>
+		 *   </ul>
+		 *  @member
+		 */
+		"aoColumnDefs": null,
+	
+	
+		/**
+		 * Basically the same as oSearch, this parameter defines the individual column
+		 * filtering state at initialisation time. The array must be of the same size 
+		 * as the number of columns, and each element be an object with the parameters
+		 * "sSearch" and "bEscapeRegex" (the latter is optional). 'null' is also
+		 * accepted and the default will be used.
+		 *  @type array
+		 *  @default []
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoSearchCols": [
+		 *          null,
+		 *          { "sSearch": "My filter" },
+		 *          null,
+		 *          { "sSearch": "^[0-9]", "bEscapeRegex": false }
+		 *        ]
+		 *      } );
+		 *    } )
+		 */
+		"aoSearchCols": [],
+	
+	
+		/**
+		 * An array of CSS classes that should be applied to displayed rows. This 
+		 * array may be of any length, and DataTables will apply each class 
+		 * sequentially, looping when required.
+		 *  @type array
+		 *  @default null <i>Will take the values determined by the oClasses.sStripe*
+		 *    options</i>
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "asStripeClasses": [ 'strip1', 'strip2', 'strip3' ]
+		 *      } );
+		 *    } )
+		 */
+		"asStripeClasses": null,
+	
+	
+		/**
+		 * Enable or disable automatic column width calculation. This can be disabled
+		 * as an optimisation (it takes some time to calculate the widths) if the
+		 * tables widths are passed in using aoColumns.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bAutoWidth": false
+		 *      } );
+		 *    } );
+		 */
+		"bAutoWidth": true,
+	
+	
+		/**
+		 * Deferred rendering can provide DataTables with a huge speed boost when you
+		 * are using an Ajax or JS data source for the table. This option, when set to
+		 * true, will cause DataTables to defer the creation of the table elements for
+		 * each row until they are needed for a draw - saving a significant amount of
+		 * time.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/arrays.txt",
+		 *        "bDeferRender": true
+		 *      } );
+		 *    } );
+		 */
+		"bDeferRender": false,
+	
+	
+		/**
+		 * Replace a DataTable which matches the given selector and replace it with 
+		 * one which has the properties of the new initialisation object passed. If no
+		 * table matches the selector, then the new DataTable will be constructed as
+		 * per normal.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *      
+		 *      // Some time later....
+		 *      $('#example').dataTable( {
+		 *        "bFilter": false,
+		 *        "bDestroy": true
+		 *      } );
+		 *    } );
+		 */
+		"bDestroy": false,
+	
+	
+		/**
+		 * Enable or disable filtering of data. Filtering in DataTables is "smart" in
+		 * that it allows the end user to input multiple words (space separated) and
+		 * will match a row containing those words, even if not in the order that was
+		 * specified (this allow matching across multiple columns). Note that if you
+		 * wish to use filtering in DataTables this must remain 'true' - to remove the
+		 * default filtering input box and retain filtering abilities, please use
+		 * {@link DataTable.defaults.sDom}.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bFilter": false
+		 *      } );
+		 *    } );
+		 */
+		"bFilter": true,
+	
+	
+		/**
+		 * Enable or disable the table information display. This shows information 
+		 * about the data that is currently visible on the page, including information
+		 * about filtered data if that action is being performed.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bInfo": false
+		 *      } );
+		 *    } );
+		 */
+		"bInfo": true,
+	
+	
+		/**
+		 * Enable jQuery UI ThemeRoller support (required as ThemeRoller requires some
+		 * slightly different and additional mark-up from what DataTables has
+		 * traditionally used).
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bJQueryUI": true
+		 *      } );
+		 *    } );
+		 */
+		"bJQueryUI": false,
+	
+	
+		/**
+		 * Allows the end user to select the size of a formatted page from a select
+		 * menu (sizes are 10, 25, 50 and 100). Requires pagination (bPaginate).
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bLengthChange": false
+		 *      } );
+		 *    } );
+		 */
+		"bLengthChange": true,
+	
+	
+		/**
+		 * Enable or disable pagination.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bPaginate": false
+		 *      } );
+		 *    } );
+		 */
+		"bPaginate": true,
+	
+	
+		/**
+		 * Enable or disable the display of a 'processing' indicator when the table is
+		 * being processed (e.g. a sort). This is particularly useful for tables with
+		 * large amounts of data where it can take a noticeable amount of time to sort
+		 * the entries.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true
+		 *      } );
+		 *    } );
+		 */
+		"bProcessing": false,
+	
+	
+		/**
+		 * Retrieve the DataTables object for the given selector. Note that if the
+		 * table has already been initialised, this parameter will cause DataTables
+		 * to simply return the object that has already been set up - it will not take
+		 * account of any changes you might have made to the initialisation object
+		 * passed to DataTables (setting this parameter to true is an acknowledgement
+		 * that you understand this). bDestroy can be used to reinitialise a table if
+		 * you need.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      initTable();
+		 *      tableActions();
+		 *    } );
+		 *    
+		 *    function initTable ()
+		 *    {
+		 *      return $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false,
+		 *        "bRetrieve": true
+		 *      } );
+		 *    }
+		 *    
+		 *    function tableActions ()
+		 *    {
+		 *      var oTable = initTable();
+		 *      // perform API operations with oTable 
+		 *    }
+		 */
+		"bRetrieve": false,
+	
+	
+		/**
+		 * Indicate if DataTables should be allowed to set the padding / margin
+		 * etc for the scrolling header elements or not. Typically you will want
+		 * this.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollAutoCss": false,
+		 *        "sScrollY": "200px"
+		 *      } );
+		 *    } );
+		 */
+		"bScrollAutoCss": true,
+	
+	
+		/**
+		 * When vertical (y) scrolling is enabled, DataTables will force the height of
+		 * the table's viewport to the given height at all times (useful for layout).
+		 * However, this can look odd when filtering data down to a small data set,
+		 * and the footer is left "floating" further down. This parameter (when
+		 * enabled) will cause DataTables to collapse the table's viewport down when
+		 * the result set will fit within the given Y height.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200",
+		 *        "bScrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"bScrollCollapse": false,
+	
+	
+		/**
+		 * Enable infinite scrolling for DataTables (to be used in combination with
+		 * sScrollY). Infinite scrolling means that DataTables will continually load
+		 * data as a user scrolls through a table, which is very useful for large
+		 * dataset. This cannot be used with pagination, which is automatically
+		 * disabled. Note - the Scroller extra for DataTables is recommended in
+		 * in preference to this option.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollInfinite": true,
+		 *        "bScrollCollapse": true,
+		 *        "sScrollY": "200px"
+		 *      } );
+		 *    } );
+		 */
+		"bScrollInfinite": false,
+	
+	
+		/**
+		 * Configure DataTables to use server-side processing. Note that the
+		 * sAjaxSource parameter must also be given in order to give DataTables a
+		 * source to obtain the required data for each draw.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "xhr.php"
+		 *      } );
+		 *    } );
+		 */
+		"bServerSide": false,
+	
+	
+		/**
+		 * Enable or disable sorting of columns. Sorting of individual columns can be
+		 * disabled by the "bSortable" option for each column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bSort": false
+		 *      } );
+		 *    } );
+		 */
+		"bSort": true,
+	
+	
+		/**
+		 * Allows control over whether DataTables should use the top (true) unique
+		 * cell that is found for a single column, or the bottom (false - default).
+		 * This is useful when using complex headers.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bSortCellsTop": true
+		 *      } );
+		 *    } );
+		 */
+		"bSortCellsTop": false,
+	
+	
+		/**
+		 * Enable or disable the addition of the classes 'sorting_1', 'sorting_2' and
+		 * 'sorting_3' to the columns which are currently being sorted on. This is
+		 * presented as a feature switch as it can increase processing time (while
+		 * classes are removed and added) so for large data sets you might want to
+		 * turn this off.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bSortClasses": false
+		 *      } );
+		 *    } );
+		 */
+		"bSortClasses": true,
+	
+	
+		/**
+		 * Enable or disable state saving. When enabled a cookie will be used to save
+		 * table display information such as pagination information, display length,
+		 * filtering and sorting. As such when the end user reloads the page the
+		 * display display will match what thy had previously set up.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true
+		 *      } );
+		 *    } );
+		 */
+		"bStateSave": false,
+	
+	
+		/**
+		 * Customise the cookie and / or the parameters being stored when using
+		 * DataTables with state saving enabled. This function is called whenever
+		 * the cookie is modified, and it expects a fully formed cookie string to be
+		 * returned. Note that the data object passed in is a Javascript object which
+		 * must be converted to a string (JSON.stringify for example).
+		 *  @type function
+		 *  @param {string} sName Name of the cookie defined by DataTables
+		 *  @param {object} oData Data to be stored in the cookie
+		 *  @param {string} sExpires Cookie expires string
+		 *  @param {string} sPath Path of the cookie to set
+		 *  @returns {string} Cookie formatted string (which should be encoded by
+		 *    using encodeURIComponent())
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "fnCookieCallback": function (sName, oData, sExpires, sPath) {
+		 *          // Customise oData or sName or whatever else here
+		 *          return sName + "="+JSON.stringify(oData)+"; expires=" + sExpires +"; path=" + sPath;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCookieCallback": null,
+	
+	
+		/**
+		 * This function is called when a TR element is created (and all TD child
+		 * elements have been inserted), or registered if using a DOM source, allowing
+		 * manipulation of the TR element (adding classes etc).
+		 *  @type function
+		 *  @param {node} nRow "TR" element for the current row
+		 *  @param {array} aData Raw data array for this row
+		 *  @param {int} iDataIndex The index of this row in aoData
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnCreatedRow": function( nRow, aData, iDataIndex ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( aData[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', nRow).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCreatedRow": null,
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify any aspect you want about the created DOM.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnDrawCallback": function( oSettings ) {
+		 *          alert( 'DataTables has redrawn the table' );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnDrawCallback": null,
+	
+	
+		/**
+		 * Identical to fnHeaderCallback() but for the table footer this function
+		 * allows you to modify the table footer on every 'draw' even.
+		 *  @type function
+		 *  @param {node} nFoot "TR" element for the footer
+		 *  @param {array} aData Full table data (as derived from the original HTML)
+		 *  @param {int} iStart Index for the current display starting point in the 
+		 *    display array
+		 *  @param {int} iEnd Index for the current display ending point in the 
+		 *    display array
+		 *  @param {array int} aiDisplay Index array to translate the visual position
+		 *    to the full data array
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnFooterCallback": function( nFoot, aData, iStart, iEnd, aiDisplay ) {
+		 *          nFoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+iStart;
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnFooterCallback": null,
+	
+	
+		/**
+		 * When rendering large numbers in the information element for the table
+		 * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers
+		 * to have a comma separator for the 'thousands' units (e.g. 1 million is
+		 * rendered as "1,000,000") to help readability for the end user. This
+		 * function will override the default method DataTables uses.
+		 *  @type function
+		 *  @member
+		 *  @param {int} iIn number to be formatted
+		 *  @returns {string} formatted string for DataTables to show the number
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnFormatNumber": function ( iIn ) {
+		 *          if ( iIn &lt; 1000 ) {
+		 *            return iIn;
+		 *          } else {
+		 *            var 
+		 *              s=(iIn+""), 
+		 *              a=s.split(""), out="", 
+		 *              iLen=s.length;
+		 *            
+		 *            for ( var i=0 ; i&lt;iLen ; i++ ) {
+		 *              if ( i%3 === 0 &amp;&amp; i !== 0 ) {
+		 *                out = "'"+out;
+		 *              }
+		 *              out = a[iLen-i-1]+out;
+		 *            }
+		 *          }
+		 *          return out;
+		 *        };
+		 *      } );
+		 *    } );
+		 */
+		"fnFormatNumber": function ( iIn ) {
+			if ( iIn < 1000 )
+			{
+				// A small optimisation for what is likely to be the majority of use cases
+				return iIn;
+			}
+	
+			var s=(iIn+""), a=s.split(""), out="", iLen=s.length;
+			
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				if ( i%3 === 0 && i !== 0 )
+				{
+					out = this.oLanguage.sInfoThousands+out;
+				}
+				out = a[iLen-i-1]+out;
+			}
+			return out;
+		},
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify the header row. This can be used to calculate and
+		 * display useful information about the table.
+		 *  @type function
+		 *  @param {node} nHead "TR" element for the header
+		 *  @param {array} aData Full table data (as derived from the original HTML)
+		 *  @param {int} iStart Index for the current display starting point in the
+		 *    display array
+		 *  @param {int} iEnd Index for the current display ending point in the
+		 *    display array
+		 *  @param {array int} aiDisplay Index array to translate the visual position
+		 *    to the full data array
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnHeaderCallback": function( nHead, aData, iStart, iEnd, aiDisplay ) {
+		 *          nHead.getElementsByTagName('th')[0].innerHTML = "Displaying "+(iEnd-iStart)+" records";
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnHeaderCallback": null,
+	
+	
+		/**
+		 * The information element can be used to convey information about the current
+		 * state of the table. Although the internationalisation options presented by
+		 * DataTables are quite capable of dealing with most customisations, there may
+		 * be times where you wish to customise the string further. This callback
+		 * allows you to do exactly that.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {int} iStart Starting position in data for the draw
+		 *  @param {int} iEnd End position in data for the draw
+		 *  @param {int} iMax Total number of rows in the table (regardless of
+		 *    filtering)
+		 *  @param {int} iTotal Total number of rows in the data set, after filtering
+		 *  @param {string} sPre The string that DataTables has formatted using it's
+		 *    own rules
+		 *  @returns {string} The string to be displayed in the information element.
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $('#example').dataTable( {
+		 *      "fnInfoCallback": function( oSettings, iStart, iEnd, iMax, iTotal, sPre ) {
+		 *        return iStart +" to "+ iEnd;
+		 *      }
+		 *    } );
+		 */
+		"fnInfoCallback": null,
+	
+	
+		/**
+		 * Called when the table has been initialised. Normally DataTables will
+		 * initialise sequentially and there will be no need for this function,
+		 * however, this does not hold true when using external language information
+		 * since that is obtained using an async XHR call.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} json The JSON object request from the server - only
+		 *    present if client-side Ajax sourced data is used
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnInitComplete": function(oSettings, json) {
+		 *          alert( 'DataTables has finished its initialisation.' );
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnInitComplete": null,
+	
+	
+		/**
+		 * Called at the very start of each table draw and can be used to cancel the
+		 * draw by returning false, any other return (including undefined) results in
+		 * the full draw occurring).
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @returns {boolean} False will cancel the draw, anything else (including no
+		 *    return) will allow it to complete.
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnPreDrawCallback": function( oSettings ) {
+		 *          if ( $('#test').val() == 1 ) {
+		 *            return false;
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnPreDrawCallback": null,
+	
+	
+		/**
+		 * This function allows you to 'post process' each row after it have been
+		 * generated for each table draw, but before it is rendered on screen. This
+		 * function might be used for setting the row class name etc.
+		 *  @type function
+		 *  @param {node} nRow "TR" element for the current row
+		 *  @param {array} aData Raw data array for this row
+		 *  @param {int} iDisplayIndex The display index for the current table draw
+		 *  @param {int} iDisplayIndexFull The index of the data in the full list of
+		 *    rows (after filtering)
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( aData[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', nRow).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnRowCallback": null,
+	
+	
+		/**
+		 * This parameter allows you to override the default function which obtains
+		 * the data from the server ($.getJSON) so something more suitable for your
+		 * application. For example you could use POST data, or pull information from
+		 * a Gears or AIR database.
+		 *  @type function
+		 *  @member
+		 *  @param {string} sSource HTTP source to obtain the data from (sAjaxSource)
+		 *  @param {array} aoData A key/value pair object containing the data to send
+		 *    to the server
+		 *  @param {function} fnCallback to be called on completion of the data get
+		 *    process that will draw the data on the page.
+		 *  @param {object} oSettings DataTables settings object
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    // POST data to server
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true,
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "xhr.php",
+		 *        "fnServerData": function ( sSource, aoData, fnCallback, oSettings ) {
+		 *          oSettings.jqXHR = $.ajax( {
+		 *            "dataType": 'json', 
+		 *            "type": "POST", 
+		 *            "url": sSource, 
+		 *            "data": aoData, 
+		 *            "success": fnCallback
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnServerData": function ( sUrl, aoData, fnCallback, oSettings ) {
+			oSettings.jqXHR = $.ajax( {
+				"url":  sUrl,
+				"data": aoData,
+				"success": function (json) {
+					if ( json.sError ) {
+						oSettings.oApi._fnLog( oSettings, 0, json.sError );
+					}
+					
+					$(oSettings.oInstance).trigger('xhr', [oSettings, json]);
+					fnCallback( json );
+				},
+				"dataType": "json",
+				"cache": false,
+				"type": oSettings.sServerMethod,
+				"error": function (xhr, error, thrown) {
+					if ( error == "parsererror" ) {
+						oSettings.oApi._fnLog( oSettings, 0, "DataTables warning: JSON data from "+
+							"server could not be parsed. This is caused by a JSON formatting error." );
+					}
+				}
+			} );
+		},
+	
+	
+		/**
+		 * It is often useful to send extra data to the server when making an Ajax
+		 * request - for example custom filtering information, and this callback
+		 * function makes it trivial to send extra information to the server. The
+		 * passed in parameter is the data set that has been constructed by
+		 * DataTables, and you can add to this or modify it as you require.
+		 *  @type function
+		 *  @param {array} aoData Data array (array of objects which are name/value
+		 *    pairs) that has been constructed by DataTables and will be sent to the
+		 *    server. In the case of Ajax sourced data with server-side processing
+		 *    this will be an empty array, for server-side processing there will be a
+		 *    significant number of parameters!
+		 *  @returns {undefined} Ensure that you modify the aoData array passed in,
+		 *    as this is passed by reference.
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true,
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "fnServerParams": function ( aoData ) {
+		 *          aoData.push( { "name": "more_data", "value": "my_value" } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnServerParams": null,
+	
+	
+		/**
+		 * Load the table state. With this function you can define from where, and how, the
+		 * state of a table is loaded. By default DataTables will load from its state saving
+		 * cookie, but you might wish to use local storage (HTML5) or a server-side database.
+		 *  @type function
+		 *  @member
+		 *  @param {object} oSettings DataTables settings object
+		 *  @return {object} The DataTables state object to be loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoad": function (oSettings) {
+		 *          var o;
+		 *          
+		 *          // Send an Ajax request to the server to get the data. Note that
+		 *          // this is a synchronous request.
+		 *          $.ajax( {
+		 *            "url": "/state_load",
+		 *            "async": false,
+		 *            "dataType": "json",
+		 *            "success": function (json) {
+		 *              o = json;
+		 *            }
+		 *          } );
+		 *          
+		 *          return o;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoad": function ( oSettings ) {
+			var sData = this.oApi._fnReadCookie( oSettings.sCookiePrefix+oSettings.sInstance );
+			var oData;
+	
+			try {
+				oData = (typeof $.parseJSON === 'function') ? 
+					$.parseJSON(sData) : eval( '('+sData+')' );
+			} catch (e) {
+				oData = null;
+			}
+	
+			return oData;
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the saved state prior to loading that state.
+		 * This callback is called when the table is loading state from the stored data, but
+		 * prior to the settings object being modified by the saved state. Note that for 
+		 * plug-in authors, you should use the 'stateLoadParams' event to load parameters for 
+		 * a plug-in.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object that is to be loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never loaded
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoadParams": function (oSettings, oData) {
+		 *          oData.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Disallow state loading by returning false
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoadParams": function (oSettings, oData) {
+		 *          return false;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoadParams": null,
+	
+	
+		/**
+		 * Callback that is called when the state has been loaded from the state saving method
+		 * and the DataTables settings object has been modified as a result of the loaded state.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object that was loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Show an alert with the filtering value that was saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoaded": function (oSettings, oData) {
+		 *          alert( 'Saved filter was: '+oData.oSearch.sSearch );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoaded": null,
+	
+	
+		/**
+		 * Save the table state. This function allows you to define where and how the state
+		 * information for the table is stored - by default it will use a cookie, but you
+		 * might want to use local storage (HTML5) or a server-side database.
+		 *  @type function
+		 *  @member
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object to be saved
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateSave": function (oSettings, oData) {
+		 *          // Send an Ajax request to the server with the state object
+		 *          $.ajax( {
+		 *            "url": "/state_save",
+		 *            "data": oData,
+		 *            "dataType": "json",
+		 *            "method": "POST"
+		 *            "success": function () {}
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSave": function ( oSettings, oData ) {
+			this.oApi._fnCreateCookie( 
+				oSettings.sCookiePrefix+oSettings.sInstance, 
+				this.oApi._fnJsonString(oData), 
+				oSettings.iCookieDuration, 
+				oSettings.sCookiePrefix, 
+				oSettings.fnCookieCallback
+			);
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the state to be saved. Called when the table 
+		 * has changed state a new state save is required. This method allows modification of
+		 * the state saving object prior to actually doing the save, including addition or 
+		 * other state properties or modification. Note that for plug-in authors, you should 
+		 * use the 'stateSaveParams' event to save parameters for a plug-in.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object to be saved
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateSaveParams": function (oSettings, oData) {
+		 *          oData.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSaveParams": null,
+	
+	
+		/**
+		 * Duration of the cookie which is used for storing session information. This
+		 * value is given in seconds.
+		 *  @type int
+		 *  @default 7200 <i>(2 hours)</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iCookieDuration": 60*60*24; // 1 day
+		 *      } );
+		 *    } )
+		 */
+		"iCookieDuration": 7200,
+	
+	
+		/**
+		 * When enabled DataTables will not make a request to the server for the first
+		 * page draw - rather it will use the data already on the page (no sorting etc
+		 * will be applied to it), thus saving on an XHR at load time. iDeferLoading
+		 * is used to indicate that deferred loading is required, but it is also used
+		 * to tell DataTables how many records there are in the full table (allowing
+		 * the information element and pagination to be displayed correctly). In the case
+		 * where a filtering is applied to the table on initial load, this can be
+		 * indicated by giving the parameter as an array, where the first element is
+		 * the number of records available after filtering and the second element is the
+		 * number of records without filtering (allowing the table information element
+		 * to be shown correctly).
+		 *  @type int | array
+		 *  @default null
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    // 57 records available in the table, no filtering applied
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "iDeferLoading": 57
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // 57 records after filtering, 100 without filtering (an initial filter applied)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "iDeferLoading": [ 57, 100 ],
+		 *        "oSearch": {
+		 *          "sSearch": "my_filter"
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"iDeferLoading": null,
+	
+	
+		/**
+		 * Number of rows to display on a single page when using pagination. If
+		 * feature enabled (bLengthChange) then the end user will be able to override
+		 * this to a custom setting using a pop-up menu.
+		 *  @type int
+		 *  @default 10
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayLength": 50
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayLength": 10,
+	
+	
+		/**
+		 * Define the starting point for data display when using DataTables with
+		 * pagination. Note that this parameter is the number of records, rather than
+		 * the page number, so if you have 10 records per page and want to start on
+		 * the third page, it should be "20".
+		 *  @type int
+		 *  @default 0
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayStart": 20
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayStart": 0,
+	
+	
+		/**
+		 * The scroll gap is the amount of scrolling that is left to go before
+		 * DataTables will load the next 'page' of data automatically. You typically
+		 * want a gap which is big enough that the scrolling will be smooth for the
+		 * user, while not so large that it will load more data than need.
+		 *  @type int
+		 *  @default 100
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollInfinite": true,
+		 *        "bScrollCollapse": true,
+		 *        "sScrollY": "200px",
+		 *        "iScrollLoadGap": 50
+		 *      } );
+		 *    } );
+		 */
+		"iScrollLoadGap": 100,
+	
+	
+		/**
+		 * By default DataTables allows keyboard navigation of the table (sorting, paging,
+		 * and filtering) by adding a tabindex attribute to the required elements. This
+		 * allows you to tab through the controls and press the enter key to activate them.
+		 * The tabindex is default 0, meaning that the tab follows the flow of the document.
+		 * You can overrule this using this parameter if you wish. Use a value of -1 to
+		 * disable built-in keyboard navigation.
+		 *  @type int
+		 *  @default 0
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iTabIndex": 1
+		 *      } );
+		 *    } );
+		 */
+		"iTabIndex": 0,
+	
+	
+		/**
+		 * All strings that DataTables uses in the user interface that it creates
+		 * are defined in this object, allowing you to modified them individually or
+		 * completely replace them all as required.
+		 *  @namespace
+		 */
+		"oLanguage": {
+			/**
+			 * Strings that are used for WAI-ARIA labels and controls only (these are not
+			 * actually visible on the page, but will be read by screenreaders, and thus
+			 * must be internationalised as well).
+			 *  @namespace
+			 */
+			"oAria": {
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted ascending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oAria": {
+				 *            "sSortAscending": " - click/return to sort ascending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortAscending": ": activate to sort column ascending",
+	
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted descending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oAria": {
+				 *            "sSortDescending": " - click/return to sort descending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortDescending": ": activate to sort column descending"
+			},
+	
+			/**
+			 * Pagination string used by DataTables for the two built-in pagination
+			 * control types ("two_button" and "full_numbers")
+			 *  @namespace
+			 */
+			"oPaginate": {
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the first page.
+				 *  @type string
+				 *  @default First
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sFirst": "First page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sFirst": "First",
+			
+			
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the last page.
+				 *  @type string
+				 *  @default Last
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sLast": "Last page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sLast": "Last",
+			
+			
+				/**
+				 * Text to use for the 'next' pagination button (to take the user to the 
+				 * next page).
+				 *  @type string
+				 *  @default Next
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sNext": "Next page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sNext": "Next",
+			
+			
+				/**
+				 * Text to use for the 'previous' pagination button (to take the user to  
+				 * the previous page).
+				 *  @type string
+				 *  @default Previous
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sPrevious": "Previous page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sPrevious": "Previous"
+			},
+		
+			/**
+			 * This string is shown in preference to sZeroRecords when the table is
+			 * empty of data (regardless of filtering). Note that this is an optional
+			 * parameter - if it is not given, the value of sZeroRecords will be used
+			 * instead (either the default or given value).
+			 *  @type string
+			 *  @default No data available in table
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sEmptyTable": "No data available in table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sEmptyTable": "No data available in table",
+		
+		
+			/**
+			 * This string gives information to the end user about the information that 
+			 * is current on display on the page. The _START_, _END_ and _TOTAL_ 
+			 * variables are all dynamically replaced as the table display updates, and 
+			 * can be freely moved or removed as the language requirements change.
+			 *  @type string
+			 *  @default Showing _START_ to _END_ of _TOTAL_ entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfo": "Got a total of _TOTAL_ entries to show (_START_ to _END_)"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
+		
+		
+			/**
+			 * Display information string for when the table is empty. Typically the 
+			 * format of this string should match sInfo.
+			 *  @type string
+			 *  @default Showing 0 to 0 of 0 entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoEmpty": "No entries to show"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoEmpty": "Showing 0 to 0 of 0 entries",
+		
+		
+			/**
+			 * When a user filters the information in a table, this string is appended 
+			 * to the information (sInfo) to give an idea of how strong the filtering 
+			 * is. The variable _MAX_ is dynamically updated.
+			 *  @type string
+			 *  @default (filtered from _MAX_ total entries)
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoFiltered": " - filtering from _MAX_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoFiltered": "(filtered from _MAX_ total entries)",
+		
+		
+			/**
+			 * If can be useful to append extra information to the info string at times,
+			 * and this variable does exactly that. This information will be appended to
+			 * the sInfo (sInfoEmpty and sInfoFiltered in whatever combination they are
+			 * being used) at all times.
+			 *  @type string
+			 *  @default <i>Empty string</i>
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoPostFix": "All records shown are derived from real information."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoPostFix": "",
+		
+		
+			/**
+			 * DataTables has a build in number formatter (fnFormatNumber) which is used
+			 * to format large numbers that are used in the table information. By
+			 * default a comma is used, but this can be trivially changed to any
+			 * character you wish with this parameter.
+			 *  @type string
+			 *  @default ,
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoThousands": "'"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoThousands": ",",
+		
+		
+			/**
+			 * Detail the action that will be taken when the drop down menu for the
+			 * pagination length option is changed. The '_MENU_' variable is replaced
+			 * with a default select list of 10, 25, 50 and 100, and can be replaced
+			 * with a custom select box if required.
+			 *  @type string
+			 *  @default Show _MENU_ entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    // Language change only
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLengthMenu": "Display _MENU_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 *    
+			 *  @example
+			 *    // Language and options change
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLengthMenu": 'Display <select>'+
+			 *            '<option value="10">10</option>'+
+			 *            '<option value="20">20</option>'+
+			 *            '<option value="30">30</option>'+
+			 *            '<option value="40">40</option>'+
+			 *            '<option value="50">50</option>'+
+			 *            '<option value="-1">All</option>'+
+			 *            '</select> records'
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLengthMenu": "Show _MENU_ entries",
+		
+		
+			/**
+			 * When using Ajax sourced data and during the first draw when DataTables is
+			 * gathering the data, this message is shown in an empty row in the table to
+			 * indicate to the end user the the data is being loaded. Note that this
+			 * parameter is not used when loading data by server-side processing, just
+			 * Ajax sourced data with client-side processing.
+			 *  @type string
+			 *  @default Loading...
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLoadingRecords": "Please wait - loading..."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLoadingRecords": "Loading...",
+		
+		
+			/**
+			 * Text which is displayed when the table is processing a user action
+			 * (usually a sort command or similar).
+			 *  @type string
+			 *  @default Processing...
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sProcessing": "DataTables is currently busy"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sProcessing": "Processing...",
+		
+		
+			/**
+			 * Details the actions that will be taken when the user types into the
+			 * filtering input text box. The variable "_INPUT_", if used in the string,
+			 * is replaced with the HTML text box for the filtering input allowing
+			 * control over where it appears in the string. If "_INPUT_" is not given
+			 * then the input box is appended to the string automatically.
+			 *  @type string
+			 *  @default Search:
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    // Input text box will be appended at the end automatically
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sSearch": "Filter records:"
+			 *        }
+			 *      } );
+			 *    } );
+			 *    
+			 *  @example
+			 *    // Specify where the filter should appear
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sSearch": "Apply filter _INPUT_ to table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sSearch": "Search:",
+		
+		
+			/**
+			 * All of the language information can be stored in a file on the
+			 * server-side, which DataTables will look up if this parameter is passed.
+			 * It must store the URL of the language file, which is in a JSON format,
+			 * and the object has the same properties as the oLanguage object in the
+			 * initialiser object (i.e. the above parameters). Please refer to one of
+			 * the example language files to see how this works in action.
+			 *  @type string
+			 *  @default <i>Empty string - i.e. disabled</i>
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sUrl": "http://www.sprymedia.co.uk/dataTables/lang.txt"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sUrl": "",
+		
+		
+			/**
+			 * Text shown inside the table records when the is no information to be
+			 * displayed after filtering. sEmptyTable is shown when there is simply no
+			 * information in the table at all (regardless of filtering).
+			 *  @type string
+			 *  @default No matching records found
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sZeroRecords": "No records to display"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sZeroRecords": "No matching records found"
+		},
+	
+	
+		/**
+		 * This parameter allows you to have define the global filtering state at
+		 * initialisation time. As an object the "sSearch" parameter must be
+		 * defined, but all other parameters are optional. When "bRegex" is true,
+		 * the search string will be treated as a regular expression, when false
+		 * (default) it will be treated as a straight string. When "bSmart"
+		 * DataTables will use it's smart filtering methods (to word match at
+		 * any point in the data), when false this will not be done.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "oSearch": {"sSearch": "Initial search"}
+		 *      } );
+		 *    } )
+		 */
+		"oSearch": $.extend( {}, DataTable.models.oSearch ),
+	
+	
+		/**
+		 * By default DataTables will look for the property 'aaData' when obtaining
+		 * data from an Ajax source or for server-side processing - this parameter
+		 * allows that property to be changed. You can use Javascript dotted object
+		 * notation to get a data source for multiple levels of nesting.
+		 *  @type string
+		 *  @default aaData
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    // Get data from { "data": [...] }
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/data.txt",
+		 *        "sAjaxDataProp": "data"
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Get data from { "data": { "inner": [...] } }
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/data.txt",
+		 *        "sAjaxDataProp": "data.inner"
+		 *      } );
+		 *    } );
+		 */
+		"sAjaxDataProp": "aaData",
+	
+	
+		/**
+		 * You can instruct DataTables to load data from an external source using this
+		 * parameter (use aData if you want to pass data in you already have). Simply
+		 * provide a url a JSON object can be obtained from. This object must include
+		 * the parameter 'aaData' which is the data source for the table.
+		 *  @type string
+		 *  @default null
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sAjaxSource": "http://www.sprymedia.co.uk/dataTables/json.php"
+		 *      } );
+		 *    } )
+		 */
+		"sAjaxSource": null,
+	
+	
+		/**
+		 * This parameter can be used to override the default prefix that DataTables
+		 * assigns to a cookie when state saving is enabled.
+		 *  @type string
+		 *  @default SpryMedia_DataTables_
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sCookiePrefix": "my_datatable_",
+		 *      } );
+		 *    } );
+		 */
+		"sCookiePrefix": "SpryMedia_DataTables_",
+	
+	
+		/**
+		 * This initialisation variable allows you to specify exactly where in the
+		 * DOM you want DataTables to inject the various controls it adds to the page
+		 * (for example you might want the pagination controls at the top of the
+		 * table). DIV elements (with or without a custom class) can also be added to
+		 * aid styling. The follow syntax is used:
+		 *   <ul>
+		 *     <li>The following options are allowed:	
+		 *       <ul>
+		 *         <li>'l' - Length changing</li
+		 *         <li>'f' - Filtering input</li>
+		 *         <li>'t' - The table!</li>
+		 *         <li>'i' - Information</li>
+		 *         <li>'p' - Pagination</li>
+		 *         <li>'r' - pRocessing</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following constants are allowed:
+		 *       <ul>
+		 *         <li>'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')</li>
+		 *         <li>'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following syntax is expected:
+		 *       <ul>
+		 *         <li>'&lt;' and '&gt;' - div elements</li>
+		 *         <li>'&lt;"class" and '&gt;' - div with a class</li>
+		 *         <li>'&lt;"#id" and '&gt;' - div with an ID</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>Examples:
+		 *       <ul>
+		 *         <li>'&lt;"wrapper"flipt&gt;'</li>
+		 *         <li>'&lt;lf&lt;t&gt;ip&gt;'</li>
+		 *       </ul>
+		 *     </li>
+		 *   </ul>
+		 *  @type string
+		 *  @default lfrtip <i>(when bJQueryUI is false)</i> <b>or</b> 
+		 *    <"H"lfr>t<"F"ip> <i>(when bJQueryUI is true)</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sDom": '&lt;"top"i&gt;rt&lt;"bottom"flp&gt;&lt;"clear"&gt;'
+		 *      } );
+		 *    } );
+		 */
+		"sDom": "lfrtip",
+	
+	
+		/**
+		 * DataTables features two different built-in pagination interaction methods
+		 * ('two_button' or 'full_numbers') which present different page controls to
+		 * the end user. Further methods can be added using the API (see below).
+		 *  @type string
+		 *  @default two_button
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sPaginationType": "full_numbers"
+		 *      } );
+		 *    } )
+		 */
+		"sPaginationType": "two_button",
+	
+	
+		/**
+		 * Enable horizontal scrolling. When a table is too wide to fit into a certain
+		 * layout, or you have a large number of columns in the table, you can enable
+		 * x-scrolling to show the table in a viewport, which can be scrolled. This
+		 * property can be any CSS unit, or a number (in which case it will be treated
+		 * as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollX": "100%",
+		 *        "bScrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"sScrollX": "",
+	
+	
+		/**
+		 * This property can be used to force a DataTable to use more width than it
+		 * might otherwise do when x-scrolling is enabled. For example if you have a
+		 * table which requires to be well spaced, this parameter is useful for
+		 * "over-sizing" the table, and thus forcing scrolling. This property can by
+		 * any CSS unit, or a number (in which case it will be treated as a pixel
+		 * measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollX": "100%",
+		 *        "sScrollXInner": "110%"
+		 *      } );
+		 *    } );
+		 */
+		"sScrollXInner": "",
+	
+	
+		/**
+		 * Enable vertical scrolling. Vertical scrolling will constrain the DataTable
+		 * to the given height, and enable scrolling for any data which overflows the
+		 * current viewport. This can be used as an alternative to paging to display
+		 * a lot of data in a small area (although paging and scrolling can both be
+		 * enabled at the same time). This property can be any CSS unit, or a number
+		 * (in which case it will be treated as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *    } );
+		 */
+		"sScrollY": "",
+	
+	
+		/**
+		 * Set the HTTP method that is used to make the Ajax call for server-side
+		 * processing or Ajax sourced data.
+		 *  @type string
+		 *  @default GET
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/post.php",
+		 *        "sServerMethod": "POST"
+		 *      } );
+		 *    } );
+		 */
+		"sServerMethod": "GET"
+	};
+	
+	
+	
+	/**
+	 * Column options that can be given to DataTables at initialisation time.
+	 *  @namespace
+	 */
+	DataTable.defaults.columns = {
+		/**
+		 * Allows a column's sorting to take multiple columns into account when 
+		 * doing a sort. For example first name / last name columns make sense to 
+		 * do a multi-column sort over the two columns.
+		 *  @type array
+		 *  @default null <i>Takes the value of the column index automatically</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "aDataSort": [ 0, 1 ], "aTargets": [ 0 ] },
+		 *          { "aDataSort": [ 1, 0 ], "aTargets": [ 1 ] },
+		 *          { "aDataSort": [ 2, 3, 4 ], "aTargets": [ 2 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          { "aDataSort": [ 0, 1 ] },
+		 *          { "aDataSort": [ 1, 0 ] },
+		 *          { "aDataSort": [ 2, 3, 4 ] },
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aDataSort": null,
+	
+	
+		/**
+		 * You can control the default sorting direction, and even alter the behaviour
+		 * of the sort handler (i.e. only allow ascending sorting etc) using this
+		 * parameter.
+		 *  @type array
+		 *  @default [ 'asc', 'desc' ]
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "asSorting": [ "asc" ], "aTargets": [ 1 ] },
+		 *          { "asSorting": [ "desc", "asc", "asc" ], "aTargets": [ 2 ] },
+		 *          { "asSorting": [ "desc" ], "aTargets": [ 3 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          null,
+		 *          { "asSorting": [ "asc" ] },
+		 *          { "asSorting": [ "desc", "asc", "asc" ] },
+		 *          { "asSorting": [ "desc" ] },
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"asSorting": [ 'asc', 'desc' ],
+	
+	
+		/**
+		 * Enable or disable filtering on the data in this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bSearchable": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bSearchable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSearchable": true,
+	
+	
+		/**
+		 * Enable or disable sorting on this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bSortable": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bSortable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSortable": true,
+	
+	
+		/**
+		 * <code>Deprecated</code> When using fnRender() for a column, you may wish 
+		 * to use the original data (before rendering) for sorting and filtering 
+		 * (the default is to used the rendered data that the user can see). This 
+		 * may be useful for dates etc.
+		 * 
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 *  @deprecated
+		 */
+		"bUseRendered": true,
+	
+	
+		/**
+		 * Enable or disable the display of this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bVisible": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bVisible": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bVisible": true,
+		
+		
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @param {int} iCol The column index for aoColumns
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [3],
+		 *          "fnCreatedCell": function (nTd, sData, oData, iRow, iCol) {
+		 *            if ( sData == "1.7" ) {
+		 *              $(nTd).css('color', 'blue')
+		 *            }
+		 *          }
+		 *        } ]
+		 *      });
+		 *    } );
+		 */
+		"fnCreatedCell": null,
+	
+	
+		/**
+		 * <code>Deprecated</code> Custom display function that will be called for the 
+		 * display of each cell in this column.
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type function
+		 *  @param {object} o Object with the following parameters:
+		 *  @param {int}    o.iDataRow The row in aoData
+		 *  @param {int}    o.iDataColumn The column in question
+		 *  @param {array}  o.aData The data for the row in question
+		 *  @param {object} o.oSettings The settings object for this DataTables instance
+		 *  @param {object} o.mDataProp The data property used for this column
+		 *  @param {*}      val The current cell value
+		 *  @returns {string} The string you which to use in the display
+		 *  @dtopt Columns
+		 *  @deprecated
+		 */
+		"fnRender": null,
+	
+	
+		/**
+		 * The column index (starting from 0!) that you wish a sort to be performed
+		 * upon when this column is selected for sorting. This can be used for sorting
+		 * on hidden columns for example.
+		 *  @type int
+		 *  @default -1 <i>Use automatically calculated column index</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "iDataSort": 1, "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "iDataSort": 1 },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"iDataSort": -1,
+	
+	
+		/**
+		 * This parameter has been replaced by mData in DataTables to ensure naming
+		 * consistency. mDataProp can still be used, as there is backwards compatibility
+		 * in DataTables for this option, but it is strongly recommended that you use
+		 * mData in preference to mDataProp.
+		 *  @name DataTable.defaults.columns.mDataProp
+		 */
+	
+	
+		/**
+		 * This property can be used to read data from any JSON data source property,
+		 * including deeply nested objects / properties. mData can be given in a
+		 * number of different ways which effect its behaviour:
+		 *   <ul>
+		 *     <li>integer - treated as an array index for the data source. This is the
+		 *       default that DataTables uses (incrementally increased for each column).</li>
+		 *     <li>string - read an object property from the data source. Note that you can
+		 *       use Javascript dotted notation to read deep properties / arrays from the
+		 *       data source.</li>
+		 *     <li>null - the sDefaultContent option will be used for the cell (null
+		 *       by default, so you will need to specify the default content you want -
+		 *       typically an empty string). This can be useful on generated columns such 
+		 *       as edit / delete action columns.</li>
+		 *     <li>function - the function given will be executed whenever DataTables 
+		 *       needs to set or get the data for a cell in the column. The function 
+		 *       takes three parameters:
+		 *       <ul>
+		 *         <li>{array|object} The data source for the row</li>
+		 *         <li>{string} The type call data requested - this will be 'set' when
+		 *           setting data or 'filter', 'display', 'type', 'sort' or undefined when 
+		 *           gathering data. Note that when <i>undefined</i> is given for the type
+		 *           DataTables expects to get the raw data for the object back</li>
+		 *         <li>{*} Data to set when the second parameter is 'set'.</li>
+		 *       </ul>
+		 *       The return value from the function is not required when 'set' is the type
+		 *       of call, but otherwise the return is what will be used for the data
+		 *       requested.</li>
+		 *    </ul>
+		 *
+		 * Note that prior to DataTables 1.9.2 mData was called mDataProp. The name change
+		 * reflects the flexibility of this property and is consistent with the naming of
+		 * mRender. If 'mDataProp' is given, then it will still be used by DataTables, as
+		 * it automatically maps the old name to the new if required.
+		 *  @type string|int|function|null
+		 *  @default null <i>Use automatically calculated column index</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Read table data from objects
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/deep.txt",
+		 *        "aoColumns": [
+		 *          { "mData": "engine" },
+		 *          { "mData": "browser" },
+		 *          { "mData": "platform.inner" },
+		 *          { "mData": "platform.details.0" },
+		 *          { "mData": "platform.details.1" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Using mData as a function to provide different information for
+		 *    // sorting, filtering and display. In this case, currency (price)
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [ 0 ],
+		 *          "mData": function ( source, type, val ) {
+		 *            if (type === 'set') {
+		 *              source.price = val;
+		 *              // Store the computed dislay and filter values for efficiency
+		 *              source.price_display = val=="" ? "" : "$"+numberFormat(val);
+		 *              source.price_filter  = val=="" ? "" : "$"+numberFormat(val)+" "+val;
+		 *              return;
+		 *            }
+		 *            else if (type === 'display') {
+		 *              return source.price_display;
+		 *            }
+		 *            else if (type === 'filter') {
+		 *              return source.price_filter;
+		 *            }
+		 *            // 'sort', 'type' and undefined all just use the integer
+		 *            return source.price;
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"mData": null,
+	
+	
+		/**
+		 * This property is the rendering partner to mData and it is suggested that
+		 * when you want to manipulate data for display (including filtering, sorting etc)
+		 * but not altering the underlying data for the table, use this property. mData
+		 * can actually do everything this property can and more, but this parameter is
+		 * easier to use since there is no 'set' option. Like mData is can be given
+		 * in a number of different ways to effect its behaviour, with the addition of 
+		 * supporting array syntax for easy outputting of arrays (including arrays of
+		 * objects):
+		 *   <ul>
+		 *     <li>integer - treated as an array index for the data source. This is the
+		 *       default that DataTables uses (incrementally increased for each column).</li>
+		 *     <li>string - read an object property from the data source. Note that you can
+		 *       use Javascript dotted notation to read deep properties / arrays from the
+		 *       data source and also array brackets to indicate that the data reader should
+		 *       loop over the data source array. When characters are given between the array
+		 *       brackets, these characters are used to join the data source array together.
+		 *       For example: "accounts[, ].name" would result in a comma separated list with
+		 *       the 'name' value from the 'accounts' array of objects.</li>
+		 *     <li>function - the function given will be executed whenever DataTables 
+		 *       needs to set or get the data for a cell in the column. The function 
+		 *       takes three parameters:
+		 *       <ul>
+		 *         <li>{array|object} The data source for the row (based on mData)</li>
+		 *         <li>{string} The type call data requested - this will be 'filter', 'display', 
+		 *           'type' or 'sort'.</li>
+		 *         <li>{array|object} The full data source for the row (not based on mData)</li>
+		 *       </ul>
+		 *       The return value from the function is what will be used for the data
+		 *       requested.</li>
+		 *    </ul>
+		 *  @type string|int|function|null
+		 *  @default null <i>Use mData</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Create a comma separated list from an array of objects
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/deep.txt",
+		 *        "aoColumns": [
+		 *          { "mData": "engine" },
+		 *          { "mData": "browser" },
+		 *          {
+		 *            "mData": "platform",
+		 *            "mRender": "[, ].name"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Use as a function to create a link from the data source
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *        {
+		 *          "aTargets": [ 0 ],
+		 *          "mData": "download_link",
+		 *          "mRender": function ( data, type, full ) {
+		 *            return '<a href="'+data+'">Download</a>';
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"mRender": null,
+	
+	
+		/**
+		 * Change the cell type created for the column - either TD cells or TH cells. This
+		 * can be useful as TH cells have semantic meaning in the table body, allowing them
+		 * to act as a header for a row (you may wish to add scope='row' to the TH elements).
+		 *  @type string
+		 *  @default td
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Make the first column use TH cells
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [ 0 ],
+		 *          "sCellType": "th"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"sCellType": "td",
+	
+	
+		/**
+		 * Class to give to each cell in this column.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sClass": "my_class", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sClass": "my_class" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sClass": "",
+		
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer 
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 * Generally you shouldn't need this, and it is not documented on the 
+		 * general DataTables.net documentation
+		 *  @type string
+		 *  @default <i>Empty string<i>
+		 *  @dtopt Columns
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "sContentPadding": "mmm"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sContentPadding": "",
+	
+	
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          {
+		 *            "mData": null,
+		 *            "sDefaultContent": "Edit",
+		 *            "aTargets": [ -1 ]
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "mData": null,
+		 *            "sDefaultContent": "Edit"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sDefaultContent": null,
+	
+	
+		/**
+		 * This parameter is only used in DataTables' server-side processing. It can
+		 * be exceptionally useful to know what columns are being displayed on the
+		 * client side, and to map these to database fields. When defined, the names
+		 * also allow DataTables to reorder information from the server if it comes
+		 * back in an unexpected order (i.e. if you switch your columns around on the
+		 * client-side, your server-side code does not also need updating).
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sName": "engine", "aTargets": [ 0 ] },
+		 *          { "sName": "browser", "aTargets": [ 1 ] },
+		 *          { "sName": "platform", "aTargets": [ 2 ] },
+		 *          { "sName": "version", "aTargets": [ 3 ] },
+		 *          { "sName": "grade", "aTargets": [ 4 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sName": "engine" },
+		 *          { "sName": "browser" },
+		 *          { "sName": "platform" },
+		 *          { "sName": "version" },
+		 *          { "sName": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sName": "",
+	
+	
+		/**
+		 * Defines a data source type for the sorting which can be used to read
+		 * real-time information from the table (updating the internally cached
+		 * version) prior to sorting. This allows sorting to occur on user editable
+		 * elements such as form inputs.
+		 *  @type string
+		 *  @default std
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "sSortDataType": "dom-text", "aTargets": [ 2, 3 ] },
+		 *          { "sType": "numeric", "aTargets": [ 3 ] },
+		 *          { "sSortDataType": "dom-select", "aTargets": [ 4 ] },
+		 *          { "sSortDataType": "dom-checkbox", "aTargets": [ 5 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          null,
+		 *          null,
+		 *          { "sSortDataType": "dom-text" },
+		 *          { "sSortDataType": "dom-text", "sType": "numeric" },
+		 *          { "sSortDataType": "dom-select" },
+		 *          { "sSortDataType": "dom-checkbox" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sSortDataType": "std",
+	
+	
+		/**
+		 * The title of this column.
+		 *  @type string
+		 *  @default null <i>Derived from the 'TH' value for this column in the 
+		 *    original HTML table.</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sTitle": "My column title", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sTitle": "My column title" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sTitle": null,
+	
+	
+		/**
+		 * The type allows you to specify how the data for this column will be sorted.
+		 * Four types (string, numeric, date and html (which will strip HTML tags
+		 * before sorting)) are currently available. Note that only date formats
+		 * understood by Javascript's Date() object will be accepted as type date. For
+		 * example: "Mar 26, 2008 5:03 PM". May take the values: 'string', 'numeric',
+		 * 'date' or 'html' (by default). Further types can be adding through
+		 * plug-ins.
+		 *  @type string
+		 *  @default null <i>Auto-detected from raw data</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sType": "html", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sType": "html" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sType": null,
+	
+	
+		/**
+		 * Defining the width of the column, this parameter may take any CSS value
+		 * (3em, 20px etc). DataTables apples 'smart' widths to columns which have not
+		 * been given a specific width through this interface ensuring that the table
+		 * remains readable.
+		 *  @type string
+		 *  @default null <i>Automatic</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sWidth": "20%", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sWidth": "20%" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sWidth": null
+	};
+	
+	
+	
+	/**
+	 * DataTables settings object - this holds all the information needed for a
+	 * given table, including configuration, data and current application of the
+	 * table options. DataTables does not have a single instance for each DataTable
+	 * with the settings attached to that instance, but rather instances of the
+	 * DataTable "class" are created on-the-fly as needed (typically by a 
+	 * $().dataTable() call) and the settings object is then applied to that
+	 * instance.
+	 * 
+	 * Note that this object is related to {@link DataTable.defaults} but this 
+	 * one is the internal data store for DataTables's cache of columns. It should
+	 * NOT be manipulated outside of DataTables. Any configuration should be done
+	 * through the initialisation options.
+	 *  @namespace
+	 *  @todo Really should attach the settings object to individual instances so we
+	 *    don't need to create new instances on each $().dataTable() call (if the
+	 *    table already exists). It would also save passing oSettings around and
+	 *    into every single function. However, this is a very significant 
+	 *    architecture change for DataTables and will almost certainly break
+	 *    backwards compatibility with older installations. This is something that
+	 *    will be done in 2.0.
+	 */
+	DataTable.models.oSettings = {
+		/**
+		 * Primary features of DataTables and their enablement state.
+		 *  @namespace
+		 */
+		"oFeatures": {
+			
+			/**
+			 * Flag to say if DataTables should automatically try to calculate the
+			 * optimum table and columns widths (true) or not (false).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoWidth": null,
+	
+			/**
+			 * Delay the creation of TR and TD elements until they are actually
+			 * needed by a driven page draw. This can give a significant speed
+			 * increase for Ajax source and Javascript source data, but makes no
+			 * difference at all fro DOM and server-side processing tables.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bDeferRender": null,
+			
+			/**
+			 * Enable filtering on the table or not. Note that if this is disabled
+			 * then there is no filtering at all on the table, including fnFilter.
+			 * To just remove the filtering input use sDom and remove the 'f' option.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bFilter": null,
+			
+			/**
+			 * Table information element (the 'Showing x of y records' div) enable
+			 * flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfo": null,
+			
+			/**
+			 * Present a user control allowing the end user to change the page size
+			 * when pagination is enabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bLengthChange": null,
+	
+			/**
+			 * Pagination enabled or not. Note that if this is disabled then length
+			 * changing must also be disabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bPaginate": null,
+			
+			/**
+			 * Processing indicator enable flag whenever DataTables is enacting a
+			 * user request - typically an Ajax request for server-side processing.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bProcessing": null,
+			
+			/**
+			 * Server-side processing enabled flag - when enabled DataTables will
+			 * get all data from the server for every draw - there is no filtering,
+			 * sorting or paging done on the client-side.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bServerSide": null,
+			
+			/**
+			 * Sorting enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSort": null,
+			
+			/**
+			 * Apply a class to the columns which are being sorted to provide a
+			 * visual highlight or not. This can slow things down when enabled since
+			 * there is a lot of DOM interaction.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSortClasses": null,
+			
+			/**
+			 * State saving enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bStateSave": null
+		},
+		
+	
+		/**
+		 * Scrolling settings for a table.
+		 *  @namespace
+		 */
+		"oScroll": {
+			/**
+			 * Indicate if DataTables should be allowed to set the padding / margin
+			 * etc for the scrolling header elements or not. Typically you will want
+			 * this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoCss": null,
+			
+			/**
+			 * When the table is shorter in height than sScrollY, collapse the
+			 * table container down to the height of the table (when true).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bCollapse": null,
+			
+			/**
+			 * Infinite scrolling enablement flag. Now deprecated in favour of
+			 * using the Scroller plug-in.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfinite": null,
+			
+			/**
+			 * Width of the scrollbar for the web-browser's platform. Calculated
+			 * during table initialisation.
+			 *  @type int
+			 *  @default 0
+			 */
+			"iBarWidth": 0,
+			
+			/**
+			 * Space (in pixels) between the bottom of the scrolling container and 
+			 * the bottom of the scrolling viewport before the next page is loaded
+			 * when using infinite scrolling.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type int
+			 */
+			"iLoadGap": null,
+			
+			/**
+			 * Viewport width for horizontal scrolling. Horizontal scrolling is 
+			 * disabled if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sX": null,
+			
+			/**
+			 * Width to expand the table to when using x-scrolling. Typically you
+			 * should not need to use this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 *  @deprecated
+			 */
+			"sXInner": null,
+			
+			/**
+			 * Viewport height for vertical scrolling. Vertical scrolling is disabled
+			 * if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sY": null
+		},
+		
+		/**
+		 * Language information for the table.
+		 *  @namespace
+		 *  @extends DataTable.defaults.oLanguage
+		 */
+		"oLanguage": {
+			/**
+			 * Information callback function. See 
+			 * {@link DataTable.defaults.fnInfoCallback}
+			 *  @type function
+			 *  @default null
+			 */
+			"fnInfoCallback": null
+		},
+		
+		/**
+		 * Browser support parameters
+		 *  @namespace
+		 */
+		"oBrowser": {
+			/**
+			 * Indicate if the browser incorrectly calculates width:100% inside a
+			 * scrolling element (IE6/7)
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bScrollOversize": false
+		},
+		
+		/**
+		 * Array referencing the nodes which are used for the features. The 
+		 * parameters of this object match what is allowed by sDom - i.e.
+		 *   <ul>
+		 *     <li>'l' - Length changing</li>
+		 *     <li>'f' - Filtering input</li>
+		 *     <li>'t' - The table!</li>
+		 *     <li>'i' - Information</li>
+		 *     <li>'p' - Pagination</li>
+		 *     <li>'r' - pRocessing</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aanFeatures": [],
+		
+		/**
+		 * Store data information - see {@link DataTable.models.oRow} for detailed
+		 * information.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoData": [],
+		
+		/**
+		 * Array of indexes which are in the current display (after filtering etc)
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplay": [],
+		
+		/**
+		 * Array of indexes for display - no filtering
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplayMaster": [],
+		
+		/**
+		 * Store information about each column that is in use
+		 *  @type array
+		 *  @default []
+		 */
+		"aoColumns": [],
+		
+		/**
+		 * Store information about the table's header
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeader": [],
+		
+		/**
+		 * Store information about the table's footer
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooter": [],
+		
+		/**
+		 * Search data array for regular expression searching
+		 *  @type array
+		 *  @default []
+		 */
+		"asDataSearch": [],
+		
+		/**
+		 * Store the applied global search information in case we want to force a 
+		 * research or compare the old search to a new one.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 */
+		"oPreviousSearch": {},
+		
+		/**
+		 * Store the applied search for each column - see 
+		 * {@link DataTable.models.oSearch} for the format that is used for the
+		 * filtering information for each column.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreSearchCols": [],
+		
+		/**
+		 * Sorting that is applied to the table. Note that the inner arrays are
+		 * used in the following manner:
+		 * <ul>
+		 *   <li>Index 0 - column number</li>
+		 *   <li>Index 1 - current sorting direction</li>
+		 *   <li>Index 2 - index of asSorting for this column</li>
+		 * </ul>
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @todo These inner arrays should really be objects
+		 */
+		"aaSorting": null,
+		
+		/**
+		 * Sorting that is always applied to the table (i.e. prefixed in front of
+		 * aaSorting).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array|null
+		 *  @default null
+		 */
+		"aaSortingFixed": null,
+		
+		/**
+		 * Classes to use for the striping of a table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"asStripeClasses": null,
+		
+		/**
+		 * If restoring a table - we should restore its striping classes as well
+		 *  @type array
+		 *  @default []
+		 */
+		"asDestroyStripes": [],
+		
+		/**
+		 * If restoring a table - we should restore its width 
+		 *  @type int
+		 *  @default 0
+		 */
+		"sDestroyWidth": 0,
+		
+		/**
+		 * Callback functions array for every time a row is inserted (i.e. on a draw).
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCallback": [],
+		
+		/**
+		 * Callback functions for the header on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeaderCallback": [],
+		
+		/**
+		 * Callback function for the footer on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooterCallback": [],
+		
+		/**
+		 * Array of callback functions for draw callback functions
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDrawCallback": [],
+		
+		/**
+		 * Array of callback functions for row created function
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCreatedCallback": [],
+		
+		/**
+		 * Callback functions for just before the table is redrawn. A return of 
+		 * false will be used to cancel the draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreDrawCallback": [],
+		
+		/**
+		 * Callback functions for when the table has been initialised.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoInitComplete": [],
+	
+		
+		/**
+		 * Callbacks for modifying the settings to be stored for state saving, prior to
+		 * saving state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSaveParams": [],
+		
+		/**
+		 * Callbacks for modifying the settings that have been stored for state saving
+		 * prior to using the stored values to restore the state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoadParams": [],
+		
+		/**
+		 * Callbacks for operating on the settings object once the saved state has been
+		 * loaded
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoaded": [],
+		
+		/**
+		 * Cache the table ID for quick access
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sTableId": "",
+		
+		/**
+		 * The TABLE node for the main table
+		 *  @type node
+		 *  @default null
+		 */
+		"nTable": null,
+		
+		/**
+		 * Permanent ref to the thead element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTHead": null,
+		
+		/**
+		 * Permanent ref to the tfoot element - if it exists
+		 *  @type node
+		 *  @default null
+		 */
+		"nTFoot": null,
+		
+		/**
+		 * Permanent ref to the tbody element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTBody": null,
+		
+		/**
+		 * Cache the wrapper node (contains all DataTables controlled elements)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTableWrapper": null,
+		
+		/**
+		 * Indicate if when using server-side processing the loading of data 
+		 * should be deferred until the second draw.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDeferLoading": false,
+		
+		/**
+		 * Indicate if all required information has been read in
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bInitialised": false,
+		
+		/**
+		 * Information about open rows. Each object in the array has the parameters
+		 * 'nTr' and 'nParent'
+		 *  @type array
+		 *  @default []
+		 */
+		"aoOpenRows": [],
+		
+		/**
+		 * Dictate the positioning of DataTables' control elements - see
+		 * {@link DataTable.model.oInit.sDom}.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sDom": null,
+		
+		/**
+		 * Which type of pagination should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string 
+		 *  @default two_button
+		 */
+		"sPaginationType": "two_button",
+		
+		/**
+		 * The cookie duration (for bStateSave) in seconds.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type int
+		 *  @default 0
+		 */
+		"iCookieDuration": 0,
+		
+		/**
+		 * The cookie name prefix.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sCookiePrefix": "",
+		
+		/**
+		 * Callback function for cookie creation.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 *  @default null
+		 */
+		"fnCookieCallback": null,
+		
+		/**
+		 * Array of callback functions for state saving. Each array element is an 
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings
+		 *       and the JSON string to save that has been thus far created. Returns
+		 *       a JSON string to be inserted into a json object 
+		 *       (i.e. '"param": [ 0, 1, 2]')</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSave": [],
+		
+		/**
+		 * Array of callback functions for state loading. Each array element is an 
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings 
+		 *       and the object stored. May return false to cancel state loading</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoad": [],
+		
+		/**
+		 * State that was loaded from the cookie. Useful for back reference
+		 *  @type object
+		 *  @default null
+		 */
+		"oLoadedState": null,
+		
+		/**
+		 * Source url for AJAX data for the table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sAjaxSource": null,
+		
+		/**
+		 * Property from a given object from which to read the table data from. This
+		 * can be an empty string (when not server-side processing), in which case 
+		 * it is  assumed an an array is given directly.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sAjaxDataProp": null,
+		
+		/**
+		 * Note if draw should be blocked while getting data
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bAjaxDataGet": true,
+		
+		/**
+		 * The last jQuery XHR object that was used for server-side data gathering. 
+		 * This can be used for working with the XHR information in one of the 
+		 * callbacks
+		 *  @type object
+		 *  @default null
+		 */
+		"jqXHR": null,
+		
+		/**
+		 * Function to get the server-side data.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnServerData": null,
+		
+		/**
+		 * Functions which are called prior to sending an Ajax request so extra 
+		 * parameters can easily be sent to the server
+		 *  @type array
+		 *  @default []
+		 */
+		"aoServerParams": [],
+		
+		/**
+		 * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if 
+		 * required).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sServerMethod": null,
+		
+		/**
+		 * Format numbers for display.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnFormatNumber": null,
+		
+		/**
+		 * List of options that can be used for the user selectable length menu.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"aLengthMenu": null,
+		
+		/**
+		 * Counter for the draws that the table does. Also used as a tracker for
+		 * server-side processing
+		 *  @type int
+		 *  @default 0
+		 */
+		"iDraw": 0,
+		
+		/**
+		 * Indicate if a redraw is being done - useful for Ajax
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDrawing": false,
+		
+		/**
+		 * Draw index (iDraw) of the last error when parsing the returned data
+		 *  @type int
+		 *  @default -1
+		 */
+		"iDrawError": -1,
+		
+		/**
+		 * Paging display length
+		 *  @type int
+		 *  @default 10
+		 */
+		"_iDisplayLength": 10,
+	
+		/**
+		 * Paging start point - aiDisplay index
+		 *  @type int
+		 *  @default 0
+		 */
+		"_iDisplayStart": 0,
+	
+		/**
+		 * Paging end point - aiDisplay index. Use fnDisplayEnd rather than
+		 * this property to get the end point
+		 *  @type int
+		 *  @default 10
+		 *  @private
+		 */
+		"_iDisplayEnd": 10,
+		
+		/**
+		 * Server-side processing - number of records in the result set
+		 * (i.e. before filtering), Use fnRecordsTotal rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type int
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsTotal": 0,
+	
+		/**
+		 * Server-side processing - number of records in the current display set
+		 * (i.e. after filtering). Use fnRecordsDisplay rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type boolean
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsDisplay": 0,
+		
+		/**
+		 * Flag to indicate if jQuery UI marking and classes should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bJUI": null,
+		
+		/**
+		 * The classes to use for the table
+		 *  @type object
+		 *  @default {}
+		 */
+		"oClasses": {},
+		
+		/**
+		 * Flag attached to the settings object so you can check in the draw 
+		 * callback if filtering has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bFiltered": false,
+		
+		/**
+		 * Flag attached to the settings object so you can check in the draw 
+		 * callback if sorting has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bSorted": false,
+		
+		/**
+		 * Indicate that if multiple rows are in the header and there is more than 
+		 * one unique cell per column, if the top one (true) or bottom one (false) 
+		 * should be used for sorting / title by DataTables.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bSortCellsTop": null,
+		
+		/**
+		 * Initialisation object that is used for the table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInit": null,
+		
+		/**
+		 * Destroy callback functions - for plug-ins to attach themselves to the
+		 * destroy so they can clean up markup and events.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDestroyCallback": [],
+	
+		
+		/**
+		 * Get the number of records in the current record set, before filtering
+		 *  @type function
+		 */
+		"fnRecordsTotal": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				return parseInt(this._iRecordsTotal, 10);
+			} else {
+				return this.aiDisplayMaster.length;
+			}
+		},
+		
+		/**
+		 * Get the number of records in the current record set, after filtering
+		 *  @type function
+		 */
+		"fnRecordsDisplay": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				return parseInt(this._iRecordsDisplay, 10);
+			} else {
+				return this.aiDisplay.length;
+			}
+		},
+		
+		/**
+		 * Set the display end point - aiDisplay index
+		 *  @type function
+		 *  @todo Should do away with _iDisplayEnd and calculate it on-the-fly here
+		 */
+		"fnDisplayEnd": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				if ( this.oFeatures.bPaginate === false || this._iDisplayLength == -1 ) {
+					return this._iDisplayStart+this.aiDisplay.length;
+				} else {
+					return Math.min( this._iDisplayStart+this._iDisplayLength, 
+						this._iRecordsDisplay );
+				}
+			} else {
+				return this._iDisplayEnd;
+			}
+		},
+		
+		/**
+		 * The DataTables object for this table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInstance": null,
+		
+		/**
+		 * Unique identifier for each instance of the DataTables object. If there
+		 * is an ID on the table node, then it takes that value, otherwise an
+		 * incrementing internal counter is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sInstance": null,
+	
+		/**
+		 * tabindex attribute value that is added to DataTables control elements, allowing
+		 * keyboard navigation of the table and its controls.
+		 */
+		"iTabIndex": 0,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollHead": null,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollFoot": null
+	};
+
+	/**
+	 * Extension object for DataTables that is used to provide all extension options.
+	 * 
+	 * Note that the <i>DataTable.ext</i> object is available through
+	 * <i>jQuery.fn.dataTable.ext</i> where it may be accessed and manipulated. It is
+	 * also aliased to <i>jQuery.fn.dataTableExt</i> for historic reasons.
+	 *  @namespace
+	 *  @extends DataTable.models.ext
+	 */
+	DataTable.ext = $.extend( true, {}, DataTable.models.ext );
+	
+	$.extend( DataTable.ext.oStdClasses, {
+		"sTable": "dataTable",
+	
+		/* Two buttons buttons */
+		"sPagePrevEnabled": "paginate_enabled_previous",
+		"sPagePrevDisabled": "paginate_disabled_previous",
+		"sPageNextEnabled": "paginate_enabled_next",
+		"sPageNextDisabled": "paginate_disabled_next",
+		"sPageJUINext": "",
+		"sPageJUIPrev": "",
+		
+		/* Full numbers paging buttons */
+		"sPageButton": "paginate_button",
+		"sPageButtonActive": "paginate_active",
+		"sPageButtonStaticDisabled": "paginate_button paginate_button_disabled",
+		"sPageFirst": "first",
+		"sPagePrevious": "previous",
+		"sPageNext": "next",
+		"sPageLast": "last",
+		
+		/* Striping classes */
+		"sStripeOdd": "odd",
+		"sStripeEven": "even",
+		
+		/* Empty row */
+		"sRowEmpty": "dataTables_empty",
+		
+		/* Features */
+		"sWrapper": "dataTables_wrapper",
+		"sFilter": "dataTables_filter",
+		"sInfo": "dataTables_info",
+		"sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */
+		"sLength": "dataTables_length",
+		"sProcessing": "dataTables_processing",
+		
+		/* Sorting */
+		"sSortAsc": "sorting_asc",
+		"sSortDesc": "sorting_desc",
+		"sSortable": "sorting", /* Sortable in both directions */
+		"sSortableAsc": "sorting_asc_disabled",
+		"sSortableDesc": "sorting_desc_disabled",
+		"sSortableNone": "sorting_disabled",
+		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
+		"sSortJUIAsc": "",
+		"sSortJUIDesc": "",
+		"sSortJUI": "",
+		"sSortJUIAscAllowed": "",
+		"sSortJUIDescAllowed": "",
+		"sSortJUIWrapper": "",
+		"sSortIcon": "",
+		
+		/* Scrolling */
+		"sScrollWrapper": "dataTables_scroll",
+		"sScrollHead": "dataTables_scrollHead",
+		"sScrollHeadInner": "dataTables_scrollHeadInner",
+		"sScrollBody": "dataTables_scrollBody",
+		"sScrollFoot": "dataTables_scrollFoot",
+		"sScrollFootInner": "dataTables_scrollFootInner",
+		
+		/* Misc */
+		"sFooterTH": "",
+		"sJUIHeader": "",
+		"sJUIFooter": ""
+	} );
+	
+	
+	$.extend( DataTable.ext.oJUIClasses, DataTable.ext.oStdClasses, {
+		/* Two buttons buttons */
+		"sPagePrevEnabled": "fg-button ui-button ui-state-default ui-corner-left",
+		"sPagePrevDisabled": "fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",
+		"sPageNextEnabled": "fg-button ui-button ui-state-default ui-corner-right",
+		"sPageNextDisabled": "fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",
+		"sPageJUINext": "ui-icon ui-icon-circle-arrow-e",
+		"sPageJUIPrev": "ui-icon ui-icon-circle-arrow-w",
+		
+		/* Full numbers paging buttons */
+		"sPageButton": "fg-button ui-button ui-state-default",
+		"sPageButtonActive": "fg-button ui-button ui-state-default ui-state-disabled",
+		"sPageButtonStaticDisabled": "fg-button ui-button ui-state-default ui-state-disabled",
+		"sPageFirst": "first ui-corner-tl ui-corner-bl",
+		"sPageLast": "last ui-corner-tr ui-corner-br",
+		
+		/* Features */
+		"sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
+			"ui-buttonset-multi paging_", /* Note that the type is postfixed */
+		
+		/* Sorting */
+		"sSortAsc": "ui-state-default",
+		"sSortDesc": "ui-state-default",
+		"sSortable": "ui-state-default",
+		"sSortableAsc": "ui-state-default",
+		"sSortableDesc": "ui-state-default",
+		"sSortableNone": "ui-state-default",
+		"sSortJUIAsc": "css_right ui-icon ui-icon-triangle-1-n",
+		"sSortJUIDesc": "css_right ui-icon ui-icon-triangle-1-s",
+		"sSortJUI": "css_right ui-icon ui-icon-carat-2-n-s",
+		"sSortJUIAscAllowed": "css_right ui-icon ui-icon-carat-1-n",
+		"sSortJUIDescAllowed": "css_right ui-icon ui-icon-carat-1-s",
+		"sSortJUIWrapper": "DataTables_sort_wrapper",
+		"sSortIcon": "DataTables_sort_icon",
+		
+		/* Scrolling */
+		"sScrollHead": "dataTables_scrollHead ui-state-default",
+		"sScrollFoot": "dataTables_scrollFoot ui-state-default",
+		
+		/* Misc */
+		"sFooterTH": "ui-state-default",
+		"sJUIHeader": "fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix",
+		"sJUIFooter": "fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix"
+	} );
+	
+	/*
+	 * Variable: oPagination
+	 * Purpose:  
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	$.extend( DataTable.ext.oPagination, {
+		/*
+		 * Variable: two_button
+		 * Purpose:  Standard two button (forward/back) pagination
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"two_button": {
+			/*
+			 * Function: oPagination.two_button.fnInit
+			 * Purpose:  Initialise dom elements required for pagination with forward/back buttons only
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           node:nPaging - the DIV which contains this pagination control
+			 *           function:fnCallbackDraw - draw function which must be called on update
+			 */
+			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )
+			{
+				var oLang = oSettings.oLanguage.oPaginate;
+				var oClasses = oSettings.oClasses;
+				var fnClickHandler = function ( e ) {
+					if ( oSettings.oApi._fnPageChange( oSettings, e.data.action ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				};
+	
+				var sAppend = (!oSettings.bJUI) ?
+					'<a class="'+oSettings.oClasses.sPagePrevDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button">'+oLang.sPrevious+'</a>'+
+					'<a class="'+oSettings.oClasses.sPageNextDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button">'+oLang.sNext+'</a>'
+					:
+					'<a class="'+oSettings.oClasses.sPagePrevDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button"><span class="'+oSettings.oClasses.sPageJUIPrev+'"></span></a>'+
+					'<a class="'+oSettings.oClasses.sPageNextDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button"><span class="'+oSettings.oClasses.sPageJUINext+'"></span></a>';
+				$(nPaging).append( sAppend );
+				
+				var els = $('a', nPaging);
+				var nPrevious = els[0],
+					nNext = els[1];
+				
+				oSettings.oApi._fnBindAction( nPrevious, {action: "previous"}, fnClickHandler );
+				oSettings.oApi._fnBindAction( nNext,     {action: "next"},     fnClickHandler );
+				
+				/* ID the first elements only */
+				if ( !oSettings.aanFeatures.p )
+				{
+					nPaging.id = oSettings.sTableId+'_paginate';
+					nPrevious.id = oSettings.sTableId+'_previous';
+					nNext.id = oSettings.sTableId+'_next';
+	
+					nPrevious.setAttribute('aria-controls', oSettings.sTableId);
+					nNext.setAttribute('aria-controls', oSettings.sTableId);
+				}
+			},
+			
+			/*
+			 * Function: oPagination.two_button.fnUpdate
+			 * Purpose:  Update the two button pagination at the end of the draw
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           function:fnCallbackDraw - draw function to call on page change
+			 */
+			"fnUpdate": function ( oSettings, fnCallbackDraw )
+			{
+				if ( !oSettings.aanFeatures.p )
+				{
+					return;
+				}
+				
+				var oClasses = oSettings.oClasses;
+				var an = oSettings.aanFeatures.p;
+				var nNode;
+	
+				/* Loop over each instance of the pager */
+				for ( var i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					nNode = an[i].firstChild;
+					if ( nNode )
+					{
+						/* Previous page */
+						nNode.className = ( oSettings._iDisplayStart === 0 ) ?
+						    oClasses.sPagePrevDisabled : oClasses.sPagePrevEnabled;
+						    
+						/* Next page */
+						nNode = nNode.nextSibling;
+						nNode.className = ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) ?
+						    oClasses.sPageNextDisabled : oClasses.sPageNextEnabled;
+					}
+				}
+			}
+		},
+		
+		
+		/*
+		 * Variable: iFullNumbersShowPages
+		 * Purpose:  Change the number of pages which can be seen
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"iFullNumbersShowPages": 5,
+		
+		/*
+		 * Variable: full_numbers
+		 * Purpose:  Full numbers pagination
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"full_numbers": {
+			/*
+			 * Function: oPagination.full_numbers.fnInit
+			 * Purpose:  Initialise dom elements required for pagination with a list of the pages
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           node:nPaging - the DIV which contains this pagination control
+			 *           function:fnCallbackDraw - draw function which must be called on update
+			 */
+			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )
+			{
+				var oLang = oSettings.oLanguage.oPaginate;
+				var oClasses = oSettings.oClasses;
+				var fnClickHandler = function ( e ) {
+					if ( oSettings.oApi._fnPageChange( oSettings, e.data.action ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				};
+	
+				$(nPaging).append(
+					'<a  tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageFirst+'">'+oLang.sFirst+'</a>'+
+					'<a  tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPagePrevious+'">'+oLang.sPrevious+'</a>'+
+					'<span></span>'+
+					'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageNext+'">'+oLang.sNext+'</a>'+
+					'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageLast+'">'+oLang.sLast+'</a>'
+				);
+				var els = $('a', nPaging);
+				var nFirst = els[0],
+					nPrev = els[1],
+					nNext = els[2],
+					nLast = els[3];
+				
+				oSettings.oApi._fnBindAction( nFirst, {action: "first"},    fnClickHandler );
+				oSettings.oApi._fnBindAction( nPrev,  {action: "previous"}, fnClickHandler );
+				oSettings.oApi._fnBindAction( nNext,  {action: "next"},     fnClickHandler );
+				oSettings.oApi._fnBindAction( nLast,  {action: "last"},     fnClickHandler );
+				
+				/* ID the first elements only */
+				if ( !oSettings.aanFeatures.p )
+				{
+					nPaging.id = oSettings.sTableId+'_paginate';
+					nFirst.id =oSettings.sTableId+'_first';
+					nPrev.id =oSettings.sTableId+'_previous';
+					nNext.id =oSettings.sTableId+'_next';
+					nLast.id =oSettings.sTableId+'_last';
+				}
+			},
+			
+			/*
+			 * Function: oPagination.full_numbers.fnUpdate
+			 * Purpose:  Update the list of page buttons shows
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           function:fnCallbackDraw - draw function to call on page change
+			 */
+			"fnUpdate": function ( oSettings, fnCallbackDraw )
+			{
+				if ( !oSettings.aanFeatures.p )
+				{
+					return;
+				}
+				
+				var iPageCount = DataTable.ext.oPagination.iFullNumbersShowPages;
+				var iPageCountHalf = Math.floor(iPageCount / 2);
+				var iPages = Math.ceil((oSettings.fnRecordsDisplay()) / oSettings._iDisplayLength);
+				var iCurrentPage = Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength) + 1;
+				var sList = "";
+				var iStartButton, iEndButton, i, iLen;
+				var oClasses = oSettings.oClasses;
+				var anButtons, anStatic, nPaginateList, nNode;
+				var an = oSettings.aanFeatures.p;
+				var fnBind = function (j) {
+					oSettings.oApi._fnBindAction( this, {"page": j+iStartButton-1}, function(e) {
+						/* Use the information in the element to jump to the required page */
+						oSettings.oApi._fnPageChange( oSettings, e.data.page );
+						fnCallbackDraw( oSettings );
+						e.preventDefault();
+					} );
+				};
+				
+				/* Pages calculation */
+				if ( oSettings._iDisplayLength === -1 )
+				{
+					iStartButton = 1;
+					iEndButton = 1;
+					iCurrentPage = 1;
+				}
+				else if (iPages < iPageCount)
+				{
+					iStartButton = 1;
+					iEndButton = iPages;
+				}
+				else if (iCurrentPage <= iPageCountHalf)
+				{
+					iStartButton = 1;
+					iEndButton = iPageCount;
+				}
+				else if (iCurrentPage >= (iPages - iPageCountHalf))
+				{
+					iStartButton = iPages - iPageCount + 1;
+					iEndButton = iPages;
+				}
+				else
+				{
+					iStartButton = iCurrentPage - Math.ceil(iPageCount / 2) + 1;
+					iEndButton = iStartButton + iPageCount - 1;
+				}
+	
+				
+				/* Build the dynamic list */
+				for ( i=iStartButton ; i<=iEndButton ; i++ )
+				{
+					sList += (iCurrentPage !== i) ?
+						'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+'">'+oSettings.fnFormatNumber(i)+'</a>' :
+						'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButtonActive+'">'+oSettings.fnFormatNumber(i)+'</a>';
+				}
+				
+				/* Loop over each instance of the pager */
+				for ( i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					nNode = an[i];
+					if ( !nNode.hasChildNodes() )
+					{
+						continue;
+					}
+					
+					/* Build up the dynamic list first - html and listeners */
+					$('span:eq(0)', nNode)
+						.html( sList )
+						.children('a').each( fnBind );
+					
+					/* Update the permanent button's classes */
+					anButtons = nNode.getElementsByTagName('a');
+					anStatic = [
+						anButtons[0], anButtons[1], 
+						anButtons[anButtons.length-2], anButtons[anButtons.length-1]
+					];
+	
+					$(anStatic).removeClass( oClasses.sPageButton+" "+oClasses.sPageButtonActive+" "+oClasses.sPageButtonStaticDisabled );
+					$([anStatic[0], anStatic[1]]).addClass( 
+						(iCurrentPage==1) ?
+							oClasses.sPageButtonStaticDisabled :
+							oClasses.sPageButton
+					);
+					$([anStatic[2], anStatic[3]]).addClass(
+						(iPages===0 || iCurrentPage===iPages || oSettings._iDisplayLength===-1) ?
+							oClasses.sPageButtonStaticDisabled :
+							oClasses.sPageButton
+					);
+				}
+			}
+		}
+	} );
+	
+	$.extend( DataTable.ext.oSort, {
+		/*
+		 * text sorting
+		 */
+		"string-pre": function ( a )
+		{
+			if ( typeof a != 'string' ) {
+				a = (a !== null && a.toString) ? a.toString() : '';
+			}
+			return a.toLowerCase();
+		},
+	
+		"string-asc": function ( x, y )
+		{
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"string-desc": function ( x, y )
+		{
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * html sorting (ignore html tags)
+		 */
+		"html-pre": function ( a )
+		{
+			return a.replace( /<.*?>/g, "" ).toLowerCase();
+		},
+		
+		"html-asc": function ( x, y )
+		{
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"html-desc": function ( x, y )
+		{
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * date sorting
+		 */
+		"date-pre": function ( a )
+		{
+			var x = Date.parse( a );
+			
+			if ( isNaN(x) || x==="" )
+			{
+				x = Date.parse( "01/01/1970 00:00:00" );
+			}
+			return x;
+		},
+	
+		"date-asc": function ( x, y )
+		{
+			return x - y;
+		},
+		
+		"date-desc": function ( x, y )
+		{
+			return y - x;
+		},
+		
+		
+		/*
+		 * numerical sorting
+		 */
+		"numeric-pre": function ( a )
+		{
+			return (a=="-" || a==="") ? 0 : a*1;
+		},
+	
+		"numeric-asc": function ( x, y )
+		{
+			return x - y;
+		},
+		
+		"numeric-desc": function ( x, y )
+		{
+			return y - x;
+		}
+	} );
+	
+	
+	$.extend( DataTable.ext.aTypes, [
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string is numeric
+		 * Returns:  string:'numeric' or null
+		 * Inputs:   mixed:sText - string to check
+		 */
+		function ( sData )
+		{
+			/* Allow zero length strings as a number */
+			if ( typeof sData === 'number' )
+			{
+				return 'numeric';
+			}
+			else if ( typeof sData !== 'string' )
+			{
+				return null;
+			}
+			
+			var sValidFirstChars = "0123456789-";
+			var sValidChars = "0123456789.";
+			var Char;
+			var bDecimal = false;
+			
+			/* Check for a valid first char (no period and allow negatives) */
+			Char = sData.charAt(0); 
+			if (sValidFirstChars.indexOf(Char) == -1) 
+			{
+				return null;
+			}
+			
+			/* Check all the other characters are valid */
+			for ( var i=1 ; i<sData.length ; i++ ) 
+			{
+				Char = sData.charAt(i); 
+				if (sValidChars.indexOf(Char) == -1) 
+				{
+					return null;
+				}
+				
+				/* Only allowed one decimal place... */
+				if ( Char == "." )
+				{
+					if ( bDecimal )
+					{
+						return null;
+					}
+					bDecimal = true;
+				}
+			}
+			
+			return 'numeric';
+		},
+		
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string is actually a formatted date
+		 * Returns:  string:'date' or null
+		 * Inputs:   string:sText - string to check
+		 */
+		function ( sData )
+		{
+			var iParse = Date.parse(sData);
+			if ( (iParse !== null && !isNaN(iParse)) || (typeof sData === 'string' && sData.length === 0) )
+			{
+				return 'date';
+			}
+			return null;
+		},
+		
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string should be treated as an HTML string
+		 * Returns:  string:'html' or null
+		 * Inputs:   string:sText - string to check
+		 */
+		function ( sData )
+		{
+			if ( typeof sData === 'string' && sData.indexOf('<') != -1 && sData.indexOf('>') != -1 )
+			{
+				return 'html';
+			}
+			return null;
+		}
+	] );
+	
+
+	// jQuery aliases
+	$.fn.DataTable = DataTable;
+	$.fn.dataTable = DataTable;
+	$.fn.dataTableSettings = DataTable.settings;
+	$.fn.dataTableExt = DataTable.ext;
+
+
+	// Information about events fired by DataTables - for documentation.
+	/**
+	 * Draw event, fired whenever the table is redrawn on the page, at the same point as
+	 * fnDrawCallback. This may be useful for binding events or performing calculations when
+	 * the table is altered at all.
+	 *  @name DataTable#draw
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Filter event, fired when the filtering applied to the table (using the build in global
+	 * global filter, or column filters) is altered.
+	 *  @name DataTable#filter
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Page change event, fired when the paging of the table is altered.
+	 *  @name DataTable#page
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Sort event, fired when the sorting applied to the table is altered.
+	 *  @name DataTable#sort
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * DataTables initialisation complete event, fired when the table is fully drawn,
+	 * including Ajax data loaded, if Ajax data is required.
+	 *  @name DataTable#init
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The JSON object request from the server - only
+	 *    present if client-side Ajax sourced data is used</li></ol>
+	 */
+
+	/**
+	 * State save event, fired when the table has changed state a new state save is required.
+	 * This method allows modification of the state saving object prior to actually doing the
+	 * save, including addition or other state properties (for plug-ins) or modification
+	 * of a DataTables core property.
+	 *  @name DataTable#stateSaveParams
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The state information to be saved
+	 */
+
+	/**
+	 * State load event, fired when the table is loading state from the stored data, but
+	 * prior to the settings object being modified by the saved state - allowing modification
+	 * of the saved state is required or loading of state for a plug-in.
+	 *  @name DataTable#stateLoadParams
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * State loaded event, fired when state has been loaded from stored data and the settings
+	 * object has been modified by the loaded data.
+	 *  @name DataTable#stateLoaded
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * Processing event, fired when DataTables is doing some kind of processing (be it,
+	 * sort, filter or anything else). Can be used to indicate to the end user that
+	 * there is something happening, or that something has finished.
+	 *  @name DataTable#processing
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {boolean} bShow Flag for if DataTables is doing processing or not
+	 */
+
+	/**
+	 * Ajax (XHR) event, fired whenever an Ajax request is completed from a request to 
+	 * made to the server for new data (note that this trigger is called in fnServerData,
+	 * if you override fnServerData and which to use this event, you need to trigger it in
+	 * you success function).
+	 *  @name DataTable#xhr
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {object} json JSON returned from the server
+	 */
+
+	/**
+	 * Destroy event, fired when the DataTable is destroyed by calling fnDestroy or passing
+	 * the bDestroy:true parameter in the initialisation object. This can be used to remove
+	 * bound events, added DOM nodes, etc.
+	 *  @name DataTable#destroy
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+}));
+
+}(window, document));
+
Index: /main/waeup.ikoba/branches/uli-payments/layout/static/js/jquery.dataTables.min.js
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/static/js/jquery.dataTables.min.js	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/static/js/jquery.dataTables.min.js	(revision 12677)
@@ -0,0 +1,155 @@
+/*
+ * File:        jquery.dataTables.min.js
+ * Version:     1.9.4
+ * Author:      Allan Jardine (www.sprymedia.co.uk)
+ * Info:        www.datatables.net
+ * 
+ * Copyright 2008-2012 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ * 
+ * This source file is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
+ */
+(function(X,l,n){var L=function(h){var j=function(e){function o(a,b){var c=j.defaults.columns,d=a.aoColumns.length,c=h.extend({},j.models.oColumn,c,{sSortingClass:a.oClasses.sSortable,sSortingClassJUI:a.oClasses.sSortJUI,nTh:b?b:l.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.oDefaults:d});a.aoColumns.push(c);if(a.aoPreSearchCols[d]===n||null===a.aoPreSearchCols[d])a.aoPreSearchCols[d]=h.extend({},j.models.oSearch);else if(c=a.aoPreSearchCols[d],
+c.bRegex===n&&(c.bRegex=!0),c.bSmart===n&&(c.bSmart=!0),c.bCaseInsensitive===n)c.bCaseInsensitive=!0;m(a,d,null)}function m(a,b,c){var d=a.aoColumns[b];c!==n&&null!==c&&(c.mDataProp&&!c.mData&&(c.mData=c.mDataProp),c.sType!==n&&(d.sType=c.sType,d._bAutoType=!1),h.extend(d,c),p(d,c,"sWidth","sWidthOrig"),c.iDataSort!==n&&(d.aDataSort=[c.iDataSort]),p(d,c,"aDataSort"));var i=d.mRender?Q(d.mRender):null,f=Q(d.mData);d.fnGetData=function(a,b){var c=f(a,b);return d.mRender&&b&&""!==b?i(c,b,a):c};d.fnSetData=
+L(d.mData);a.oFeatures.bSort||(d.bSortable=!1);!d.bSortable||-1==h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortableNone,d.sSortingClassJUI=""):-1==h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortable,d.sSortingClassJUI=a.oClasses.sSortJUI):-1!=h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortableAsc,d.sSortingClassJUI=a.oClasses.sSortJUIAscAllowed):-1==
+h.inArray("asc",d.asSorting)&&-1!=h.inArray("desc",d.asSorting)&&(d.sSortingClass=a.oClasses.sSortableDesc,d.sSortingClassJUI=a.oClasses.sSortJUIDescAllowed)}function k(a){if(!1===a.oFeatures.bAutoWidth)return!1;da(a);for(var b=0,c=a.aoColumns.length;b<c;b++)a.aoColumns[b].nTh.style.width=a.aoColumns[b].sWidth}function G(a,b){var c=r(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function R(a,b){var c=r(a,"bVisible"),c=h.inArray(b,c);return-1!==c?c:null}function t(a){return r(a,"bVisible").length}
+function r(a,b){var c=[];h.map(a.aoColumns,function(a,i){a[b]&&c.push(i)});return c}function B(a){for(var b=j.ext.aTypes,c=b.length,d=0;d<c;d++){var i=b[d](a);if(null!==i)return i}return"string"}function u(a,b){for(var c=b.split(","),d=[],i=0,f=a.aoColumns.length;i<f;i++)for(var g=0;g<f;g++)if(a.aoColumns[i].sName==c[g]){d.push(g);break}return d}function M(a){for(var b="",c=0,d=a.aoColumns.length;c<d;c++)b+=a.aoColumns[c].sName+",";return b.length==d?"":b.slice(0,-1)}function ta(a,b,c,d){var i,f,
+g,e,w;if(b)for(i=b.length-1;0<=i;i--){var j=b[i].aTargets;h.isArray(j)||D(a,1,"aTargets must be an array of targets, not a "+typeof j);f=0;for(g=j.length;f<g;f++)if("number"===typeof j[f]&&0<=j[f]){for(;a.aoColumns.length<=j[f];)o(a);d(j[f],b[i])}else if("number"===typeof j[f]&&0>j[f])d(a.aoColumns.length+j[f],b[i]);else if("string"===typeof j[f]){e=0;for(w=a.aoColumns.length;e<w;e++)("_all"==j[f]||h(a.aoColumns[e].nTh).hasClass(j[f]))&&d(e,b[i])}}if(c){i=0;for(a=c.length;i<a;i++)d(i,c[i])}}function H(a,
+b){var c;c=h.isArray(b)?b.slice():h.extend(!0,{},b);var d=a.aoData.length,i=h.extend(!0,{},j.models.oRow);i._aData=c;a.aoData.push(i);for(var f,i=0,g=a.aoColumns.length;i<g;i++)c=a.aoColumns[i],"function"===typeof c.fnRender&&c.bUseRendered&&null!==c.mData?F(a,d,i,S(a,d,i)):F(a,d,i,v(a,d,i)),c._bAutoType&&"string"!=c.sType&&(f=v(a,d,i,"type"),null!==f&&""!==f&&(f=B(f),null===c.sType?c.sType=f:c.sType!=f&&"html"!=c.sType&&(c.sType="string")));a.aiDisplayMaster.push(d);a.oFeatures.bDeferRender||ea(a,
+d);return d}function ua(a){var b,c,d,i,f,g,e;if(a.bDeferLoading||null===a.sAjaxSource)for(b=a.nTBody.firstChild;b;){if("TR"==b.nodeName.toUpperCase()){c=a.aoData.length;b._DT_RowIndex=c;a.aoData.push(h.extend(!0,{},j.models.oRow,{nTr:b}));a.aiDisplayMaster.push(c);f=b.firstChild;for(d=0;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)F(a,c,d,h.trim(f.innerHTML)),d++;f=f.nextSibling}}b=b.nextSibling}i=T(a);d=[];b=0;for(c=i.length;b<c;b++)for(f=i[b].firstChild;f;)g=f.nodeName.toUpperCase(),("TD"==
+g||"TH"==g)&&d.push(f),f=f.nextSibling;c=0;for(i=a.aoColumns.length;c<i;c++){e=a.aoColumns[c];null===e.sTitle&&(e.sTitle=e.nTh.innerHTML);var w=e._bAutoType,o="function"===typeof e.fnRender,k=null!==e.sClass,n=e.bVisible,m,p;if(w||o||k||!n){g=0;for(b=a.aoData.length;g<b;g++)f=a.aoData[g],m=d[g*i+c],w&&"string"!=e.sType&&(p=v(a,g,c,"type"),""!==p&&(p=B(p),null===e.sType?e.sType=p:e.sType!=p&&"html"!=e.sType&&(e.sType="string"))),e.mRender?m.innerHTML=v(a,g,c,"display"):e.mData!==c&&(m.innerHTML=v(a,
+g,c,"display")),o&&(p=S(a,g,c),m.innerHTML=p,e.bUseRendered&&F(a,g,c,p)),k&&(m.className+=" "+e.sClass),n?f._anHidden[c]=null:(f._anHidden[c]=m,m.parentNode.removeChild(m)),e.fnCreatedCell&&e.fnCreatedCell.call(a.oInstance,m,v(a,g,c,"display"),f._aData,g,c)}}if(0!==a.aoRowCreatedCallback.length){b=0;for(c=a.aoData.length;b<c;b++)f=a.aoData[b],A(a,"aoRowCreatedCallback",null,[f.nTr,f._aData,b])}}function I(a,b){return b._DT_RowIndex!==n?b._DT_RowIndex:null}function fa(a,b,c){for(var b=J(a,b),d=0,a=
+a.aoColumns.length;d<a;d++)if(b[d]===c)return d;return-1}function Y(a,b,c,d){for(var i=[],f=0,g=d.length;f<g;f++)i.push(v(a,b,d[f],c));return i}function v(a,b,c,d){var i=a.aoColumns[c];if((c=i.fnGetData(a.aoData[b]._aData,d))===n)return a.iDrawError!=a.iDraw&&null===i.sDefaultContent&&(D(a,0,"Requested unknown parameter "+("function"==typeof i.mData?"{mData function}":"'"+i.mData+"'")+" from the data source for row "+b),a.iDrawError=a.iDraw),i.sDefaultContent;if(null===c&&null!==i.sDefaultContent)c=
+i.sDefaultContent;else if("function"===typeof c)return c();return"display"==d&&null===c?"":c}function F(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d)}function Q(a){if(null===a)return function(){return null};if("function"===typeof a)return function(b,d,i){return a(b,d,i)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("["))){var b=function(a,d,i){var f=i.split("."),g;if(""!==i){var e=0;for(g=f.length;e<g;e++){if(i=f[e].match(U)){f[e]=f[e].replace(U,"");""!==f[e]&&(a=a[f[e]]);
+g=[];f.splice(0,e+1);for(var f=f.join("."),e=0,h=a.length;e<h;e++)g.push(b(a[e],d,f));a=i[0].substring(1,i[0].length-1);a=""===a?g:g.join(a);break}if(null===a||a[f[e]]===n)return n;a=a[f[e]]}}return a};return function(c,d){return b(c,d,a)}}return function(b){return b[a]}}function L(a){if(null===a)return function(){};if("function"===typeof a)return function(b,d){a(b,"set",d)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("["))){var b=function(a,d,i){var i=i.split("."),f,g,e=0;for(g=
+i.length-1;e<g;e++){if(f=i[e].match(U)){i[e]=i[e].replace(U,"");a[i[e]]=[];f=i.slice();f.splice(0,e+1);g=f.join(".");for(var h=0,j=d.length;h<j;h++)f={},b(f,d[h],g),a[i[e]].push(f);return}if(null===a[i[e]]||a[i[e]]===n)a[i[e]]={};a=a[i[e]]}a[i[i.length-1].replace(U,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Z(a){for(var b=[],c=a.aoData.length,d=0;d<c;d++)b.push(a.aoData[d]._aData);return b}function ga(a){a.aoData.splice(0,a.aoData.length);a.aiDisplayMaster.splice(0,
+a.aiDisplayMaster.length);a.aiDisplay.splice(0,a.aiDisplay.length);y(a)}function ha(a,b){for(var c=-1,d=0,i=a.length;d<i;d++)a[d]==b?c=d:a[d]>b&&a[d]--; -1!=c&&a.splice(c,1)}function S(a,b,c){var d=a.aoColumns[c];return d.fnRender({iDataRow:b,iDataColumn:c,oSettings:a,aData:a.aoData[b]._aData,mDataProp:d.mData},v(a,b,c,"display"))}function ea(a,b){var c=a.aoData[b],d;if(null===c.nTr){c.nTr=l.createElement("tr");c.nTr._DT_RowIndex=b;c._aData.DT_RowId&&(c.nTr.id=c._aData.DT_RowId);c._aData.DT_RowClass&&
+(c.nTr.className=c._aData.DT_RowClass);for(var i=0,f=a.aoColumns.length;i<f;i++){var g=a.aoColumns[i];d=l.createElement(g.sCellType);d.innerHTML="function"===typeof g.fnRender&&(!g.bUseRendered||null===g.mData)?S(a,b,i):v(a,b,i,"display");null!==g.sClass&&(d.className=g.sClass);g.bVisible?(c.nTr.appendChild(d),c._anHidden[i]=null):c._anHidden[i]=d;g.fnCreatedCell&&g.fnCreatedCell.call(a.oInstance,d,v(a,b,i,"display"),c._aData,b,i)}A(a,"aoRowCreatedCallback",null,[c.nTr,c._aData,b])}}function va(a){var b,
+c,d;if(0!==h("th, td",a.nTHead).length){b=0;for(d=a.aoColumns.length;b<d;b++)if(c=a.aoColumns[b].nTh,c.setAttribute("role","columnheader"),a.aoColumns[b].bSortable&&(c.setAttribute("tabindex",a.iTabIndex),c.setAttribute("aria-controls",a.sTableId)),null!==a.aoColumns[b].sClass&&h(c).addClass(a.aoColumns[b].sClass),a.aoColumns[b].sTitle!=c.innerHTML)c.innerHTML=a.aoColumns[b].sTitle}else{var i=l.createElement("tr");b=0;for(d=a.aoColumns.length;b<d;b++)c=a.aoColumns[b].nTh,c.innerHTML=a.aoColumns[b].sTitle,
+c.setAttribute("tabindex","0"),null!==a.aoColumns[b].sClass&&h(c).addClass(a.aoColumns[b].sClass),i.appendChild(c);h(a.nTHead).html("")[0].appendChild(i);V(a.aoHeader,a.nTHead)}h(a.nTHead).children("tr").attr("role","row");if(a.bJUI){b=0;for(d=a.aoColumns.length;b<d;b++){c=a.aoColumns[b].nTh;i=l.createElement("div");i.className=a.oClasses.sSortJUIWrapper;h(c).contents().appendTo(i);var f=l.createElement("span");f.className=a.oClasses.sSortIcon;i.appendChild(f);c.appendChild(i)}}if(a.oFeatures.bSort)for(b=
+0;b<a.aoColumns.length;b++)!1!==a.aoColumns[b].bSortable?ia(a,a.aoColumns[b].nTh,b):h(a.aoColumns[b].nTh).addClass(a.oClasses.sSortableNone);""!==a.oClasses.sFooterTH&&h(a.nTFoot).children("tr").children("th").addClass(a.oClasses.sFooterTH);if(null!==a.nTFoot){c=N(a,null,a.aoFooter);b=0;for(d=a.aoColumns.length;b<d;b++)c[b]&&(a.aoColumns[b].nTf=c[b],a.aoColumns[b].sClass&&h(c[b]).addClass(a.aoColumns[b].sClass))}}function W(a,b,c){var d,i,f,g=[],e=[],h=a.aoColumns.length,j;c===n&&(c=!1);d=0;for(i=
+b.length;d<i;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=h-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);e.push([])}d=0;for(i=g.length;d<i;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(j=h=1,e[d][f]===n){a.appendChild(g[d][f].cell);for(e[d][f]=1;g[d+h]!==n&&g[d][f].cell==g[d+h][f].cell;)e[d+h][f]=1,h++;for(;g[d][f+j]!==n&&g[d][f].cell==g[d][f+j].cell;){for(c=0;c<h;c++)e[d+c][f+j]=1;j++}g[d][f].cell.rowSpan=h;g[d][f].cell.colSpan=j}}}function x(a){var b=
+A(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))E(a,!1);else{var c,d,b=[],i=0,f=a.asStripeClasses.length;c=a.aoOpenRows.length;a.bDrawing=!0;a.iInitDisplayStart!==n&&-1!=a.iInitDisplayStart&&(a._iDisplayStart=a.oFeatures.bServerSide?a.iInitDisplayStart:a.iInitDisplayStart>=a.fnRecordsDisplay()?0:a.iInitDisplayStart,a.iInitDisplayStart=-1,y(a));if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++;else if(a.oFeatures.bServerSide){if(!a.bDestroying&&!wa(a))return}else a.iDraw++;if(0!==a.aiDisplay.length){var g=
+a._iDisplayStart;d=a._iDisplayEnd;a.oFeatures.bServerSide&&(g=0,d=a.aoData.length);for(;g<d;g++){var e=a.aoData[a.aiDisplay[g]];null===e.nTr&&ea(a,a.aiDisplay[g]);var j=e.nTr;if(0!==f){var o=a.asStripeClasses[i%f];e._sRowStripe!=o&&(h(j).removeClass(e._sRowStripe).addClass(o),e._sRowStripe=o)}A(a,"aoRowCallback",null,[j,a.aoData[a.aiDisplay[g]]._aData,i,g]);b.push(j);i++;if(0!==c)for(e=0;e<c;e++)if(j==a.aoOpenRows[e].nParent){b.push(a.aoOpenRows[e].nTr);break}}}else b[0]=l.createElement("tr"),a.asStripeClasses[0]&&
+(b[0].className=a.asStripeClasses[0]),c=a.oLanguage,f=c.sZeroRecords,1==a.iDraw&&null!==a.sAjaxSource&&!a.oFeatures.bServerSide?f=c.sLoadingRecords:c.sEmptyTable&&0===a.fnRecordsTotal()&&(f=c.sEmptyTable),c=l.createElement("td"),c.setAttribute("valign","top"),c.colSpan=t(a),c.className=a.oClasses.sRowEmpty,c.innerHTML=ja(a,f),b[i].appendChild(c);A(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Z(a),a._iDisplayStart,a.fnDisplayEnd(),a.aiDisplay]);A(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],
+Z(a),a._iDisplayStart,a.fnDisplayEnd(),a.aiDisplay]);i=l.createDocumentFragment();c=l.createDocumentFragment();if(a.nTBody){f=a.nTBody.parentNode;c.appendChild(a.nTBody);if(!a.oScroll.bInfinite||!a._bInitComplete||a.bSorted||a.bFiltered)for(;c=a.nTBody.firstChild;)a.nTBody.removeChild(c);c=0;for(d=b.length;c<d;c++)i.appendChild(b[c]);a.nTBody.appendChild(i);null!==f&&f.appendChild(a.nTBody)}A(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1;a.oFeatures.bServerSide&&(E(a,!1),
+a._bInitComplete||$(a))}}function aa(a){a.oFeatures.bSort?O(a,a.oPreviousSearch):a.oFeatures.bFilter?K(a,a.oPreviousSearch):(y(a),x(a))}function xa(a){var b=h("<div></div>")[0];a.nTable.parentNode.insertBefore(b,a.nTable);a.nTableWrapper=h('<div id="'+a.sTableId+'_wrapper" class="'+a.oClasses.sWrapper+'" role="grid"></div>')[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var c=a.nTableWrapper,d=a.sDom.split(""),i,f,g,e,w,o,k,m=0;m<d.length;m++){f=0;g=d[m];if("<"==g){e=h("<div></div>")[0];w=d[m+
+1];if("'"==w||'"'==w){o="";for(k=2;d[m+k]!=w;)o+=d[m+k],k++;"H"==o?o=a.oClasses.sJUIHeader:"F"==o&&(o=a.oClasses.sJUIFooter);-1!=o.indexOf(".")?(w=o.split("."),e.id=w[0].substr(1,w[0].length-1),e.className=w[1]):"#"==o.charAt(0)?e.id=o.substr(1,o.length-1):e.className=o;m+=k}c.appendChild(e);c=e}else if(">"==g)c=c.parentNode;else if("l"==g&&a.oFeatures.bPaginate&&a.oFeatures.bLengthChange)i=ya(a),f=1;else if("f"==g&&a.oFeatures.bFilter)i=za(a),f=1;else if("r"==g&&a.oFeatures.bProcessing)i=Aa(a),f=
+1;else if("t"==g)i=Ba(a),f=1;else if("i"==g&&a.oFeatures.bInfo)i=Ca(a),f=1;else if("p"==g&&a.oFeatures.bPaginate)i=Da(a),f=1;else if(0!==j.ext.aoFeatures.length){e=j.ext.aoFeatures;k=0;for(w=e.length;k<w;k++)if(g==e[k].cFeature){(i=e[k].fnInit(a))&&(f=1);break}}1==f&&null!==i&&("object"!==typeof a.aanFeatures[g]&&(a.aanFeatures[g]=[]),a.aanFeatures[g].push(i),c.appendChild(i))}b.parentNode.replaceChild(a.nTableWrapper,b)}function V(a,b){var c=h(b).children("tr"),d,i,f,g,e,j,o,k,m,p;a.splice(0,a.length);
+f=0;for(j=c.length;f<j;f++)a.push([]);f=0;for(j=c.length;f<j;f++){d=c[f];for(i=d.firstChild;i;){if("TD"==i.nodeName.toUpperCase()||"TH"==i.nodeName.toUpperCase()){k=1*i.getAttribute("colspan");m=1*i.getAttribute("rowspan");k=!k||0===k||1===k?1:k;m=!m||0===m||1===m?1:m;g=0;for(e=a[f];e[g];)g++;o=g;p=1===k?!0:!1;for(e=0;e<k;e++)for(g=0;g<m;g++)a[f+g][o+e]={cell:i,unique:p},a[f+g].nTr=d}i=i.nextSibling}}}function N(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],V(c,b)));for(var b=0,i=c.length;b<i;b++)for(var f=
+0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function wa(a){if(a.bAjaxDataGet){a.iDraw++;E(a,!0);var b=Ea(a);ka(a,b);a.fnServerData.call(a.oInstance,a.sAjaxSource,b,function(b){Fa(a,b)},a);return!1}return!0}function Ea(a){var b=a.aoColumns.length,c=[],d,i,f,g;c.push({name:"sEcho",value:a.iDraw});c.push({name:"iColumns",value:b});c.push({name:"sColumns",value:M(a)});c.push({name:"iDisplayStart",value:a._iDisplayStart});c.push({name:"iDisplayLength",
+value:!1!==a.oFeatures.bPaginate?a._iDisplayLength:-1});for(f=0;f<b;f++)d=a.aoColumns[f].mData,c.push({name:"mDataProp_"+f,value:"function"===typeof d?"function":d});if(!1!==a.oFeatures.bFilter){c.push({name:"sSearch",value:a.oPreviousSearch.sSearch});c.push({name:"bRegex",value:a.oPreviousSearch.bRegex});for(f=0;f<b;f++)c.push({name:"sSearch_"+f,value:a.aoPreSearchCols[f].sSearch}),c.push({name:"bRegex_"+f,value:a.aoPreSearchCols[f].bRegex}),c.push({name:"bSearchable_"+f,value:a.aoColumns[f].bSearchable})}if(!1!==
+a.oFeatures.bSort){var e=0;d=null!==a.aaSortingFixed?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(f=0;f<d.length;f++){i=a.aoColumns[d[f][0]].aDataSort;for(g=0;g<i.length;g++)c.push({name:"iSortCol_"+e,value:i[g]}),c.push({name:"sSortDir_"+e,value:d[f][1]}),e++}c.push({name:"iSortingCols",value:e});for(f=0;f<b;f++)c.push({name:"bSortable_"+f,value:a.aoColumns[f].bSortable})}return c}function ka(a,b){A(a,"aoServerParams","serverParams",[b])}function Fa(a,b){if(b.sEcho!==n){if(1*b.sEcho<
+a.iDraw)return;a.iDraw=1*b.sEcho}(!a.oScroll.bInfinite||a.oScroll.bInfinite&&(a.bSorted||a.bFiltered))&&ga(a);a._iRecordsTotal=parseInt(b.iTotalRecords,10);a._iRecordsDisplay=parseInt(b.iTotalDisplayRecords,10);var c=M(a),c=b.sColumns!==n&&""!==c&&b.sColumns!=c,d;c&&(d=u(a,b.sColumns));for(var i=Q(a.sAjaxDataProp)(b),f=0,g=i.length;f<g;f++)if(c){for(var e=[],h=0,j=a.aoColumns.length;h<j;h++)e.push(i[f][d[h]]);H(a,e)}else H(a,i[f]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;x(a);a.bAjaxDataGet=
+!0;E(a,!1)}function za(a){var b=a.oPreviousSearch,c=a.oLanguage.sSearch,c=-1!==c.indexOf("_INPUT_")?c.replace("_INPUT_",'<input type="text" />'):""===c?'<input type="text" />':c+' <input type="text" />',d=l.createElement("div");d.className=a.oClasses.sFilter;d.innerHTML="<label>"+c+"</label>";a.aanFeatures.f||(d.id=a.sTableId+"_filter");c=h('input[type="text"]',d);d._DT_Input=c[0];c.val(b.sSearch.replace('"',"&quot;"));c.bind("keyup.DT",function(){for(var c=a.aanFeatures.f,d=this.value===""?"":this.value,
+g=0,e=c.length;g<e;g++)c[g]!=h(this).parents("div.dataTables_filter")[0]&&h(c[g]._DT_Input).val(d);d!=b.sSearch&&K(a,{sSearch:d,bRegex:b.bRegex,bSmart:b.bSmart,bCaseInsensitive:b.bCaseInsensitive})});c.attr("aria-controls",a.sTableId).bind("keypress.DT",function(a){if(a.keyCode==13)return false});return d}function K(a,b,c){var d=a.oPreviousSearch,i=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};if(a.oFeatures.bServerSide)f(b);
+else{Ga(a,b.sSearch,c,b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<a.aoPreSearchCols.length;b++)Ha(a,i[b].sSearch,b,i[b].bRegex,i[b].bSmart,i[b].bCaseInsensitive);Ia(a)}a.bFiltered=!0;h(a.oInstance).trigger("filter",a);a._iDisplayStart=0;y(a);x(a);la(a,0)}function Ia(a){for(var b=j.ext.afnFiltering,c=r(a,"bSearchable"),d=0,i=b.length;d<i;d++)for(var f=0,g=0,e=a.aiDisplay.length;g<e;g++){var h=a.aiDisplay[g-f];b[d](a,Y(a,h,"filter",c),h)||(a.aiDisplay.splice(g-f,1),f++)}}function Ha(a,b,c,
+d,i,f){if(""!==b)for(var g=0,b=ma(b,d,i,f),d=a.aiDisplay.length-1;0<=d;d--)i=Ja(v(a,a.aiDisplay[d],c,"filter"),a.aoColumns[c].sType),b.test(i)||(a.aiDisplay.splice(d,1),g++)}function Ga(a,b,c,d,i,f){d=ma(b,d,i,f);i=a.oPreviousSearch;c||(c=0);0!==j.ext.afnFiltering.length&&(c=1);if(0>=b.length)a.aiDisplay.splice(0,a.aiDisplay.length),a.aiDisplay=a.aiDisplayMaster.slice();else if(a.aiDisplay.length==a.aiDisplayMaster.length||i.sSearch.length>b.length||1==c||0!==b.indexOf(i.sSearch)){a.aiDisplay.splice(0,
+a.aiDisplay.length);la(a,1);for(b=0;b<a.aiDisplayMaster.length;b++)d.test(a.asDataSearch[b])&&a.aiDisplay.push(a.aiDisplayMaster[b])}else for(b=c=0;b<a.asDataSearch.length;b++)d.test(a.asDataSearch[b])||(a.aiDisplay.splice(b-c,1),c++)}function la(a,b){if(!a.oFeatures.bServerSide){a.asDataSearch=[];for(var c=r(a,"bSearchable"),d=1===b?a.aiDisplayMaster:a.aiDisplay,i=0,f=d.length;i<f;i++)a.asDataSearch[i]=na(a,Y(a,d[i],"filter",c))}}function na(a,b){var c=b.join("  ");-1!==c.indexOf("&")&&(c=h("<div>").html(c).text());
+return c.replace(/[\n\r]/g," ")}function ma(a,b,c,d){if(c)return a=b?a.split(" "):oa(a).split(" "),a="^(?=.*?"+a.join(")(?=.*?")+").*$",RegExp(a,d?"i":"");a=b?a:oa(a);return RegExp(a,d?"i":"")}function Ja(a,b){return"function"===typeof j.ext.ofnSearch[b]?j.ext.ofnSearch[b](a):null===a?"":"html"==b?a.replace(/[\r\n]/g," ").replace(/<.*?>/g,""):"string"===typeof a?a.replace(/[\r\n]/g," "):a}function oa(a){return a.replace(RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),
+"\\$1")}function Ca(a){var b=l.createElement("div");b.className=a.oClasses.sInfo;a.aanFeatures.i||(a.aoDrawCallback.push({fn:Ka,sName:"information"}),b.id=a.sTableId+"_info");a.nTable.setAttribute("aria-describedby",a.sTableId+"_info");return b}function Ka(a){if(a.oFeatures.bInfo&&0!==a.aanFeatures.i.length){var b=a.oLanguage,c=a._iDisplayStart+1,d=a.fnDisplayEnd(),i=a.fnRecordsTotal(),f=a.fnRecordsDisplay(),g;g=0===f?b.sInfoEmpty:b.sInfo;f!=i&&(g+=" "+b.sInfoFiltered);g+=b.sInfoPostFix;g=ja(a,g);
+null!==b.fnInfoCallback&&(g=b.fnInfoCallback.call(a.oInstance,a,c,d,i,f,g));a=a.aanFeatures.i;b=0;for(c=a.length;b<c;b++)h(a[b]).html(g)}}function ja(a,b){var c=a.fnFormatNumber(a._iDisplayStart+1),d=a.fnDisplayEnd(),d=a.fnFormatNumber(d),i=a.fnRecordsDisplay(),i=a.fnFormatNumber(i),f=a.fnRecordsTotal(),f=a.fnFormatNumber(f);a.oScroll.bInfinite&&(c=a.fnFormatNumber(1));return b.replace(/_START_/g,c).replace(/_END_/g,d).replace(/_TOTAL_/g,i).replace(/_MAX_/g,f)}function ba(a){var b,c,d=a.iInitDisplayStart;
+if(!1===a.bInitialised)setTimeout(function(){ba(a)},200);else{xa(a);va(a);W(a,a.aoHeader);a.nTFoot&&W(a,a.aoFooter);E(a,!0);a.oFeatures.bAutoWidth&&da(a);b=0;for(c=a.aoColumns.length;b<c;b++)null!==a.aoColumns[b].sWidth&&(a.aoColumns[b].nTh.style.width=q(a.aoColumns[b].sWidth));a.oFeatures.bSort?O(a):a.oFeatures.bFilter?K(a,a.oPreviousSearch):(a.aiDisplay=a.aiDisplayMaster.slice(),y(a),x(a));null!==a.sAjaxSource&&!a.oFeatures.bServerSide?(c=[],ka(a,c),a.fnServerData.call(a.oInstance,a.sAjaxSource,
+c,function(c){var f=a.sAjaxDataProp!==""?Q(a.sAjaxDataProp)(c):c;for(b=0;b<f.length;b++)H(a,f[b]);a.iInitDisplayStart=d;if(a.oFeatures.bSort)O(a);else{a.aiDisplay=a.aiDisplayMaster.slice();y(a);x(a)}E(a,false);$(a,c)},a)):a.oFeatures.bServerSide||(E(a,!1),$(a))}}function $(a,b){a._bInitComplete=!0;A(a,"aoInitComplete","init",[a,b])}function pa(a){var b=j.defaults.oLanguage;!a.sEmptyTable&&(a.sZeroRecords&&"No data available in table"===b.sEmptyTable)&&p(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&
+(a.sZeroRecords&&"Loading..."===b.sLoadingRecords)&&p(a,a,"sZeroRecords","sLoadingRecords")}function ya(a){if(a.oScroll.bInfinite)return null;var b='<select size="1" '+('name="'+a.sTableId+'_length"')+">",c,d,i=a.aLengthMenu;if(2==i.length&&"object"===typeof i[0]&&"object"===typeof i[1]){c=0;for(d=i[0].length;c<d;c++)b+='<option value="'+i[0][c]+'">'+i[1][c]+"</option>"}else{c=0;for(d=i.length;c<d;c++)b+='<option value="'+i[c]+'">'+i[c]+"</option>"}b+="</select>";i=l.createElement("div");a.aanFeatures.l||
+(i.id=a.sTableId+"_length");i.className=a.oClasses.sLength;i.innerHTML="<label>"+a.oLanguage.sLengthMenu.replace("_MENU_",b)+"</label>";h('select option[value="'+a._iDisplayLength+'"]',i).attr("selected",!0);h("select",i).bind("change.DT",function(){var b=h(this).val(),i=a.aanFeatures.l;c=0;for(d=i.length;c<d;c++)i[c]!=this.parentNode&&h("select",i[c]).val(b);a._iDisplayLength=parseInt(b,10);y(a);if(a.fnDisplayEnd()==a.fnRecordsDisplay()){a._iDisplayStart=a.fnDisplayEnd()-a._iDisplayLength;if(a._iDisplayStart<
+0)a._iDisplayStart=0}if(a._iDisplayLength==-1)a._iDisplayStart=0;x(a)});h("select",i).attr("aria-controls",a.sTableId);return i}function y(a){a._iDisplayEnd=!1===a.oFeatures.bPaginate?a.aiDisplay.length:a._iDisplayStart+a._iDisplayLength>a.aiDisplay.length||-1==a._iDisplayLength?a.aiDisplay.length:a._iDisplayStart+a._iDisplayLength}function Da(a){if(a.oScroll.bInfinite)return null;var b=l.createElement("div");b.className=a.oClasses.sPaging+a.sPaginationType;j.ext.oPagination[a.sPaginationType].fnInit(a,
+b,function(a){y(a);x(a)});a.aanFeatures.p||a.aoDrawCallback.push({fn:function(a){j.ext.oPagination[a.sPaginationType].fnUpdate(a,function(a){y(a);x(a)})},sName:"pagination"});return b}function qa(a,b){var c=a._iDisplayStart;if("number"===typeof b)a._iDisplayStart=b*a._iDisplayLength,a._iDisplayStart>a.fnRecordsDisplay()&&(a._iDisplayStart=0);else if("first"==b)a._iDisplayStart=0;else if("previous"==b)a._iDisplayStart=0<=a._iDisplayLength?a._iDisplayStart-a._iDisplayLength:0,0>a._iDisplayStart&&(a._iDisplayStart=
+0);else if("next"==b)0<=a._iDisplayLength?a._iDisplayStart+a._iDisplayLength<a.fnRecordsDisplay()&&(a._iDisplayStart+=a._iDisplayLength):a._iDisplayStart=0;else if("last"==b)if(0<=a._iDisplayLength){var d=parseInt((a.fnRecordsDisplay()-1)/a._iDisplayLength,10)+1;a._iDisplayStart=(d-1)*a._iDisplayLength}else a._iDisplayStart=0;else D(a,0,"Unknown paging action: "+b);h(a.oInstance).trigger("page",a);return c!=a._iDisplayStart}function Aa(a){var b=l.createElement("div");a.aanFeatures.r||(b.id=a.sTableId+
+"_processing");b.innerHTML=a.oLanguage.sProcessing;b.className=a.oClasses.sProcessing;a.nTable.parentNode.insertBefore(b,a.nTable);return b}function E(a,b){if(a.oFeatures.bProcessing)for(var c=a.aanFeatures.r,d=0,i=c.length;d<i;d++)c[d].style.visibility=b?"visible":"hidden";h(a.oInstance).trigger("processing",[a,b])}function Ba(a){if(""===a.oScroll.sX&&""===a.oScroll.sY)return a.nTable;var b=l.createElement("div"),c=l.createElement("div"),d=l.createElement("div"),i=l.createElement("div"),f=l.createElement("div"),
+g=l.createElement("div"),e=a.nTable.cloneNode(!1),j=a.nTable.cloneNode(!1),o=a.nTable.getElementsByTagName("thead")[0],k=0===a.nTable.getElementsByTagName("tfoot").length?null:a.nTable.getElementsByTagName("tfoot")[0],m=a.oClasses;c.appendChild(d);f.appendChild(g);i.appendChild(a.nTable);b.appendChild(c);b.appendChild(i);d.appendChild(e);e.appendChild(o);null!==k&&(b.appendChild(f),g.appendChild(j),j.appendChild(k));b.className=m.sScrollWrapper;c.className=m.sScrollHead;d.className=m.sScrollHeadInner;
+i.className=m.sScrollBody;f.className=m.sScrollFoot;g.className=m.sScrollFootInner;a.oScroll.bAutoCss&&(c.style.overflow="hidden",c.style.position="relative",f.style.overflow="hidden",i.style.overflow="auto");c.style.border="0";c.style.width="100%";f.style.border="0";d.style.width=""!==a.oScroll.sXInner?a.oScroll.sXInner:"100%";e.removeAttribute("id");e.style.marginLeft="0";a.nTable.style.marginLeft="0";null!==k&&(j.removeAttribute("id"),j.style.marginLeft="0");d=h(a.nTable).children("caption");0<
+d.length&&(d=d[0],"top"===d._captionSide?e.appendChild(d):"bottom"===d._captionSide&&k&&j.appendChild(d));""!==a.oScroll.sX&&(c.style.width=q(a.oScroll.sX),i.style.width=q(a.oScroll.sX),null!==k&&(f.style.width=q(a.oScroll.sX)),h(i).scroll(function(){c.scrollLeft=this.scrollLeft;if(k!==null)f.scrollLeft=this.scrollLeft}));""!==a.oScroll.sY&&(i.style.height=q(a.oScroll.sY));a.aoDrawCallback.push({fn:La,sName:"scrolling"});a.oScroll.bInfinite&&h(i).scroll(function(){if(!a.bDrawing&&h(this).scrollTop()!==
+0&&h(this).scrollTop()+h(this).height()>h(a.nTable).height()-a.oScroll.iLoadGap&&a.fnDisplayEnd()<a.fnRecordsDisplay()){qa(a,"next");y(a);x(a)}});a.nScrollHead=c;a.nScrollFoot=f;return b}function La(a){var b=a.nScrollHead.getElementsByTagName("div")[0],c=b.getElementsByTagName("table")[0],d=a.nTable.parentNode,i,f,g,e,j,o,k,m,p=[],n=[],l=null!==a.nTFoot?a.nScrollFoot.getElementsByTagName("div")[0]:null,R=null!==a.nTFoot?l.getElementsByTagName("table")[0]:null,r=a.oBrowser.bScrollOversize,s=function(a){k=
+a.style;k.paddingTop="0";k.paddingBottom="0";k.borderTopWidth="0";k.borderBottomWidth="0";k.height=0};h(a.nTable).children("thead, tfoot").remove();i=h(a.nTHead).clone()[0];a.nTable.insertBefore(i,a.nTable.childNodes[0]);g=a.nTHead.getElementsByTagName("tr");e=i.getElementsByTagName("tr");null!==a.nTFoot&&(j=h(a.nTFoot).clone()[0],a.nTable.insertBefore(j,a.nTable.childNodes[1]),o=a.nTFoot.getElementsByTagName("tr"),j=j.getElementsByTagName("tr"));""===a.oScroll.sX&&(d.style.width="100%",b.parentNode.style.width=
+"100%");var t=N(a,i);i=0;for(f=t.length;i<f;i++)m=G(a,i),t[i].style.width=a.aoColumns[m].sWidth;null!==a.nTFoot&&C(function(a){a.style.width=""},j);a.oScroll.bCollapse&&""!==a.oScroll.sY&&(d.style.height=d.offsetHeight+a.nTHead.offsetHeight+"px");i=h(a.nTable).outerWidth();if(""===a.oScroll.sX){if(a.nTable.style.width="100%",r&&(h("tbody",d).height()>d.offsetHeight||"scroll"==h(d).css("overflow-y")))a.nTable.style.width=q(h(a.nTable).outerWidth()-a.oScroll.iBarWidth)}else""!==a.oScroll.sXInner?a.nTable.style.width=
+q(a.oScroll.sXInner):i==h(d).width()&&h(d).height()<h(a.nTable).height()?(a.nTable.style.width=q(i-a.oScroll.iBarWidth),h(a.nTable).outerWidth()>i-a.oScroll.iBarWidth&&(a.nTable.style.width=q(i))):a.nTable.style.width=q(i);i=h(a.nTable).outerWidth();C(s,e);C(function(a){p.push(q(h(a).width()))},e);C(function(a,b){a.style.width=p[b]},g);h(e).height(0);null!==a.nTFoot&&(C(s,j),C(function(a){n.push(q(h(a).width()))},j),C(function(a,b){a.style.width=n[b]},o),h(j).height(0));C(function(a,b){a.innerHTML=
+"";a.style.width=p[b]},e);null!==a.nTFoot&&C(function(a,b){a.innerHTML="";a.style.width=n[b]},j);if(h(a.nTable).outerWidth()<i){g=d.scrollHeight>d.offsetHeight||"scroll"==h(d).css("overflow-y")?i+a.oScroll.iBarWidth:i;if(r&&(d.scrollHeight>d.offsetHeight||"scroll"==h(d).css("overflow-y")))a.nTable.style.width=q(g-a.oScroll.iBarWidth);d.style.width=q(g);a.nScrollHead.style.width=q(g);null!==a.nTFoot&&(a.nScrollFoot.style.width=q(g));""===a.oScroll.sX?D(a,1,"The table cannot fit into the current element which will cause column misalignment. The table has been drawn at its minimum possible width."):
+""!==a.oScroll.sXInner&&D(a,1,"The table cannot fit into the current element which will cause column misalignment. Increase the sScrollXInner value or remove it to allow automatic calculation")}else d.style.width=q("100%"),a.nScrollHead.style.width=q("100%"),null!==a.nTFoot&&(a.nScrollFoot.style.width=q("100%"));""===a.oScroll.sY&&r&&(d.style.height=q(a.nTable.offsetHeight+a.oScroll.iBarWidth));""!==a.oScroll.sY&&a.oScroll.bCollapse&&(d.style.height=q(a.oScroll.sY),r=""!==a.oScroll.sX&&a.nTable.offsetWidth>
+d.offsetWidth?a.oScroll.iBarWidth:0,a.nTable.offsetHeight<d.offsetHeight&&(d.style.height=q(a.nTable.offsetHeight+r)));r=h(a.nTable).outerWidth();c.style.width=q(r);b.style.width=q(r);c=h(a.nTable).height()>d.clientHeight||"scroll"==h(d).css("overflow-y");b.style.paddingRight=c?a.oScroll.iBarWidth+"px":"0px";null!==a.nTFoot&&(R.style.width=q(r),l.style.width=q(r),l.style.paddingRight=c?a.oScroll.iBarWidth+"px":"0px");h(d).scroll();if(a.bSorted||a.bFiltered)d.scrollTop=0}function C(a,b,c){for(var d=
+0,i=0,f=b.length,g,e;i<f;){g=b[i].firstChild;for(e=c?c[i].firstChild:null;g;)1===g.nodeType&&(c?a(g,e,d):a(g,d),d++),g=g.nextSibling,e=c?e.nextSibling:null;i++}}function Ma(a,b){if(!a||null===a||""===a)return 0;b||(b=l.body);var c,d=l.createElement("div");d.style.width=q(a);b.appendChild(d);c=d.offsetWidth;b.removeChild(d);return c}function da(a){var b=0,c,d=0,i=a.aoColumns.length,f,e,j=h("th",a.nTHead),o=a.nTable.getAttribute("width");e=a.nTable.parentNode;for(f=0;f<i;f++)a.aoColumns[f].bVisible&&
+(d++,null!==a.aoColumns[f].sWidth&&(c=Ma(a.aoColumns[f].sWidthOrig,e),null!==c&&(a.aoColumns[f].sWidth=q(c)),b++));if(i==j.length&&0===b&&d==i&&""===a.oScroll.sX&&""===a.oScroll.sY)for(f=0;f<a.aoColumns.length;f++)c=h(j[f]).width(),null!==c&&(a.aoColumns[f].sWidth=q(c));else{b=a.nTable.cloneNode(!1);f=a.nTHead.cloneNode(!0);d=l.createElement("tbody");c=l.createElement("tr");b.removeAttribute("id");b.appendChild(f);null!==a.nTFoot&&(b.appendChild(a.nTFoot.cloneNode(!0)),C(function(a){a.style.width=
+""},b.getElementsByTagName("tr")));b.appendChild(d);d.appendChild(c);d=h("thead th",b);0===d.length&&(d=h("tbody tr:eq(0)>td",b));j=N(a,f);for(f=d=0;f<i;f++){var k=a.aoColumns[f];k.bVisible&&null!==k.sWidthOrig&&""!==k.sWidthOrig?j[f-d].style.width=q(k.sWidthOrig):k.bVisible?j[f-d].style.width="":d++}for(f=0;f<i;f++)a.aoColumns[f].bVisible&&(d=Na(a,f),null!==d&&(d=d.cloneNode(!0),""!==a.aoColumns[f].sContentPadding&&(d.innerHTML+=a.aoColumns[f].sContentPadding),c.appendChild(d)));e.appendChild(b);
+""!==a.oScroll.sX&&""!==a.oScroll.sXInner?b.style.width=q(a.oScroll.sXInner):""!==a.oScroll.sX?(b.style.width="",h(b).width()<e.offsetWidth&&(b.style.width=q(e.offsetWidth))):""!==a.oScroll.sY?b.style.width=q(e.offsetWidth):o&&(b.style.width=q(o));b.style.visibility="hidden";Oa(a,b);i=h("tbody tr:eq(0)",b).children();0===i.length&&(i=N(a,h("thead",b)[0]));if(""!==a.oScroll.sX){for(f=d=e=0;f<a.aoColumns.length;f++)a.aoColumns[f].bVisible&&(e=null===a.aoColumns[f].sWidthOrig?e+h(i[d]).outerWidth():
+e+(parseInt(a.aoColumns[f].sWidth.replace("px",""),10)+(h(i[d]).outerWidth()-h(i[d]).width())),d++);b.style.width=q(e);a.nTable.style.width=q(e)}for(f=d=0;f<a.aoColumns.length;f++)a.aoColumns[f].bVisible&&(e=h(i[d]).width(),null!==e&&0<e&&(a.aoColumns[f].sWidth=q(e)),d++);i=h(b).css("width");a.nTable.style.width=-1!==i.indexOf("%")?i:q(h(b).outerWidth());b.parentNode.removeChild(b)}o&&(a.nTable.style.width=q(o))}function Oa(a,b){""===a.oScroll.sX&&""!==a.oScroll.sY?(h(b).width(),b.style.width=q(h(b).outerWidth()-
+a.oScroll.iBarWidth)):""!==a.oScroll.sX&&(b.style.width=q(h(b).outerWidth()))}function Na(a,b){var c=Pa(a,b);if(0>c)return null;if(null===a.aoData[c].nTr){var d=l.createElement("td");d.innerHTML=v(a,c,b,"");return d}return J(a,c)[b]}function Pa(a,b){for(var c=-1,d=-1,i=0;i<a.aoData.length;i++){var e=v(a,i,b,"display")+"",e=e.replace(/<.*?>/g,"");e.length>c&&(c=e.length,d=i)}return d}function q(a){if(null===a)return"0px";if("number"==typeof a)return 0>a?"0px":a+"px";var b=a.charCodeAt(a.length-1);
+return 48>b||57<b?a:a+"px"}function Qa(){var a=l.createElement("p"),b=a.style;b.width="100%";b.height="200px";b.padding="0px";var c=l.createElement("div"),b=c.style;b.position="absolute";b.top="0px";b.left="0px";b.visibility="hidden";b.width="200px";b.height="150px";b.padding="0px";b.overflow="hidden";c.appendChild(a);l.body.appendChild(c);b=a.offsetWidth;c.style.overflow="scroll";a=a.offsetWidth;b==a&&(a=c.clientWidth);l.body.removeChild(c);return b-a}function O(a,b){var c,d,i,e,g,k,o=[],m=[],p=
+j.ext.oSort,l=a.aoData,q=a.aoColumns,G=a.oLanguage.oAria;if(!a.oFeatures.bServerSide&&(0!==a.aaSorting.length||null!==a.aaSortingFixed)){o=null!==a.aaSortingFixed?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(c=0;c<o.length;c++)if(d=o[c][0],i=R(a,d),e=a.aoColumns[d].sSortDataType,j.ext.afnSortData[e])if(g=j.ext.afnSortData[e].call(a.oInstance,a,d,i),g.length===l.length){i=0;for(e=l.length;i<e;i++)F(a,i,d,g[i])}else D(a,0,"Returned data sort array (col "+d+") is the wrong length");c=
+0;for(d=a.aiDisplayMaster.length;c<d;c++)m[a.aiDisplayMaster[c]]=c;var r=o.length,s;c=0;for(d=l.length;c<d;c++)for(i=0;i<r;i++){s=q[o[i][0]].aDataSort;g=0;for(k=s.length;g<k;g++)e=q[s[g]].sType,e=p[(e?e:"string")+"-pre"],l[c]._aSortData[s[g]]=e?e(v(a,c,s[g],"sort")):v(a,c,s[g],"sort")}a.aiDisplayMaster.sort(function(a,b){var c,d,e,i,f;for(c=0;c<r;c++){f=q[o[c][0]].aDataSort;d=0;for(e=f.length;d<e;d++)if(i=q[f[d]].sType,i=p[(i?i:"string")+"-"+o[c][1]](l[a]._aSortData[f[d]],l[b]._aSortData[f[d]]),0!==
+i)return i}return p["numeric-asc"](m[a],m[b])})}(b===n||b)&&!a.oFeatures.bDeferRender&&P(a);c=0;for(d=a.aoColumns.length;c<d;c++)e=q[c].sTitle.replace(/<.*?>/g,""),i=q[c].nTh,i.removeAttribute("aria-sort"),i.removeAttribute("aria-label"),q[c].bSortable?0<o.length&&o[0][0]==c?(i.setAttribute("aria-sort","asc"==o[0][1]?"ascending":"descending"),i.setAttribute("aria-label",e+("asc"==(q[c].asSorting[o[0][2]+1]?q[c].asSorting[o[0][2]+1]:q[c].asSorting[0])?G.sSortAscending:G.sSortDescending))):i.setAttribute("aria-label",
+e+("asc"==q[c].asSorting[0]?G.sSortAscending:G.sSortDescending)):i.setAttribute("aria-label",e);a.bSorted=!0;h(a.oInstance).trigger("sort",a);a.oFeatures.bFilter?K(a,a.oPreviousSearch,1):(a.aiDisplay=a.aiDisplayMaster.slice(),a._iDisplayStart=0,y(a),x(a))}function ia(a,b,c,d){Ra(b,{},function(b){if(!1!==a.aoColumns[c].bSortable){var e=function(){var d,e;if(b.shiftKey){for(var f=!1,h=0;h<a.aaSorting.length;h++)if(a.aaSorting[h][0]==c){f=!0;d=a.aaSorting[h][0];e=a.aaSorting[h][2]+1;a.aoColumns[d].asSorting[e]?
+(a.aaSorting[h][1]=a.aoColumns[d].asSorting[e],a.aaSorting[h][2]=e):a.aaSorting.splice(h,1);break}!1===f&&a.aaSorting.push([c,a.aoColumns[c].asSorting[0],0])}else 1==a.aaSorting.length&&a.aaSorting[0][0]==c?(d=a.aaSorting[0][0],e=a.aaSorting[0][2]+1,a.aoColumns[d].asSorting[e]||(e=0),a.aaSorting[0][1]=a.aoColumns[d].asSorting[e],a.aaSorting[0][2]=e):(a.aaSorting.splice(0,a.aaSorting.length),a.aaSorting.push([c,a.aoColumns[c].asSorting[0],0]));O(a)};a.oFeatures.bProcessing?(E(a,!0),setTimeout(function(){e();
+a.oFeatures.bServerSide||E(a,!1)},0)):e();"function"==typeof d&&d(a)}})}function P(a){var b,c,d,e,f,g=a.aoColumns.length,j=a.oClasses;for(b=0;b<g;b++)a.aoColumns[b].bSortable&&h(a.aoColumns[b].nTh).removeClass(j.sSortAsc+" "+j.sSortDesc+" "+a.aoColumns[b].sSortingClass);c=null!==a.aaSortingFixed?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(b=0;b<a.aoColumns.length;b++)if(a.aoColumns[b].bSortable){f=a.aoColumns[b].sSortingClass;e=-1;for(d=0;d<c.length;d++)if(c[d][0]==b){f="asc"==c[d][1]?
+j.sSortAsc:j.sSortDesc;e=d;break}h(a.aoColumns[b].nTh).addClass(f);a.bJUI&&(f=h("span."+j.sSortIcon,a.aoColumns[b].nTh),f.removeClass(j.sSortJUIAsc+" "+j.sSortJUIDesc+" "+j.sSortJUI+" "+j.sSortJUIAscAllowed+" "+j.sSortJUIDescAllowed),f.addClass(-1==e?a.aoColumns[b].sSortingClassJUI:"asc"==c[e][1]?j.sSortJUIAsc:j.sSortJUIDesc))}else h(a.aoColumns[b].nTh).addClass(a.aoColumns[b].sSortingClass);f=j.sSortColumn;if(a.oFeatures.bSort&&a.oFeatures.bSortClasses){a=J(a);e=[];for(b=0;b<g;b++)e.push("");b=0;
+for(d=1;b<c.length;b++)j=parseInt(c[b][0],10),e[j]=f+d,3>d&&d++;f=RegExp(f+"[123]");var o;b=0;for(c=a.length;b<c;b++)j=b%g,d=a[b].className,o=e[j],j=d.replace(f,o),j!=d?a[b].className=h.trim(j):0<o.length&&-1==d.indexOf(o)&&(a[b].className=d+" "+o)}}function ra(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b,c;b=a.oScroll.bInfinite;var d={iCreate:(new Date).getTime(),iStart:b?0:a._iDisplayStart,iEnd:b?a._iDisplayLength:a._iDisplayEnd,iLength:a._iDisplayLength,aaSorting:h.extend(!0,[],a.aaSorting),
+oSearch:h.extend(!0,{},a.oPreviousSearch),aoSearchCols:h.extend(!0,[],a.aoPreSearchCols),abVisCols:[]};b=0;for(c=a.aoColumns.length;b<c;b++)d.abVisCols.push(a.aoColumns[b].bVisible);A(a,"aoStateSaveParams","stateSaveParams",[a,d]);a.fnStateSave.call(a.oInstance,a,d)}}function Sa(a,b){if(a.oFeatures.bStateSave){var c=a.fnStateLoad.call(a.oInstance,a);if(c){var d=A(a,"aoStateLoadParams","stateLoadParams",[a,c]);if(-1===h.inArray(!1,d)){a.oLoadedState=h.extend(!0,{},c);a._iDisplayStart=c.iStart;a.iInitDisplayStart=
+c.iStart;a._iDisplayEnd=c.iEnd;a._iDisplayLength=c.iLength;a.aaSorting=c.aaSorting.slice();a.saved_aaSorting=c.aaSorting.slice();h.extend(a.oPreviousSearch,c.oSearch);h.extend(!0,a.aoPreSearchCols,c.aoSearchCols);b.saved_aoColumns=[];for(d=0;d<c.abVisCols.length;d++)b.saved_aoColumns[d]={},b.saved_aoColumns[d].bVisible=c.abVisCols[d];A(a,"aoStateLoaded","stateLoaded",[a,c])}}}}function s(a){for(var b=0;b<j.settings.length;b++)if(j.settings[b].nTable===a)return j.settings[b];return null}function T(a){for(var b=
+[],a=a.aoData,c=0,d=a.length;c<d;c++)null!==a[c].nTr&&b.push(a[c].nTr);return b}function J(a,b){var c=[],d,e,f,g,h,j;e=0;var o=a.aoData.length;b!==n&&(e=b,o=b+1);for(f=e;f<o;f++)if(j=a.aoData[f],null!==j.nTr){e=[];for(d=j.nTr.firstChild;d;)g=d.nodeName.toLowerCase(),("td"==g||"th"==g)&&e.push(d),d=d.nextSibling;g=d=0;for(h=a.aoColumns.length;g<h;g++)a.aoColumns[g].bVisible?c.push(e[g-d]):(c.push(j._anHidden[g]),d++)}return c}function D(a,b,c){a=null===a?"DataTables warning: "+c:"DataTables warning (table id = '"+
+a.sTableId+"'): "+c;if(0===b)if("alert"==j.ext.sErrMode)alert(a);else throw Error(a);else X.console&&console.log&&console.log(a)}function p(a,b,c,d){d===n&&(d=c);b[c]!==n&&(a[d]=b[c])}function Ta(a,b){var c,d;for(d in b)b.hasOwnProperty(d)&&(c=b[d],"object"===typeof e[d]&&null!==c&&!1===h.isArray(c)?h.extend(!0,a[d],c):a[d]=c);return a}function Ra(a,b,c){h(a).bind("click.DT",b,function(b){a.blur();c(b)}).bind("keypress.DT",b,function(a){13===a.which&&c(a)}).bind("selectstart.DT",function(){return!1})}
+function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function A(a,b,c,d){for(var b=a[b],e=[],f=b.length-1;0<=f;f--)e.push(b[f].fn.apply(a.oInstance,d));null!==c&&h(a.oInstance).trigger(c,d);return e}function Ua(a){var b=h('<div style="position:absolute; top:0; left:0; height:1px; width:1px; overflow:hidden"><div style="position:absolute; top:1px; left:1px; width:100px; overflow:scroll;"><div id="DT_BrowserTest" style="width:100%; height:10px;"></div></div></div>')[0];l.body.appendChild(b);a.oBrowser.bScrollOversize=
+100===h("#DT_BrowserTest",b)[0].offsetWidth?!0:!1;l.body.removeChild(b)}function Va(a){return function(){var b=[s(this[j.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return j.ext.oApi[a].apply(this,b)}}var U=/\[.*?\]$/,Wa=X.JSON?JSON.stringify:function(a){var b=typeof a;if("object"!==b||null===a)return"string"===b&&(a='"'+a+'"'),a+"";var c,d,e=[],f=h.isArray(a);for(c in a)d=a[c],b=typeof d,"string"===b?d='"'+d+'"':"object"===b&&null!==d&&(d=Wa(d)),e.push((f?"":'"'+c+'":')+d);return(f?
+"[":"{")+e+(f?"]":"}")};this.$=function(a,b){var c,d,e=[],f;d=s(this[j.ext.iApiIndex]);var g=d.aoData,o=d.aiDisplay,k=d.aiDisplayMaster;b||(b={});b=h.extend({},{filter:"none",order:"current",page:"all"},b);if("current"==b.page){c=d._iDisplayStart;for(d=d.fnDisplayEnd();c<d;c++)(f=g[o[c]].nTr)&&e.push(f)}else if("current"==b.order&&"none"==b.filter){c=0;for(d=k.length;c<d;c++)(f=g[k[c]].nTr)&&e.push(f)}else if("current"==b.order&&"applied"==b.filter){c=0;for(d=o.length;c<d;c++)(f=g[o[c]].nTr)&&e.push(f)}else if("original"==
+b.order&&"none"==b.filter){c=0;for(d=g.length;c<d;c++)(f=g[c].nTr)&&e.push(f)}else if("original"==b.order&&"applied"==b.filter){c=0;for(d=g.length;c<d;c++)f=g[c].nTr,-1!==h.inArray(c,o)&&f&&e.push(f)}else D(d,1,"Unknown selection options");e=h(e);c=e.filter(a);e=e.find(a);return h([].concat(h.makeArray(c),h.makeArray(e)))};this._=function(a,b){var c=[],d,e,f=this.$(a,b);d=0;for(e=f.length;d<e;d++)c.push(this.fnGetData(f[d]));return c};this.fnAddData=function(a,b){if(0===a.length)return[];var c=[],
+d,e=s(this[j.ext.iApiIndex]);if("object"===typeof a[0]&&null!==a[0])for(var f=0;f<a.length;f++){d=H(e,a[f]);if(-1==d)return c;c.push(d)}else{d=H(e,a);if(-1==d)return c;c.push(d)}e.aiDisplay=e.aiDisplayMaster.slice();(b===n||b)&&aa(e);return c};this.fnAdjustColumnSizing=function(a){var b=s(this[j.ext.iApiIndex]);k(b);a===n||a?this.fnDraw(!1):(""!==b.oScroll.sX||""!==b.oScroll.sY)&&this.oApi._fnScrollDraw(b)};this.fnClearTable=function(a){var b=s(this[j.ext.iApiIndex]);ga(b);(a===n||a)&&x(b)};this.fnClose=
+function(a){for(var b=s(this[j.ext.iApiIndex]),c=0;c<b.aoOpenRows.length;c++)if(b.aoOpenRows[c].nParent==a)return(a=b.aoOpenRows[c].nTr.parentNode)&&a.removeChild(b.aoOpenRows[c].nTr),b.aoOpenRows.splice(c,1),0;return 1};this.fnDeleteRow=function(a,b,c){var d=s(this[j.ext.iApiIndex]),e,f,a="object"===typeof a?I(d,a):a,g=d.aoData.splice(a,1);e=0;for(f=d.aoData.length;e<f;e++)null!==d.aoData[e].nTr&&(d.aoData[e].nTr._DT_RowIndex=e);e=h.inArray(a,d.aiDisplay);d.asDataSearch.splice(e,1);ha(d.aiDisplayMaster,
+a);ha(d.aiDisplay,a);"function"===typeof b&&b.call(this,d,g);d._iDisplayStart>=d.fnRecordsDisplay()&&(d._iDisplayStart-=d._iDisplayLength,0>d._iDisplayStart&&(d._iDisplayStart=0));if(c===n||c)y(d),x(d);return g};this.fnDestroy=function(a){var b=s(this[j.ext.iApiIndex]),c=b.nTableWrapper.parentNode,d=b.nTBody,i,f,a=a===n?!1:a;b.bDestroying=!0;A(b,"aoDestroyCallback","destroy",[b]);if(!a){i=0;for(f=b.aoColumns.length;i<f;i++)!1===b.aoColumns[i].bVisible&&this.fnSetColumnVis(i,!0)}h(b.nTableWrapper).find("*").andSelf().unbind(".DT");
+h("tbody>tr>td."+b.oClasses.sRowEmpty,b.nTable).parent().remove();b.nTable!=b.nTHead.parentNode&&(h(b.nTable).children("thead").remove(),b.nTable.appendChild(b.nTHead));b.nTFoot&&b.nTable!=b.nTFoot.parentNode&&(h(b.nTable).children("tfoot").remove(),b.nTable.appendChild(b.nTFoot));b.nTable.parentNode.removeChild(b.nTable);h(b.nTableWrapper).remove();b.aaSorting=[];b.aaSortingFixed=[];P(b);h(T(b)).removeClass(b.asStripeClasses.join(" "));h("th, td",b.nTHead).removeClass([b.oClasses.sSortable,b.oClasses.sSortableAsc,
+b.oClasses.sSortableDesc,b.oClasses.sSortableNone].join(" "));b.bJUI&&(h("th span."+b.oClasses.sSortIcon+", td span."+b.oClasses.sSortIcon,b.nTHead).remove(),h("th, td",b.nTHead).each(function(){var a=h("div."+b.oClasses.sSortJUIWrapper,this),c=a.contents();h(this).append(c);a.remove()}));!a&&b.nTableReinsertBefore?c.insertBefore(b.nTable,b.nTableReinsertBefore):a||c.appendChild(b.nTable);i=0;for(f=b.aoData.length;i<f;i++)null!==b.aoData[i].nTr&&d.appendChild(b.aoData[i].nTr);!0===b.oFeatures.bAutoWidth&&
+(b.nTable.style.width=q(b.sDestroyWidth));if(f=b.asDestroyStripes.length){a=h(d).children("tr");for(i=0;i<f;i++)a.filter(":nth-child("+f+"n + "+i+")").addClass(b.asDestroyStripes[i])}i=0;for(f=j.settings.length;i<f;i++)j.settings[i]==b&&j.settings.splice(i,1);e=b=null};this.fnDraw=function(a){var b=s(this[j.ext.iApiIndex]);!1===a?(y(b),x(b)):aa(b)};this.fnFilter=function(a,b,c,d,e,f){var g=s(this[j.ext.iApiIndex]);if(g.oFeatures.bFilter){if(c===n||null===c)c=!1;if(d===n||null===d)d=!0;if(e===n||null===
+e)e=!0;if(f===n||null===f)f=!0;if(b===n||null===b){if(K(g,{sSearch:a+"",bRegex:c,bSmart:d,bCaseInsensitive:f},1),e&&g.aanFeatures.f){b=g.aanFeatures.f;c=0;for(d=b.length;c<d;c++)try{b[c]._DT_Input!=l.activeElement&&h(b[c]._DT_Input).val(a)}catch(o){h(b[c]._DT_Input).val(a)}}}else h.extend(g.aoPreSearchCols[b],{sSearch:a+"",bRegex:c,bSmart:d,bCaseInsensitive:f}),K(g,g.oPreviousSearch,1)}};this.fnGetData=function(a,b){var c=s(this[j.ext.iApiIndex]);if(a!==n){var d=a;if("object"===typeof a){var e=a.nodeName.toLowerCase();
+"tr"===e?d=I(c,a):"td"===e&&(d=I(c,a.parentNode),b=fa(c,d,a))}return b!==n?v(c,d,b,""):c.aoData[d]!==n?c.aoData[d]._aData:null}return Z(c)};this.fnGetNodes=function(a){var b=s(this[j.ext.iApiIndex]);return a!==n?b.aoData[a]!==n?b.aoData[a].nTr:null:T(b)};this.fnGetPosition=function(a){var b=s(this[j.ext.iApiIndex]),c=a.nodeName.toUpperCase();return"TR"==c?I(b,a):"TD"==c||"TH"==c?(c=I(b,a.parentNode),a=fa(b,c,a),[c,R(b,a),a]):null};this.fnIsOpen=function(a){for(var b=s(this[j.ext.iApiIndex]),c=0;c<
+b.aoOpenRows.length;c++)if(b.aoOpenRows[c].nParent==a)return!0;return!1};this.fnOpen=function(a,b,c){var d=s(this[j.ext.iApiIndex]),e=T(d);if(-1!==h.inArray(a,e)){this.fnClose(a);var e=l.createElement("tr"),f=l.createElement("td");e.appendChild(f);f.className=c;f.colSpan=t(d);"string"===typeof b?f.innerHTML=b:h(f).html(b);b=h("tr",d.nTBody);-1!=h.inArray(a,b)&&h(e).insertAfter(a);d.aoOpenRows.push({nTr:e,nParent:a});return e}};this.fnPageChange=function(a,b){var c=s(this[j.ext.iApiIndex]);qa(c,a);
+y(c);(b===n||b)&&x(c)};this.fnSetColumnVis=function(a,b,c){var d=s(this[j.ext.iApiIndex]),e,f,g=d.aoColumns,h=d.aoData,o,m;if(g[a].bVisible!=b){if(b){for(e=f=0;e<a;e++)g[e].bVisible&&f++;m=f>=t(d);if(!m)for(e=a;e<g.length;e++)if(g[e].bVisible){o=e;break}e=0;for(f=h.length;e<f;e++)null!==h[e].nTr&&(m?h[e].nTr.appendChild(h[e]._anHidden[a]):h[e].nTr.insertBefore(h[e]._anHidden[a],J(d,e)[o]))}else{e=0;for(f=h.length;e<f;e++)null!==h[e].nTr&&(o=J(d,e)[a],h[e]._anHidden[a]=o,o.parentNode.removeChild(o))}g[a].bVisible=
+b;W(d,d.aoHeader);d.nTFoot&&W(d,d.aoFooter);e=0;for(f=d.aoOpenRows.length;e<f;e++)d.aoOpenRows[e].nTr.colSpan=t(d);if(c===n||c)k(d),x(d);ra(d)}};this.fnSettings=function(){return s(this[j.ext.iApiIndex])};this.fnSort=function(a){var b=s(this[j.ext.iApiIndex]);b.aaSorting=a;O(b)};this.fnSortListener=function(a,b,c){ia(s(this[j.ext.iApiIndex]),a,b,c)};this.fnUpdate=function(a,b,c,d,e){var f=s(this[j.ext.iApiIndex]),b="object"===typeof b?I(f,b):b;if(h.isArray(a)&&c===n){f.aoData[b]._aData=a.slice();
+for(c=0;c<f.aoColumns.length;c++)this.fnUpdate(v(f,b,c),b,c,!1,!1)}else if(h.isPlainObject(a)&&c===n){f.aoData[b]._aData=h.extend(!0,{},a);for(c=0;c<f.aoColumns.length;c++)this.fnUpdate(v(f,b,c),b,c,!1,!1)}else{F(f,b,c,a);var a=v(f,b,c,"display"),g=f.aoColumns[c];null!==g.fnRender&&(a=S(f,b,c),g.bUseRendered&&F(f,b,c,a));null!==f.aoData[b].nTr&&(J(f,b)[c].innerHTML=a)}c=h.inArray(b,f.aiDisplay);f.asDataSearch[c]=na(f,Y(f,b,"filter",r(f,"bSearchable")));(e===n||e)&&k(f);(d===n||d)&&aa(f);return 0};
+this.fnVersionCheck=j.ext.fnVersionCheck;this.oApi={_fnExternApiFunc:Va,_fnInitialise:ba,_fnInitComplete:$,_fnLanguageCompat:pa,_fnAddColumn:o,_fnColumnOptions:m,_fnAddData:H,_fnCreateTr:ea,_fnGatherData:ua,_fnBuildHead:va,_fnDrawHead:W,_fnDraw:x,_fnReDraw:aa,_fnAjaxUpdate:wa,_fnAjaxParameters:Ea,_fnAjaxUpdateDraw:Fa,_fnServerParams:ka,_fnAddOptionsHtml:xa,_fnFeatureHtmlTable:Ba,_fnScrollDraw:La,_fnAdjustColumnSizing:k,_fnFeatureHtmlFilter:za,_fnFilterComplete:K,_fnFilterCustom:Ia,_fnFilterColumn:Ha,
+_fnFilter:Ga,_fnBuildSearchArray:la,_fnBuildSearchRow:na,_fnFilterCreateSearch:ma,_fnDataToSearch:Ja,_fnSort:O,_fnSortAttachListener:ia,_fnSortingClasses:P,_fnFeatureHtmlPaginate:Da,_fnPageChange:qa,_fnFeatureHtmlInfo:Ca,_fnUpdateInfo:Ka,_fnFeatureHtmlLength:ya,_fnFeatureHtmlProcessing:Aa,_fnProcessingDisplay:E,_fnVisibleToColumnIndex:G,_fnColumnIndexToVisible:R,_fnNodeToDataIndex:I,_fnVisbleColumns:t,_fnCalculateEnd:y,_fnConvertToWidth:Ma,_fnCalculateColumnWidths:da,_fnScrollingWidthAdjust:Oa,_fnGetWidestNode:Na,
+_fnGetMaxLenString:Pa,_fnStringToCss:q,_fnDetectType:B,_fnSettingsFromNode:s,_fnGetDataMaster:Z,_fnGetTrNodes:T,_fnGetTdNodes:J,_fnEscapeRegex:oa,_fnDeleteIndex:ha,_fnReOrderIndex:u,_fnColumnOrdering:M,_fnLog:D,_fnClearTable:ga,_fnSaveState:ra,_fnLoadState:Sa,_fnCreateCookie:function(a,b,c,d,e){var f=new Date;f.setTime(f.getTime()+1E3*c);var c=X.location.pathname.split("/"),a=a+"_"+c.pop().replace(/[\/:]/g,"").toLowerCase(),g;null!==e?(g="function"===typeof h.parseJSON?h.parseJSON(b):eval("("+b+")"),
+b=e(a,g,f.toGMTString(),c.join("/")+"/")):b=a+"="+encodeURIComponent(b)+"; expires="+f.toGMTString()+"; path="+c.join("/")+"/";a=l.cookie.split(";");e=b.split(";")[0].length;f=[];if(4096<e+l.cookie.length+10){for(var j=0,o=a.length;j<o;j++)if(-1!=a[j].indexOf(d)){var k=a[j].split("=");try{(g=eval("("+decodeURIComponent(k[1])+")"))&&g.iCreate&&f.push({name:k[0],time:g.iCreate})}catch(m){}}for(f.sort(function(a,b){return b.time-a.time});4096<e+l.cookie.length+10;){if(0===f.length)return;d=f.pop();l.cookie=
+d.name+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+c.join("/")+"/"}}l.cookie=b},_fnReadCookie:function(a){for(var b=X.location.pathname.split("/"),a=a+"_"+b[b.length-1].replace(/[\/:]/g,"").toLowerCase()+"=",b=l.cookie.split(";"),c=0;c<b.length;c++){for(var d=b[c];" "==d.charAt(0);)d=d.substring(1,d.length);if(0===d.indexOf(a))return decodeURIComponent(d.substring(a.length,d.length))}return null},_fnDetectHeader:V,_fnGetUniqueThs:N,_fnScrollBarWidth:Qa,_fnApplyToChildren:C,_fnMap:p,_fnGetRowData:Y,
+_fnGetCellData:v,_fnSetCellData:F,_fnGetObjectDataFn:Q,_fnSetObjectDataFn:L,_fnApplyColumnDefs:ta,_fnBindAction:Ra,_fnExtend:Ta,_fnCallbackReg:z,_fnCallbackFire:A,_fnJsonString:Wa,_fnRender:S,_fnNodeToColumnIndex:fa,_fnInfoMacros:ja,_fnBrowserDetect:Ua,_fnGetColumns:r};h.extend(j.ext.oApi,this.oApi);for(var sa in j.ext.oApi)sa&&(this[sa]=Va(sa));var ca=this;this.each(function(){var a=0,b,c,d;c=this.getAttribute("id");var i=!1,f=!1;if("table"!=this.nodeName.toLowerCase())D(null,0,"Attempted to initialise DataTables on a node which is not a table: "+
+this.nodeName);else{a=0;for(b=j.settings.length;a<b;a++){if(j.settings[a].nTable==this){if(e===n||e.bRetrieve)return j.settings[a].oInstance;if(e.bDestroy){j.settings[a].oInstance.fnDestroy();break}else{D(j.settings[a],0,"Cannot reinitialise DataTable.\n\nTo retrieve the DataTables object for this table, pass no arguments or see the docs for bRetrieve and bDestroy");return}}if(j.settings[a].sTableId==this.id){j.settings.splice(a,1);break}}if(null===c||""===c)this.id=c="DataTables_Table_"+j.ext._oExternConfig.iNextUnique++;
+var g=h.extend(!0,{},j.models.oSettings,{nTable:this,oApi:ca.oApi,oInit:e,sDestroyWidth:h(this).width(),sInstance:c,sTableId:c});j.settings.push(g);g.oInstance=1===ca.length?ca:h(this).dataTable();e||(e={});e.oLanguage&&pa(e.oLanguage);e=Ta(h.extend(!0,{},j.defaults),e);p(g.oFeatures,e,"bPaginate");p(g.oFeatures,e,"bLengthChange");p(g.oFeatures,e,"bFilter");p(g.oFeatures,e,"bSort");p(g.oFeatures,e,"bInfo");p(g.oFeatures,e,"bProcessing");p(g.oFeatures,e,"bAutoWidth");p(g.oFeatures,e,"bSortClasses");
+p(g.oFeatures,e,"bServerSide");p(g.oFeatures,e,"bDeferRender");p(g.oScroll,e,"sScrollX","sX");p(g.oScroll,e,"sScrollXInner","sXInner");p(g.oScroll,e,"sScrollY","sY");p(g.oScroll,e,"bScrollCollapse","bCollapse");p(g.oScroll,e,"bScrollInfinite","bInfinite");p(g.oScroll,e,"iScrollLoadGap","iLoadGap");p(g.oScroll,e,"bScrollAutoCss","bAutoCss");p(g,e,"asStripeClasses");p(g,e,"asStripClasses","asStripeClasses");p(g,e,"fnServerData");p(g,e,"fnFormatNumber");p(g,e,"sServerMethod");p(g,e,"aaSorting");p(g,
+e,"aaSortingFixed");p(g,e,"aLengthMenu");p(g,e,"sPaginationType");p(g,e,"sAjaxSource");p(g,e,"sAjaxDataProp");p(g,e,"iCookieDuration");p(g,e,"sCookiePrefix");p(g,e,"sDom");p(g,e,"bSortCellsTop");p(g,e,"iTabIndex");p(g,e,"oSearch","oPreviousSearch");p(g,e,"aoSearchCols","aoPreSearchCols");p(g,e,"iDisplayLength","_iDisplayLength");p(g,e,"bJQueryUI","bJUI");p(g,e,"fnCookieCallback");p(g,e,"fnStateLoad");p(g,e,"fnStateSave");p(g.oLanguage,e,"fnInfoCallback");z(g,"aoDrawCallback",e.fnDrawCallback,"user");
+z(g,"aoServerParams",e.fnServerParams,"user");z(g,"aoStateSaveParams",e.fnStateSaveParams,"user");z(g,"aoStateLoadParams",e.fnStateLoadParams,"user");z(g,"aoStateLoaded",e.fnStateLoaded,"user");z(g,"aoRowCallback",e.fnRowCallback,"user");z(g,"aoRowCreatedCallback",e.fnCreatedRow,"user");z(g,"aoHeaderCallback",e.fnHeaderCallback,"user");z(g,"aoFooterCallback",e.fnFooterCallback,"user");z(g,"aoInitComplete",e.fnInitComplete,"user");z(g,"aoPreDrawCallback",e.fnPreDrawCallback,"user");g.oFeatures.bServerSide&&
+g.oFeatures.bSort&&g.oFeatures.bSortClasses?z(g,"aoDrawCallback",P,"server_side_sort_classes"):g.oFeatures.bDeferRender&&z(g,"aoDrawCallback",P,"defer_sort_classes");e.bJQueryUI?(h.extend(g.oClasses,j.ext.oJUIClasses),e.sDom===j.defaults.sDom&&"lfrtip"===j.defaults.sDom&&(g.sDom='<"H"lfr>t<"F"ip>')):h.extend(g.oClasses,j.ext.oStdClasses);h(this).addClass(g.oClasses.sTable);if(""!==g.oScroll.sX||""!==g.oScroll.sY)g.oScroll.iBarWidth=Qa();g.iInitDisplayStart===n&&(g.iInitDisplayStart=e.iDisplayStart,
+g._iDisplayStart=e.iDisplayStart);e.bStateSave&&(g.oFeatures.bStateSave=!0,Sa(g,e),z(g,"aoDrawCallback",ra,"state_save"));null!==e.iDeferLoading&&(g.bDeferLoading=!0,a=h.isArray(e.iDeferLoading),g._iRecordsDisplay=a?e.iDeferLoading[0]:e.iDeferLoading,g._iRecordsTotal=a?e.iDeferLoading[1]:e.iDeferLoading);null!==e.aaData&&(f=!0);""!==e.oLanguage.sUrl?(g.oLanguage.sUrl=e.oLanguage.sUrl,h.getJSON(g.oLanguage.sUrl,null,function(a){pa(a);h.extend(true,g.oLanguage,e.oLanguage,a);ba(g)}),i=!0):h.extend(!0,
+g.oLanguage,e.oLanguage);null===e.asStripeClasses&&(g.asStripeClasses=[g.oClasses.sStripeOdd,g.oClasses.sStripeEven]);b=g.asStripeClasses.length;g.asDestroyStripes=[];if(b){c=!1;d=h(this).children("tbody").children("tr:lt("+b+")");for(a=0;a<b;a++)d.hasClass(g.asStripeClasses[a])&&(c=!0,g.asDestroyStripes.push(g.asStripeClasses[a]));c&&d.removeClass(g.asStripeClasses.join(" "))}c=[];a=this.getElementsByTagName("thead");0!==a.length&&(V(g.aoHeader,a[0]),c=N(g));if(null===e.aoColumns){d=[];a=0;for(b=
+c.length;a<b;a++)d.push(null)}else d=e.aoColumns;a=0;for(b=d.length;a<b;a++)e.saved_aoColumns!==n&&e.saved_aoColumns.length==b&&(null===d[a]&&(d[a]={}),d[a].bVisible=e.saved_aoColumns[a].bVisible),o(g,c?c[a]:null);ta(g,e.aoColumnDefs,d,function(a,b){m(g,a,b)});a=0;for(b=g.aaSorting.length;a<b;a++){g.aaSorting[a][0]>=g.aoColumns.length&&(g.aaSorting[a][0]=0);var k=g.aoColumns[g.aaSorting[a][0]];g.aaSorting[a][2]===n&&(g.aaSorting[a][2]=0);e.aaSorting===n&&g.saved_aaSorting===n&&(g.aaSorting[a][1]=
+k.asSorting[0]);c=0;for(d=k.asSorting.length;c<d;c++)if(g.aaSorting[a][1]==k.asSorting[c]){g.aaSorting[a][2]=c;break}}P(g);Ua(g);a=h(this).children("caption").each(function(){this._captionSide=h(this).css("caption-side")});b=h(this).children("thead");0===b.length&&(b=[l.createElement("thead")],this.appendChild(b[0]));g.nTHead=b[0];b=h(this).children("tbody");0===b.length&&(b=[l.createElement("tbody")],this.appendChild(b[0]));g.nTBody=b[0];g.nTBody.setAttribute("role","alert");g.nTBody.setAttribute("aria-live",
+"polite");g.nTBody.setAttribute("aria-relevant","all");b=h(this).children("tfoot");if(0===b.length&&0<a.length&&(""!==g.oScroll.sX||""!==g.oScroll.sY))b=[l.createElement("tfoot")],this.appendChild(b[0]);0<b.length&&(g.nTFoot=b[0],V(g.aoFooter,g.nTFoot));if(f)for(a=0;a<e.aaData.length;a++)H(g,e.aaData[a]);else ua(g);g.aiDisplay=g.aiDisplayMaster.slice();g.bInitialised=!0;!1===i&&ba(g)}});ca=null;return this};j.fnVersionCheck=function(e){for(var h=function(e,h){for(;e.length<h;)e+="0";return e},m=j.ext.sVersion.split("."),
+e=e.split("."),k="",n="",l=0,t=e.length;l<t;l++)k+=h(m[l],3),n+=h(e[l],3);return parseInt(k,10)>=parseInt(n,10)};j.fnIsDataTable=function(e){for(var h=j.settings,m=0;m<h.length;m++)if(h[m].nTable===e||h[m].nScrollHead===e||h[m].nScrollFoot===e)return!0;return!1};j.fnTables=function(e){var o=[];jQuery.each(j.settings,function(j,k){(!e||!0===e&&h(k.nTable).is(":visible"))&&o.push(k.nTable)});return o};j.version="1.9.4";j.settings=[];j.models={};j.models.ext={afnFiltering:[],afnSortData:[],aoFeatures:[],
+aTypes:[],fnVersionCheck:j.fnVersionCheck,iApiIndex:0,ofnSearch:{},oApi:{},oStdClasses:{},oJUIClasses:{},oPagination:{},oSort:{},sVersion:j.version,sErrMode:"alert",_oExternConfig:{iNextUnique:0}};j.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};j.models.oRow={nTr:null,_aData:[],_aSortData:[],_anHidden:[],_sRowStripe:""};j.models.oColumn={aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bUseRendered:null,bVisible:null,_bAutoType:!0,fnCreatedCell:null,fnGetData:null,
+fnRender:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};j.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,
+bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollAutoCss:!0,bScrollCollapse:!1,bScrollInfinite:!1,bServerSide:!1,bSort:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCookieCallback:null,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(e){if(1E3>e)return e;for(var h=e+"",e=h.split(""),j="",h=h.length,k=0;k<h;k++)0===k%3&&0!==k&&(j=this.oLanguage.sInfoThousands+j),j=e[h-k-1]+j;return j},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,
+fnRowCallback:null,fnServerData:function(e,j,m,k){k.jqXHR=h.ajax({url:e,data:j,success:function(e){e.sError&&k.oApi._fnLog(k,0,e.sError);h(k.oInstance).trigger("xhr",[k,e]);m(e)},dataType:"json",cache:!1,type:k.sServerMethod,error:function(e,h){"parsererror"==h&&k.oApi._fnLog(k,0,"DataTables warning: JSON data from server could not be parsed. This is caused by a JSON formatting error.")}})},fnServerParams:null,fnStateLoad:function(e){var e=this.oApi._fnReadCookie(e.sCookiePrefix+e.sInstance),j;try{j=
+"function"===typeof h.parseJSON?h.parseJSON(e):eval("("+e+")")}catch(m){j=null}return j},fnStateLoadParams:null,fnStateLoaded:null,fnStateSave:function(e,h){this.oApi._fnCreateCookie(e.sCookiePrefix+e.sInstance,this.oApi._fnJsonString(h),e.iCookieDuration,e.sCookiePrefix,e.fnCookieCallback)},fnStateSaveParams:null,iCookieDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iScrollLoadGap:100,iTabIndex:0,oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},
+oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sInfoThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},j.models.oSearch),sAjaxDataProp:"aaData",
+sAjaxSource:null,sCookiePrefix:"SpryMedia_DataTables_",sDom:"lfrtip",sPaginationType:"two_button",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET"};j.defaults.columns={aDataSort:null,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bUseRendered:!0,bVisible:!0,fnCreatedCell:null,fnRender:null,iDataSort:-1,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};j.models.oSettings={oFeatures:{bAutoWidth:null,
+bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortClasses:null,bStateSave:null},oScroll:{bAutoCss:null,bCollapse:null,bInfinite:null,iBarWidth:0,iLoadGap:null,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1},aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aoColumns:[],aoHeader:[],aoFooter:[],asDataSearch:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:null,
+asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,sPaginationType:"two_button",iCookieDuration:0,sCookiePrefix:"",fnCookieCallback:null,aoStateSave:[],aoStateLoad:[],
+oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iDisplayEnd:10,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return this.oFeatures.bServerSide?parseInt(this._iRecordsTotal,10):this.aiDisplayMaster.length},
+fnRecordsDisplay:function(){return this.oFeatures.bServerSide?parseInt(this._iRecordsDisplay,10):this.aiDisplay.length},fnDisplayEnd:function(){return this.oFeatures.bServerSide?!1===this.oFeatures.bPaginate||-1==this._iDisplayLength?this._iDisplayStart+this.aiDisplay.length:Math.min(this._iDisplayStart+this._iDisplayLength,this._iRecordsDisplay):this._iDisplayEnd},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null};j.ext=h.extend(!0,{},j.models.ext);h.extend(j.ext.oStdClasses,
+{sTable:"dataTable",sPagePrevEnabled:"paginate_enabled_previous",sPagePrevDisabled:"paginate_disabled_previous",sPageNextEnabled:"paginate_enabled_next",sPageNextDisabled:"paginate_disabled_next",sPageJUINext:"",sPageJUIPrev:"",sPageButton:"paginate_button",sPageButtonActive:"paginate_active",sPageButtonStaticDisabled:"paginate_button paginate_button_disabled",sPageFirst:"first",sPagePrevious:"previous",sPageNext:"next",sPageLast:"last",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",
+sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",
+sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sFooterTH:"",sJUIHeader:"",sJUIFooter:""});h.extend(j.ext.oJUIClasses,j.ext.oStdClasses,{sPagePrevEnabled:"fg-button ui-button ui-state-default ui-corner-left",sPagePrevDisabled:"fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",sPageNextEnabled:"fg-button ui-button ui-state-default ui-corner-right",
+sPageNextDisabled:"fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",sPageJUINext:"ui-icon ui-icon-circle-arrow-e",sPageJUIPrev:"ui-icon ui-icon-circle-arrow-w",sPageButton:"fg-button ui-button ui-state-default",sPageButtonActive:"fg-button ui-button ui-state-default ui-state-disabled",sPageButtonStaticDisabled:"fg-button ui-button ui-state-default ui-state-disabled",sPageFirst:"first ui-corner-tl ui-corner-bl",sPageLast:"last ui-corner-tr ui-corner-br",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",
+sSortAsc:"ui-state-default",sSortDesc:"ui-state-default",sSortable:"ui-state-default",sSortableAsc:"ui-state-default",sSortableDesc:"ui-state-default",sSortableNone:"ui-state-default",sSortJUIAsc:"css_right ui-icon ui-icon-triangle-1-n",sSortJUIDesc:"css_right ui-icon ui-icon-triangle-1-s",sSortJUI:"css_right ui-icon ui-icon-carat-2-n-s",sSortJUIAscAllowed:"css_right ui-icon ui-icon-carat-1-n",sSortJUIDescAllowed:"css_right ui-icon ui-icon-carat-1-s",sSortJUIWrapper:"DataTables_sort_wrapper",sSortIcon:"DataTables_sort_icon",
+sScrollHead:"dataTables_scrollHead ui-state-default",sScrollFoot:"dataTables_scrollFoot ui-state-default",sFooterTH:"ui-state-default",sJUIHeader:"fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix",sJUIFooter:"fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix"});h.extend(j.ext.oPagination,{two_button:{fnInit:function(e,j,m){var k=e.oLanguage.oPaginate,n=function(h){e.oApi._fnPageChange(e,h.data.action)&&m(e)},k=!e.bJUI?'<a class="'+
+e.oClasses.sPagePrevDisabled+'" tabindex="'+e.iTabIndex+'" role="button">'+k.sPrevious+'</a><a class="'+e.oClasses.sPageNextDisabled+'" tabindex="'+e.iTabIndex+'" role="button">'+k.sNext+"</a>":'<a class="'+e.oClasses.sPagePrevDisabled+'" tabindex="'+e.iTabIndex+'" role="button"><span class="'+e.oClasses.sPageJUIPrev+'"></span></a><a class="'+e.oClasses.sPageNextDisabled+'" tabindex="'+e.iTabIndex+'" role="button"><span class="'+e.oClasses.sPageJUINext+'"></span></a>';h(j).append(k);var l=h("a",j),
+k=l[0],l=l[1];e.oApi._fnBindAction(k,{action:"previous"},n);e.oApi._fnBindAction(l,{action:"next"},n);e.aanFeatures.p||(j.id=e.sTableId+"_paginate",k.id=e.sTableId+"_previous",l.id=e.sTableId+"_next",k.setAttribute("aria-controls",e.sTableId),l.setAttribute("aria-controls",e.sTableId))},fnUpdate:function(e){if(e.aanFeatures.p)for(var h=e.oClasses,j=e.aanFeatures.p,k,l=0,n=j.length;l<n;l++)if(k=j[l].firstChild)k.className=0===e._iDisplayStart?h.sPagePrevDisabled:h.sPagePrevEnabled,k=k.nextSibling,
+k.className=e.fnDisplayEnd()==e.fnRecordsDisplay()?h.sPageNextDisabled:h.sPageNextEnabled}},iFullNumbersShowPages:5,full_numbers:{fnInit:function(e,j,m){var k=e.oLanguage.oPaginate,l=e.oClasses,n=function(h){e.oApi._fnPageChange(e,h.data.action)&&m(e)};h(j).append('<a  tabindex="'+e.iTabIndex+'" class="'+l.sPageButton+" "+l.sPageFirst+'">'+k.sFirst+'</a><a  tabindex="'+e.iTabIndex+'" class="'+l.sPageButton+" "+l.sPagePrevious+'">'+k.sPrevious+'</a><span></span><a tabindex="'+e.iTabIndex+'" class="'+
+l.sPageButton+" "+l.sPageNext+'">'+k.sNext+'</a><a tabindex="'+e.iTabIndex+'" class="'+l.sPageButton+" "+l.sPageLast+'">'+k.sLast+"</a>");var t=h("a",j),k=t[0],l=t[1],r=t[2],t=t[3];e.oApi._fnBindAction(k,{action:"first"},n);e.oApi._fnBindAction(l,{action:"previous"},n);e.oApi._fnBindAction(r,{action:"next"},n);e.oApi._fnBindAction(t,{action:"last"},n);e.aanFeatures.p||(j.id=e.sTableId+"_paginate",k.id=e.sTableId+"_first",l.id=e.sTableId+"_previous",r.id=e.sTableId+"_next",t.id=e.sTableId+"_last")},
+fnUpdate:function(e,o){if(e.aanFeatures.p){var m=j.ext.oPagination.iFullNumbersShowPages,k=Math.floor(m/2),l=Math.ceil(e.fnRecordsDisplay()/e._iDisplayLength),n=Math.ceil(e._iDisplayStart/e._iDisplayLength)+1,t="",r,B=e.oClasses,u,M=e.aanFeatures.p,L=function(h){e.oApi._fnBindAction(this,{page:h+r-1},function(h){e.oApi._fnPageChange(e,h.data.page);o(e);h.preventDefault()})};-1===e._iDisplayLength?n=k=r=1:l<m?(r=1,k=l):n<=k?(r=1,k=m):n>=l-k?(r=l-m+1,k=l):(r=n-Math.ceil(m/2)+1,k=r+m-1);for(m=r;m<=k;m++)t+=
+n!==m?'<a tabindex="'+e.iTabIndex+'" class="'+B.sPageButton+'">'+e.fnFormatNumber(m)+"</a>":'<a tabindex="'+e.iTabIndex+'" class="'+B.sPageButtonActive+'">'+e.fnFormatNumber(m)+"</a>";m=0;for(k=M.length;m<k;m++)u=M[m],u.hasChildNodes()&&(h("span:eq(0)",u).html(t).children("a").each(L),u=u.getElementsByTagName("a"),u=[u[0],u[1],u[u.length-2],u[u.length-1]],h(u).removeClass(B.sPageButton+" "+B.sPageButtonActive+" "+B.sPageButtonStaticDisabled),h([u[0],u[1]]).addClass(1==n?B.sPageButtonStaticDisabled:
+B.sPageButton),h([u[2],u[3]]).addClass(0===l||n===l||-1===e._iDisplayLength?B.sPageButtonStaticDisabled:B.sPageButton))}}}});h.extend(j.ext.oSort,{"string-pre":function(e){"string"!=typeof e&&(e=null!==e&&e.toString?e.toString():"");return e.toLowerCase()},"string-asc":function(e,h){return e<h?-1:e>h?1:0},"string-desc":function(e,h){return e<h?1:e>h?-1:0},"html-pre":function(e){return e.replace(/<.*?>/g,"").toLowerCase()},"html-asc":function(e,h){return e<h?-1:e>h?1:0},"html-desc":function(e,h){return e<
+h?1:e>h?-1:0},"date-pre":function(e){e=Date.parse(e);if(isNaN(e)||""===e)e=Date.parse("01/01/1970 00:00:00");return e},"date-asc":function(e,h){return e-h},"date-desc":function(e,h){return h-e},"numeric-pre":function(e){return"-"==e||""===e?0:1*e},"numeric-asc":function(e,h){return e-h},"numeric-desc":function(e,h){return h-e}});h.extend(j.ext.aTypes,[function(e){if("number"===typeof e)return"numeric";if("string"!==typeof e)return null;var h,j=!1;h=e.charAt(0);if(-1=="0123456789-".indexOf(h))return null;
+for(var k=1;k<e.length;k++){h=e.charAt(k);if(-1=="0123456789.".indexOf(h))return null;if("."==h){if(j)return null;j=!0}}return"numeric"},function(e){var h=Date.parse(e);return null!==h&&!isNaN(h)||"string"===typeof e&&0===e.length?"date":null},function(e){return"string"===typeof e&&-1!=e.indexOf("<")&&-1!=e.indexOf(">")?"html":null}]);h.fn.DataTable=j;h.fn.dataTable=j;h.fn.dataTableSettings=j.settings;h.fn.dataTableExt=j.ext};"function"===typeof define&&define.amd?define(["jquery"],L):jQuery&&!jQuery.fn.dataTable&&
+L(jQuery)})(window,document);
Index: /main/waeup.ikoba/branches/uli-payments/layout/theme.html
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/layout/theme.html	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/layout/theme.html	(revision 12677)
@@ -0,0 +1,541 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Base · Twitter Bootstrap</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="">
+    <meta name="author" content="">
+
+    <!-- stylesheets -->
+    <link href="static/bootstrap/css/bootstrap.min.css"
+          rel="stylesheet">
+    <link href="static/bootstrap/css/bootstrap-theme.min.css"
+          rel="stylesheet">
+    <link href="static/css/jquery.dataTables.css"
+          rel="stylesheet">
+    <link href="static/css/jquery-ui.css"
+          rel="stylesheet">
+    <link href="static/css/base.css" rel="stylesheet">
+    <link href="static/css/file-upload.css" rel="stylesheet">
+
+    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
+    <!--[if lt IE 9]>
+      <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
+      <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
+      <link href="static/css/IE8.css" rel="stylesheet">
+    <![endif]-->
+
+    <script src="static/js/ikoba-functions.js"></script>
+
+    <!-- icons -->
+    <link rel="shortcut icon" href="static/ico/favicon.ico">
+
+
+  </head>
+
+  <body id="ikoba-body">
+
+    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
+      <div class="navbar-header">
+        <button type="button" class="navbar-toggle" data-toggle="collapse"
+                  data-target="#ikoba-primary-nav">
+          <span class="icon-bar"></span>
+          <span class="icon-bar"></span>
+          <span class="icon-bar"></span>
+        </button>
+        <a class="navbar-brand active ikoba-brand-link" href="/">WAeUP.Ikoba</a>
+      </div>
+      <div class="navbar-collapse collapse"
+           id="ikoba-primary-nav">
+        <ul class="nav navbar-nav ikoba-navbar-left">
+          <li class="active"><a href="#">Products</a></li>
+          <li><a href="#about">Customers</a></li>
+          <li class="dropdown">
+            <a href="#" class="dropdown-toggle"
+               data-toggle="dropdown">My Data
+               <b class="caret"></b>
+            <ul class="dropdown-menu">
+              <li><a href="#">Base Data</a></li>
+              <li><a href="#">Documents</a></li>
+              <li><a href="#">Contracts</a></li>
+              <li><a href="#">History</a></li>
+            </ul>
+          </li>
+        </ul>
+
+        <ul class="nav navbar-nav navbar-right ikoba-navbar-right">
+          <li class="dropdown ikoba-langselect">
+            <a href="#" class="dropdown-toggle"
+               data-toggle="dropdown">
+               <img src="static/img/earth.png" />
+            <ul class="dropdown-menu">
+              <li class="ikoba-language"><a href="#">English</a></li>
+              <li class="ikoba-language"><a href="#">Français</a></li>
+              <li class="ikoba-language"><a href="#">Deutsch</a></li>
+              <li class="ikoba-language"><a href="#">Hausa</a></li>
+              <li class="ikoba-language"><a href="#">Yoruba</a></li>
+              <li class="ikoba-language"><a href="#">Igbo</a></li>
+            </ul>
+          </li>
+          <li><a href="#">Contact</a></li>
+          <li><a href="#">Logout</a></li>
+        </ul>
+      </div><!--/.nav-collapse -->
+    </div><!-- /.navbar -->
+
+    <div class="container">
+
+      <div class="row">
+        <div class="col-md-12 container ikoba-breadcrumbs-box-long">
+          <div class="well">
+            <ol class="breadcrumb ikoba-breadcrumbs">
+              <li>
+                <a href="#">
+                  Home
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 1
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 2
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 3
+                </a>
+              </li>
+            </ol><!-- /ikoba-breadcrumbs -->
+          </div> <!-- /well -->
+        </div> <!-- /ikoba-breadcrumbs-box-short -->
+      </div>
+
+      <div class="row">
+        <div class="col-md-8 container ikoba-breadcrumbs-box-short">
+          <div class="well">
+            <ol class="breadcrumb ikoba-breadcrumbs">
+              <li>
+                <a href="#">
+                  Home
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 1
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 2
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 3
+                </a>
+              </li>
+            </ol><!-- /ikoba-breadcrumbs -->
+          </div> <!-- /well -->
+        </div> <!-- /ikoba-breadcrumbs-box-short -->
+
+        <div class="col-md-4 container ikoba-customer-status-box">
+          <div class="well">
+            <div class="wfstatus">
+                K1000000 &bull; quite long customer status
+            </div> <!-- /wfstatus -->
+          </div> <!-- /well -->
+        </div> <!-- /ikoba-customer-status-box -->
+
+      </div> <!-- /row -->
+
+      <div class="row">
+        <div class="col-md-2 ikoba-sidebar">
+          <div class="sidebar-nav well">
+            <div>
+              <div class="ikoba-sidebar-title">Customer Name</div>
+              <ul class="nav">
+                <li><a href="#">Base Data</a></li>
+                <li><a href="#">Documents</a></li>
+                <li><a href="#">Contracts</a></li>
+                <li><a href="#">History</a></li>
+              </ul>
+            </div>
+            <div>
+              <div class="ikoba-sidebar-title">Manager Name</div>
+              <ul class="nav">
+                <li><a href="#">Portal Configuration</a></li>
+                <li><a href="#">Portal Users</a></li>
+                <li><a href="#">Data Center</a></li>
+                <li><a href="#">Reports</a></li>
+                <li><a href="#">My Preferences</a></li>
+                <li><a href="#">My Roles</a></li>
+              </ul>
+            </div>
+          </div> <!-- /well -->
+        </div> <!-- /ikoba-sidebar -->
+
+        <div class="col-md-10 content ikoba-content-narrow">
+          <div class="well">
+            <div class="ikoba-content">
+              <div class="actionbar">
+                <a class="btn btn-default" href="">
+                  <img src="static/img/actionicon_modify.png"
+                       alt="Manage" />
+                       Manage
+                </a>
+                <a class="btn btn-default" href="">
+                  <img src="static/img/actionicon_trigtrans.png"
+                       alt="Trigger transition" />
+                       Trigger transition
+                </a>
+              </div>
+              <div class="alert alert-danger">
+                  There were errors ...
+              </div>
+              <div class="alert alert-warning">
+                  Be careful ...
+              </div>
+              <div class="alert alert-success">
+                  You have successfully ...
+              </div>
+              <h1>WAeUP.Ikoba Theme</h1>
+
+              <p class="ikoba-content-desc">
+                A sample Diazo theme for WAeUP.Ikoba
+                <br />
+                Fusce dapibus, tellus ac cursus commodo.
+              </p>
+              <p>
+                Donec id elit non mi porta gravida at eget metus. Fusce
+                dapibus, tellus ac cursus commodo, tortor mauris
+                condimentum nibh, ut fermentum massa justo sit amet
+                risus. Etiam porta sem malesuada magna mollis
+                euismod. Donec sed odio dui.
+              </p>
+              <img src="static/img/bg.png" />
+
+              <!-- Tabs... -->
+              <br /><br />
+              <h3>Sample Tabs</h3>
+              <ul id="tabs" class="tabs nav nav-tabs" data-tabs="tabs">
+                <li class="active">
+                  <a href="#tab-1" data-toggle="tab">Tab 1</a>
+                </li>
+                <li class="">
+                  <a href="#tab-2" data-toggle="tab">Tab 2</a>
+                </li>
+                <li class="">
+                  <a href="#tab-3" data-toggle="tab">Tab 3</a>
+                </li>
+              </ul>
+              <div class="tab-content">
+                <div id="tab-1" class="tab-pane active">
+                  <br />
+                  <p>
+                    Tab 1 content...<br />  Donec id elit non mi porta
+                    gravida at eget metus. Fusce dapibus, tellus ac
+                    cursus commodo.
+                  </p>
+                </div>
+                <div id="tab-2" class="tab-pane">
+                  <br />
+                  <p>
+                    Tab 2 content...<br />  Tortor mauris condimentum
+                    nibh, ut fermentum massa justo sit amet
+                    risus. Etiam porta sem malesuada.
+                  </p>
+                </div>
+                <div id="tab-3" class="tab-pane">
+                  <br />
+                  <p>
+                    Tab 3 content...<br /> Magna mollis euismod. Donec
+                    sed odio dui.
+                  </p>
+                </div>
+
+                <br>
+
+                <h3>Sample Tooltip</h3>
+                <br>Move the mouse over the button:
+                <div class="btn btn-default" data-toggle="tooltip"
+                     title="This is a tooltip">Sample Tooltip
+                </div>
+
+                <br><br>
+
+                <h3>File Upload</h3>
+                <br>
+                <a href="#">Birth Certificate Scan</a>
+                <input type="submit"
+                       value="Delete file"
+                       class="btn btn-danger" />
+                <div class="btn btn-default btn-file">
+                  Select new file&hellip;
+                  <input type="file"/>
+                </div>
+                <input type="submit"
+                       value="Upload selected file"
+                       class="btn btn-primary" />
+                (10 kB max.)
+                <br><br>
+
+        				<div class="input-group">
+        					<div class="input-group-btn">
+        						<div class="btn btn-default btn-file">
+                      Select new file&hellip;
+                      <input type="file">
+                    </div>
+        					</div>
+        					<input type="text" class="form-control" readonly>
+                  <div class="input-group-btn">
+                    <input type="submit"
+                           value="Upload selected file"
+                           class="btn btn-primary" />
+                    (10 kB max.)
+                  </div>
+                </div>
+
+
+              </div><!-- /tab-content -->
+            </div> <!-- /ikoba-content -->
+          </div> <!-- /well -->
+        </div> <!-- /ikoba-content-narrow -->
+      </div> <!-- /row -->
+
+      <div class="row">
+        <div class="col-md-12 content ikoba-content-wide">
+          <div class="well ikoba-content">
+            <div class="actionbar">
+              <a class="btn btn-default" href="">
+                <img src="static/img/actionicon_modify.png"
+                     alt="Manage" />
+                     Manage
+              </a>
+              <a class="btn btn-default" href="">
+                <img src="static/img/actionicon_trigtrans.png"
+                     alt="Trigger transition" />
+                     Trigger transition
+              </a>
+            </div>
+            <div class="hero-unit">
+              <h1>Hello there!</h1>
+              <p>this is...</p>
+            </div>
+            <div>...content that spans the whole page.</div>
+            <br><br>
+            <h3>A Sample Data Table</h3>
+            <div class="table-responsive">
+              <table id="#datatable" class="table table-striped dataTable">
+                <thead>
+                  <tr>
+                    <th>Id</th>
+                    <th>Name</th>
+                    <th>Portal Roles</th>
+                    <th>Local Roles</th>
+                    <th></th>
+                  </tr>
+                </thead><tbody><tr><td>admin</td>
+                    <td>John</td>
+                    <td nowrap="nowrap">Portal Manager <br />
+                      Academics Officer (view only)
+                    </td>
+                    <td>Owner</td>
+                    <td class="text-right">
+                      <form method="post">
+                        <input type="hidden" name="userid" value="admin" />
+                        <input class="btn btn-primary btn-sm" type="submit"
+                               name="manage" value="Manage" />
+                        <input class="btn btn-default btn-sm" type="submit"
+                               name="delete" value="Remove" />
+                      </form>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>tester</td>
+                    <td>Vitali</td>
+                    <td nowrap="nowrap">Academics Officer (view only)</td>
+                    <td>Owner</td>
+                    <td class="text-right">
+                      <form method="post">
+                        <input type="hidden" name="userid" value="your_id" />
+                        <input class="btn btn-sm btn-primary" type="submit"
+                               name="manage" value="Manage" />
+                        <input class="btn btn-sm btn-default" type="submit"
+                               name="delete" value="Remove" />
+                      </form>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+
+            <br><br>
+            <h3>A Sample Inline Form</h3>
+            <form method="POST" class="form-inline">
+            <br />
+              <div class="form-group">
+                <input class="btn btn-primary" type="submit" name="search"
+                  value="Find customers(s)" />
+              </div>
+              <div class="form-group">
+                <select name="searchtype" class="form-control">
+                  <option value="customer_id">with id</option>
+                  <option value="fullname">with name</option>
+                  <option value="reg_number">with registration number</option>
+                  <option value="suspended">
+                    with deactivated account (search term omitted)
+                  </option>
+                </select>
+              </div>
+              <div class="form-group">
+                <input type="text" class="form-control" name="searchterm" />
+              </div>
+            </form>
+
+            <br><br>
+            <h3>A Sample Display Form Table</h3>
+            <table class="form-table">
+              <tbody>
+                <tr>
+                  <td class="fieldname">Code:</td>
+                  <td>app2012</td>
+                </tr>
+                <tr>
+                 <td class="fieldname">Application Target:</td>
+                  <td>General Studies</td>
+                </tr>
+                 <tr>
+                  <td class="fieldname">
+                    Application Mode (Donec id elit non mi porta gravida.):
+                  </td>
+                  <td>Create Application Records</td>
+                </tr>
+              </tbody>
+            </table>
+
+            <br><br>
+            <h3>A Sample Edit/Manage Form Table</h3>
+            <table class="form-table">
+              <tbody>
+                <tr>
+                  <td class="fieldname">Code:</td>
+                  <td>app2012</td>
+                </tr>
+                <tr>
+                  <td class="fieldname">* Title:</td>
+                  <td>
+                    <input class="textType" id="form.title" name="form.title"
+                           size="20" type="text"
+                           value="General Studies 2012/2013" />
+                  </td>
+                </tr>
+                <tr>
+                  <td class="fieldname">Any phone number:</td>
+                  <td>
+                    <select id="form.phone.country" name="form.phone.country"
+                              size="1" class="phone-input">
+                      <option value="+234">Nigeria (+234)</option>
+                      <option value="+49">Germany (+49)</option>
+                      <option value="+1">U.S. (+1)</option>
+                    </select>-
+                    <input class=" phone-input textType" id="form.phone.area"
+                           name="form.phone.area" size="20" type="text"
+                           value="" />-
+                    <input class=" phone-input textType" id="form.phone.ext"
+                           name="form.phone.ext" size="20" type="text" value="" />
+                  </td>
+                </tr>
+                <tr>
+                  <td class="fieldname">Any text field:</td>
+                  <td>
+                      <textarea id="any_id"
+                          name="any_name" rows="5">Placeholder text</textarea>
+                  </td>
+                </tr>
+                <tr>
+                  <td class="fieldname">*
+                      A select field coming from zope.formlib:
+                  </td>
+                  <td>
+                    <div class="value">
+                      <select id="form.mode" name="form.mode" size="1">
+                        <option selected="selected" value="create">
+                          Create Records
+                        </option>
+                        <option value="update">Update Records
+                        </option>
+                      </select>
+                    </div>
+                    <input name="form.mode-empty-marker"
+                           type="hidden" value="1" />
+                  </td>
+                </tr>
+                <tr>
+                  <td class="fieldname">A datepicker field:</td>
+                  <td>
+                    <input class="datepicker-le-year textType"
+                                 id="form.any_date" name="form.any_date"
+                                 size="20" type="text" value="" />
+                  </td>
+                </tr>
+
+                <tr>
+                  <td class="fieldname">
+                      A select field in Ikoba pagetemplates:
+                  </td>
+                  <td>
+                      <select id="any_id" name="any_name" size="1"
+                              class="form-control half">
+                        <option selected="selected" value="create">
+                            Create Records
+                        </option>
+                        <option value="update">
+                            Update Records
+                        </option>
+                      </select>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div> <!-- /well ikoba-content-->
+        </div> <!-- /ikoba-content-wide -->
+      </div> <!-- /row -->
+      <div id="bottom" class="footer">
+        <p class="credit muted text-right">
+          Ikoba - Registration & Application System<br />
+          Copyright &copy; WAeUP Group
+          <script type="text/javascript">
+          renderCurrentYear();
+          </script>
+        </p>
+      </div>
+    </div> <!-- /container -->
+
+    <script src="https://code.jquery.com/jquery-1.9.1.min.js"></script>
+    <script src="static/bootstrap/js/bootstrap.min.js"></script>
+    <script src="static/js/ikoba-scroll.js"></script>
+    <script src="static/js/ikoba-activate-tab.js"></script>
+    <script src="static/js/ikoba-scrollto.js"></script>
+    <script src="static/js/file-upload.js"></script>
+    <script class="ikoba-script-tooltip"
+        src="static/js/ikoba-tooltip.js">
+    </script>
+    <script class="ikoba-script-datatable"
+        src="static/js/jquery.dataTables.min.js">
+    </script>
+    <script class="ikoba-script-datatable"
+        src="static/js/ikoba-datatables.js">
+    </script>
+    <script class="jquery-ui"
+        src="static/js/jquery-ui.min.js">
+    </script>
+    <script class="ikoba-script-datepicker"
+        src="static/js/ikoba-datepicker.js">
+    </script>
+  </body>
+</html>
Index: /main/waeup.ikoba/branches/uli-payments/setup.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/setup.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/setup.py	(revision 12677)
@@ -0,0 +1,139 @@
+import os
+from setuptools import setup, find_packages
+
+version = '0.2dev'
+
+install_requires =[
+    'setuptools',
+    #'gp.fileupload',
+    'grok',
+    'grokcore.startup',
+    'grokui.admin',
+    'hurry.query',
+    'hurry.jquery',
+    'hurry.jqueryui',
+    'hurry.workflow >= 0.11',
+    # Add extra requirements here
+    'docutils', # For RST-processing...
+    'zope.xmlpickle',
+    'hurry.file',
+    #'hurry.yui',
+    'hurry.zoperesource',
+    'zc.sourcefactory',
+    'megrok.layout',
+    'reportlab',
+    'Pillow',
+    'psutil',
+    'unicodecsv',
+    'zope.app.authentication', # BBB: During switch to grok 1.1
+    'zope.app.file',
+    'zope.app.testing',        # XXX: test_permissions needs this
+    'zope.app.undo',
+    'zope.file',
+    'zope.interface >= 3.6.0',
+    'zope.testbrowser',        # XXX: test_permissions needs this
+    'zope.i18n',
+    'zope.mimetype',
+    'zope.errorview',
+    'zope.schema >= 3.8.0',
+    'zope.sendmail',
+    'ulif.loghandlers',
+    'zc.async[z3]',
+    'z3c.evalexception',
+    'paypalrestsdk',
+    'PyPDF2',
+    ],
+
+diazo_require = [
+    'diazo',
+    'webob',
+    ]
+
+# Having beaker installed additionally is a feature very recommended
+# for production use. The default buildout includes beaker for tests,
+# start scripts, and other parts that can benefit from it. The windows
+# buildout does not include it due to compiling problems with the
+# beaker package.
+beaker_require = [
+    'dolmen.beaker',
+    ]
+
+tests_require = [
+    'z3c.testsetup',
+    'zope.app.testing',
+    'zope.testbrowser',
+    'zope.testing',
+    'unittest2',
+    ]
+
+docs_require = [
+    'Sphinx',
+    'collective.recipe.sphinxbuilder',
+    'docutils',
+    'roman',
+    'repoze.sphinx.autointerface',
+    ]
+
+def read(*rnames):
+    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+long_description = (
+    read('README.txt')
+    + '\n\n'
+    + read('CHANGES.txt')
+    + '\n\n'
+    + 'Download\n'
+    + '********\n'
+    )
+
+setup(name = 'waeup.ikoba',
+      version = version,
+      description = "An online application and registration portal",
+      long_description = long_description,
+
+      keywords = "portal waeup ikoba company application registration grok zope",
+      # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      classifiers = [
+        'Development Status :: 4 - Beta',
+        'Environment :: Web Environment',
+        'Intended Audience :: Education',
+        'License :: OSI Approved :: GNU General Public License (GPL)',
+        'Programming Language :: Python',
+        'Operating System :: POSIX',
+        'Operating System :: POSIX :: Linux',
+        'Framework :: Zope3',
+        'Topic :: Education',
+        'Topic :: Internet :: WWW/HTTP',
+        ],
+      author = "The WAeUP team.",
+      author_email = "",
+      url = "http://www.waeup.org/",
+      license = "GPL",
+      package_dir = {'': 'src'},
+      packages= find_packages('src'),
+      namespace_packages = ['waeup',],
+      include_package_data = True,
+      zip_safe = False,
+      install_requires = install_requires,
+      tests_require = tests_require,
+      extras_require = dict(
+        test = tests_require,
+        docs = docs_require,
+        beaker = beaker_require,
+        diazo = diazo_require,
+        ),
+      entry_points="""
+      # Add entry points here
+      #[hurry.resource.libraries]
+      #waeup_ikoba = waeup.ikoba.browser.resources:waeup_ikoba
+      [console_scripts]
+      ikoba-debug = grokcore.startup:interactive_debug_prompt
+      ikobactl = grokcore.startup:zdaemon_controller
+      analyze = waeup.ikoba.maintenance:db_analyze
+      fsdiff = waeup.ikoba.maintenance:db_diff
+      [paste.app_factory]
+      main = waeup.ikoba.startup:env_app_factory
+      debug = waeup.ikoba.startup:env_debug_app_factory
+
+      """,
+      )
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/__init__.py	(revision 12677)
@@ -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.ikoba/branches/uli-payments/src/waeup/ikoba/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/__init__.py	(revision 12677)
@@ -0,0 +1,13 @@
+import grok
+from waeup.ikoba.interfaces import IIkobaObject
+from zope.annotation.attribute import AttributeAnnotations
+from zope.annotation.interfaces import IAnnotations
+
+class IkobaAttributeAnnotations(AttributeAnnotations, grok.Adapter):
+    """An adapter to IAnnotations for any IkobaObject.
+
+    Providing this adapter, each Ikoba object becomes (attribute)
+    annotatable.
+    """
+    grok.provides(IAnnotations)
+    grok.context(IIkobaObject)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/app.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/app.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/app.py	(revision 12677)
@@ -0,0 +1,109 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+from zope.authentication.interfaces import IAuthentication
+from zope.component import getUtility, getUtilitiesFor
+from zope.component.interfaces import ObjectEvent
+from zope.pluggableauth import PluggableAuthentication
+from waeup.ikoba.authentication import setup_authentication
+from waeup.ikoba.datacenter import DataCenter
+from waeup.ikoba.mandates.container import MandatesContainer
+from waeup.ikoba.interfaces import (
+    ICompany, IIkobaPluggable, IObjectUpgradeEvent, IJobManager,
+    VIRT_JOBS_CONTAINER_NAME)
+from waeup.ikoba.userscontainer import UsersContainer
+from waeup.ikoba.utils.logger import Logger
+from waeup.ikoba.utils.helpers import attrs_to_fields
+from waeup.ikoba.configuration import ConfigurationContainer
+
+from waeup.ikoba.customers.container import CustomersContainer
+from waeup.ikoba.products.container import ProductsContainer
+from waeup.ikoba.documents.container import DocumentsContainer
+
+
+class Company(grok.Application, grok.Container, Logger):
+    """A company.
+    """
+    grok.implements(ICompany)
+
+    # Setup authentication for this app. Note: this is only
+    # initialized, when a new instance of this app is created.
+    grok.local_utility(
+        PluggableAuthentication, provides = IAuthentication,
+        setup = setup_authentication,)
+
+    def __init__(self, *args, **kw):
+        super(Company, self).__init__(*args, **kw)
+        self.setup()
+        return
+
+    def setup(self):
+        """Setup some hard-wired components.
+
+        Create local datacenter, containers for users and
+        the like.
+        """
+
+        self['users'] = UsersContainer()
+        self['datacenter'] = DataCenter()
+        self['configuration'] = ConfigurationContainer()
+        self['mandates'] = MandatesContainer()
+        self['customers'] = CustomersContainer()
+        self['products'] = ProductsContainer()
+        self['documents'] = DocumentsContainer()
+        self._createPlugins()
+
+    def _createPlugins(self):
+        """Create instances of all plugins defined somewhere.
+        """
+        for name, plugin in getUtilitiesFor(IIkobaPluggable):
+            plugin.setup(self, name, self.logger)
+        return
+
+    def traverse(self, name):
+        if name == VIRT_JOBS_CONTAINER_NAME:
+            return getUtility(IJobManager)
+        return None
+
+    def updatePlugins(self):
+        """Lookup all plugins and call their `update()` method.
+        """
+        name = getattr(self, '__name__', '<Unnamed>')
+        self.logger.info('Fire upgrade event for site %s' % name)
+        grok.notify(ObjectUpgradeEvent(self))
+        self.logger.info('Done.')
+        self.logger.info('Now upgrading any plugins.')
+        for name, plugin in getUtilitiesFor(IIkobaPluggable):
+            plugin.update(self, name, self.logger)
+        self.logger.info('Plugin update finished.')
+        return
+
+attrs_to_fields(Company)
+
+
+class ObjectUpgradeEvent(ObjectEvent):
+    """An event fired, when datacenter storage moves.
+    """
+    grok.implements(IObjectUpgradeEvent)
+
+@grok.subscribe(Company, grok.IObjectAddedEvent)
+def handle_company_added(app, event):
+    """If a company is added, a message is logged.
+    """
+    app.logger.info('Company `%s` added.' % getattr(app, '__name__', None))
+    return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/app.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/app.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/app.txt	(revision 12677)
@@ -0,0 +1,111 @@
+:mod:`waeup.ikoba.app` -- central components for Ikoba
+******************************************************
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+.. module:: waeup.ikoba.app
+
+.. class:: Company
+
+  The main Ikoba application object is given with
+  :class:`Company`. It provides the main containers as faculties,
+  hostels, etc.
+
+  .. attribute:: name
+
+     A string. The name of a company.
+
+  .. attribute:: faculties
+
+     An arbitrary object containing "faculties". In the case of
+     `Company` it is a container of type
+     `waeup.ikoba.interfaces.IFacultiesContainer`.
+
+
+Creating `Company` instances
+============================
+
+As :class:`Company` 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:`Company` class and create an
+instance:
+
+    >>> from waeup.ikoba.app import Company
+    >>> mycompany = Company()
+    >>> mycompany
+    <waeup.ikoba.app.Company object at 0x...>
+
+Instances of `Company` comply with the interface
+`waeup.ikoba.interfaces.ICompany`:
+
+    >>> from zope.interface.verify import verifyClass
+    >>> from waeup.ikoba.interfaces import ICompany
+    >>> verifyClass(ICompany, Company)
+    True
+
+A freshly created instance provides the attributes promised by the
+interface:
+
+    >>> from waeup.ikoba.app import Company
+    >>> mycompany = Company()
+    >>> mycompany['configuration'].name
+    u'Sample Company'
+
+  >>> mycompany['users']
+  <waeup.ikoba.userscontainer.UsersContainer object at 0x...>
+
+  >>> mycompany['datacenter']
+  <waeup.ikoba.datacenter.DataCenter object at 0x...>
+
+  >>> mycompany['configuration']
+  <waeup.ikoba.configuration.ConfigurationContainer object at 0x...>
+
+
+Ikoba plugins
+=============
+
+waeup.ikoba provides an API to 'plugin' components. Things that should
+be setup at creation time of a Ikoba application can indicate
+that by providing a utility providing IIkobaPlugin.
+
+The plugins are looked up by an created app, which then will call the
+``setup()`` method of each plugin.
+
+   >>> from waeup.ikoba.interfaces import IIkobaPluggable
+   >>> from zope.component import getAdapters, getUtilitiesFor
+   >>> sorted(list(getUtilitiesFor(IIkobaPluggable)))
+   [(u'documents', <waeup.ikoba.documents.container.DocumentsPlugin ...)]
+
+
+We can provide a new plugin like this:
+
+   >>> import grok
+   >>> from waeup.ikoba.interfaces import IIkobaPluggable
+   >>> class MyPlugin(grok.GlobalUtility):
+   ...   grok.implements(IIkobaPluggable)
+   ...   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 Ikoba instance, we will get a message:
+
+   >>> from waeup.ikoba.app import Company
+   >>> site = Company()
+   Setup was called for
+   <waeup.ikoba.app.Company object at 0x...>
+
+Apparently the plugin can do with the Company object whatever it
+likes. That's what plugins are for.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async.pt	(revision 12677)
@@ -0,0 +1,53 @@
+<html>
+  <head>
+    <title>Async testing</title>
+    <script type="text/javascript">
+$(document).ready(function() {
+  var myLoadingDiv = $("#progressbar");
+  var mySpinner = $("#loadspinner");
+  myLoadingDiv.progressbar({disabled:true});
+  function update() {
+  $.getJSON("status?",  {jobid: "4",}, function(data) {
+    if (data == null) {
+      myLoadingDiv.progressbar("disable");
+      myLoadingDiv.progressbar("value", 0);
+      mySpinner.html('No Data');
+      $("#msg").html("Syncing...");
+      clearInterval(intervalID);
+      //alert('No Data'):
+      }
+    else {
+      myLoadingDiv.progressbar("enable");
+      myLoadingDiv.progressbar("value", data.percent);
+      $("#msg").html(data.msg + ' (' + data.status + ') ');
+      //alert(data.msg);
+      if (data.percent == 100) {
+        mySpinner.html('Job finished.')
+        clearInterval(intervalID);
+        }
+      }
+  });}
+  intervalID = setInterval(update, 500);
+  //update();
+});
+    </script>
+  </head>
+  <body>
+    <h1>Async stuff</h1>
+    <div>Dispatcher: <span tal:content="view/dispatcher"></span></div>
+    <div>Queue: <span tal:content="view/queue"></span></div>
+    <div>Jobs: <span tal:content="structure view/job_status"></span></div>
+    <form method="POST">
+      <input type="submit" name="reload" value="reload" />
+      <input type="submit" name="start" value="start up" />
+    </form>
+    <div id="progressbar"
+	 class="ui-progressbar ui-widget ui-widget-content ui-corner-all">
+    </div>
+    <div id="msg">Hi there</div>
+    <div id="loadspinner">
+      <img src="" alt="Please wait..."
+	   tal:attributes="src view/spinner_url" />
+    </div>
+  </body>
+</html>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async.py	(revision 12677)
@@ -0,0 +1,277 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components for asynchronous job (task) handling, mainly based on
+:mod:`zc.async`.
+"""
+import grok
+import time
+import transaction
+import zc.async.dispatcher
+import zc.async.utils
+import zope.component
+from zc.async.job import Job as AsyncJob
+from zc.async.queue import getDefaultQueue
+from zc.async.interfaces import IJob, ObjectAdded
+from ZODB.utils import u64
+from zope.component.hooks import getSite
+from zope.event import notify
+from waeup.ikoba.interfaces import (
+    IJobManager, IProgressable, IJobContainer, WAEUP_KEY,
+    )
+
+def compute(num, duration=60):
+    """A convenience function to test asynchronous jobs.
+
+    `duration` gives the seconds, this job should (artificially) need
+    for completing.
+    """
+    start = time.time()
+    end = start + duration
+    print "MyJob starts computation at ", start
+    myjob = zc.async.local.getJob()
+    print "MyJob's job: ", zc.async.local.getJob()
+    print "MyJob's queue: ", zc.async.local.getQueue(), list(
+        zc.async.local.getQueue())
+    while True:
+        if time.time() > end:
+            break
+        time.sleep(1)
+        percent = (time.time() - start) * 100.0 / duration
+        if percent > 100.0:
+            percent = 100.0
+        print "MyJob percent: ", percent
+        zc.async.local.setLiveAnnotation('percent', percent)
+        print "MyJob does something at %s of %s" % (
+            time.time() - start, duration)
+        print "MyJob's annotations: %r" % dict(myjob.annotations)
+        print "MyJob's percent: %r" % zc.async.local.getLiveAnnotation(
+            'percent')
+    zc.async.local.setLiveAnnotation('percent', 100.0)
+    return num * 2
+
+#
+# Content components
+#
+class ProgressableJob(AsyncJob):
+    """A job that can indicate its progress via a `percent` attribute.
+    """
+    grok.implements(IJob, IProgressable)
+    grok.provides(IJob)
+    percent = None
+
+
+def get_job_id(persisted_job):
+    """Get the object id of an already persisted job.
+
+    The `persisted_job` must provide a `_p_oid` attribute.
+    """
+    job_id = u64(persisted_job._p_oid)
+    return "%s" % job_id
+
+class JobContainer(grok.Container):
+    """A container for :class:`IIkoba` jobs.
+    """
+    grok.implements(IJobContainer)
+
+
+class JobManager(grok.GlobalUtility):
+    """A manager for asynchronous running jobs (tasks).
+
+    Registered as a global utility for the
+    `waeup.ikoba.interfaces.IJobManager` interface.
+
+    This is the central location for managing asynchronous running
+    jobs/tasks.
+
+    It works roughly like this: for usual tasks it looks up some
+    JobContainer installed in a ZODB database root (the installation
+    can happen during startup; see the respective installer classes
+    and functions in this module) and then interacts with this
+    JobContainer.
+
+    The optional `site` parameter for most methods in here serves for
+    finding the databases' roots. It is sufficient to pass any
+    persisted object (or more precisely: some object with a valid
+    ``_p_jar__`` attribte). As long as some site was already set (for
+    instance during regular requests), the site is looked up
+    automatically and you don't have to pass the `site` parameter
+    then. So, in most cases you won't have to give a `site` parameter.
+    """
+    grok.implements(IJobManager)
+    grok.provides(IJobManager)
+
+    def _get_site(self, site):
+        # in fact we get some persisted object if available.
+        # As sites are normally persisted and easy to lookup, we use them
+        # to get _some_ persisted object.
+        if not hasattr(site, '_p_jar'):
+            site = getSite()
+            if site is None:
+                raise LookupError('cannot find a valid site')
+        return site
+
+    def _get_jobs_container(self, site):
+        # in fact we need _some_ persisted object, not necessarily a site
+        return site._p_jar.root()[WAEUP_KEY]
+
+    def put(self, job, site=None):
+        """Start the `job` and store it in local `site`.
+
+        The `job` must be an `IJob` instance.
+
+        It will be put into the default queue and then stored in local
+        site. The status of the job can be seen immediately in
+        `job.status`.
+
+        Please specify special treatments like `begin_after` or
+        `begin_by` by setting the respectives attributes of the job
+        itself.
+        """
+        site = self._get_site(site)
+        container = self._get_jobs_container(site)
+        transaction.begin()
+        queue = getDefaultQueue(site)
+        new_job = queue.put(job)
+        job_id = get_job_id(new_job)
+        container[job_id] = new_job
+        transaction.commit()
+        return job_id
+
+    def get(self, job_id, site=None):
+        """Get the job with `job_id` from local `site`.
+
+        If `job_id` cannot be found, ``None`` is returned. This
+        suitable e.g. when used with a traverser.
+        """
+        site = self._get_site(site)
+        container = self._get_jobs_container(site)
+        return container.get(job_id, None)
+
+    def jobs(self, site=None):
+        """Get all stored jobs as an iterable.
+
+        Result provides tuples (JOB_ID, JOB_OBJECT).
+        """
+        site = self._get_site(site)
+        container = self._get_jobs_container(site)
+        for job_id, job in container.items():
+            yield (job_id, job)
+
+    def remove(self, job_id, site=None):
+        """Remove job with `job_id` from local job container.
+
+        If no such job can be found this is silently ignored.
+
+        Please note: removing a job from the job container does not
+        mean to stop its execution (if it wasn't started yet or is
+        currently running).
+        """
+        site = self._get_site(site)
+        container = self._get_jobs_container(site)
+        if job_id in container.keys():
+            del container[job_id]
+        return
+
+    def start_test_job(self, duration=60, site=None):
+        """Start a test job.
+
+        A method for testing the general asynchronous functionality of
+        waeup.ikoba. The test job started here executes the local
+        :func:`compute` function with ``23`` as argument.
+        """
+        job = AsyncJob(compute, 23, duration)
+        job_id = self.put(job, site=site)
+        return job_id
+
+class JobManagerTraverser(grok.Traverser):
+    """A traverser for the global ``IJobManager``.
+
+    Looks up jobs by job_id and returns the respective job if it
+    can be found.
+    """
+    grok.context(IJobManager)
+
+    def traverse(self, name):
+        # ``None`` if name cannot be found.
+        return self.context.get(name)
+
+##
+## configuration (permissions, subscribers, etc.) ...
+##
+
+class ViewJobs(grok.Permission):
+    grok.name('waeup.viewJobs')
+
+class ManageJobs(grok.Permission):
+    grok.name('waeup.manageJobs')
+
+class JobContainerInstaller(object):
+    """Install a JobContainer in root of given DB.
+
+    Instances of this installer can be called when a Zope instance
+    comes up (i.e. when an IDatabaseOpenedEvent was triggered).
+
+    It looks for some database named as in `db_name` and installs a
+    job container in the root of this database (``None`` by default)
+    if it does not exist already.
+    """
+    def __init__(self, db_name=None):
+        # This IDatabaseOpenedEvent will be from zope.app.appsetup if that
+        # package is around
+        zope.component.adapter(zc.async.interfaces.IDatabaseOpenedEvent)(self)
+        self.db_name = db_name
+        return
+
+    def __call__(self, ev):
+        db = ev.database
+        tm = transaction.TransactionManager()
+        conn = db.open(transaction_manager=tm)
+        tm.begin()
+        try:
+            try:
+                root = conn.root()
+                if WAEUP_KEY in root:
+                    return
+                if self.db_name is not None:
+                    other = conn.get_connection(self.db_name)
+                    container = other.root()[WAEUP_KEY] = JobContainer()
+                    other.add(container)
+                else:
+                    container = JobContainer()
+                root[WAEUP_KEY] = container
+                notify(ObjectAdded(container, root, WAEUP_KEY))
+                tm.commit()
+                zc.async.utils.log.info('job container added')
+            except:
+                tm.abort()
+                raise
+        finally:
+            conn.close()
+        return
+
+#: Can be used as event subscriber from ZCML; installs a job container
+#: in default database (named ``''``) root.
+job_container_installer = JobContainerInstaller()
+
+#: An installer instance that installs a job container in a ZODB
+#: called ``async`` - this name is used in several components of the
+#: `zc.async` package we don't want to reimplement here.
+#:
+#: To use this installer as an event subscriber by ZCML, make sure the
+#: instance provides also a ZODB called `async`.
+multidb_job_container_installer = JobContainerInstaller(db_name='async')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async_multidb.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async_multidb.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async_multidb.zcml	(revision 12677)
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configure xmlns="http://namespaces.zope.org/zope">
+  <subscriber handler=".async.multidb_job_container_installer" />
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async_single.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async_single.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/async_single.zcml	(revision 12677)
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configure xmlns="http://namespaces.zope.org/zope">
+  <subscriber handler=".async.job_container_installer" />
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/authentication.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/authentication.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/authentication.py	(revision 12677)
@@ -0,0 +1,567 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Authentication for Ikoba.
+"""
+import grok
+import time
+from zope.event import notify
+from zope.component import getUtility, getUtilitiesFor
+from zope.component.interfaces import IFactory
+from zope.interface import Interface, implementedBy
+from zope.schema import getFields
+from zope.securitypolicy.interfaces import (
+    IPrincipalRoleMap, IPrincipalRoleManager)
+from zope.pluggableauth.factories import Principal
+from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
+from zope.pluggableauth.interfaces import (
+        ICredentialsPlugin, IAuthenticatorPlugin,
+        IAuthenticatedPrincipalFactory, AuthenticatedPrincipalCreated)
+from zope.publisher.interfaces import IRequest
+from zope.password.interfaces import IPasswordManager
+from zope.securitypolicy.principalrole import principalRoleManager
+from waeup.ikoba.interfaces import (ILocalRoleSetEvent,
+    IUserAccount, IAuthPluginUtility, IPasswordValidator,
+    IIkobaPrincipal, IIkobaPrincipalInfo, IIkobaPluggable,
+    IBatchProcessor, IGNORE_MARKER, IFailedLoginInfo)
+from waeup.ikoba.utils.batching import BatchProcessor
+from waeup.ikoba.permissions import get_all_roles
+
+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 IkobaSessionCredentialsPlugin(grok.GlobalUtility,
+                                    SessionCredentialsPlugin):
+    grok.provides(ICredentialsPlugin)
+    grok.name('credentials')
+
+    loginpagename = 'login'
+    loginfield = 'form.login'
+    passwordfield = 'form.password'
+
+class IkobaPrincipalInfo(object):
+    """An implementation of IIkobaPrincipalInfo.
+
+    A Ikoba principal info is created with id, login, title, description,
+    phone, email, public_name and user_type.
+    """
+    grok.implements(IIkobaPrincipalInfo)
+
+    def __init__(self, id, title, description, email, phone, public_name,
+                 user_type):
+        self.id = id
+        self.title = title
+        self.description = description
+        self.email = email
+        self.phone = phone
+        self.public_name = public_name
+        self.user_type = user_type
+        self.credentialsPlugin = None
+        self.authenticatorPlugin = None
+
+    def __eq__(self, obj):
+        default = object()
+        result = []
+        for name in ('id', 'title', 'description', 'email', 'phone',
+                     'public_name', 'user_type', 'credentialsPlugin',
+                     'authenticatorPlugin'):
+            result.append(
+                getattr(self, name) == getattr(obj, name, default))
+        return False not in result
+
+class IkobaPrincipal(Principal):
+    """A portal principal.
+
+    Ikoba principals provide an extra `email`, `phone`, `public_name`
+    and `user_type` attribute extending ordinary principals.
+    """
+
+    grok.implements(IIkobaPrincipal)
+
+    def __init__(self, id, title=u'', description=u'', email=u'',
+                 phone=None, public_name=u'', user_type=u'', prefix=None):
+        self.id = id
+        if prefix is not None:
+            self.id = '%s.%s' % (prefix, self.id)
+        self.title = title
+        self.description = description
+        self.groups = []
+        self.email = email
+        self.phone = phone
+        self.public_name = public_name
+        self.user_type = user_type
+
+    def __repr__(self):
+        return 'IkobaPrincipal(%r)' % self.id
+
+class AuthenticatedIkobaPrincipalFactory(grok.MultiAdapter):
+    """Creates 'authenticated' Ikoba principals.
+
+    Adapts (principal info, request) to a IkobaPrincipal instance.
+
+    This adapter is used by the standard PAU to transform
+    IkobaPrincipalInfos into IkobaPrincipal instances.
+    """
+    grok.adapts(IIkobaPrincipalInfo, IRequest)
+    grok.implements(IAuthenticatedPrincipalFactory)
+
+    def __init__(self, info, request):
+        self.info = info
+        self.request = request
+
+    def __call__(self, authentication):
+        principal = IkobaPrincipal(
+            self.info.id,
+            self.info.title,
+            self.info.description,
+            self.info.email,
+            self.info.phone,
+            self.info.public_name,
+            self.info.user_type,
+            authentication.prefix,
+            )
+        notify(
+            AuthenticatedPrincipalCreated(
+                authentication, principal, self.info, self.request))
+        return principal
+
+class FailedLoginInfo(grok.Model):
+    grok.implements(IFailedLoginInfo)
+
+    def __init__(self, num=0, last=None):
+        self.num = num
+        self.last = last
+        return
+
+    def as_tuple(self):
+        return (self.num, self.last)
+
+    def set_values(self, num=0, last=None):
+        self.num, self.last = num, last
+        self._p_changed = True
+        pass
+
+    def increase(self):
+        self.set_values(num=self.num + 1, last=time.time())
+        pass
+
+    def reset(self):
+        self.set_values(num=0, last=None)
+        pass
+
+class Account(grok.Model):
+    """Ikoba user accounts store infos about a user.
+
+    Beside the usual data and an (encrypted) password, accounts also
+    have a persistent attribute `failed_logins` which is an instance
+    of `waeup.ikoba.authentication.FailedLoginInfo`.
+
+    This attribute can be manipulated directly (set new value,
+    increase values, or reset).
+    """
+    grok.implements(IUserAccount)
+
+    def __init__(self, name, password, title=None, description=None,
+                 email=None, phone=None, public_name=None, roles = []):
+        self.name = name
+        if title is None:
+            title = name
+        self.title = title
+        self.description = description
+        self.email = email
+        self.phone = phone
+        self.public_name = public_name
+        self.setPassword(password)
+        self.setSiteRolesForPrincipal(roles)
+
+        # We don't want to share this dict with other accounts
+        self._local_roles = dict()
+        self.failed_logins = FailedLoginInfo()
+
+    def setPassword(self, password):
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        self.password = passwordmanager.encodePassword(password)
+
+    def checkPassword(self, password):
+        if not isinstance(password, basestring):
+            return False
+        if not self.password:
+            # unset/empty passwords do never match
+            return False
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        return passwordmanager.checkPassword(self.password, password)
+
+    def getSiteRolesForPrincipal(self):
+        prm = get_principal_role_manager()
+        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
+                 if x[0].startswith('waeup.')]
+        return roles
+
+    def setSiteRolesForPrincipal(self, roles):
+        prm = get_principal_role_manager()
+        old_roles = self.getSiteRolesForPrincipal()
+        if sorted(old_roles) == sorted(roles):
+            return
+        for role in old_roles:
+            # Remove old roles, not to be set now...
+            if role.startswith('waeup.') and role not in roles:
+                prm.unsetRoleForPrincipal(role, self.name)
+        for role in roles:
+            # Convert role to ASCII string to be in line with the
+            # event handler
+            prm.assignRoleToPrincipal(str(role), self.name)
+        return
+
+    roles = property(getSiteRolesForPrincipal, setSiteRolesForPrincipal)
+
+    def getLocalRoles(self):
+        return self._local_roles
+
+    def notifyLocalRoleChanged(self, obj, role_id, granted=True):
+        objects = self._local_roles.get(role_id, [])
+        if granted and obj not in objects:
+            objects.append(obj)
+        if not granted and obj in objects:
+            objects.remove(obj)
+        self._local_roles[role_id] = objects
+        if len(objects) == 0:
+            del self._local_roles[role_id]
+        self._p_changed = True
+        return
+
+class UserAuthenticatorPlugin(grok.GlobalUtility):
+    grok.implements(IAuthenticatorPlugin)
+    grok.provides(IAuthenticatorPlugin)
+    grok.name('users')
+
+    def authenticateCredentials(self, credentials):
+        if not isinstance(credentials, dict):
+            return None
+        if not ('login' in credentials and 'password' in credentials):
+            return None
+        account = self.getAccount(credentials['login'])
+        if account is None:
+            return None
+        # The following shows how 'time penalties' could be enforced
+        # on failed logins. First three failed logins are 'for
+        # free'. After that the user has to wait for 1, 2, 4, 8, 16,
+        # 32, ... seconds before a login can succeed.
+        # There are, however, some problems to discuss, before we
+        # really use this in all authenticators.
+
+        #num, last = account.failed_logins.as_tuple()
+        #if (num > 2) and (time.time() < (last + 2**(num-3))):
+        #    # tried login while account still blocked due to previous
+        #    # login errors.
+        #    return None
+        if not account.checkPassword(credentials['password']):
+            #account.failed_logins.increase()
+            return None
+        return IkobaPrincipalInfo(
+            id=account.name,
+            title=account.title,
+            description=account.description,
+            email=account.email,
+            phone=account.phone,
+            public_name=account.public_name,
+            user_type=u'user')
+
+    def principalInfo(self, id):
+        account = self.getAccount(id)
+        if account is None:
+            return None
+        return IkobaPrincipalInfo(
+            id=account.name,
+            title=account.title,
+            description=account.description,
+            email=account.email,
+            phone=account.phone,
+            public_name=account.public_name,
+            user_type=u'user')
+
+    def getAccount(self, login):
+        # ... look up the account object and return it ...
+        userscontainer = self.getUsersContainer()
+        if userscontainer is None:
+            return
+        return userscontainer.get(login, None)
+
+    def addAccount(self, account):
+        userscontainer = self.getUsersContainer()
+        if userscontainer is None:
+            return
+        # XXX: complain if name already exists...
+        userscontainer.addAccount(account)
+
+    def addUser(self, name, password, title=None, description=None):
+        userscontainer = self.getUsersContainer()
+        if userscontainer is None:
+            return
+        userscontainer.addUser(name, password, title, description)
+
+    def getUsersContainer(self):
+        site = grok.getSite()
+        return site['users']
+
+class PasswordValidator(grok.GlobalUtility):
+
+  grok.implements(IPasswordValidator)
+
+  def validate_password(self, pw, pw_repeat):
+       errors = []
+       if len(pw) < 3:
+         errors.append('Password must have at least 3 chars.')
+       if pw != pw_repeat:
+         errors.append('Passwords do not match.')
+       return errors
+
+class LocalRoleSetEvent(object):
+
+    grok.implements(ILocalRoleSetEvent)
+
+    def __init__(self, object, role_id, principal_id, granted=True):
+        self.object = object
+        self.role_id = role_id
+        self.principal_id = principal_id
+        self.granted = granted
+
+@grok.subscribe(IUserAccount, grok.IObjectRemovedEvent)
+def handle_account_removed(account, event):
+    """When an account is removed, local and global roles might
+    have to be deleted.
+    """
+    local_roles = account.getLocalRoles()
+    principal = account.name
+
+    for role_id, object_list in local_roles.items():
+        for object in object_list:
+            try:
+                role_manager = IPrincipalRoleManager(object)
+            except TypeError:
+                # No Account object, no role manager, no local roles to remove
+                continue
+            role_manager.unsetRoleForPrincipal(role_id, principal)
+    role_manager = IPrincipalRoleManager(grok.getSite())
+    roles = account.getSiteRolesForPrincipal()
+    for role_id in roles:
+        role_manager.unsetRoleForPrincipal(role_id, principal)
+    return
+
+@grok.subscribe(IUserAccount, grok.IObjectAddedEvent)
+def handle_account_added(account, event):
+    """When an account is added, the local owner role and the global
+    ProductsOfficer role must be set.
+    """
+    # We set the local Owner role
+    role_manager_account = IPrincipalRoleManager(account)
+    role_manager_account.assignRoleToPrincipal(
+        'waeup.local.Owner', account.name)
+    # We set the global ProductsOfficer role
+    site = grok.getSite()
+    role_manager_site = IPrincipalRoleManager(site)
+    role_manager_site.assignRoleToPrincipal(
+        'waeup.ProductsOfficer', account.name)
+    # Finally we have to notify the user account that the local role
+    # of the same object has changed
+    notify(LocalRoleSetEvent(
+        account, 'waeup.local.Owner', account.name, granted=True))
+    return
+
+@grok.subscribe(Interface, ILocalRoleSetEvent)
+def handle_local_role_changed(obj, event):
+    site = grok.getSite()
+    if site is None:
+        return
+    users = site.get('users', None)
+    if users is None:
+        return
+    if event.principal_id not in users.keys():
+        return
+    user = users[event.principal_id]
+    user.notifyLocalRoleChanged(event.object, event.role_id, event.granted)
+    return
+
+@grok.subscribe(Interface, grok.IObjectRemovedEvent)
+def handle_local_roles_on_obj_removed(obj, event):
+    try:
+        role_map = IPrincipalRoleMap(obj)
+    except TypeError:
+        # no map, no roles to remove
+        return
+    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
+        notify(LocalRoleSetEvent(
+                obj, local_role, user_name, granted=False))
+    return
+
+class UserAccountFactory(grok.GlobalUtility):
+    """A factory for user accounts.
+
+    This factory is only needed for imports.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.UserAccount')
+    title = u"Create a user.",
+    description = u"This factory instantiates new user account instances."
+
+    def __call__(self, *args, **kw):
+        return Account(name=None, password='')
+
+    def getInterfaces(self):
+        return implementedBy(Account)
+
+class UserProcessor(BatchProcessor):
+    """A batch processor for IUserAccount objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'userprocessor'
+    grok.name(util_name)
+
+    name = u'User Processor'
+    iface = IUserAccount
+
+    location_fields = ['name',]
+    factory_name = 'waeup.UserAccount'
+
+    mode = None
+
+    def parentsExist(self, row, site):
+        return 'users' in site.keys()
+
+    def entryExists(self, row, site):
+        return row['name'] in site['users'].keys()
+
+    def getParent(self, row, site):
+        return site['users']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['name'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addAccount(obj)
+        return
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        changed = []
+        for key, value in row.items():
+            if  key == 'roles':
+                # We cannot simply set the roles attribute here because
+                # we can't assure that the name attribute is set before
+                # the roles attribute is set.
+                continue
+            # Skip fields to be ignored.
+            if value == IGNORE_MARKER:
+                continue
+            if not hasattr(obj, key):
+                continue
+            setattr(obj, key, value)
+            changed.append('%s=%s' % (key, value))
+        roles = row.get('roles', IGNORE_MARKER)
+        if roles not in ('', IGNORE_MARKER):
+            evalvalue = eval(roles)
+            if isinstance(evalvalue, list):
+                setattr(obj, 'roles', evalvalue)
+                changed.append('roles=%s' % roles)
+        # Log actions...
+        items_changed = ', '.join(changed)
+        grok.getSite().logger.info('%s - %s - %s - updated: %s'
+            % (self.name, filename, row['name'], items_changed))
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            UserProcessor, self).checkConversion(row, mode=mode)
+        # We need to check if roles exist.
+        roles = row.get('roles', None)
+        all_roles = [i[0] for i in get_all_roles()]
+        if roles not in ('', IGNORE_MARKER):
+            evalvalue = eval(roles)
+            for role in evalvalue:
+                if role not in all_roles:
+                    errs.append(('roles','invalid role'))
+        return errs, inv_errs, conv_dict
+
+class UsersPlugin(grok.GlobalUtility):
+    """A plugin that updates users.
+    """
+    grok.implements(IIkobaPluggable)
+    grok.name('users')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        users = site['users']
+        items = getFields(IUserAccount).items()
+        for user in users.values():
+            # Add new attributes
+            for i in items:
+                if not hasattr(user,i[0]):
+                    setattr(user,i[0],i[1].missing_value)
+                    logger.info(
+                        'UsersPlugin: %s attribute %s added.' % (
+                        user.name,i[0]))
+            if not hasattr(user, 'failed_logins'):
+                # add attribute `failed_logins`...
+                user.failed_logins = FailedLoginInfo()
+                logger.info(
+                    'UsersPlugin: attribute failed_logins added.')
+            # Remove deprecated attributes
+            for i in self.deprecated_attributes:
+                try:
+                    delattr(user,i)
+                    logger.info(
+                        'UsersPlugin: %s attribute %s deleted.' % (
+                        user.name,i))
+                except AttributeError:
+                    pass
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/authentication.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/authentication.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/authentication.txt	(revision 12677)
@@ -0,0 +1,149 @@
+Ikoba authentication
+*******************
+
+We need to protect most pieces of our portals from unauthenticated
+access.
+
+Therefore users have to login to access main functionality and they
+are able to log out afterwards.
+
+Before we can check access we have to create an app:
+
+  >>> from zope.component.hooks import setSite # only needed in tests
+  >>> from waeup.ikoba.app import Company
+  >>> root = getRootFolder()
+  >>> u = Company()
+  >>> 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.ikoba.authentication import Account
+  >>> alice = Account('alice', 'alicesecret',roles=['waeup.ManageDataCenter'])
+  >>> root['app']['users'].addAccount(alice)
+
+See ``userscontainer.txt`` for details about the UsersContainer we use here.
+
+Users and local roles
+=====================
+
+Accounts also hold infos about local roles assigned to a user. In the
+beginning, users have the local owner role of their own account object:
+
+  >>> alice.getLocalRoles()
+  {'waeup.local.Owner': [<waeup.ikoba.authentication.Account object at 0x...>]}
+
+User  automatically get the global ProductsOfficer role:
+
+  >>> alice.getSiteRolesForPrincipal()
+  ['waeup.ManageDataCenter', 'waeup.ProductsOfficer']
+
+We can tell an account, that Alice got some role for another object:
+
+  >>> chalet = object()
+  >>> root['app']['chalet'] = chalet
+  >>> alice.notifyLocalRoleChanged(chalet, 'BigBoss', granted=True)
+
+Now Alice is the Big Boss:
+
+  >>> alice.getLocalRoles()
+  {'BigBoss': [<object object at 0x...>]}
+
+When we do not want Alice to be the Big Boss we can tell that too:
+
+  >>> alice.notifyLocalRoleChanged(chalet, 'BigBoss', granted=False)
+  >>> alice.getLocalRoles()
+  {'waeup.local.Owner': [<waeup.ikoba.authentication.Account object at 0x...>]}
+
+We can also use events to trigger such actions. This is recommended
+because we do not neccessarily know where Alice lives:
+
+  >>> from waeup.ikoba.authentication import LocalRoleSetEvent
+  >>> from zope.event import notify
+  >>> notify(LocalRoleSetEvent(chalet, 'BigBoss', 'alice',
+  ...                          granted=True))
+  >>> alice.getLocalRoles()
+  {'BigBoss': [<object object at 0x...>]}
+
+When objects are deleted, local roles are also deleted
+semi-magically. This happens through event subscribers listening to
+IObjectRemovedEvents. The latters are naturally only fired when ZODB
+stored objects are removed. Furthermore this subscriber reads the
+internal local roles table.
+
+
+Logging in via side bar
+=======================
+
+We can access the front page without restrictions:
+
+  >>> browser.open('http://localhost/app')
+  >>> print browser.headers['Status']
+  200 Ok
+
+We have to go to one of the login pages first:
+
+  >>> browser.open('http://localhost/app')
+  >>> browser.getLink('Login').click()
+  >>> print browser.headers['Status']
+  200 Ok
+
+There is a login form on tis page:
+
+  >>> 'form.login' in browser.contents
+  True
+
+  >>> 'form.logout' in browser.contents
+  False
+
+We use this form:
+
+  >>> browser.getControl(name='form.login').value = 'bob'
+  >>> browser.getControl(name='form.password').value = 'invalidpw'
+  >>> browser.getControl('Login').click()
+  >>> 'You entered invalid credentials' in browser.contents
+  True
+
+  >>> browser.getControl(name='form.login').value = 'bob'
+  >>> browser.getControl(name='form.password').value = 'bobsecret'
+  >>> browser.getControl('Login').click()
+
+Now the login form is gone. Instead we have the opportunity to logout:
+
+  >>> 'form.login' in browser.contents
+  False
+
+  >>> logout = browser.getLink('Logout')
+  >>> logout
+  <Link text='Logout' url='http://localhost/app/@@logout'>
+
+The user title is also displayed in the sidebar:
+
+  >>> 'Bob' in browser.contents
+  True
+
+We can also log out afterwards:
+
+  >>> logout.click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Login
+  ...
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/beaker.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/beaker.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/beaker.py	(revision 12677)
@@ -0,0 +1,60 @@
+"""Configure dolmen.beaker.
+
+One main advantage of using dolmen.beaker is that session data for a
+certain user can be stored almost automatically in a cookie, thus
+reducing the number of needed ZODB operations.
+
+Security Advisory
+-----------------
+
+To prevent users from playing around with their session data, these
+data is encrypted by some keys stored in a dict registered as a global
+utility.
+
+These keys are set each time the Zope instance starts. If they change,
+all existing cookies will become unreadable and therefore the stored
+sessions will be lost. Already logged-in users will have to login
+again and all other session based operations might have to be
+restarted.
+
+Changing the keys might therefore have sideeffects.
+
+On the other hand static keys stored in SVN might become known to
+users and enable them to manipulate their session data.
+
+For better security the keys (or one of the keys) could therefore be
+gathered from 'outside' (a file in filesystem, some environment var,
+or whatever).
+"""
+import grok
+from zope.app.appsetup.interfaces import IDatabaseOpenedWithRootEvent
+from zope.component import getUtility
+
+#: Our configuration for dolmen.beaker sessions.
+#:
+#: See http://gitweb.dolmen-project.org/dolmen.beaker.git?a=blob;f=src/dolmen/beaker/utilities.py
+#:
+#: for default configuration.
+BEAKER_CONFIG = dict(
+    data_dir=None,
+    invalidate_corrupt=True,
+    key='waeup.ikoba.session.id',
+    log_file=None,
+    secret="IkobaRocks",
+    timeout=600,
+    type="cookie",
+    validate_key="thisMightBeChanged",
+    )
+
+@grok.subscribe(IDatabaseOpenedWithRootEvent)
+def set_beaker_conf(event):
+    # Set beaker conf once when ZODB was opened
+    try:
+        from dolmen.beaker.interfaces import ISessionConfig
+    except ImportError:
+        # we seem to work without dolmen.beaker
+        return
+
+    config = getUtility(ISessionConfig)
+    config.update(BEAKER_CONFIG)
+    return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/__init__.py	(revision 12677)
@@ -0,0 +1,15 @@
+import os
+
+#from waeup.ikoba.browser.layout import (
+#    IkobaPage, IkobaForm, IkobaLayout, IkobaDisplayFormPage, IkobaEditFormPage,
+#    IkobaAddFormPage, NullValidator)
+#from waeup.ikoba.browser.pages import ContactAdminForm
+
+IMAGE_PATH = os.path.join(
+    os.path.dirname(__file__),
+    'static'
+    )
+
+#: Filesystem paths to default images
+DEFAULT_IMAGE_PATH = os.path.join(IMAGE_PATH, 'placeholder.jpg')
+DEFAULT_PASSPORT_IMAGE_PATH = os.path.join(IMAGE_PATH, 'placeholder.jpg')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/async.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/async.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/async.py	(revision 12677)
@@ -0,0 +1,148 @@
+import grok
+from zc.async.interfaces import COMPLETED, NEW
+from zope.interface import Interface
+from waeup.ikoba.async import IJob, IJobManager
+from waeup.ikoba.browser.layout import IkobaPage
+from waeup.ikoba.interfaces import IIkobaObject, IDataCenter
+from waeup.ikoba.interfaces import MessageFactory as _
+
+grok.templatedir('templates')
+grok.context(IIkobaObject)
+
+loadingbar_template = '''
+<input id="status_url" type="hidden" name="status_url" value="%s" />
+<div id="loadbar">%%s</div>
+'''
+
+class LoadingBar(grok.ViewletManager):
+    grok.name('loadingbar')
+
+class LoadingBarViewlet(grok.Viewlet):
+    grok.context(Interface)
+    grok.viewletmanager(LoadingBar)
+
+    _running_exports = None
+
+    def getRunningExports(self):
+        """Returns running exports as list of tuples.
+
+        Only exports triggered by the current user (identified by
+        principal.id) are returned.
+
+        Each tuple has the form (<STATUS>, <STATUS_TITLE>, <EXPORTER_NAME>).
+
+        ``STATUS``:
+           the status as machine readable string (something like
+           ``'completed'``)
+
+        ``STATUS_TITLE``:
+           status of export as translated string.
+
+        ``EXPORTER_NAME``:
+           string representing the exporter title used when triggering
+           the export job.
+        """
+        result = self.context.get_export_jobs_status(self.user_id)
+        return result
+
+    def update(self):
+        self.user_id = self.request.principal.id
+        self.exports = self.getRunningExports()
+        self.uncompleted = [x for x in self.exports if x[0] != 'completed']
+
+    def render(self):
+        status_url = self.view.url('status')
+        template = loadingbar_template % (status_url,)
+        content = ''
+        return template % content
+
+class DataCenterJSON(grok.JSON):
+    grok.context(IDataCenter)
+
+    def status(self):
+        """Get status of currently handled export jobs for users.
+
+        The result is a set of valus useful for JavaScript handlers to
+        update some page handling data center exports.
+
+        Returns a mapping::
+
+          {html=<string>, interval=<int>, reload=<bool>}
+
+        where `html` is some HTML code that might be shown in a
+        loading bar section.
+
+        `interval` is the time in microseconds after which the loading
+        bar should be reloaded (by a new JSON call). If the value is
+        zero, then the loading bar should not be renewed and further
+        JSON calls be canceled.
+
+        `reload` is a hint to tell, whether the complete page should
+        be reloaded or not. Reloading is neccessary, if the export
+        file creation finished and download buttons or similar should
+        appear.
+        """
+        user_id = self.request.principal.id
+        jobs = self.context.get_export_jobs_status(user_id)
+        uncompleted = [x for x in jobs if x[0] != 'completed']
+        result = dict(
+            html='', interval=0, reload=False)
+        if len(uncompleted):
+            html = '<em>%s</em>' % (_("Please wait..."),)
+            result.update(interval=500, html=html)
+        else:
+            if len(jobs):
+                result.update(reload=True)
+        return result
+
+
+class JobManagerView(IkobaPage):
+    """The main view for the job container.
+    """
+    grok.name('index.html')
+    grok.context(IJobManager)
+    grok.template('virtjobscontainerindex')
+    grok.require('waeup.manageJobs')
+
+    def jobs(self):
+        for job_id, job in sorted(self.context.jobs(),
+                                  cmp=lambda x, y: cmp(int(x[0]), int(y[0]))):
+            link = self.url(self.context, job_id)
+            removable = job.status in (COMPLETED, NEW)
+            yield dict(
+                job_id = job_id,
+                job = job,
+                link = link,
+                status = job.status,
+                begin_after = job.begin_after,
+                begin_by = job.begin_by,
+                annotations = '%r' % [
+                    (x, y) for x, y in job.annotations.items()],
+                removable = removable,
+                failure = '%r' % getattr(job, 'traceback', None),
+                )
+
+    def update(self, START_NEW=None, REMOVE=None, job_id=None):
+        if REMOVE and job_id:
+            self.context.remove(job_id)
+            self.flash('Removed job "%s"' % job_id)
+        if START_NEW:
+            self.context.start_test_job()                    #pragma NO COVER
+            self.flash('Started new test job "%s"' % job_id) #pragma NO COVER
+        return
+
+class JobView(grok.View):
+    grok.name('index.html')
+    grok.context(IJob)
+    grok.template('jobindex')
+    grok.require('waeup.manageJobs')
+
+    def percent(self):
+        value = self.context.annotations.get('percent', None)
+        if not value:
+            return
+        value = int(value+0.5)
+        return min(value, 100)
+
+    def update(self):
+        pass
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/batchprocessing.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/batchprocessing.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/batchprocessing.txt	(revision 12677)
@@ -0,0 +1,293 @@
+Batch processing via browser
+****************************
+
+Preliminaries:
+
+We define a function that looks up a form with several submit buttons
+for the one with a given value (this functionality is missing in
+zope.testbrowser):
+
+    >>> def lookup_submit_value(name, value, browser):
+    ...   """Find a button with a certain value."""
+    ...   for num in range(0, 100):
+    ...     try:
+    ...       button = browser.getControl(name=name, index=num)
+    ...       if button.value.endswith(value):
+    ...         return button
+    ...     except IndexError:
+    ...       break
+    ...   return None
+
+Create a site:
+
+    >>> from waeup.ikoba.app import Company
+    >>> getRootFolder()['app'] = Company()
+    >>> from zope.component.hooks import setSite
+    >>> setSite(getRootFolder()['app'])
+
+Create a datacenter storage path:
+
+    >>> import os
+    >>> import tempfile
+    >>> dc_path = tempfile.mkdtemp()
+
+Log in:
+
+    >>> from zope.testbrowser.testing import Browser
+    >>> browser = Browser()
+    >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+    >>> browser.handleErrors = False
+
+Set datacenter path and deselect moving old data:
+
+    >>> browser.open('http://localhost/app')
+    >>> browser.getLink('Data Center').click()
+    >>> browser.getLink('Edit settings').click()
+    >>> browser.getControl(name='newpath').value = dc_path
+    >>> browser.getControl(name='move').value = False
+    >>> browser.getControl(name='save').click()
+
+Set non-usable datacenter path:
+
+    >>> browser.getLink('Edit settings').click()
+    >>> browser.getControl(name='newpath').value = '/'
+    >>> browser.getControl(name='save').click()
+    >>> 'Given storage path cannot be used' in browser.contents
+    True
+    >>> browser.getControl('Back to Data Center').click()
+
+
+Batch processing users
+======================
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for users:
+
+    >>> open('users.csv', 'wb').write(
+    ... """name,title,public_name,email,phone,roles
+    ... uli,Uli Fouquet,Chief Developer,uli@abc.de,+49-234-567,[]
+    ... henrik,Henrik Bettermann,Admin,henrik@abc.de,+49-234-567,"['waeup.PortalManager', 'waeup.ImportManager']"
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('users.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'users.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'users_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('User Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+We get informed that there are no problems with the current header:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    Header fields OK
+    ...
+
+The submit button is enabled:
+
+    >>> browser.getControl('Perform import').disabled
+    False
+
+    >>> browser.getControl('Perform import').click()
+
+Step 4: See import results
+
+The import was successful:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully processed 2 rows...
+    ...Batch processing finished...
+    ...File:...users_zope.mgr.csv...
+
+User henrik has got the global roles:
+
+   >>> henrik = getRootFolder()['app']['users']['henrik']
+   >>> henrik.roles
+   ['waeup.ProductsOfficer', 'waeup.PortalManager', 'waeup.ImportManager']
+
+Pending files
+=============
+
+When an error occurs during an import, two files are generated: a CSV
+file with finished files and a CSV file with pending data. Both are
+stored in the appropriate subdirectories in datacenter. We try to
+create faculties, from which one already exists.
+
+Go to datacenter page:
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for users:
+
+    >>> open('newusers.csv', 'wb').write(
+    ... """name,title,public_name,email,phone,roles
+    ... isouaba,Issoufou Abba Moussa,Chief Developer,isouaba@abc.de,+49-234-567,[]
+    ... henrik,Henrik Bettermann,Admin,henrik@abc.de,+49-234-567,"['waeup.PortalManager', 'waeup.ImportManager']"
+    ... anne,Anne Palina,,anne@abc.de,+49-234-567,"['waeup.Nonsense']"
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('newusers.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'newusers.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Since we now have a user with waeup.ImportManager role, an email has been sent:
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    ...All import managers have been notified by email...
+    ...
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'newusers_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('User Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+As there should be no problem with the headers, we can immediately
+perfom the import:
+
+    >>> browser.getControl('Perform import').click()
+
+Two lines could not be imported:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC...
+    ...
+    ...Processing of 2 rows failed...
+    ...Successfully processed 1 rows...
+    ...
+
+Now there are two files as a result in datacenter storage's root and
+``finished`` dirs:
+
+    >>> pending_file = dc_path + '/newusers_zope.mgr.create.pending.csv'
+    >>> print open(pending_file).read()
+    name,roles,title,public_name,phone,email,--ERRORS--
+    henrik,"['waeup.PortalManager', 'waeup.ImportManager']",Henrik Bettermann,Admin,+49-234-567,henrik@abc.de,This object already exists. Skipping.
+    anne,['waeup.Nonsense'],Anne Palina,,+49-234-567,anne@abc.de,roles: invalid role
+
+    >>> finished_file = dc_path + '/finished/newusers_zope.mgr.create.finished.csv'
+    >>> print open(finished_file).read()
+    name,roles,title,public_name,phone,email
+    isouaba,[],Issoufou Abba Moussa,Chief Developer,+49-234-567,isouaba@abc.de
+
+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 by removing anne and replacing henrik by gbenga 
+and finish the import this way:
+
+    >>> open(dc_path + '/newusers_zope.mgr.create.pending.csv', 'wb').write(
+    ... """name,title,public_name,email,phone,roles
+    ... gbenga,Jason Bamidele,Chief Developer,gbenga@abc.de,+49-234-567,[]
+    ... """)
+
+Step 1: start batch processing:
+
+    >>> browser.open('http://localhost/app/datacenter')
+    >>> browser.getLink('Process data').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'newusers_zope.mgr.create.pending.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('User Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3/4: Fix headerlines and import:
+
+As there should be no problem with the headers, we can immediately
+perfom the import:
+
+    >>> browser.getControl('Perform import').click()
+
+This time everything should work:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC...
+    ...
+    ...Successfully processed 1 rows...
+    ...
+
+    >>> sorted(os.listdir(dc_path))
+    ['deleted', 'finished', 'logs', 'unfinished']
+
+    >>> os.listdir(dc_path + '/unfinished')
+    []
+
+    >>> sorted(os.listdir(dc_path + '/finished'))
+    ['newusers_zope.mgr.create.finished.csv', 'newusers_zope.mgr.csv', 'users_zope.mgr.create.finished.csv', 'users_zope.mgr.csv']
+
+Processed (finished) Files
+==========================
+
+    >>> browser.open('http://localhost/app/datacenter/processed')
+    >>> 'download?filename=finished/users_zope.mgr.create.finished.csv' in browser.contents
+    True
+
+Log Files
+=========
+
+    >>> browser.open('http://localhost/app/datacenter/logs')
+    >>> 'datacenter.log' in browser.contents
+    True
+    >>> browser.getControl('Show', index=0).click()
+    >>> browser.getControl('Back', index=0).click()
+    >>> browser.getControl('Back to Data Center').click()
+    >>> 'Storage path:' in browser.contents
+    True
+
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(dc_path)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/breadcrumbs.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/breadcrumbs.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/breadcrumbs.py	(revision 12677)
@@ -0,0 +1,209 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+""" Components to get breadcrumbs for any object.
+"""
+import grok
+from grokcore.view.interfaces import IGrokView
+from zope.component import getAdapter
+from zope.publisher.browser import TestRequest
+
+from waeup.ikoba.interfaces import (
+    IConfigurationContainer, IExportJobContainer)
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.browser.interfaces import (
+    IBreadcrumb, IBreadcrumbIgnorable, IBreadcrumbContainer, IIkobaObject,
+    ICompany, IUsersContainer, IDataCenter,
+    )
+from waeup.ikoba.reports import IReportsContainer
+
+class Breadcrumb(grok.Adapter):
+    """A most general breadcrumb generator.
+    """
+    grok.provides(IBreadcrumb)
+    grok.context(IIkobaObject)
+    grok.name('index')
+
+    _title = None
+    _parent = 0
+    _request = None
+    parent_viewname = 'index'
+    viewname = 'index'
+
+    def __init__(self, context):
+        """Turn a context into a breadcrumb.
+        """
+        self.context = context
+
+    @property
+    def title(self):
+        """Get a title for a context.
+        """
+        if self._title is not None:
+            return self._title
+        if hasattr(self.context, 'title'):
+            return self.context.title
+        if hasattr(self.context, 'name'):
+            return self.context.name
+        return None
+
+    @property
+    def parent(self):
+        """Get the contexts parent object and viewname or None.
+        """
+        if self._parent is None:
+            return None
+        if self._parent is not 0:
+            return (self._parent, self.parent_viewname)
+
+        if self.viewname != 'index':
+            self._parent = self.context
+        else:
+            site = grok.getSite()
+            if self.context is not site:
+                self._parent = getattr(self.context, '__parent__', None)
+
+        if self._parent is not None:
+            return (self._parent, self.parent_viewname)
+        return None
+
+    @property
+    def target(self):
+        return self.viewname
+
+class CompanyBreadcrumb(Breadcrumb):
+    """A breadcrumb for company index pages.
+    """
+    grok.context(ICompany)
+    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 Company 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(ICompany)
+    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 AdministrationBreadcrumb(Breadcrumb):
+    """A breadcrumb for administration areas of Company instances.
+    """
+    grok.context(ICompany)
+    grok.name('administration')
+    title = _(u'Administration')
+    viewname = 'administration'
+
+class ConfigurationContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for the configuration container.
+    """
+    grok.context(IConfigurationContainer)
+    title = _(u'Portal Configuration')
+    parent_viewname = 'administration'
+
+class UsersContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for user containers.
+    """
+    grok.context(IUsersContainer)
+    title = _(u'Officers')
+    parent_viewname = 'administration'
+
+class DataCenterBreadcrumb(Breadcrumb):
+    """A breadcrumb for data centers.
+    """
+    grok.context(IDataCenter)
+    title = _(u'Data Center')
+    parent_viewname = 'administration'
+
+class ReportsBreadcrumb(Breadcrumb):
+    """A breadcrumb for reports.
+    """
+    grok.context(IReportsContainer)
+    title = _(u'Reports')
+    parent_viewname = 'administration'
+    target = None
+
+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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/breadcrumbs.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/breadcrumbs.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/breadcrumbs.txt	(revision 12677)
@@ -0,0 +1,243 @@
+Breadcrumbs
+***********
+
+We use an adapter approach to get a flexible system of creating and
+retrieving breadcrumbs for certain views.
+
+
+How to get existing breadcrumbs
+===============================
+
+We create a company to check the breadcrumb functionality.
+
+    >>> from waeup.ikoba.app import Company
+    >>> root = getRootFolder()
+    >>> root['app'] = Company()
+    >>> 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.ikoba.browser.breadcrumbs import getBreadcrumbList
+    >>> blist1 = getBreadcrumbList(app, 'index')
+    >>> blist1
+    [<waeup.ikoba.browser.breadcrumbs.CompanyBreadcrumb object at 0x...>]
+
+A slightly more extensive list for the datacenter:
+
+    >>> blist2 = getBreadcrumbList(app['datacenter'], 'index')
+    >>> from pprint import pprint
+    >>> pprint(blist2)
+    [<waeup.ikoba.browser.breadcrumbs.CompanyBreadcrumb object at 0x...>,
+     <waeup.ikoba.browser.breadcrumbs.AdministrationBreadcrumb object at 0x...>,
+     <waeup.ikoba.browser.breadcrumbs.DataCenterBreadcrumb object at 0x...>]
+
+We get a breadcrumb for company, 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.ikoba.app.Company object at 0x...>, 'index'),
+      (<waeup.ikoba.app.Company object at 0x...>, 'administration'),
+      (<waeup.ikoba.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 company 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 Company
+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.ikoba.browser.breadcrumbs import getBreadcrumbListForView
+    >>> blist3 = getBreadcrumbListForView(page)
+    >>> [x.title for x in blist3]
+    [u'Home', u'Administration', u'Officers']
+
+..note:: This works only for views created with grokcore.component
+         (i.e. Grok Views, Pages, etc.) but not for simple Zope3
+         views, as the viewname can not easily be extracted from the
+         view for the latter ones.
+
+
+Type-safe retrieval of breadcrumbs for a view
+---------------------------------------------
+
+If you want to make sure, that a view for which you want to get
+breadcrumbs is really a Grok view, then you can use the
+IBreadcrumbContainer adapter. This one adapts only grok views.
+
+The returned breadcrumb container supports iteration:
+
+    >>> from waeup.ikoba.browser.interfaces import IBreadcrumbContainer
+    >>> mybccontainer = IBreadcrumbContainer(page)
+    >>> [x.title for x in mybccontainer]
+    [u'Home', u'Administration', u'Officers']
+
+It is, however, not a real Python list but only an iterable
+container. You can also get the real list of breadcrumbs:
+
+    >>> pprint(mybccontainer.getList())
+    [<...breadcrumbs.CompanyBreadcrumb object at 0x...>,
+     <...breadcrumbs.AdministrationBreadcrumb object at 0x...>,
+     <...breadcrumbs.UsersContainerBreadcrumb object at 0x...>]
+
+
+
+The general details
+-------------------
+
+Now we can get breadcrumbs for contexts and view names. For example a
+breadcrumb for the 'index' view of our Company object:
+
+    >>> from zope.component import getAdapter
+    >>> from waeup.ikoba.browser.interfaces import IBreadcrumb
+    >>> b1 = getAdapter(app, IBreadcrumb, 'index')
+    >>> b1
+    <waeup.ikoba.browser.breadcrumbs.CompanyBreadcrumb 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.ikoba.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.ikoba.app.Company object at 0x...>, 'administration')
+
+This result denotes a new context object (the Company 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.ikoba...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
+Company 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.ikoba.app.Company 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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/browser.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/browser.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/browser.txt	(revision 12677)
@@ -0,0 +1,368 @@
+Browsing Ikoba
+*************
+
+Here we visit all parts of a Ikoba portal using a browser.
+
+Company
+==========
+
+We can watch universities in the browser.
+
+We create an company object and put into the ZODB root::
+
+  >>> root = getRootFolder()
+  >>> list(root)
+  []
+
+  >>> from waeup.ikoba.app import Company
+  >>> u = Company()
+  >>> root['mycompany'] = u
+  >>> list(root)
+  [u'mycompany']
+
+  >>> from zope.component.hooks import setSite
+  >>> setSite(root['mycompany'])
+
+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 company::
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Welcome to WAeUP.Ikoba...
+  ...
+
+We can change to German::
+  >>> browser.getLink('de').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Anfragen...
+  ...
+
+And then change back to English by clicking on the third link containing 'en'
+behind 'Anfragen' and 'Einloggen'::
+  >>> browser.getLink('en', index=2).click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Enquiries...
+  ...
+
+The contact form for anonymous users is called 'Enquiries'::
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> browser.getLink('Enquiries').click()
+  >>> browser.getControl('Send').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Required input is missing...
+  ...
+
+  >>> browser.getControl(name='form.fullname').value = "Bob Tester"
+  >>> browser.getControl(name='form.email_from').value = "xx@yy.zz"
+  >>> browser.getControl(name='form.body').value = u'test message'
+  >>> browser.getControl('Send').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Your message has been sent...
+  ...
+
+Registered users with an email address can request a password change:
+
+  >>> root['mycompany']['users'].addUser('forgetful', 'secret',title='Bob Forgetful',
+  ...                               description='A forgetful user', email='aa@aa.ng')
+  >>> browser.open('http://localhost/mycompany/changepw')
+  >>> browser.getControl(name="form.email").value = 'aa@aa.ng'
+  >>> browser.getControl("Send login credentials").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...An email with your user name and password has been sent to aa@aa.ng...
+
+Now we login as manager::
+
+  >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+  >>> browser.handleErrors = False
+
+We can then get an edit view of the configuration container::
+
+  >>> browser.open('http://localhost/mycompany/configuration')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<form action="http://localhost/mycompany/configuration/@@index"
+  ...
+
+The edit form contains the default value for the company name::
+
+  >>> 'Sample Company' in browser.contents
+  True
+
+We can perform several actions on the edit form::
+
+  >>> browser.getControl("Save", index=0).click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Form has been saved...
+  ...
+
+  >>> browser.open('http://localhost/mycompany/configuration')
+  >>> browser.getControl("Update plugins").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Plugins were updated. See log file for details...
+  ...
+
+The default frontpage HTML has been saved in a dictionary
+and is properly rendered on the frontpage of the portal:
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h1>Welcome to WAeUP.Ikoba...</h1>
+  ...
+
+The German part is really not being rendered:
+
+  >>> 'Willkommen' in browser.contents
+  False
+
+
+If we change to German so that the German part of frontpage.rst is rendered:
+
+  >>> browser.open('http://localhost/mycompany//@@change_language?lang=de')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h1>Willkommen auf WAeUP.Ikoba...</h1>
+  ...
+
+The English part is really not being rendered:
+
+  >>> 'Welcome' in browser.contents
+  False
+
+Switch back to English:
+
+  >>> browser.open('http://localhost/mycompany//@@change_language?lang=en')
+
+
+Officers
+========
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> browser.getLink('Officers').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Officers...
+  ...
+
+We can add users:
+
+  >>> browser.getLink("Add user").click()
+  >>> browser.getControl(name="form.name").value = 'bob'
+  >>> browser.getControl(name="form.title").value = 'Bob The User'
+  >>> browser.getControl(name="password").value = 'secret'
+  >>> browser.getControl(name="control_password").value = 'secret'
+  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
+  >>> browser.getControl(name="form.phone.country").value = ['+234']
+  >>> browser.getControl(name="form.phone.area").value = '123'
+  >>> browser.getControl(name="form.phone.ext").value = '45678'
+  >>> browser.getControl("Add user").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>bob</td>
+  ...
+
+We can edit user bob:
+
+  >>> browser.getControl("Manage", index=0).click()
+  >>> browser.getControl("Save", index=0).click()
+  >>> browser.getControl("Cancel", index=0).click()
+
+We can add site roles which are then displayed on the user container page.
+Since the test browser does not use javascript, we have to add site roles
+manually by setting the roles attribute:
+
+Since JS is not supported by the test browser, the default role has been removed
+when clicking the Save button.
+
+  >>> print root['mycompany']['users']['bob'].roles
+  []
+  >>> root['mycompany']['users']['bob'].roles = ['waeup.UsersManager']
+  >>> print root['mycompany']['users']['bob'].roles
+  ['waeup.UsersManager']
+  >>> browser.open('http://localhost/mycompany/users')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td nowrap>Users Manager <br /></td>
+  ...
+
+Users can't be added twice:
+
+  >>> browser.open('http://localhost/mycompany/users/add')
+  >>> browser.getControl(name="form.name").value = 'bob'
+  >>> browser.getControl(name="form.title").value = 'Bob The User'
+  >>> browser.getControl(name="password").value = 'secret'
+  >>> browser.getControl(name="control_password").value = 'secret'
+  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
+  >>> browser.getControl(name="form.phone.country").value = ['+234']
+  >>> browser.getControl(name="form.phone.area").value = '123'
+  >>> browser.getControl(name="form.phone.ext").value = '45678'
+  >>> browser.getControl("Add user").click()
+  >>> 'The userid chosen already exists' in browser.contents
+  True
+
+Users can be deleted:
+
+  >>> browser.open('http://localhost/mycompany/users')
+  >>> browser.getControl("Remove", index=0).click()
+  >>> 'User account bob successfully deleted' in browser.contents
+  True
+
+
+
+Contact Form
+============
+
+Let's enter the contact form::
+
+  >>> browser.open('http://localhost/mycompany/contactadmin')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Contact
+  ...
+
+We fill the form  (this will send a real message to
+contact@waeup.org)::
+
+  >>> browser.open('http://localhost/mycompany/contactadmin')
+  >>> browser.getControl(name='form.body').value = "test message"
+  >>> browser.getControl('Send').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Your message has been sent...
+  ...
+
+If this test fails, chances are, that the local machine has no SMTP
+server installed.
+
+
+Data Center
+===========
+
+The data center helps us uploading files for later import or similar.
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> browser.getLink('Data Center').click()
+
+Setting the file path
+---------------------
+
+A datacenter stores files in a path in filesystem. By default this is
+a directory in the sources:
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  ...
+  <b>Storage path:</b> <span>.../parts/test/datacenter</span>
+  ...
+
+Going to 'Settings` we can change the path:
+
+  >>> browser.getLink('Edit settings').click()
+  >>> pathsetting = browser.getControl(name='newpath')
+
+We create a directory and set it as our upload path:
+
+  >>> import os
+  >>> cwd = os.getcwd()
+  >>> uploadpath = os.path.join(cwd, 'testfiles')
+  >>> os.mkdir(uploadpath)
+  >>> pathsetting.value = uploadpath
+
+And submit the form:
+
+  >>> browser.getControl(name='save').click()
+
+We clean up the set directory path, as there might be some files be
+copied some files from installation:
+
+  >>> files = os.listdir(uploadpath)
+  >>> for filename in files:
+  ...   if os.path.isdir(os.path.join(uploadpath, filename)):
+  ...     continue
+  ...   os.unlink(os.path.join(uploadpath, filename))
+
+We also remove any existing 'accesscodes' subdir:
+
+  >>> import shutil
+  >>> for filename in files:
+  ...   if not os.path.isdir(os.path.join(uploadpath, filename)):
+  ...     continue
+  ...   if filename != 'accesscodes':
+  ...     continue
+  ...   shutil.rmtree(os.path.join(uploadpath, filename))
+
+The new upload directory is now empty, except from the logs and other
+standard dirs, which are created automatically:
+
+  >>> sorted(os.listdir(uploadpath))
+  ['deleted', 'finished', 'logs', 'unfinished']
+
+
+Uploading files
+---------------
+
+Now we can upload files. Most interesting files might be CSV files,
+that can be imported lateron. We create a CSV file containing faculty
+descriptions:
+
+  >>> open('faculties.csv', 'wb').write(
+  ... """code,title,title_prefix
+  ... FA,Arts,faculty
+  ... FS,Sciences,faculty
+  ... """)
+
+Now we can upload this file. To do this, we first go to the upload
+page:
+
+  >>> browser.getLink('Upload data').click()
+
+and enter the appropriate data in the form:
+
+  >>> filewidget = browser.getControl(name='uploadfile:file')
+  >>> filewidget
+  <Control name='uploadfile:file' type='file'>
+
+A sidenote for developers: by marking the filewidget with the
+``:file`` extension, we tell Zope to handle this field as a file
+widget.
+
+  >>> import cStringIO
+  >>> filecontents = cStringIO.StringIO(
+  ...   open('faculties.csv', 'rb').read())
+  >>> filewidget.add_file(filecontents, 'text/plain', 'myfaculties.csv')
+
+  >>> browser.getControl(name='SUBMIT').click()
+
+The file was indeed uploaded, with the current userid inserted:
+
+  >>> sorted(os.listdir(uploadpath))
+  ['deleted', 'finished', 'logs', 'myfaculties_zope.mgr.csv', 'unfinished']
+
+
+
+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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/captcha.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/captcha.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/captcha.py	(revision 12677)
@@ -0,0 +1,343 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components to add captcha functionality in views/pages.
+
+This is currently a playground, stuff still to be worked out more properly.
+"""
+import grok
+import urllib
+import urllib2
+from random import SystemRandom as random
+from zope import schema
+from zope.component import getUtilitiesFor, getUtility, queryUtility
+from zope.interface import Interface
+from zope.publisher.interfaces.http import IHTTPRequest
+from waeup.ikoba.browser.layout import IkobaPage
+from waeup.ikoba.browser.interfaces import (
+    ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
+    ICaptchaManager)
+from waeup.ikoba.interfaces import ICompany
+
+#
+# Global captcha manager
+#
+class CaptchaManager(grok.GlobalUtility):
+
+    grok.implements(ICaptchaManager)
+
+    def getAvailCaptchas(self):
+        """Get all available captchas registered as utils for ICaptcha.
+
+        The default captcha (as it most probably is a copy of another
+        registered captcha) is left out of the result.
+
+        Result will be a dict with registration names as keys and the
+        specific captcha instances as values.
+        """
+        result = getUtilitiesFor(ICaptcha)
+        return dict([(name,inst) for name,inst in result
+                       if name != u''])
+
+    def getCaptcha(self):
+        """Get captcha chosen to be used.
+
+        Sites can activate a specific captcha by setting
+        ``site['configuration'].captcha``. The attribute should be a
+        string under which the specific captcha is registered.
+
+        If this attribute is not set or we are
+        not 'in a site', the default captcha is returned.
+        """
+        site = grok.getSite()
+        name = ''
+        if site is None:
+            return getUtility(ICaptcha)
+        name = getattr(site.get('configuration', {}), 'captcha', u'')
+        return queryUtility(ICaptcha, name=name,
+                            default=getUtility(ICaptcha))
+
+##
+## Trivial default captcha
+##
+class CaptchaResponse(object):
+    grok.implements(ICaptchaResponse)
+    def __init__(self, is_valid, error_code=None):
+        self.is_valid = is_valid
+        self.error_code = error_code
+        return
+
+class CaptchaRequest(object):
+    grok.implements(ICaptchaRequest)
+    def __init__(self, solution=None, challenge=None):
+        self.solution = solution
+        self.challenge = challenge
+        return
+
+class NullCaptcha(object):
+    """A captcha that does not expect any input.
+
+    Meant as a placeholder for sites that do not want captchas at all.
+
+    NullCaptchas do not render any HTML/JavaScript and accept any
+    request when asked for verification.
+
+    They can be used in pages prepared for captchas where the site
+    maintainer decides not to use it at all.
+    """
+    grok.implements(ICaptcha)
+
+    def verify(self, request):
+        return CaptchaResponse(True, None)
+
+    def display(self, error_code=None):
+        return u''
+
+
+# This captcha is registered twice: one time as a 'no captcha' captcha
+# and then also as the default captcha (with empty name)
+grok.global_utility(NullCaptcha, name=u'No captcha')
+grok.global_utility(NullCaptcha, name=u'')
+
+##
+## TestCaptcha
+##
+class StaticCaptcha(object):
+    """The StaticCaptcha always has the same solution: 'the-solution'.
+
+    It is of no use for real world but for tests. In tests we cannot
+    easily solve really strong captchas. But we can use a captcha that
+    works like a real one but is easy to solve even for machines.
+
+    The HTML form piece generated is even prefilled with the correct
+    solution. So in tests it is not necessary to always 'type' the
+    solution string in the correct field.
+
+    You can, however, fill in a wrong solution and it will be detected
+    as such.
+    """
+    grok.implements(ICaptcha)
+
+    #: name of solution field in HTTP request
+    sol_field = 'solution'
+    #: name of challenge field in HTTP request
+    chal_field = 'challenge'
+
+    def verify(self, request):
+        """Verify that a solution sent equals the challenge.
+        """
+        form = getattr(request, 'form', {})
+        solution=form.get(self.sol_field, None)
+        challenge=form.get(self.chal_field, None)
+        if solution == challenge and solution:
+            return CaptchaResponse(is_valid=True)
+        return CaptchaResponse(is_valid=False)
+
+    def display(self, error_code=None):
+        """Display challenge and input field for solution as HTML.
+        """
+        html = (
+            u'<input type="hidden" name="challenge"'
+            u'       value="the-solution" /><br />'
+            u'Type: %s<br />'
+            u'<input type="text" name="solution" value="the-solution"'
+            u' /><br />')
+        return html
+
+grok.global_utility(StaticCaptcha, name=u'Testing captcha')
+
+
+##
+## ReCaptcha
+##
+API_SSL_SERVER = "https://www.google.com/recaptcha/api"
+VERIFY_SERVER = "https://www.google.com/recaptcha/api"
+
+class ReCaptcha(StaticCaptcha):
+    """ReCaptcha - strong captchas with images, sound, etc.
+
+    This is the Ikoba implementation to support captchas as provided by
+    http://www.google.com/recaptcha.
+
+    ReCaptcha is widely used and adopted in web applications. See the
+    above web page to learn more about recaptcha.
+
+    Basically, it generates a captcha box in a page loaded by a
+    client. The client can then enter a solution for a challenge
+    picture (or audio file) and send the solution back to our server
+    along with the challenge string generated locally on the client.
+
+    This component then verifies the entered solution deploying the
+    private key set by asking a verification server. The result (valid
+    or invalid solution) is then returned to any calling component.
+
+    As any captcha-component, :class:`ReCaptcha` can be used by any
+    other component that wants to display/verify captchas.
+
+    To incorporate captcha usage in a view, page, or viewlet, the
+    following steps have to be performed:
+
+    * get the currently site-wide selected captcha type by doing::
+
+        mycaptcha = getUtility(ICaptchaManager).getCaptcha()
+
+    * if you want a specific captcha type (like ReCaptcha)::
+
+        mycaptcha = getUtility(ICaptcha, name='ReCaptcha')
+
+    Now, as you have a captcha, you can verify sent data by doing::
+
+        result = mycaptcha.verify(self.request)
+
+    where ``self.request`` should be the sent HTTP request and
+    ``result`` will be an instance of class:``CaptchaResponse``. The
+    response will contain an error code (``result.error_code``) that
+    might be ``None`` but can (and should) be passed to the
+    :meth:``display`` method to display error messages in the captcha
+    box. The error code is most probably not a human readable string
+    but some code you shouldn't rely upon.
+
+    All this could be done in the ``update()`` method of a view, page,
+    or viewlet.
+
+    To render the needed HTML code, you can deploy the
+    :meth:`display`` method of ``mycaptcha``.
+
+    This captcha is available at runtime as a global utility named
+    ``'ReCaptcha'``.
+    """
+
+    grok.implements(ICaptcha)
+
+    #: name of solution field in HTTP request
+    sol_field = 'recaptcha_response_field'
+    #: name of challenge field in HTTP request
+    chal_field = 'recaptcha_challenge_field'
+
+    # Do not use the following keys in productive environments!  As
+    # they are both made publicly available, they are not secure any
+    # more!  Use them for testing and evaluating only!
+    PUBLIC_KEY = "6Lc0y8oSAAAAAHwdojrqPtcKn7Rww5qGprb0rrSk"
+    PRIVATE_KEY = "6Lc0y8oSAAAAAMHVbMrGWLLjw2pm8v2Uprwm9AbR"
+
+    def verify(self, request):
+        """Grab challenge/solution from HTTP request and verify it.
+
+        Verification happens against recaptcha remote API servers. It
+        only happens, when really a solution was sent with the
+        request.
+
+        Returns a :class:`CaptchaResponse` indicating that the
+        verification failed or succeeded.
+        """
+        form = getattr(request, 'form', {})
+        solution=form.get(self.sol_field, None)
+        challenge=form.get(self.chal_field, None)
+        if not challenge or not solution:
+            # Might be first-time display of the captcha: not a valid
+            # solution but no error code to prevent any error
+            # messages. Skip further verification.
+            return CaptchaResponse(
+                is_valid=False)
+        params = urllib.urlencode(
+            {
+                'privatekey': self.PRIVATE_KEY,
+                'remoteip': '127.0.0.1',
+                'challenge': challenge,
+                'response': solution,
+                })
+        request = urllib2.Request(
+            url = "%s/verify" % VERIFY_SERVER,
+            data = params,
+            headers = {
+                "Content-type": "application/x-www-form-urlencoded",
+                "User-agent": "reCAPTCHA Python Ikoba",
+                }
+            )
+        resp = urllib2.urlopen(request)
+        ret_vals = resp.read().splitlines()
+        resp.close()
+        ret_code, err_code = ret_vals
+
+        if ret_code == "true":
+            return CaptchaResponse(is_valid=True)
+        return CaptchaResponse(is_valid=False, error_code=ret_vals[1])
+
+    def display(self, error_code=None):
+        """Display challenge and input field for solution as HTML.
+
+        Returns the HTML code to be placed inside an existing ``<form>``
+        of your page. You can add other fields and should add a submit
+        button to send the form.
+
+        The ``error_code`` can be taken from a previously fetched
+        :class:``CaptchaResponse`` instance (as returned by
+        :meth:``verify``). If it is not ``None``, it might be
+        displayed inside the generated captcha box (in human readable
+        form).
+        """
+        error_param = ''
+        if error_code:
+            error_param = '&error=%s' % error_code
+
+        html = (
+            u'<script type="text/javascript" '
+            u'src="%(ApiServer)s/challenge?k=%(PublicKey)s%(ErrorParam)s">'
+            u'</script>'
+            u''
+            u'<noscript>'
+            u'<iframe'
+            u'    src="%(ApiServer)s/noscript?k=%(PublicKey)s%(ErrorParam)s"'
+            u'    height="300" width="500" frameborder="0"></iframe><br />'
+            u'<textarea name="recaptcha_challenge_field"'
+            u'          rows="3" cols="40"></textarea>'
+            u'<input type="hidden" name="recaptcha_response_field"'
+            u'       value="manual_challenge" />'
+            u'</noscript>' % {
+                'ApiServer' : API_SSL_SERVER,
+                'PublicKey' : self.PUBLIC_KEY,
+                'ErrorParam' : error_param,
+                }
+            )
+        return html
+
+grok.global_utility(ReCaptcha, name=u'ReCaptcha')
+
+
+class CaptchaTestPage(IkobaPage):
+    # A test page to see a captcha in action
+    grok.name('captcha')
+    grok.context(ICompany)
+    grok.require('waeup.Public')
+    title = 'Captcha Test'
+    label = title
+
+    def update(self, recaptcha_challenge_field=None,
+               recaptcha_response_field=None):
+        self.captcha = getUtility(ICaptcha, name='ReCaptcha')
+        result = self.captcha.verify(self.request)
+        self.captcha_error = result.error_code
+        print "VERIFY: ", result.is_valid, result.error_code
+        return
+
+    def render(self):
+        return """
+  <form method="POST" action="">
+    %s
+    <input type="submit" name="SUBMIT" />
+  </form>
+  """ % (self.captcha.display(self.captcha_error),)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/exceptions.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/exceptions.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/exceptions.py	(revision 12677)
@@ -0,0 +1,80 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+import zope.errorview.browser
+from zope.interface.common.interfaces import IException
+from zope.errorview.http import SystemErrorViewMixin
+from zope.publisher.interfaces import INotFound
+from zope.security.interfaces import IUnauthorized
+from waeup.ikoba.browser.layout import IkobaPage
+
+grok.templatedir('templates')
+
+class ExceptionView(grok.View, zope.errorview.browser.ExceptionView,
+                    SystemErrorViewMixin):
+    """Base class for rendering views for uncaught exceptions that occur during
+    the application run-time and are not otherwise rendered.
+
+    Backport from Grok 1.6.
+
+    XXX: This view is shared for all apps and objects in the ZODB root.
+    """
+    grok.context(IException)
+    grok.name('index.html')
+    grok.template('exception')
+
+    def update(self):
+        return zope.errorview.browser.ExceptionView.update(self)
+
+class UnauthorizedView(grok.View, zope.errorview.browser.UnauthorizedView):
+    """Base class for rendering views for IUnauthorized exceptions.
+
+    Backport from grok 1.6. This is only a view (not a page) because
+    we cannot trust that the associated :exc:`Unauthorized` exceptions
+    happened inside a w.k. site.
+
+    If not, then we have no layout available at time of rendering.
+
+    XXX: This view is shared for all apps and objects in the ZODB root.
+    """
+    grok.context(IUnauthorized)
+    grok.name('index.html')
+    grok.template('unauthorized')
+
+    def update(self):
+        return zope.errorview.browser.UnauthorizedView.update(self)
+
+
+class NotFoundPage(IkobaPage):
+    """A page rendered when an object cannot be found.
+
+    XXX: This page won't work for objects above a w.k.Company.
+    """
+    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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/fileviewlets.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/fileviewlets.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/fileviewlets.py	(revision 12677)
@@ -0,0 +1,198 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+import os
+import grok
+from zope.component import getUtility
+from zope.interface import Interface
+from waeup.ikoba.interfaces import (
+    IIkobaObject, IExtFileStore, IFileStoreNameChooser)
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.browser import DEFAULT_IMAGE_PATH
+from waeup.ikoba.utils.helpers import (
+    string_from_bytes, file_size, get_fileformat)
+from waeup.ikoba.browser.layout import (
+    default_filedisplay_template,
+    default_fileupload_template)
+
+ALLOWED_FILE_EXTENSIONS = ('jpg', 'png', 'pdf', 'tif', 'fpm')
+
+grok.context(IIkobaObject)  # Make IIkobaObject the default context
+
+def handle_file_delete(context, view, download_name):
+    """Handle deletion of file.
+
+    """
+    store = getUtility(IExtFileStore)
+    store.deleteFileByContext(context, attr=download_name)
+    context.writeLogMessage(view, 'deleted: %s' % download_name)
+    view.flash(_('${a} deleted.', mapping={'a': download_name}))
+    return
+
+
+def handle_file_upload(upload, context, view, max_size, download_name=None):
+    """Handle upload of file.
+
+    Returns `True` in case of success or `False`.
+
+    Please note that file pointer passed in (`upload`) most probably
+    points to end of file when leaving this function.
+    """
+    # Check some file requirements first
+    size = file_size(upload)
+    if size > max_size:
+        view.flash(_('Uploaded file is too big.'), type="danger")
+        return False
+    upload.seek(0)  # file pointer moved when determining size
+    dummy,ext = os.path.splitext(upload.filename)
+    # fpm files are expected to be fingerprint minutiae, file
+    # format is not yet checked
+    if ext == '.fpm':
+        file_format = 'fpm'
+    else:
+        file_format = get_fileformat(None, upload.read(512))
+        upload.seek(0)  # same here
+    if file_format is None:
+        view.flash(_('Could not determine file type.'), type="danger")
+        return False
+    basename, expected_ext = os.path.splitext(download_name)
+    if expected_ext:
+        if '.' + file_format != expected_ext:
+            view.flash(_('${a} file format expected.',
+                mapping={'a': expected_ext[1:]}), type="danger")
+            return False
+    else:
+        if not file_format in ALLOWED_FILE_EXTENSIONS:
+            view.flash(
+                _('Only the following extensions are allowed: ${a}',
+                mapping={'a': ', '.join(ALLOWED_FILE_EXTENSIONS)}),
+                type="danger")
+            return False
+        download_name += '.' + file_format
+    store = getUtility(IExtFileStore)
+    file_id = IFileStoreNameChooser(context).chooseName(attr=download_name)
+    store.createFile(file_id, upload)
+    context.writeLogMessage(view, 'uploaded: %s (%s)' % (
+        download_name,upload.filename))
+    view.flash(_('File ${a} uploaded.', mapping={'a': download_name}))
+    return True
+
+
+class FileManager(grok.ViewletManager):
+    """Viewlet manager for uploading files, preferably scanned images.
+    """
+    grok.name('files')
+
+
+class FileDisplay(grok.Viewlet):
+    """Base file display viewlet.
+    """
+    grok.baseclass()
+    grok.viewletmanager(FileManager)
+    template = default_filedisplay_template
+    label = _(u'File')
+    title = _(u'Scan')
+
+    @property
+    def file_exists(self):
+        image = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr=self.download_name)
+        if image:
+            return True
+        else:
+            return False
+
+
+class FileUpload(FileDisplay):
+    """Base upload viewlet.
+    """
+    grok.baseclass()
+    grok.viewletmanager(FileManager)
+    template = default_fileupload_template
+    tab_redirect = '#tab2-top'
+    mus = 1024 * 150
+    upload_button =_('Upload selected file')
+    delete_button = _('Delete')
+
+    @property
+    def show_viewlet(self):
+        return True
+
+    @property
+    def input_name(self):
+        return "%s" % self.__name__
+
+    def update(self):
+        self.max_upload_size = string_from_bytes(self.mus)
+        delete_button = self.request.form.get(
+            'delete_%s' % self.input_name, None)
+        upload_button = self.request.form.get(
+            'upload_%s' % self.input_name, None)
+        if delete_button:
+            handle_file_delete(
+                context=self.context, view=self.view,
+                download_name=self.download_name)
+            self.view.redirect(
+                self.view.url(
+                    self.context, self.view.__name__) + self.tab_redirect)
+            return
+        if upload_button:
+            upload = self.request.form.get(self.input_name, None)
+            if upload:
+                # We got a fresh upload
+                handle_file_upload(upload,
+                    self.context, self.view, self.mus, self.download_name)
+                self.view.redirect(
+                    self.view.url(
+                        self.context, self.view.__name__) + self.tab_redirect)
+            else:
+                self.view.flash(_('No local file selected.'), type="danger")
+                self.view.redirect(
+                    self.view.url(
+                        self.context, self.view.__name__) + self.tab_redirect)
+        return
+
+
+class Image(grok.View):
+    """Renders images.
+    """
+    grok.baseclass()
+
+    def render(self):
+        # A filename chooser turns a context into a filename suitable
+        # for file storage.
+        image = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr=self.download_name)
+        if image is None:
+            # show placeholder image
+            self.response.setHeader('Content-Type', 'image/jpeg')
+            return open(DEFAULT_IMAGE_PATH, 'rb').read()
+        dummy,ext = os.path.splitext(image.name)
+        if ext == '.jpg':
+            self.response.setHeader('Content-Type', 'image/jpeg')
+        elif ext == '.fpm':
+            self.response.setHeader('Content-Type', 'application/binary')
+        elif ext == '.png':
+            self.response.setHeader('Content-Type', 'image/png')
+        elif ext == '.tif':
+            self.response.setHeader('Content-Type', 'image/tiff')
+        elif ext == '.pdf':
+            self.response.setHeader('Content-Type', 'application/pdf')
+            self.response.setHeader('Content-Disposition',
+                'attachment; filename="%s.pdf' % self.download_filename)
+        return image
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/interfaces.py	(revision 12677)
@@ -0,0 +1,184 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Interfaces for viewing components.
+"""
+from zope import schema
+from zope.interface import Interface, Attribute
+from waeup.ikoba.interfaces import (
+    IIkobaObject, ICompany, IUsersContainer, IDataCenter, validate_email)
+from waeup.ikoba.interfaces import MessageFactory as _
+
+class IBreadcrumb(Interface):
+    """Provide breadcrumbs.
+    """
+    title = Attribute("Context description")
+    viewname = Attribute("The viewname, for which the breadcrumb is provided.")
+    parent_viewname = Attribute("The viewname of the parent to use.")
+    parent = Attribute("The parent object.")
+    context = Attribute("The context of the breadcrumb.")
+    target = Attribute("The link target.")
+
+class IBreadcrumbIgnorable(Interface):
+    """A marker interface for breadcrumbs that should be skipped in output.
+
+    If a breadcrumb wants to be skipped in real output (for instance,
+    because it is set on a layer in site hierarchy that should not be
+    accessed by users), it can also provide this interface. The
+    getBreadcrumbList() function defined here will exclude IIgnorables
+    by default.
+    """
+    pass
+
+class IBreadcrumbContainer(Interface):
+    """A container of breadcrumbs.
+    """
+    def __iter__():
+        """Allow iteration over the container.
+        """
+
+    def getList():
+        """Get the list of breadcrumbs as real Python list.
+        """
+class ICaptchaRequest(Interface):
+    """A set of data required to verify captcha solutions.
+
+    To solve a captcha we need at least a solution. Many types of
+    captchas might also need a challenge to compare whether the
+    solution is correct.
+    """
+    solution = schema.TextLine(
+        title = u'Solution string a user entered',
+        default = None,
+        )
+    challenge = schema.TextLine(
+        title = u'The challenge the solution might match',
+        default = None,
+        )
+
+class ICaptchaResponse(Interface):
+    """A formalized response for captcha solutions.
+    """
+    is_valid = schema.Bool(
+        title = u'Indicates validity of entered captcha data.',
+        default = False,
+        )
+    error_code = schema.TextLine(
+        title = u'Error when trying to validate entered captcha data.',
+        description = u'Error codes are not expected to be readable strings.',
+        default = None,
+        )
+
+class ICaptcha(Interface):
+
+    def verify(request):
+        """Verify data entered in an HTTP request.
+
+        Expects some IHTTPRequest object and returns an
+        ICaptchaResponse indicating that the solution was correct or
+        not.
+
+        If the solution could not be verified (this might also happen
+        because of technical reasons), the response might contain an
+        error code.
+        """
+
+    def display(error_code=None):
+        """Returns a piece of HTML code that displays the captcha.
+
+        The generated HTML code might depend on any previous error
+        code as returned from :meth:`verify`.
+
+        The code is expected to appear inside a ``<form>``. It
+        therefore should not contain a ``<form>`` nor any submit
+        buttons.
+        """
+
+class ICaptchaConfig(Interface):
+    """Any type of captcha might need some configuration data.
+
+    By default we require no configuration data.
+    """
+
+class ICaptchaManager(Interface):
+    """A chooser that collects available captchas.
+    """
+    def getAvailCaptchas():
+        """Return a dict of available captchas with registered names as keys.
+        """
+
+    def getCaptcha():
+        """Get captcha chosen for a certain site or default.
+        """
+
+class IPDFCreator(Interface):
+    """A component that knows where logo graphics for PDFs are stored
+    and can generate PDF documents.
+
+    It is a utility (and not a simple function or class) to make these
+    data customizable in derived packages.
+    """
+    header_logo_path = schema.TextLine(
+        title = u'Path to header logo JPG')
+    watermark_path = schema.TextLine(
+        title = u'Path to watermark logo JPG')
+    def paint_background(canvas, doc):
+        """A callback function to render background of PDFs.
+        """
+    def create_pdf(data, headerline=None, title=None, author=None,
+                   footer='', note=None, sigs_in_footer=[]):
+        """Create a PDF.
+
+        `data` is expected to be a list of reportlab flowables
+        (paragraphs, images, tables, etc.), that will be rendered into
+        some background.
+
+        `headerline` will be displayed in page head and `title` under
+        the top bar.
+
+        `note` is optional HTML markup added at bottom of created
+        document.
+
+        `footer` is the text rendered in the bottom line next to the
+        page numbers.
+
+        `sigs_in_footer` is a set of translateable strings that will be
+        rendered into signature boxes at bottom of each page.
+
+        If no `headerline` is given, a default will be rendered (name
+        of company).
+
+        If no `title` is given, nothing will be rendered.
+        """
+
+class IChangePassword(IIkobaObject):
+    """Interface needed for change pasword page.
+
+    """
+    email = schema.TextLine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        description = _('Enter your email address stored in Ikoba.')
+        )
+
+class ICustomerNavigationBase(IIkobaObject):
+    """Objects that provide customer navigation should
+       implement this interface.
+    """
+    customer = Attribute('''Some customer object that has a '''
+                        ''' `display_fullname` attribute.''')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/layout.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/layout.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/layout.py	(revision 12677)
@@ -0,0 +1,300 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Basic layout components.
+"""
+import os
+import grok
+from datetime import date, datetime
+from grokcore.view import PageTemplateFile
+from grokcore.formlib.formlib import Action
+from cgi import escape
+from zope.i18n import translate
+from zope.i18nmessageid import Message
+from megrok.layout import Page, Layout, Form, EditForm, DisplayForm, AddForm
+from z3c.flashmessage.interfaces import IMessageSource, IMessageReceiver
+from zope.component import getUtility, queryUtility, ComponentLookupError
+from zope.formlib.utility import setUpWidgets
+from zope.interface import Interface
+from zope.site.hooks import getSite
+from waeup.ikoba.interfaces import IIkobaObject, IUserAccount, IIkobaUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.utils.helpers import to_timezone, format_date
+from waeup.ikoba.authentication import get_principal_role_manager
+from waeup.ikoba.browser.interfaces import ICustomerNavigationBase
+
+grok.templatedir('templates')
+default_waeup_display_template = PageTemplateFile(
+    os.path.join('templates', 'default_waeup_display_form.pt'))
+default_waeup_display_template.__grok_name__ = 'default_waeup_display_form'
+
+default_waeup_edit_template = PageTemplateFile(
+    os.path.join('templates', 'default_waeup_edit_form.pt'))
+default_waeup_edit_template.__grok_name__ = 'default_waeup_edit_form'
+
+default_primary_nav_template = PageTemplateFile(
+    os.path.join('templates', 'primarynavtab.pt'))
+default_primary_nav_template.__grok_name__ = 'default_primary_nav'
+
+default_filedisplay_template = PageTemplateFile(
+    os.path.join('templates', 'filedisplay.pt'))
+default_filedisplay_template.__grok_name__ = 'default_filedisplay'
+
+default_fileupload_template = PageTemplateFile(
+    os.path.join('templates', 'fileupload.pt'))
+default_fileupload_template.__grok_name__ = 'default_fileupload'
+
+class action(grok.action):
+
+    def __call__(self, success):
+        action = IkobaAction(self.label, success=success, **self.options)
+        self.actions.append(action)
+        return action
+
+class IkobaAction(Action):
+
+    def __init__(self, label, style='', tooltip='', warning='', **options):
+        super(IkobaAction, self).__init__(label, **options)
+        self.style = style
+        self.tooltip = tooltip
+        self.warning = warning
+        if style == '':
+            self.style = 'default'
+
+    def render(self):
+        if not self.available():
+            return ''
+        label = self.label
+        if isinstance(label, Message):
+            label = translate(self.label, context=self.form.request)
+        if self.warning:
+            warning = translate(self.warning, context=self.form.request)
+            self.warning = ' onclick="return window.confirm(\'%s\')"' % warning
+        if self.tooltip:
+            tooltip = translate(self.tooltip, context=self.form.request)
+            self.tooltip = ' data-toggle="tooltip" title="%s"' % tooltip
+        if self.style:
+            style = ' class="btn btn-%s"' % self.style
+        else:
+            style = ' class="btn btn-default"'
+        html = ('<input type="submit" id="%s" name="%s" value="%s"' %
+                (self.__name__, self.__name__, escape(label, quote=True))
+                + style + self.tooltip + self.warning
+                + ' />')
+        return html
+
+class jsaction(grok.action):
+
+    msg = _('Are you sure?')
+
+    def __call__(self, success):
+        action = IkobaAction(self.label,
+                            success=success, warning=self.msg,
+                            **self.options)
+        self.actions.append(action)
+        return action
+
+def NullValidator(*args, **kw):
+    """A validator that does not validate.
+
+    This is needed especially for cancel buttons. We don't want data
+    to be validated that will be thrown away in the next step.
+
+    You can use it with ``grok.action`` decorator like this::
+
+      @grok.action('Cancel', validator=NullValidator)
+      def cancel(self, **data):
+        self.redirect(<whereever-you-go>)
+    """
+    return dict()
+
+class Messages(grok.View):
+    """Display messages of message receivers.
+    """
+
+    grok.context(Interface)
+
+    @property
+    def messages(self):
+        receiver = getUtility(IMessageReceiver)
+        return receiver.receive()
+
+class UtilityView(object):
+    """A view mixin with useful methods.
+
+    The ``pnav`` attribute (a number) tells, to which primary
+    navigation tab a page declares to belong.
+    """
+    title = u'' # What appears in the content title...
+    pnav = 0 # Primary navigation index...
+
+    def application_url(self, name=None):
+        """Return the URL of the nearest site.
+        """
+        site = getSite()
+        #if not site:
+        #    raise ComponentLookupError("No site found.")
+        return self.url(site, name)
+
+    def flash(self, message, type='success'):
+        """Send a short message to the user.
+        """
+        cssClass = 'alert alert-%s' % type
+        source = queryUtility(IMessageSource, name='session')
+        if source is None:
+            return None
+        source.send(message, cssClass)
+        return True
+
+class IkobaLayout(UtilityView,Layout):
+    """A megrok.layout.Layout with additional methods.
+    """
+    grok.baseclass()
+
+class IkobaForm(UtilityView,Form):
+    """A megrok.layout.Form with additional methods.
+    """
+    grok.baseclass()
+
+    def setUpWidgets(self,ignore_request=False):
+        super(IkobaForm,self).setUpWidgets(ignore_request)
+        # Width parameters will be overridden by Bootstrap
+        # so we have to set the css class
+        if self.widgets.get('body'):
+            self.widgets['body'].height = 10
+
+class IkobaPage(UtilityView,Page):
+    """A megrok.layout page with additional methods.
+    """
+    grok.baseclass()
+
+class IkobaDisplayFormPage(UtilityView,DisplayForm):
+    """A megrok.layout.DisplayForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_display_template
+    hide_hint = True
+
+def large_text_field(widget):
+    for i in  ('body', 'frontpage', 'multilingual', 'condition'):
+        if i in widget.name:
+            return True
+    return False
+
+class IkobaEditFormPage(UtilityView,EditForm):
+    """A megrok.layout.EditForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_edit_template
+
+    def setUpWidgets(self,ignore_request=False):
+        super(IkobaEditFormPage,self).setUpWidgets(ignore_request)
+        for widget in self.widgets:
+            if not large_text_field(widget):
+                widget.height = 3
+
+class IkobaAddFormPage(UtilityView,AddForm):
+    """A megrok.layout.AddForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_edit_template
+
+    def setUpWidgets(self,ignore_request=False):
+        super(IkobaAddFormPage,self).setUpWidgets(ignore_request)
+        for widget in self.widgets:
+            if not large_text_field(widget):
+                widget.height = 3
+
+class SiteLayout(IkobaLayout):
+    """ The general site layout.
+    """
+    grok.context(IIkobaObject)
+
+    #: An instance of the default theme to use for the site layout
+    stafftemp = grok.PageTemplateFile('templates/staffsitelayout.pt')
+    customertemp = grok.PageTemplateFile('templates/customersitelayout.pt')
+
+    @property
+    def site(self):
+        return grok.getSite()
+
+    def getAppTitle(self):
+        return getattr(grok.getSite()['configuration'], 'name', u'Sample Company')
+
+    def getAppAcronym(self):
+        return getattr(grok.getSite()['configuration'], 'acronym', u'Acronym')
+
+    def isAuthenticated(self):
+        """Return True if the calling user is authenticated.
+        """
+        usertitle = self.request.principal.title
+        return usertitle != 'Unauthenticated User'
+
+    def getUserTitle(self):
+        """Return principal title of current user.
+        """
+        usertitle = self.request.principal.title
+        if usertitle == 'Unauthenticated User':
+            return u'Anonymous User'
+        return usertitle
+
+    def getUserId(self):
+        """Return id of current user.
+        """
+        userid = self.request.principal.id
+        return userid
+
+    def isCustomer(self):
+        usertype = getattr(self.request.principal, 'user_type', None)
+        if not usertype:
+            return False
+        return self.request.principal.user_type == 'customer'
+
+    def getCustomerName(self):
+        """Return the customer name.
+        """
+        if ICustomerNavigationBase.providedBy(self.context):
+            return self.context.customer.display_fullname
+
+    def formatDatetime(self,datetimeobj):
+        if isinstance(datetimeobj, datetime):
+            tz = getUtility(IIkobaUtils).tzinfo
+            try:
+                timestamp = to_timezone(
+                    datetimeobj, tz).strftime("%Y-%m-%d %H:%M:%S")
+            except ValueError:
+                return None
+            return timestamp
+        else:
+            return None
+
+    def formatDate(self,dateobj):
+        return format_date(dateobj, self.request)
+            
+    def formatTZDate(self,datetimeobj):
+        if isinstance(datetimeobj, datetime):
+            tz = getUtility(IIkobaUtils).tzinfo
+            date = to_timezone(
+                datetimeobj, tz).strftime("%d/%m/%Y")
+            return date
+        else:
+            return None          
+
+    def render(self):
+        if self.isCustomer() or not self.isAuthenticated():
+            return self.customertemp.render(self)
+        return self.stafftemp.render(self)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/pages.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/pages.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/pages.py	(revision 12677)
@@ -0,0 +1,1575 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+""" Viewing components for Ikoba objects.
+"""
+# XXX: All csv ops should move to a dedicated module soon
+import unicodecsv as csv
+import grok
+import os
+import re
+import sys
+from datetime import datetime, timedelta
+from urllib import urlencode
+from hurry.query import Eq, Text
+from hurry.query.query import Query
+from zope import schema
+from zope.i18n import translate
+from zope.authentication.interfaces import (
+    IAuthentication, IUnauthenticatedPrincipal, ILogout)
+from zope.catalog.interfaces import ICatalog
+from zope.component import (
+    getUtility, queryUtility, createObject, getAllUtilitiesRegisteredFor,
+    getUtilitiesFor,
+    )
+from zope.event import notify
+from zope.security import checkPermission
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.session.interfaces import ISession
+from zope.password.interfaces import IPasswordManager
+from waeup.ikoba.utils.helpers import html2dict
+from waeup.ikoba.browser.layout import (
+    IkobaPage, IkobaForm, IkobaEditFormPage, IkobaAddFormPage,
+    IkobaDisplayFormPage, NullValidator)
+from waeup.ikoba.browser.interfaces import (
+    ICompany, ICaptchaManager, IChangePassword)
+from waeup.ikoba.browser.layout import jsaction, action, UtilityView
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import(
+    IIkobaObject, IUsersContainer, IUserAccount, IDataCenter,
+    IIkobaXMLImporter, IIkobaXMLExporter, IBatchProcessor,
+    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
+    IJobManager,
+    IPasswordValidator, IContactForm, IIkobaUtils, ICSVExporter)
+from waeup.ikoba.permissions import (
+    get_users_with_local_roles, get_all_roles, get_all_users,
+    get_users_with_role)
+
+from waeup.ikoba.authentication import LocalRoleSetEvent
+from waeup.ikoba.utils.helpers import get_user_account, check_csv_charset
+from waeup.ikoba.mandates.mandate import PasswordMandate
+from waeup.ikoba.datacenter import DataCenterFile
+
+FORBIDDEN_CHARACTERS = (160,)
+
+grok.context(IIkobaObject)
+grok.templatedir('templates')
+
+def add_local_role(view, tab, **data):
+    localrole = view.request.form.get('local_role', None)
+    user = view.request.form.get('user', None)
+    if user is None or localrole is None:
+        view.flash('No user selected.', type='danger')
+        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
+        return
+    role_manager = IPrincipalRoleManager(view.context)
+    role_manager.assignRoleToPrincipal(localrole, user)
+    notify(LocalRoleSetEvent(view.context, localrole, user, granted=True))
+    ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
+    grok.getSite().logger.info(
+        '%s - added: %s|%s' % (ob_class, user, localrole))
+    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
+    return
+
+def del_local_roles(view, tab, **data):
+    child_ids = view.request.form.get('role_id', None)
+    if child_ids is None:
+        view.flash(_('No local role selected.'), type='danger')
+        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
+        return
+    if not isinstance(child_ids, list):
+        child_ids = [child_ids]
+    deleted = []
+    role_manager = IPrincipalRoleManager(view.context)
+    for child_id in child_ids:
+        localrole = child_id.split('|')[1]
+        user_name = child_id.split('|')[0]
+        try:
+            role_manager.unsetRoleForPrincipal(localrole, user_name)
+            notify(LocalRoleSetEvent(
+                    view.context, localrole, user_name, granted=False))
+            deleted.append(child_id)
+        except:
+            view.flash('Could not remove %s: %s: %s' % (
+                    child_id, sys.exc_info()[0], sys.exc_info()[1]),
+                    type='danger')
+    if len(deleted):
+        view.flash(
+            _('Local role successfully removed: ${a}',
+            mapping = {'a':', '.join(deleted)}))
+        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
+        grok.getSite().logger.info(
+            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
+    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
+    return
+
+def delSubobjects(view, redirect, tab=None, subcontainer=None):
+    form = view.request.form
+    if 'val_id' in form:
+        child_id = form['val_id']
+    else:
+        view.flash(_('No item selected.'), type='danger')
+        if tab:
+            view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
+        else:
+            view.redirect(view.url(view.context, redirect))
+        return
+    if not isinstance(child_id, list):
+        child_id = [child_id]
+    deleted = []
+    for id in child_id:
+        try:
+            if subcontainer:
+                container = getattr(view.context, subcontainer, None)
+                del container[id]
+            else:
+                del view.context[id]
+            deleted.append(id)
+        except:
+            view.flash('Could not delete %s: %s: %s' % (
+                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
+    if len(deleted):
+        view.flash(_('Successfully removed: ${a}',
+            mapping = {'a': ', '.join(deleted)}))
+        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
+        grok.getSite().logger.info(
+            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
+    if tab:
+        view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
+    else:
+        view.redirect(view.url(view.context, redirect))
+    return
+
+def getPreviewTable(view, n):
+    """Get transposed table with n sample record.
+
+    The first column contains the headers.
+    """
+    if not view.reader:
+        return
+    header = view.getPreviewHeader()
+    num = 0
+    data = []
+    for line in view.reader:
+        if num > n-1:
+            break
+        num += 1
+        data.append(line)
+    result = []
+    for name in header:
+        result_line = []
+        result_line.append(name)
+        for d in data:
+            result_line.append(d[name])
+        result.append(result_line)
+    return result
+
+# Save function used for save methods in pages
+def msave(view, **data):
+    changed_fields = view.applyData(view.context, **data)
+    # Turn list of lists into single list
+    if changed_fields:
+        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+    fields_string = ' + '.join(changed_fields)
+    view.flash(_('Form has been saved.'))
+    ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
+    if fields_string:
+        grok.getSite().logger.info('%s - %s - saved: %s' % (ob_class, view.context.__name__, fields_string))
+    return
+
+def doll_up(view, user=None):
+    """Doll up export jobs for displaying in table.
+    """
+    job_entries = view.context.get_running_export_jobs(user)
+    job_manager = getUtility(IJobManager)
+    entries = []
+    for job_id, exporter_name, user_id in job_entries:
+        job = job_manager.get(job_id)
+        exporter = getUtility(ICSVExporter, name=exporter_name)
+        exporter_title = getattr(exporter, 'title', 'Unknown')
+        args = ', '.join(['%s=%s' % (item[0], item[1])
+            for item in job.kwargs.items()])
+        status = job.finished and 'ready' or 'running'
+        status = job.failed and 'FAILED' or status
+        start_time = getattr(job, 'begin_after', None)
+        time_delta = None
+        if start_time:
+            tz = getUtility(IIkobaUtils).tzinfo
+            time_delta = datetime.now(tz) - start_time
+            start_time = start_time.astimezone(tz).strftime(
+                "%Y-%m-%d %H:%M:%S %Z")
+        download_url = view.url(view.context, 'download_export',
+                                data=dict(job_id=job_id))
+        show_download_button = job.finished and not \
+                               job.failed and time_delta and \
+                               time_delta.days < 1
+        new_entry = dict(
+            job=job_id,
+            creator=user_id,
+            args=args,
+            exporter=exporter_title,
+            status=status,
+            start_time=start_time,
+            download_url=download_url,
+            show_download_button = show_download_button,
+            show_refresh_button = not job.finished,
+            show_discard_button = job.finished,)
+        entries.append(new_entry)
+    return entries
+
+class LocalRoleAssignmentUtilityView(object):
+    """A view mixin with useful methods for local role assignment.
+
+    """
+
+    def getLocalRoles(self):
+        roles = ILocalRolesAssignable(self.context)
+        return roles()
+
+    def getUsers(self):
+        return get_all_users()
+
+    def getUsersWithLocalRoles(self):
+        return get_users_with_local_roles(self.context)
+
+#
+# Login/logout and language switch pages...
+#
+
+class LoginPage(IkobaPage):
+    """A login page, available for all objects.
+    """
+    grok.name('login')
+    grok.context(IIkobaObject)
+    grok.require('waeup.Public')
+    label = _(u'Login')
+    camefrom = None
+    login_button = label
+
+    def _comment(self, customer):
+        return getattr(customer, 'suspended_comment', None)
+
+    def update(self, SUBMIT=None, camefrom=None):
+        self.camefrom = camefrom
+        if SUBMIT is not None:
+            if self.request.principal.id != 'zope.anybody':
+                self.flash(_('You logged in.'))
+                if self.request.principal.user_type == 'customer':
+                    customer = grok.getSite()['customers'][
+                        self.request.principal.id]
+                    rel_link = '/customers/%s' % self.request.principal.id
+                    # Maybe we need this in Ikoba too
+                    #if customer.personal_data_expired:
+                    #    rel_link = '/customers/%s/edit_personal' % (
+                    #        self.request.principal.id)
+                    #    self.flash(
+                    #      _('Your personal data record is outdated. Please update.'),
+                    #      type='warning')
+                    self.redirect(self.application_url() + rel_link)
+                    return
+                if not self.camefrom:
+                    self.redirect(self.application_url() + '/index')
+                    return
+                self.redirect(self.camefrom)
+                return
+            # Display appropriate flash message if credentials are correct
+            # but customer has been deactivated or a temporary password
+            # has been set.
+            login = self.request.form['form.login']
+            if len(login) == 8 and login in grok.getSite()['customers']:
+                customer = grok.getSite()['customers'][login]
+                password = self.request.form['form.password']
+                passwordmanager = getUtility(IPasswordManager, 'SSHA')
+                if customer.password is not None and \
+                    passwordmanager.checkPassword(customer.password, password):
+                    # The customer entered valid credentials.
+                    # First we check if a temporary password has been set.
+                    delta = timedelta(minutes=10)
+                    now = datetime.utcnow()
+                    temp_password_dict = getattr(customer, 'temp_password', None)
+                    if temp_password_dict is not None and \
+                        now < temp_password_dict.get('timestamp', now) + delta:
+                        self.flash(
+                            _('Your account has been temporarily deactivated.'),
+                            type='warning')
+                        return
+                    # Now we know that the customer is suspended.
+                    comment = self._comment(customer)
+                    if comment:
+                        self.flash(comment, type='warning')
+                    else:
+                        self.flash(_('Your account has been deactivated.'),
+                                   type='warning')
+                    return
+            self.flash(_('You entered invalid credentials.'), type='danger')
+            return
+
+
+class LogoutPage(IkobaPage):
+    """A logout page. Calling this page will log the current user out.
+    """
+    grok.context(IIkobaObject)
+    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 Ikoba!"))
+        self.redirect(self.application_url() + '/index')
+        return
+
+
+class LanguageChangePage(IkobaPage):
+    """ Language switch
+    """
+    grok.context(IIkobaObject)
+    grok.name('change_language')
+    grok.require('waeup.Public')
+
+    def update(self, lang='en', view_name='@@index'):
+        self.response.setCookie('ikoba.language', lang, path='/')
+        self.redirect(self.url(self.context, view_name))
+        return
+
+    def render(self):
+        return
+
+#
+# Contact form...
+#
+
+class ContactAdminForm(IkobaForm):
+    grok.name('contactadmin')
+    #grok.context(ICompany)
+    grok.template('contactform')
+    grok.require('waeup.Authenticated')
+    pnav = 2
+    form_fields = grok.AutoFields(IContactForm).select('body')
+
+    def update(self):
+        super(ContactAdminForm, self).update()
+        self.form_fields.get('body').field.default = None
+        return
+
+    @property
+    def config(self):
+        return grok.getSite()['configuration']
+
+    def label(self):
+        return _(u'Contact ${a}', mapping = {'a': self.config.name_admin})
+
+    @property
+    def get_user_account(self):
+        return get_user_account(self.request)
+
+    @action(_('Send message now'), style='primary')
+    def send(self, *args, **data):
+        fullname = self.request.principal.title
+        try:
+            email = self.request.principal.email
+        except AttributeError:
+            email = self.config.email_admin
+        username = self.request.principal.id
+        usertype = getattr(self.request.principal,
+                           'user_type', 'system').title()
+        ikoba_utils = getUtility(IIkobaUtils)
+        success = ikoba_utils.sendContactForm(
+                fullname,email,
+                self.config.name_admin,self.config.email_admin,
+                username,usertype,self.config.name,
+                data['body'],self.config.email_subject)
+        # Success is always True if sendContactForm didn't fail.
+        # TODO: Catch exceptions.
+        if success:
+            self.flash(_('Your message has been sent.'))
+        return
+
+class EnquiriesForm(ContactAdminForm):
+    """Captcha'd page to let anonymous send emails to the administrator.
+    """
+    grok.name('enquiries')
+    grok.require('waeup.Public')
+    pnav = 2
+    form_fields = grok.AutoFields(IContactForm).select(
+                          'fullname', 'email_from', 'body')
+
+    def update(self):
+        super(EnquiriesForm, self).update()
+        # Handle captcha
+        self.captcha = getUtility(ICaptchaManager).getCaptcha()
+        self.captcha_result = self.captcha.verify(self.request)
+        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
+        return
+
+    @action(_('Send now'), style='primary')
+    def send(self, *args, **data):
+        if not self.captcha_result.is_valid:
+            # Captcha will display error messages automatically.
+            # No need to flash something.
+            return
+        ikoba_utils = getUtility(IIkobaUtils)
+        success = ikoba_utils.sendContactForm(
+                data['fullname'],data['email_from'],
+                self.config.name_admin,self.config.email_admin,
+                u'None',u'Anonymous',self.config.name,
+                data['body'],self.config.email_subject)
+        if success:
+            self.flash(_('Your message has been sent.'))
+        else:
+            self.flash(_('A smtp server error occurred.'), type='danger')
+        return
+
+#
+# Company related pages...
+#
+
+class CompanyPage(IkobaDisplayFormPage):
+    """ The main company page.
+    """
+    grok.require('waeup.Public')
+    grok.name('index')
+    grok.context(ICompany)
+    pnav = 0
+    label = ''
+
+    @property
+    def frontpage(self):
+        lang = self.request.cookies.get('ikoba.language')
+        html = self.context['configuration'].frontpage_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+            html = self.context[
+                'configuration'].frontpage_dict.get(portal_language,'')
+        if html =='':
+            return _(u'<h1>Welcome to WAeUP.Ikoba</h1>')
+        else:
+            return html
+
+class AdministrationPage(IkobaPage):
+    """ The administration overview page.
+    """
+    grok.name('administration')
+    grok.context(ICompany)
+    grok.require('waeup.managePortal')
+    label = _(u'Administration')
+    pnav = 0
+
+class RSS20Feed(grok.View):
+    """An RSS 2.0 feed.
+    """
+    grok.name('feed.rss')
+    grok.context(ICompany)
+    grok.require('waeup.Public')
+    grok.template('companyrss20feed')
+
+    name = 'General news feed'
+    description = 'waeup.ikoba 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 Company')
+
+    @property
+    def contexttitle(self):
+        return self.name
+
+    @property
+    def link(self):
+        return self.url(grok.getSite())
+
+    def update(self):
+        self.response.setHeader('Content-Type', 'text/xml; charset=UTF-8')
+
+    def entries(self):
+        return ()
+
+#
+# User container pages...
+#
+
+class UsersContainerPage(IkobaPage):
+    """Overview page for all local users.
+    """
+    grok.require('waeup.manageUsers')
+    grok.context(IUsersContainer)
+    grok.name('index')
+    label = _('Officers')
+    manage_button = _(u'Manage')
+    delete_button = _(u'Remove')
+
+    def update(self, userid=None, adduser=None, manage=None, delete=None):
+        if manage is not None and userid is not None:
+            self.redirect(self.url(userid) + '/@@manage')
+        if delete is not None and userid is not None:
+            self.context.delUser(userid)
+            self.flash(_('User account ${a} successfully deleted.',
+                mapping = {'a':  userid}))
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            self.context.__parent__.logger.info(
+                '%s - removed: %s' % (ob_class, userid))
+
+    def getLocalRoles(self, account):
+        local_roles = account.getLocalRoles()
+        local_roles_string = ''
+        site_url = self.url(grok.getSite())
+        for local_role in local_roles.keys():
+            role_title = getattr(
+                dict(get_all_roles()).get(local_role, None), 'title', None)
+            objects_string = ''
+            for object in local_roles[local_role]:
+                objects_string += '<a href="%s">%s</a>, ' %(self.url(object),
+                    self.url(object).replace(site_url,''))
+            local_roles_string += '%s: <br />%s <br />' %(role_title,
+                objects_string.rstrip(', '))
+        return local_roles_string
+
+    def getSiteRoles(self, account):
+        site_roles = account.roles
+        site_roles_string = ''
+        for site_role in site_roles:
+            role_title = dict(get_all_roles())[site_role].title
+            site_roles_string += '%s <br />' % role_title
+        return site_roles_string
+
+class AddUserFormPage(IkobaAddFormPage):
+    """Add a user account.
+    """
+    grok.require('waeup.manageUsers')
+    grok.context(IUsersContainer)
+    grok.name('add')
+    grok.template('usereditformpage')
+    form_fields = grok.AutoFields(IUserAccount)
+    label = _('Add user')
+
+    @action(_('Add user'), style='primary')
+    def addUser(self, **data):
+        name = data['name']
+        title = data['title']
+        email = data['email']
+        phone = data['phone']
+        description = data['description']
+        #password = data['password']
+        roles = data['roles']
+        form = self.request.form
+        password = form.get('password', None)
+        password_ctl = form.get('control_password', None)
+        if password:
+            validator = getUtility(IPasswordValidator)
+            errors = validator.validate_password(password, password_ctl)
+            if errors:
+                self.flash( ' '.join(errors), type='danger')
+                return
+        try:
+            self.context.addUser(name, password, title=title, email=email,
+                                 phone=phone, description=description,
+                                 roles=roles)
+            self.flash(_('User account ${a} successfully added.',
+                mapping = {'a': name}))
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            self.context.__parent__.logger.info(
+                '%s - added: %s' % (ob_class, name))
+        except KeyError:
+            self.status = self.flash('The userid chosen already exists '
+                                  'in the database.', type='danger')
+            return
+        self.redirect(self.url(self.context))
+
+class UserManageFormPage(IkobaEditFormPage):
+    """Manage a user account.
+    """
+    grok.context(IUserAccount)
+    grok.name('manage')
+    grok.template('usereditformpage')
+    grok.require('waeup.manageUsers')
+    form_fields = grok.AutoFields(IUserAccount).omit('name')
+
+    def label(self):
+        return _("Edit user ${a}", mapping = {'a':self.context.__name__})
+
+    def setUpWidgets(self, ignore_request=False):
+        super(UserManageFormPage,self).setUpWidgets(ignore_request)
+        self.widgets['title'].displayWidth = 30
+        self.widgets['description'].height = 3
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        form = self.request.form
+        password = form.get('password', None)
+        password_ctl = form.get('control_password', None)
+        if password:
+            validator = getUtility(IPasswordValidator)
+            errors = validator.validate_password(password, password_ctl)
+            if errors:
+                self.flash( ' '.join(errors), type='danger')
+                return
+        changed_fields = self.applyData(self.context, **data)
+        if changed_fields:
+            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+        else:
+            changed_fields = []
+        if password:
+            # Now we know that the form has no errors and can set password ...
+            self.context.setPassword(password)
+            changed_fields.append('password')
+        fields_string = ' + '.join(changed_fields)
+        if fields_string:
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            self.context.__parent__.logger.info(
+                '%s - %s edited: %s' % (
+                ob_class, self.context.name, fields_string))
+        self.flash(_('User settings have been saved.'))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context.__parent__))
+        return
+
+class ContactUserForm(ContactAdminForm):
+    grok.name('contactuser')
+    grok.context(IUserAccount)
+    grok.template('contactform')
+    grok.require('waeup.manageUsers')
+    pnav = 0
+    form_fields = grok.AutoFields(IContactForm).select('body')
+
+    def label(self):
+        return _(u'Send message to ${a}', mapping = {'a':self.context.title})
+
+    @action(_('Send message now'), style='primary')
+    def send(self, *args, **data):
+        try:
+            email = self.request.principal.email
+        except AttributeError:
+            email = self.config.email_admin
+        usertype = getattr(self.request.principal,
+                           'user_type', 'system').title()
+        ikoba_utils = getUtility(IIkobaUtils)
+        success = ikoba_utils.sendContactForm(
+                self.request.principal.title,email,
+                self.context.title,self.context.email,
+                self.request.principal.id,usertype,self.config.name,
+                data['body'],self.config.email_subject)
+        # Success is always True if sendContactForm didn't fail.
+        # TODO: Catch exceptions.
+        if success:
+            self.flash(_('Your message has been sent.'))
+        return
+
+class UserEditFormPage(UserManageFormPage):
+    """Edit a user account by user
+    """
+    grok.name('index')
+    grok.require('waeup.editUser')
+    form_fields = grok.AutoFields(IUserAccount).omit(
+        'name', 'description', 'roles')
+    label = _(u"My Preferences")
+
+    def setUpWidgets(self, ignore_request=False):
+        super(UserManageFormPage,self).setUpWidgets(ignore_request)
+        self.widgets['title'].displayWidth = 30
+
+class MyRolesPage(IkobaPage):
+    """Display site roles and local roles assigned to officers.
+    """
+    grok.name('my_roles')
+    grok.require('waeup.editUser')
+    grok.context(IUserAccount)
+    grok.template('myrolespage')
+    label = _(u"My Roles")
+
+    @property
+    def getLocalRoles(self):
+        local_roles = get_user_account(self.request).getLocalRoles()
+        local_roles_userfriendly = {}
+        for local_role in local_roles:
+            role_title = dict(get_all_roles())[local_role].title
+            local_roles_userfriendly[role_title] = local_roles[local_role]
+        return local_roles_userfriendly
+
+    @property
+    def getSiteRoles(self):
+        site_roles = get_user_account(self.request).roles
+        site_roles_userfriendly = []
+        for site_role in site_roles:
+            role_title = dict(get_all_roles())[site_role].title
+            site_roles_userfriendly.append(role_title)
+        return site_roles_userfriendly
+
+#
+# Configuration pages...
+#
+
+class ConfigurationContainerManageFormPage(IkobaEditFormPage):
+    """Manage page of the configuration container. We always use the
+    manage page in the UI not the view page, thus we use the index name here.
+    """
+    grok.require('waeup.managePortalConfiguration')
+    grok.name('index')
+    grok.context(IConfigurationContainer)
+    pnav = 0
+    label = _(u'Edit portal configuration')
+    form_fields = grok.AutoFields(IConfigurationContainer).omit(
+        'frontpage_dict')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        frontpage = getattr(self.context, 'frontpage', None)
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        self.context.frontpage_dict = html2dict(frontpage, portal_language)
+        return
+
+    @action(_('Update plugins'),
+              tooltip=_('For experts only!'),
+              warning=_('Plugins may only be updated after software upgrades. '
+                        'Are you really sure?'),
+              validator=NullValidator)
+    def updatePlugins(self, **data):
+        grok.getSite().updatePlugins()
+        self.flash(_('Plugins were updated. See log file for details.'))
+        return
+
+#
+# Datacenter pages...
+#
+
+class DatacenterPage(IkobaEditFormPage):
+    grok.context(IDataCenter)
+    grok.name('index')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Data Center')
+    pnav = 0
+
+    @jsaction(_('Remove selected'))
+    def delFiles(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No item selected.'), type='danger')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            fullpath = os.path.join(self.context.storage, id)
+            try:
+                os.remove(fullpath)
+                deleted.append(id)
+            except OSError:
+                self.flash(_('OSError: The file could not be deleted.'),
+                           type='danger')
+                return
+        if len(deleted):
+            self.flash(_('Successfully deleted: ${a}',
+                mapping = {'a': ', '.join(deleted)}))
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            self.context.logger.info(
+                '%s - deleted: %s' % (ob_class, ', '.join(deleted)))
+        return
+
+class DatacenterFinishedPage(IkobaEditFormPage):
+    grok.context(IDataCenter)
+    grok.name('processed')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Processed Files')
+    pnav = 0
+    cancel_button =_('Back to Data Center')
+
+    def update(self, CANCEL=None):
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        return super(DatacenterFinishedPage, self).update()
+
+class DatacenterUploadPage(IkobaPage):
+    grok.context(IDataCenter)
+    grok.name('upload')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Upload portal data as CSV file')
+    pnav = 0
+    max_files = 20
+    upload_button =_(u'Upload')
+    cancel_button =_(u'Back to Data Center')
+
+    def getPreviewHeader(self):
+        """Get the header fields of uploaded CSV file.
+        """
+        reader = csv.reader(open(self.fullpath, 'rb'))
+        return reader.next()
+
+    def _notifyImportManagers(self, filename,
+        normalized_filename, importer, import_mode):
+        """Send email to Import Managers
+        """
+        # Get information about file
+        self.fullpath = os.path.join(self.context.storage, normalized_filename)
+        uploadfile = DataCenterFile(self.fullpath)
+        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
+        table = getPreviewTable(self, 3)
+        mail_table = ''
+        for line in table:
+            header = line[0]
+            data = str(line[1:]).strip('[').strip(']')
+            mail_table += '%s: %s ...\n' % (line[0], data)
+        # Collect all recipient addresses
+        ikoba_utils = getUtility(IIkobaUtils)
+        import_managers = get_users_with_role(
+            'waeup.ImportManager', grok.getSite())
+        rcpt_addrs = ','.join(
+            [user['user_email'] for user in import_managers if
+                user['user_email'] is not None])
+        if rcpt_addrs:
+            config = grok.getSite()['configuration']
+            fullname = self.request.principal.title
+            try:
+                email = self.request.principal.email
+            except AttributeError:
+                email = config.email_admin
+            username = self.request.principal.id
+            usertype = getattr(self.request.principal,
+                               'user_type', 'system').title()
+            rcpt_name = _('Import Manager')
+            subject = translate(
+                      _('${a}: ${b} uploaded',
+                      mapping = {'a':config.acronym, 'b':filename}),
+                      'waeup.ikoba',
+                      target_language=ikoba_utils.PORTAL_LANGUAGE)
+            text = _("""File: ${a}
+Importer: ${b}
+Import Mode: ${c}
+Datasets: ${d}
+
+${e}
+
+Comment by Import Manager:""", mapping = {'a':normalized_filename,
+                'b':importer,
+                'c':import_mode,
+                'd':uploadfile.lines - 1,
+                'e':mail_table})
+            success = ikoba_utils.sendContactForm(
+                    fullname,email,
+                    rcpt_name,rcpt_addrs,
+                    username,usertype,config.name,
+                    text,subject)
+            if success:
+                self.flash(
+                    _('All import managers have been notified by email.'))
+            else:
+                self.flash(_('An smtp server error occurred.'), type='danger')
+            return
+
+    def update(self, uploadfile=None, import_mode=None,
+               importer=None, CANCEL=None, SUBMIT=None):
+        number_of_pendings = len(self.context.getPendingFiles())
+        if number_of_pendings > self.max_files:
+            self.flash(
+                _('Maximum number of files in the data center exceeded.'),
+                  type='danger')
+            self.redirect(self.url(self.context))
+            return
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        if not uploadfile:
+            return
+        try:
+            filename = uploadfile.filename
+            #if 'pending' in filename:
+            #    self.flash(_("You can't re-upload pending data files."), type='danger')
+            #    return
+            if not filename.endswith('.csv'):
+                self.flash(_("Only csv files are allowed."), type='danger')
+                return
+            normalized_filename = self.getNormalizedFileName(filename)
+            finished_file = os.path.join(
+                self.context.storage, 'finished', normalized_filename)
+            unfinished_file = os.path.join(
+                self.context.storage, 'unfinished', normalized_filename)
+            if os.path.exists(finished_file) or os.path.exists(unfinished_file):
+                self.flash(_("File with same name was uploaded earlier."),
+                           type='danger')
+                return
+            target = os.path.join(self.context.storage, normalized_filename)
+            filecontent = uploadfile.read()
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            logger = self.context.logger
+
+            # Forbid certain characters in import files.
+            invalid_line = check_csv_charset(filecontent.splitlines())
+            if invalid_line:
+                self.flash(_(
+                    "Your file contains forbidden characters or "
+                    "has invalid CSV format. "
+                    "First problematic line detected: line %s. "
+                    "Please replace." % invalid_line), type='danger')
+                logger.info('%s - invalid file uploaded: %s' %
+                            (ob_class, target))
+                return
+
+            open(target, 'wb').write(filecontent)
+            os.chmod(target, 0664)
+            logger.info('%s - uploaded: %s' % (ob_class, target))
+            self._notifyImportManagers(filename,
+                normalized_filename, importer, import_mode)
+
+        except IOError:
+            self.flash('Error while uploading file. Please retry.', type='danger')
+            self.flash('I/O error: %s' % sys.exc_info()[1], type='danger')
+            return
+        self.redirect(self.url(self.context))
+
+    def getNormalizedFileName(self, filename):
+        """Build sane filename.
+
+        An uploaded file foo.csv will be stored as foo_USERNAME.csv
+        where username is the principal id of the currently logged in
+        user.
+
+        Spaces in filename are replaced by underscore.
+        Pending data filenames remain unchanged.
+        """
+        if filename.endswith('.pending.csv'):
+            return filename
+        username = self.request.principal.id
+        filename = filename.replace(' ', '_')
+        # Only accept typical filname chars...
+        filtered_username = ''.join(re.findall('[a-zA-Z0-9_\.\-]', username))
+        base, ext = os.path.splitext(filename)
+        return '%s_%s%s' % (base, filtered_username, ext.lower())
+
+    def getImporters(self):
+        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
+        ikoba_utils = getUtility(IIkobaUtils)
+        importer_props = []
+        for x in importers:
+            if not x.util_name in ikoba_utils.BATCH_PROCESSOR_NAMES:
+                continue
+            iface_fields = schema.getFields(x.iface)
+            available_fields = []
+            for key in iface_fields.keys():
+                iface_fields[key] = (iface_fields[key].__class__.__name__,
+                    iface_fields[key].required)
+            for value in x.available_fields:
+                available_fields.append(
+                    dict(f_name=value,
+                         f_type=iface_fields.get(value, (None, False))[0],
+                         f_required=iface_fields.get(value, (None, False))[1]
+                         )
+                    )
+            available_fields = sorted(available_fields, key=lambda k: k['f_name'])
+            importer_props.append(
+                dict(title=x.name, name=x.util_name, fields=available_fields))
+        return sorted(importer_props, key=lambda k: k['title'])
+
+class FileDownloadView(UtilityView, grok.View):
+    grok.context(IDataCenter)
+    grok.name('download')
+    grok.require('waeup.manageDataCenter')
+
+    def update(self, filename=None):
+        self.filename = self.request.form['filename']
+        return
+
+    def render(self):
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        self.context.logger.info(
+            '%s - downloaded: %s' % (ob_class, self.filename))
+        self.response.setHeader(
+            'Content-Type', 'text/csv; charset=UTF-8')
+        self.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' %
+            self.filename.replace('finished/',''))
+        fullpath = os.path.join(self.context.storage, self.filename)
+        return open(fullpath, 'rb').read()
+
+class SkeletonDownloadView(UtilityView, grok.View):
+    grok.context(IDataCenter)
+    grok.name('skeleton')
+    grok.require('waeup.manageDataCenter')
+
+    def update(self, processorname=None):
+        self.processorname = self.request.form['name']
+        self.filename = ('%s_000.csv' %
+            self.processorname.replace('processor','import'))
+        return
+
+    def render(self):
+        #ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        #self.context.logger.info(
+        #    '%s - skeleton downloaded: %s' % (ob_class, self.filename))
+        self.response.setHeader(
+            'Content-Type', 'text/csv; charset=UTF-8')
+        self.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' % self.filename)
+        processor = getUtility(IBatchProcessor, name=self.processorname)
+        csv_data = processor.get_csv_skeleton()
+        return csv_data
+
+class DatacenterImportStep1(IkobaPage):
+    """Manual import step 1: choose file
+    """
+    grok.context(IDataCenter)
+    grok.name('import1')
+    grok.template('datacenterimport1page')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Process CSV file')
+    pnav = 0
+    cancel_button =_(u'Back to Data Center')
+
+    def getFiles(self):
+        files = self.context.getPendingFiles(sort='date')
+        for file in files:
+            name = file.name
+            if not name.endswith('.csv') and not name.endswith('.pending'):
+                continue
+            yield file
+
+    def update(self, filename=None, select=None, cancel=None):
+        if cancel is not None:
+            self.redirect(self.url(self.context))
+            return
+        if select is not None:
+            # A filename was selected
+            session = ISession(self.request)['waeup.ikoba']
+            session['import_filename'] = select
+            self.redirect(self.url(self.context, '@@import2'))
+
+class DatacenterImportStep2(IkobaPage):
+    """Manual import step 2: choose processor
+    """
+    grok.context(IDataCenter)
+    grok.name('import2')
+    grok.template('datacenterimport2page')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Process CSV file')
+    pnav = 0
+    cancel_button =_(u'Cancel')
+    back_button =_(u'Back to step 1')
+    proceed_button =_(u'Proceed to step 3')
+
+    filename = None
+    mode = 'create'
+    importer = None
+    mode_locked = False
+
+    def getPreviewHeader(self):
+        """Get the header fields of attached CSV file.
+        """
+        reader = csv.reader(open(self.fullpath, 'rb'))
+        return reader.next()
+
+    def getPreviewTable(self):
+        return getPreviewTable(self, 3)
+
+    def getImporters(self):
+        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
+        ikoba_utils = getUtility(IIkobaUtils)
+        importers = sorted(
+            [dict(title=x.name, name=x.util_name) for x in importers
+            if x.util_name in ikoba_utils.BATCH_PROCESSOR_NAMES])
+        return importers
+
+    def getModeFromFilename(self, filename):
+        """Lookup filename or path and return included mode name or None.
+        """
+        if not filename.endswith('.pending.csv'):
+            return None
+        base = os.path.basename(filename)
+        parts = base.rsplit('.', 3)
+        if len(parts) != 4:
+            return None
+        if parts[1] not in ['create', 'update', 'remove']:
+            return None
+        return parts[1]
+
+    def getWarnings(self):
+        import sys
+        result = []
+        try:
+            headerfields = self.getPreviewHeader()
+            headerfields_clean = list(set(headerfields))
+            if len(headerfields) > len(headerfields_clean):
+                result.append(
+                    _("Double headers: each column name may only appear once. "))
+        except:
+            fatal = '%s' % sys.exc_info()[1]
+            result.append(fatal)
+        if result:
+            warnings = ""
+            for line in result:
+                warnings += line + '<br />'
+            warnings += _('Replace imported file!')
+            return warnings
+        return False
+
+    def update(self, mode=None, importer=None,
+               back1=None, cancel=None, proceed=None):
+        session = ISession(self.request)['waeup.ikoba']
+        self.filename = session.get('import_filename', None)
+
+        if self.filename is None or back1 is not None:
+            self.redirect(self.url(self.context, '@@import1'))
+            return
+        if cancel is not None:
+            self.flash(_('Import aborted.'), type='warning')
+            self.redirect(self.url(self.context))
+            return
+        self.mode = mode or session.get('import_mode', self.mode)
+        filename_mode = self.getModeFromFilename(self.filename)
+        if filename_mode is not None:
+            self.mode = filename_mode
+            self.mode_locked = True
+        self.importer = importer or session.get('import_importer', None)
+        session['import_importer'] = self.importer
+        if self.importer and 'update' in self.importer:
+            if self.mode != 'update':
+                self.flash(_('Update mode only!'), type='warning')
+                self.mode_locked = True
+                self.mode = 'update'
+                proceed = None
+        session['import_mode'] = self.mode
+        if proceed is not None:
+            self.redirect(self.url(self.context, '@@import3'))
+            return
+        self.fullpath = os.path.join(self.context.storage, self.filename)
+        warnings = self.getWarnings()
+        if not warnings:
+            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
+        else:
+            self.reader = ()
+            self.flash(warnings, type='warning')
+
+class DatacenterImportStep3(IkobaPage):
+    """Manual import step 3: modify header
+    """
+    grok.context(IDataCenter)
+    grok.name('import3')
+    grok.template('datacenterimport3page')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Process CSV file')
+    pnav = 0
+    cancel_button =_(u'Cancel')
+    reset_button =_(u'Reset')
+    update_button =_(u'Set headerfields')
+    back_button =_(u'Back to step 2')
+    proceed_button =_(u'Perform import')
+
+    filename = None
+    mode = None
+    importername = None
+
+    @property
+    def nextstep(self):
+        return self.url(self.context, '@@import4')
+
+    def getPreviewHeader(self):
+        """Get the header fields of attached CSV file.
+        """
+        reader = csv.reader(open(self.fullpath, 'rb'))
+        return reader.next()
+
+    def getPreviewTable(self):
+        """Get transposed table with 1 sample record.
+
+        The first column contains the headers.
+        """
+        if not self.reader:
+            return
+        headers = self.getPreviewHeader()
+        num = 0
+        data = []
+        for line in self.reader:
+            if num > 0:
+                break
+            num += 1
+            data.append(line)
+        result = []
+        field_num = 0
+        for name in headers:
+            result_line = []
+            result_line.append(field_num)
+            field_num += 1
+            for d in data:
+                result_line.append(d[name])
+            result.append(result_line)
+        return result
+
+    def getPossibleHeaders(self):
+        """Get the possible headers.
+
+        The headers are described as dicts {value:internal_name,
+        title:displayed_name}
+        """
+        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
+        headers = self.importer.getHeaders()
+        result.extend([dict(title=x, value=x) for x in headers])
+        return result
+
+    def getWarnings(self):
+        import sys
+        result = []
+        try:
+            self.importer.checkHeaders(self.headerfields, mode=self.mode)
+        except:
+            fatal = '%s' % sys.exc_info()[1]
+            result.append(fatal)
+        if result:
+            warnings = ""
+            for line in result:
+                warnings += line + '<br />'
+            warnings += _('Edit headers or replace imported file!')
+            return warnings
+        return False
+
+    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
+        session = ISession(self.request)['waeup.ikoba']
+        self.filename = session.get('import_filename', None)
+        self.mode = session.get('import_mode', None)
+        self.importername = session.get('import_importer', None)
+
+        if None in (self.filename, self.mode, self.importername):
+            self.redirect(self.url(self.context, '@@import2'))
+            return
+        if back2 is not None:
+            self.redirect(self.url(self.context ,'@@import2'))
+            return
+        if cancel is not None:
+            self.flash(_('Import aborted.'), type='warning')
+            self.redirect(self.url(self.context))
+            return
+
+        self.fullpath = os.path.join(self.context.storage, self.filename)
+        self.headerfields = headerfield or self.getPreviewHeader()
+        session['import_headerfields'] = self.headerfields
+
+        if proceed is not None:
+            self.redirect(self.url(self.context, '@@import4'))
+            return
+        self.importer = getUtility(IBatchProcessor, name=self.importername)
+        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
+        warnings = self.getWarnings()
+        if warnings:
+            self.flash(warnings, type='warning')
+
+class DatacenterImportStep4(IkobaPage):
+    """Manual import step 4: do actual import
+    """
+    grok.context(IDataCenter)
+    grok.name('import4')
+    grok.template('datacenterimport4page')
+    grok.require('waeup.importData')
+    label = _(u'Process CSV file')
+    pnav = 0
+    back_button =_(u'Process next')
+
+    filename = None
+    mode = None
+    importername = None
+    headerfields = None
+    warnnum = None
+
+    def update(self, back=None, finish=None, showlog=None):
+        if finish is not None:
+            self.redirect(self.url(self.context, '@@import1'))
+            return
+        session = ISession(self.request)['waeup.ikoba']
+        self.filename = session.get('import_filename', None)
+        self.mode = session.get('import_mode', None)
+        self.importername = session.get('import_importer', None)
+        # If the import file contains only one column
+        # the import_headerfields attribute is a string.
+        ihf = session.get('import_headerfields', None)
+        if not isinstance(ihf, list):
+            self.headerfields = ihf.split()
+        else:
+            self.headerfields = ihf
+
+        if None in (self.filename, self.mode, self.importername,
+                    self.headerfields):
+            self.redirect(self.url(self.context, '@@import3'))
+            return
+
+        if showlog is not None:
+            logfilename = "datacenter.log"
+            session['logname'] = logfilename
+            self.redirect(self.url(self.context, '@@show'))
+            return
+
+        self.fullpath = os.path.join(self.context.storage, self.filename)
+        self.importer = getUtility(IBatchProcessor, name=self.importername)
+
+        # Perform batch processing...
+        # XXX: This might be better placed in datacenter module.
+        (linenum, self.warn_num,
+         fin_path, pending_path) = self.importer.doImport(
+            self.fullpath, self.headerfields, self.mode,
+            self.request.principal.id, logger=self.context.logger)
+        # Put result files in desired locations...
+        self.context.distProcessedFiles(
+            self.warn_num == 0, self.fullpath, fin_path, pending_path,
+            self.mode)
+
+        if self.warn_num:
+            self.flash(_('Processing of ${a} rows failed.',
+                mapping = {'a':self.warn_num}), type='warning')
+        self.flash(_('Successfully processed ${a} rows.',
+            mapping = {'a':linenum - self.warn_num}))
+
+class DatacenterLogsOverview(IkobaPage):
+    grok.context(IDataCenter)
+    grok.name('logs')
+    grok.template('datacenterlogspage')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Show logfiles')
+    pnav = 0
+    back_button = _(u'Back to Data Center')
+    show_button = _(u'Show')
+
+    def update(self, back=None):
+        if back is not None:
+            self.redirect(self.url(self.context))
+            return
+        self.files = self.context.getLogFiles()
+
+class DatacenterLogsFileview(IkobaPage):
+    grok.context(IDataCenter)
+    grok.name('show')
+    grok.template('datacenterlogsshowfilepage')
+    grok.require('waeup.manageDataCenter')
+    title = _(u'Data Center')
+    pnav = 0
+    search_button = _('Search')
+    back_button = _('Back to Data Center')
+    placeholder = _('Enter a regular expression here...')
+
+    def label(self):
+        return "Logfile %s" % self.filename
+
+    def update(self, back=None, query=None, logname=None):
+        if os.name != 'posix':
+            self.flash(
+                _('Log files can only be searched ' +
+                  'on Unix-based operating systems.'), type='danger')
+            self.redirect(self.url(self.context, '@@logs'))
+            return
+        if back is not None or logname is None:
+            self.redirect(self.url(self.context, '@@logs'))
+            return
+        self.filename = logname
+        self.query = query
+        if not query:
+            return
+        try:
+            self.result = ''.join(
+                self.context.queryLogfiles(logname, query))
+        except ValueError:
+            self.flash(_('Invalid search expression.'), type='danger')
+            return
+        if not self.result:
+            self.flash(_('No search results found.'), type='warning')
+        return
+
+class DatacenterSettings(IkobaPage):
+    grok.context(IDataCenter)
+    grok.name('manage')
+    grok.template('datacentermanagepage')
+    grok.require('waeup.managePortal')
+    label = _('Edit data center settings')
+    pnav = 0
+    save_button =_(u'Save')
+    reset_button =_(u'Reset')
+    cancel_button =_(u'Back to Data Center')
+
+    def update(self, newpath=None, move=False, overwrite=False,
+               save=None, cancel=None):
+        if move:
+            move = True
+        if overwrite:
+            overwrite = True
+        if newpath is None:
+            return
+        if cancel is not None:
+            self.redirect(self.url(self.context))
+            return
+        try:
+            not_copied = self.context.setStoragePath(newpath, move=move)
+            for name in not_copied:
+                self.flash(_('File already existed (not copied): ${a}',
+                    mapping = {'a':name}), type='danger')
+        except:
+            self.flash(_('Given storage path cannot be used: ${a}',
+                        mapping = {'a':sys.exc_info()[1]}), type='danger')
+            return
+        if newpath:
+            self.flash(_('New storage path succefully set.'))
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            self.context.logger.info(
+                '%s - storage path set: %s' % (ob_class, newpath))
+            self.redirect(self.url(self.context))
+        return
+
+class ExportCSVPage(IkobaPage):
+    grok.context(IDataCenter)
+    grok.name('export')
+    grok.template('datacenterexportpage')
+    grok.require('waeup.exportData')
+    label = _('Download portal data as CSV file')
+    pnav = 0
+    export_button = _(u'Create CSV file')
+    cancel_button =_(u'Back to Data Center')
+
+    def getExporters(self):
+        exporter_utils = getUtilitiesFor(ICSVExporter)
+        ikoba_utils = getUtility(IIkobaUtils)
+        title_name_tuples = [
+            (util.title, name) for name, util in exporter_utils
+            if name in ikoba_utils.EXPORTER_NAMES]
+        return sorted(title_name_tuples)
+
+    def update(self, CREATE=None, DISCARD=None, exporter=None,
+               job_id=None, CANCEL=None):
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        if CREATE:
+            job_id = self.context.start_export_job(
+                exporter, self.request.principal.id)
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            self.context.logger.info(
+                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
+        if DISCARD and job_id:
+            entry = self.context.entry_from_job_id(job_id)
+            self.context.delete_export_entry(entry)
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            self.context.logger.info(
+                '%s - discarded: job_id=%s' % (ob_class, job_id))
+            self.flash(_('Discarded export') + ' %s' % job_id)
+        self.entries = doll_up(self, user=None)
+        return
+
+class ExportCSVView(grok.View):
+    grok.context(IDataCenter)
+    grok.name('download_export')
+    grok.require('waeup.exportData')
+
+    def render(self, job_id=None):
+        manager = getUtility(IJobManager)
+        job = manager.get(job_id)
+        if job is None:
+            return
+        if hasattr(job.result, 'traceback'):
+            # XXX: Some error happened. Do something more approriate here...
+            return
+        path = job.result
+        if not os.path.exists(path):
+            # XXX: Do something more appropriate here...
+            return
+        result = open(path, 'rb').read()
+        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
+        filename = "%s_%s" % (acronym, os.path.basename(path))
+        filename = filename.replace('.csv', '_%s.csv' % job_id)
+        self.response.setHeader(
+            'Content-Type', 'text/csv; charset=UTF-8')
+        self.response.setHeader(
+            'Content-Disposition', 'attachment; filename="%s' % filename)
+        # remove job and running_exports entry from context
+        #self.context.delete_export_entry(
+        #    self.context.entry_from_job_id(job_id))
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        self.context.logger.info(
+            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
+        return result
+
+class ChangePasswordRequestPage(IkobaForm):
+    """Captcha'd page for all kind of users to request a password change.
+    """
+    grok.context(ICompany)
+    grok.name('changepw')
+    grok.require('waeup.Anonymous')
+    grok.template('changepw')
+    label = _('Send me a new password')
+    form_fields = grok.AutoFields(IChangePassword)
+
+    def update(self):
+        # Handle captcha
+        self.captcha = getUtility(ICaptchaManager).getCaptcha()
+        self.captcha_result = self.captcha.verify(self.request)
+        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
+        return
+
+    def _searchUser(self, email):
+        # Search customer
+        cat = queryUtility(ICatalog, name='customers_catalog')
+        results = list(cat.searchResults(email=(email,email)))
+        if len(results) == 1:
+            return results[0]
+        # Search portal user
+        users = grok.getSite()['users'].values()
+        results = []
+        for user in users:
+            if user.email == email:
+                results.append(user)
+            if len(results) == 1:
+                return results[0]
+        return None
+
+    @action(_('Send login credentials to email address'), style='primary')
+    def request(self, **data):
+        if not self.captcha_result.is_valid:
+            # Captcha will display error messages automatically.
+            # No need to flash something.
+            return
+        # Search customer
+        email = data['email']
+        user = self._searchUser(email)
+        if user is None:
+            self.flash(_('No record found.'), type='warning')
+            return
+        # Change password
+        ikoba_utils = getUtility(IIkobaUtils)
+        password = ikoba_utils.genPassword()
+        mandate = PasswordMandate()
+        mandate.params['password'] = password
+        mandate.params['user'] = user
+        site = grok.getSite()
+        site['mandates'].addMandate(mandate)
+        # Send email with credentials
+        args = {'mandate_id':mandate.mandate_id}
+        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
+        url_info = u'Confirmation link: %s' % mandate_url
+        msg = _('You have successfully requested a password for the')
+        success = ikoba_utils.sendCredentials(
+            IUserAccount(user),password,url_info,msg)
+        if success:
+            self.flash(_('An email with your user name and password ' +
+                'has been sent to ${a}.', mapping = {'a':email}))
+        else:
+            self.flash(_('An smtp server error occurred.'), type='danger')
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        self.context.logger.info(
+            '%s - %s - %s' % (ob_class, IUserAccount(user).name, data['email']))
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/pdf.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/pdf.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/pdf.py	(revision 12677)
@@ -0,0 +1,820 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Reusable components for pdf generation.
+"""
+import grok
+import os
+import pytz
+from cStringIO import StringIO
+from datetime import datetime
+from reportlab.graphics.barcode.qr import QrCodeWidget
+from reportlab.graphics.shapes import Drawing
+from reportlab.lib import colors
+from reportlab.lib.units import cm, inch, mm
+from reportlab.lib.pagesizes import A4, landscape, portrait
+from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
+from reportlab.pdfgen.canvas import Canvas
+from reportlab.platypus import (
+    SimpleDocTemplate, Spacer, Paragraph, Image, Table)
+from zope.formlib.form import setUpEditWidgets
+from zope.i18n import translate
+from zope.publisher.browser import TestRequest
+from zope.component import getUtility, queryUtility
+from waeup.ikoba.browser.interfaces import IPDFCreator
+from waeup.ikoba.utils.helpers import now
+from waeup.ikoba.interfaces import IIkobaUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+
+
+#: A reportlab paragraph style for 'normal' output.
+NORMAL_STYLE = getSampleStyleSheet()['Normal']
+
+#: A reportlab paragraph style for 'normal' output.
+HEADING3_STYLE = getSampleStyleSheet()['Heading3']
+
+#: A reportlab paragraph style for headings.
+HEADING_STYLE = ParagraphStyle(
+    name='Heading3',
+    parent=HEADING3_STYLE,
+    fontSize=11,
+    )
+
+#: A reportlab paragraph style for output of 'code'.
+CODE_STYLE = ParagraphStyle(
+    name='Code',
+    parent=NORMAL_STYLE,
+    fontName='Courier',
+    fontSize=10,
+    leading=10,
+    )
+
+#: A reportlab paragraph style for regular form output.
+ENTRY1_STYLE = ParagraphStyle(
+    name='Entry1',
+    parent=NORMAL_STYLE,
+    fontSize=11,
+    leading=10,
+    )
+
+#: A reportlab paragraph style for smaller form output.
+SMALL_PARA_STYLE = ParagraphStyle(
+    name='Small1',
+    parent=NORMAL_STYLE,
+    fontSize=8,
+    )
+
+#: A reportlab paragraph style for headlines or bold text in form output.
+HEADLINE1_STYLE = ParagraphStyle(
+    name='Header1',
+    parent=NORMAL_STYLE,
+    fontName='Helvetica-Bold',
+    fontSize=10,
+    )
+
+#: A reportlab paragraph style for notes output at end of documents.
+NOTE_STYLE = ParagraphStyle(
+    name='Note',
+    parent=NORMAL_STYLE,
+    fontName='Helvetica',
+    fontSize=10,
+    leading=9,
+    )
+
+#: Base style for signature tables
+SIGNATURE_TABLE_STYLE = [
+    ('VALIGN',(0,-1),(-1,-1),'TOP'),
+    #('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
+    ('BOTTOMPADDING', (0,0), (-1,0), 36),
+    ('TOPPADDING', (0,-1), (-1,-1), 0),
+    ]
+
+
+def format_html(html):
+    """Make HTML code usable for use in reportlab paragraphs.
+
+    Main things fixed here:
+    If html code:
+    - remove newlines (not visible in HTML but visible in PDF)
+    - add <br> tags after <div> (as divs break lines in HTML but not in PDF)
+    If not html code:
+    - just replace newlines by <br> tags
+    """
+    if '</' in html:
+        # Add br tag if widgets contain div tags
+        # which are not supported by reportlab
+        html = html.replace('</div>', '</div><br />')
+        html = html.replace('\n', '')
+    else:
+        html = html.replace('\n', '<br />')
+    return html
+
+def normalize_signature(signature_tuple):
+    """Normalize a signature tuple.
+
+    Returns a tuple ``(<PRE-TEXT>, <SIGNATURE>, <POST-TEXT>)`` from
+    input tuple. The following rules apply::
+
+      (pre, sig, post)  --> (pre, sig, post)
+      (pre, sig)        --> (pre, sig, None)
+      (sig)             --> (None, sig, None)
+
+    Also simple strings are accepted as input::
+
+      sig               --> (None, sig, None)
+
+    If input is not a tuple nor a basestring or if the tuple contains
+    an invalid number of elements, ``ValueError`` is raised.
+    """
+    if isinstance(signature_tuple, basestring):
+        return (None, signature_tuple, None)
+    if not isinstance(signature_tuple, tuple):
+        raise ValueError("signature_tuple must be a string or tuple")
+    if len(signature_tuple) < 1 or len(signature_tuple) > 3:
+        raise ValueError("signature_tuple must have 1, 2, or 3 elements")
+    elif len(signature_tuple) == 1:
+        signature_tuple = (None, signature_tuple[0], None)
+    elif len(signature_tuple) == 2:
+        signature_tuple = (signature_tuple[0], signature_tuple[1], None)
+    return signature_tuple
+
+def vert_signature_cell(signature, date_field=True, date_text=_('Date:'),
+                        start_row=0, start_col=0, underline=True):
+    """Generate a table part containing a vertical signature cell.
+
+    Returns the table data as list of lists and an according style.
+
+    `signature`:
+       a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
+
+    `date_field`:
+       boolean indicating that a 'Date:' text should be rendered into this
+       signature cell (or not).
+
+    `date_text`:
+       the text to be rendered into the signature field as 'Date:' text.
+
+    `start_row`:
+       starting row of the signature cell inside a broader table.
+
+    `start_col`:
+       starting column of the signature cell inside a broader table.
+
+    `underline`:
+       boolean indicating that the signature cell should provide a line on
+       top (`True` by default).
+
+    Vertical signature cells look like this::
+
+      +------------+
+      |Pre         |
+      +------------+
+      |Date:       |
+      |            |
+      +------------+
+      | ---------- |
+      | Signature  |
+      +------------+
+      |Post        |
+      +------------+
+    """
+    # split signature parts, replacing None with empty string
+    pre, sig, post = [x or '' for x in signature]
+    style = ()
+    x, y = start_col, start_row+2
+    if underline:
+        style += (('LINEABOVE', (x, y), (x, y), 1, colors.black),)
+    d_text = date_field and date_text or ''
+    data = [[pre], [d_text], [sig], [post]]
+    col_widths = [1.0]
+    return data, style, col_widths
+
+def horiz_signature_cell(signature, date_field=True, date_text=_('Date'),
+                         start_row=0, start_col=0):
+    """Generate a table part containing an horizontal signature cell
+
+    Returns the table data as list of lists and an according style.
+
+    `signature`:
+       a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
+
+    `date_field`:
+       boolean indicating that a 'Date:' text should be rendered into this
+       signature cell (or not).
+
+    `date_text`:
+       the text to be rendered into the signature field as 'Date:' text.
+
+    `start_row`:
+       starting row of the signature cell inside a broader table.
+
+    `start_col`:
+       starting column of the signature cell inside a broader table.
+
+    Horizontal signature cells look like this::
+
+      +------------+---+-----------+
+      |Pre text possibly filling   |
+      |the whole box               |
+      +------------+---+-----------+
+      |            |   |           |
+      |            |   |           |
+      +------------+---+-----------+
+      | ---------- |   | --------- |
+      | Date       |   | Signature |
+      +------------+---+-----------+
+      |Post                        |
+      +------------+---+-----------+
+
+    """
+    pre, sig, post = signature
+    if not date_field:
+        data, style, cols = vert_signature_cell(signature, date_field=False)
+        return data, style, cols
+    style = (
+        # let pre and post text span the whole signature cell
+        ('SPAN', (start_col, start_row), (start_col+2, start_row)),
+        ('SPAN', (start_col, start_row+3), (start_col+2, start_row+3)),
+        )
+    # horizontal cells are buildt from vertical ones chained together
+    cell1 = vert_signature_cell(  # leftmost date col
+        (pre, date_text, post), date_field=False,
+        start_row=start_row, start_col=start_col)
+    cell2 = vert_signature_cell(  # spacer col (between date and sig)
+        ('', '', ''), date_field=False, underline=False,
+        start_row=start_row, start_col=start_col+1)
+    cell3 = vert_signature_cell(  # rightmost signature column
+        ('', sig, ''), date_field=False,
+        start_row=start_row, start_col=start_col+2)
+    data = map(lambda x, y, z: x+y+z, cell1[0], cell2[0], cell3[0])
+    style = style + cell1[1] + cell2[1] + cell3[1]
+    col_widths  = [0.3, 0.03, 0.67] # sums up to 1.0
+    return data, style, col_widths
+
+def signature_row(signatures, start_row=0, horizontal=None, max_per_row=3):
+    data = [[], [], [], []]
+    style = ()
+    signatures = [normalize_signature(sig) for sig in signatures]
+    start_col = 0
+    col_widths = []
+
+    if horizontal is None:
+        horizontal = len(signatures) == 1
+    cell_maker = vert_signature_cell
+    if horizontal:
+        cell_maker = horiz_signature_cell
+    main_cell_height = not horizontal and 36 or 18
+
+    for sig in signatures:
+        sig_data, sig_style, sig_cols = cell_maker(
+            sig, start_row=start_row, start_col=start_col)
+        data = map(lambda x, y: x+y, data, sig_data)
+        style += sig_style
+        col_widths += sig_cols + [None,]
+
+        start_col += 1
+        # add spacer
+        spacer, spacer_style, cols = vert_signature_cell(
+            ('', '', ''), date_field=False, underline=False,
+            start_row=start_row, start_col=start_col)
+        data = map(lambda x, y: x+y, data, spacer)
+        style += spacer_style
+        start_col += 1
+
+    y = start_row
+    sig_row = start_row + 2
+    style = style + (
+        ('TOPPADDING', (0, y+2), (-1, y+2), 0), # signature row
+        ('BOTTOMPADDING', (0, y+1), (-1, y+1), main_cell_height),
+        ('LEFTPADDING', (0, y), (-1, y), 1), # pre row
+        ('LEFTPADDING', (0, y+3), (-1, y+3), 1), # post row
+        )
+
+    if len(signatures) == 1:
+        # pre and post text should span whole table
+        style += (('SPAN', (0, y), (-1, y)),
+                  ('SPAN', (0, y+3), (-1, y+3)),
+                  )
+
+    if data[0] == [''] * len(data[0]):
+        # no pre text: hide pre row by minimizing padding
+        style += (('TOPPADDING', (0,y), (-1, y), -6),
+                  ('BOTTOMPADDING', (0,y), (-1, y), -6),
+                  )
+    if data[-1] == [''] * len(data[0]):
+        # no post text: hide post row by minimizing padding
+        style += (('TOPPADDING', (0,y+3), (-1, y+3), -6),
+                  ('BOTTOMPADDING', (0,y+3), (-1, y+3), -6),
+                  )
+
+    if len(signatures) > 1:
+        data = [x[:-1] for x in data] # strip last spacer
+        col_widths = col_widths[:-1]
+    return data, style, col_widths
+
+def sig_table(signatures, lang='en', max_per_row=3, horizontal=None,
+              single_table=False, start_row=0, landscape=False):
+    if landscape:
+        space_width = 2.4  # width in cm of space between signatures
+        table_width = 24.0 # supposed width of signature table in cms
+    else:
+        space_width = 0.4  # width in cm of space between signatures
+        table_width = 16.0 # supposed width of signature table in cms
+    # width of signature cells in cm...
+    sig_num = len(signatures)
+    sig_col_width = (table_width - ((sig_num - 1) * space_width)) / sig_num
+    if sig_num == 1:
+        sig_col_width = 0.6 * table_width         # signature cell part
+        space_width = table_width - sig_col_width # spacer part on the right
+
+    if sig_num > max_per_row:
+        if horizontal is None:
+            horizontal = max_per_row == 1
+        sigs_by_row = [signatures[x:x+max_per_row] for x in range(
+            0, sig_num, max_per_row)]
+        result = []
+        curr_row = 0
+        for num, row_sigs in enumerate(sigs_by_row):
+            curr_row = 0
+            if single_table:
+                curr_row = num * 4
+            result.append(
+                sig_table(row_sigs, lang=lang, max_per_row=max_per_row,
+                          horizontal=horizontal, start_row=curr_row,
+                          landscape=landscape)[0],
+                          )
+        missing_num = len(result[-2][0][0]) - len(result[-1][0][0])
+        if missing_num:
+            # last row contained less cells, fix it...
+            result[-1] = ([x + [''] * missing_num for x in result[-1][0]],
+                          result[-1][1], result[-2][2])
+        return result
+
+    data, style, cols = signature_row(signatures, horizontal=horizontal,
+                                      start_row=start_row)
+    style += (('VALIGN', (0,0), (-1,-1), 'TOP'),)
+
+    # compute col widths...
+    col_widths = []
+    for col in cols:
+        if col is not None:
+            col = col * sig_col_width * cm
+        else:
+            col = space_width * cm
+        col_widths.append(col)
+
+    # replace strings by paragraphs and translate all contents
+    for rnum, row in enumerate(data):
+        for cnum, cell in enumerate(row):
+            if cell:
+                content = translate(cell, lang)
+                data[rnum][cnum] = Paragraph(content, NORMAL_STYLE)
+    return [(data, style, col_widths),]
+
+def get_sig_tables(signatures, lang='en', max_per_row=3, horizontal=None,
+                   single_table=False, landscape=False):
+    rows = sig_table(signatures, lang=lang, max_per_row=max_per_row,
+                     horizontal=horizontal, single_table=single_table,
+                     landscape=landscape)
+    if single_table:
+        result_data = []
+        result_style = ()
+        for row in rows:
+            data, style, col_widths = row
+            result_data += data
+            result_style += style
+        return [(result_data, result_style, col_widths),]
+    return rows
+
+def get_signature_tables(signatures, lang='en', max_per_row=3,
+                         horizontal=None, single_table=False,
+                         landscape=False):
+    """Get a list of reportlab flowables representing signature fields.
+
+    `signatures` is a list of signatures. Each signature can be a
+    simple string or a tuple of format::
+
+      (<PRE-TEXT>, <SIGNATURE>, <POST-TEXT>)
+
+    where ``<PRE-TEXT>`` and ``<POST-TEXT>`` are texts that should
+    appear on top (PRE) or below (POST) the signature cell. Both
+    formats, string and tuple, can be mixed. A single signature would
+    be given as ``[('Pre-Text', 'Signature', 'Post-Text'),]`` or
+    simply as ``['Signature']`` if not pre or post-text is wanted.
+
+    All texts (pre, sig, post) are rendered as paragraphs, so you can
+    pass in also longer texts with basic HTML formatting like ``<b>``,
+    ``<i>``, ``<br />``, etc.
+
+    ``lang`` sets the language to use in I18n context. All texts are
+    translated to the given language (``en`` by default) if a
+    translation is available.
+
+    ``max_per_row`` gives the maximum number of signatures to put into
+    a single row. The default is 3. If more signatures are passed in,
+    these signatures are put into a new row. So, for example by
+    default 8 signatures would be spread over 3 rows.
+
+    ``horizontal`` tells how the single signature cells should be
+    rendered: horizontal or vertical. While horizontal cells render
+    date and signature fields side by side, in vertical cells date is
+    rendered on top of the signature.
+
+    This parameter accepts *three* different values: ``True``,
+    ``False``, or ``None``. While with ``True`` each cell is rendered
+    in horizontal mode, ``False`` will create only vertical cells.
+
+    The ``None`` value (set by default) is different: if set, the mode
+    will be dependent on the number of signatures per row. If a row
+    contains exactly one signature (because only one sig was passed
+    in, or because ``max_per_row`` was set to ``1``), then this
+    signature is rendered in horizontal mode. Otherwise (with more
+    than one sig per row) each cell is rendered in vertical mode. This
+    pseudo-smart behaviour can be switched off by setting
+    ``horizontal`` explicitly to ``True`` or ``False``.
+
+    ``single_table`` is a boolean defaulting to ``False``. By default
+    we return the rows of a signature table in several tables, one of
+    each row. This makes it easier for reportlab to perform pagebreaks
+    in case the page is already full, without wasting space. If the
+    parameter is set to ``True``, then always a list with exactly one
+    table is returned, which will contain all rows in one table.
+
+    Generally, if a row contains only one signature, only a part of
+    the page width is used to render this signature. If two or more
+    signatures are passed in, the complete page width will be filled
+    and the single signature cells will be shrinked to fit.
+    """
+    data_list = get_sig_tables(
+        signatures, lang=lang, max_per_row=max_per_row,
+        horizontal=horizontal, single_table=single_table, landscape=landscape)
+    return [Table(row_data, style=row_style, colWidths=row_col_widths,
+                  repeatRows=4)
+            for row_data, row_style, row_col_widths in data_list]
+
+def format_signatures(signatures, max_per_row=3, lang='en',
+                      single_table=False,
+                      date_field=True, date_text=_('Date'),
+                      base_style=SIGNATURE_TABLE_STYLE):
+    result = []
+    signature_tuples = [normalize_signature(sig) for sig in signatures]
+    for pre, sig, post in signature_tuples:
+        row = []
+        if pre is not None:
+            row.append([
+                translate(pre, lang), '', '', ''])
+        row.append([
+            translate(_('Date'), lang), '',
+            translate(sig, lang), ''
+            ])
+        if post is not None:
+            row.append([
+                translate(post, lang), '', '', ''])
+        result.append((row, base_style))
+    return result
+
+
+
+class NumberedCanvas(Canvas):
+    """A reportlab canvas for numbering pages after all docs are processed.
+
+    Taken from
+    http://code.activestate.com/recipes/546511-page-x-of-y-with-reportlab/
+    http://code.activestate.com/recipes/576832/
+    """
+
+    def __init__(self, *args, **kw):
+        Canvas.__init__(self, *args, **kw)
+        self._saved_page_states = []
+        return
+
+    def showPage(self):
+        self._saved_page_states.append(dict(self.__dict__))
+        self._startPage()
+        return
+
+    def save(self):
+        """add page info to each page (page x of y)"""
+        num_pages = len(self._saved_page_states)
+        for state in self._saved_page_states:
+            self.__dict__.update(state)
+            self.draw_page_number(num_pages)
+            Canvas.showPage(self)
+        Canvas.save(self)
+        return
+
+    def draw_page_number(self, page_count):
+        """draw string at bottom right with 'page x of y'.
+
+        Location of the string is determined by canvas attributes
+        `ikoba_footer_x_pos` and `ikoba_footer_y_pos` that have to be
+        set manually.
+
+        If this canvas also provides an attribute `ikoba_footer_text`,
+        the contained text is rendered left of the ``page x of y``
+        string.
+        """
+        self.setFont("Helvetica", 9)
+        right_footer_text = _(
+            '${footer_text} Page ${num1} of ${num2}',
+            mapping = {'footer_text': self.ikoba_footer_text,
+                       'num1':self._pageNumber, 'num2':page_count})
+        self.drawRightString(
+            self.ikoba_footer_x_pos, self.ikoba_footer_y_pos,
+             translate(right_footer_text))
+        return
+
+class PDFCreator(grok.GlobalUtility):
+    """A utility to help with generating PDF docs.
+    """
+    grok.implements(IPDFCreator)
+
+    watermark_path = None
+    header_logo_path = None
+    header_logo_left_path = None
+    watermark_pos = [0, 0]
+    logo_pos = [0, 0, 0]
+    logo_left_pos = [0, 0, 0]
+    pagesize = portrait(A4)
+
+    @classmethod
+    def _setUpWidgets(cls, form_fields, context):
+        """Setup simple display widgets.
+
+        Returns the list of widgets.
+        """
+        request = TestRequest()
+        return setUpEditWidgets(
+            form_fields, 'form', context, request, {},
+            for_display=True, ignore_request=True
+            )
+
+    @classmethod
+    def _drawSignatureBoxes(cls, canvas, width, height, signatures=[]):
+        """Draw signature boxes into canvas.
+        """
+        canvas.saveState()
+        canvas.setFont("Helvetica", 10)
+        mystring = "%r" % ([translate(sig) for sig in signatures])
+        for num, sig in enumerate(signatures):
+            box_width = (width - 4.2*cm) / len(signatures)
+            x_box = 2.1*cm + (box_width) * num
+            y_box = 0.75*inch
+            canvas.rect(x_box+0.1*cm, y_box, box_width-0.2*cm, 0.75*inch)
+            canvas.drawString(
+                x_box+0.2*cm, 1.35*inch, translate(sig))
+        canvas.restoreState()
+        return canvas
+
+    @classmethod
+    def fromStringList(cls, string_list):
+        """Generate a list of reportlab paragraphs out of a list of strings.
+
+        Strings are formatted with :data:`CODE_STYLE` and a spacer is
+        appended at end.
+        """
+        result = []
+        for msg in string_list:
+            result.append(Paragraph(msg, CODE_STYLE))
+        return result
+
+    @classmethod
+    def getImage(cls, image_path, orientation='LEFT'):
+        """Get an image located at `image_path` as reportlab flowable.
+        """
+        img = Image(image_path, width=4*cm, height=3*cm, kind='bound')
+        img.hAlign=orientation
+        return img
+
+    def _getWidgetsTableData(self, widgets, separators, domain,
+                             lang, twoDataCols):
+        row_num = 0
+        table_data = []
+        for widget in widgets:
+            if separators and separators.get(widget.name):
+                f_headline = translate(
+                    separators[widget.name], domain, target_language=lang)
+                f_headline = Paragraph(f_headline, HEADLINE1_STYLE)
+                table_data.append([f_headline])
+                row_num += 1
+            f_label = translate(widget.label.strip(), domain,
+                                target_language=lang)
+            f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
+            f_text = translate(widget(), domain, target_language=lang)
+            f_text = format_html(f_text)
+            #if f_text:
+            #    hint = ' <font size=9>' + widget.hint + '</font>'
+            #    f_text = f_text + hint
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            table_data.append([f_label,f_text])
+            row_num += 1
+        return table_data, row_num
+
+    def getWidgetsTable(self, form_fields, context, view, lang='en',
+                        domain='waeup.ikoba', separators=None,
+                        colWidths=None, twoDataCols=False):
+        """Return a reportlab `Table` instance, created from widgets
+        determined by `form_fields` and `context`.
+
+        - `form_fields`
+           is a list of schema fields as created by grok.AutoFields.
+        - `context`
+           is some object whose content is rendered here.
+        - `view`
+           is currently not used but supposed to be a view which is
+           actually rendering a PDF document.
+        - `lang`
+           the portal language. Used for translations of strings.
+        - `domain`
+           the translation domain used for translations of strings.
+        - `separators`
+           a list of separators.
+        - `colWidths`
+           defines the the column widths of the data in the right column
+           of base data (right to the passport image).
+        - `twoDataCols`
+           renders data widgets in a parent table with two columns.
+        """
+        table_style = [('VALIGN', (0,0), (-1,-1), 'TOP'),
+                       ]
+        widgets = self._setUpWidgets(form_fields, context)
+
+        # Determine table data
+        table_data, row_num = self._getWidgetsTableData(
+            widgets, separators, domain, lang, twoDataCols)
+
+        # render two-column table of tables if applicable
+        lines = len(table_data)
+        middle = lines/2
+        if twoDataCols is True and lines > 2:
+            table_left = Table(table_data[:middle],
+                               style=table_style, colWidths=[3*cm, 6.3*cm])
+            table_right = Table(table_data[middle:],
+                                style=table_style, colWidths=[3*cm, 6.3*cm])
+            table_style.append(('LEFTPADDING', (0,0), (0,-1), 1.2*cm),)
+            table = Table([[table_left, table_right],],style=table_style)
+            return table
+
+        # render single table
+        table = Table(
+            table_data,style=table_style, colWidths=colWidths) #, rowHeights=14)
+        table.hAlign = 'LEFT'
+        return table
+
+
+    def paint_background(self, canvas, doc):
+        """Paint background of a PDF, including watermark, title, etc.
+
+        The `doc` is expected to be some reportlab SimpleDocTemplate
+        or similar object.
+
+        Text of headerline is extracted from doc.ikoba_headtitle, the
+        document title (under the head) from doc.ikoba_title.
+
+        This is a callback method that will be called from reportlab
+        when creating PDFs with :meth:`create_pdf`.
+        """
+        canvas.saveState()
+        width, height = doc.width, doc.height
+        width += doc.leftMargin + doc.rightMargin
+        height += doc.topMargin + doc.bottomMargin
+
+        # Watermark
+        if self.watermark_path is not None:
+            canvas.saveState()
+            canvas.drawImage(self.watermark_path,
+                self.watermark_pos[0], self.watermark_pos[1])
+            canvas.restoreState()
+
+        # Header
+        site_config = None
+        site = grok.getSite()
+        if site is not None:
+            site_config = site.get('configuration', None)
+        head_title = getattr(
+            doc, 'ikoba_headtitle', getattr(
+                site_config, 'name',
+                u'Sample Company'))
+        canvas.setFont("Helvetica-Bold", 18)
+        if self.header_logo_left_path is not None:
+            canvas.drawCentredString(width/2.0, height-1.7*cm, head_title)
+        else:
+            canvas.drawString(1.5*cm, height-1.7*cm, head_title)
+        canvas.line(1.5*cm,height-1.9*cm,width-1.5*cm,height-1.9*cm)
+        if self.header_logo_path is not None:
+            canvas.drawImage(self.header_logo_path,
+                self.logo_pos[0], self.logo_pos[1], width=self.logo_pos[2],
+                preserveAspectRatio=True, anchor='ne')
+        if self.header_logo_left_path is not None:
+            canvas.drawImage(self.header_logo_left_path,
+                self.logo_left_pos[0], self.logo_left_pos[1],
+                width=self.logo_left_pos[2],
+                preserveAspectRatio=True, anchor='ne')
+
+        # Title
+        canvas.saveState()
+        canvas.setFont("Helvetica-Bold", 14)
+        title = getattr(doc, 'ikoba_title', '')
+        if '\n' in title:
+            title_lines = title.split('\n')
+            for num, line in enumerate(title_lines):
+                canvas.drawCentredString(
+                    width/2.0, height-2.8*cm-(num*0.7*cm), line)
+        elif title:
+            canvas.drawCentredString(width/2.0, height-2.8*cm, title)
+        canvas.restoreState()
+
+        # Footer
+        canvas.saveState()
+        if getattr(doc, 'sigs_in_footer', False):
+            self._drawSignatureBoxes(
+                canvas, width, height, doc.sigs_in_footer)
+        canvas.line(2.2*cm, 0.62*inch, width-2.2*cm, 0.62*inch)
+        canvas.setFont("Helvetica", 9)
+        if not getattr(doc, 'ikoba_nodate', False):
+            tz = getattr(queryUtility(IIkobaUtils), 'tzinfo', pytz.utc)
+            #tz = getUtility(IIkobaUtils).tzinfo
+            today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
+            canvas.drawString(2.2*cm, 0.5 * inch,
+                translate(_(u'Date: ${a}', mapping = {'a': today})))
+        # set canves attributes needed to render `page x of y`
+        canvas.ikoba_footer_x_pos = width-2.2*cm
+        canvas.ikoba_footer_y_pos = 0.5 * inch
+        canvas.ikoba_footer_text =  doc.ikoba_footer
+        canvas.restoreState()
+        canvas.restoreState()
+
+        # Metadata
+        canvas.setAuthor(getattr(doc, 'ikoba_author', 'Unknown'))
+        canvas.setSubject(title)
+        canvas.setCreator(u'WAeUP Ikoba')
+        return
+
+    def create_pdf(self, data, headerline=None, title=None, author=None,
+                   footer='', note=None, sigs_in_footer=[], topMargin=1.5):
+        """Returns a binary data stream which is a PDF document.
+        """
+        pdf_stream = StringIO()
+        bottomMargin = len(sigs_in_footer) and 1.9*inch or 1.2*inch
+        doc = SimpleDocTemplate(
+            pdf_stream,
+            bottomMargin=bottomMargin,
+            topMargin=topMargin*inch,
+            title=title,
+            pagesize=self.pagesize,
+            showBoundary=False,
+            )
+        # Set some attributes that are needed when rendering the background.
+        if headerline is not None:
+            doc.ikoba_headtitle = headerline
+        doc.ikoba_title = title
+        doc.ikoba_author = author
+        doc.ikoba_footer = footer
+        doc.sigs_in_footer = sigs_in_footer
+        if note is not None:
+            html = format_html(note)
+            data.append(Paragraph(html, NOTE_STYLE))
+        doc.build(data, onFirstPage=self.paint_background,
+                  onLaterPages=self.paint_background,
+                  canvasmaker=NumberedCanvas
+                  )
+        result = pdf_stream.getvalue()
+        pdf_stream.close()
+        return result
+
+class LandscapePDFCreator(PDFCreator):
+    """A utility to help with generating PDF docs in
+    landscape format.
+    """
+    grok.name('landscape')
+    pagesize = landscape(A4)
+
+def get_qrcode(text, width=60.0):
+    """Get a QR Code as Reportlab Flowable (actually a `Drawing`).
+
+    `width` gives box width in pixels (I think)
+    """
+    widget = QrCodeWidget(text)
+    bounds = widget.getBounds()
+    w_width = bounds[2] - bounds[0]
+    w_height = bounds[3] - bounds[1]
+    drawing = Drawing(
+        width, width,
+        transform=[width/w_width, 0, 0, width/w_height, 0, 0])
+    drawing.add(widget)
+    return drawing
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/reports.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/reports.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/reports.py	(revision 12677)
@@ -0,0 +1,132 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Browser components for report generation.
+"""
+import grok
+from zope.component import getUtility, queryUtility
+from zope.location.location import located
+from waeup.ikoba.interfaces import IJobManager, IIkobaUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.browser.layout import IkobaPage
+from waeup.ikoba.reports import IReportsContainer, IReportGenerator
+from waeup.ikoba.reports import get_generators
+
+grok.templatedir('templates')
+
+class ReportsContainerPage(IkobaPage):
+    """A view on a reports container.
+    """
+    grok.name('index')
+    grok.context(IReportsContainer)
+    grok.require('waeup.manageReports')
+    label = _('Reports')
+
+    def _report_url(self, job_id):
+        """Get the PDF download URL of a report.
+        """
+        return self.url(self.context, '%s/pdf' % job_id)
+
+    def update(self, job_id=None, DISCARD=None, DOWNLOAD=None):
+        self.entries = []
+        if job_id and DISCARD:
+            entry = self.context.report_entry_from_job_id(job_id)
+            self.context.delete_report_entry(entry)
+            self.flash('Report discarded: %s' % job_id)
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            grok.getSite().logger.info(
+                '%s - report %s discarded' % (ob_class, job_id))
+        self.entries = self._generate_entries(user_id=None)
+        if job_id and DOWNLOAD:
+            self.redirect(self._report_url(job_id))
+            return
+        return
+
+    def _generate_entries(self, user_id=None):
+        entries = []
+        for entry in self.context.get_running_report_jobs(user_id=user_id):
+            job_id, gen_name, user = entry
+            job = getUtility(IJobManager).get(job_id)
+            generator = queryUtility(IReportGenerator, name=gen_name)
+            gen_title = getattr(generator, 'title', 'Unknown')
+            args = ', '.join([str(x) for x in job.kwargs['kw'].values()])
+            if args:
+                descr = '%s (%s)' % (gen_title, args)
+            else:
+                descr = gen_title
+            status = job.finished and 'ready' or 'running'
+            status = job.failed and 'FAILED' or status
+            starttime = getattr(job, 'begin_after', None)
+            if starttime:
+                starttime = starttime.astimezone(
+                    getUtility(
+                        IIkobaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+            new_entry = (job_id, descr, status, job.finished, job.finished \
+                and not job.failed, not job.finished, starttime, user)
+            entries.append(new_entry)
+        return entries
+
+
+class ReportsContainerTraverser(grok.Traverser):
+    """A traverser for reports containers.
+    """
+    grok.context(IReportsContainer)
+    def traverse(self, name):
+        """Return a report generator or report if one is registered under
+        `name`.
+
+        Generators are registered by their utility names while reports
+        are looked up by their job id. So, `name` must be a report
+        generator name or a valid job_id of a report job.
+        """
+        generators = dict(list(get_generators()))
+        result = generators.get(name, None)
+        if result:
+            # give generator a location in URLs (make url() work)
+            return located(result, self.context, name)
+        result = self.context.report_entry_from_job_id(name)
+        if result:
+            manager = getUtility(IJobManager)
+            job = manager.get(name)
+            report = job.result
+            return located(report, self.context, name)
+        return None
+
+class ReportsContainerCreate(IkobaPage):
+    """Create a new report.
+    """
+    grok.name('create')
+    grok.context(IReportsContainer)
+    grok.require('waeup.manageReports')
+    label = _('Create report')
+
+    def update(self, START_GENERATOR=None, generator=None):
+        self.creators = self.get_creators()
+        self.generator_names = [x[1] for x in self.creators]
+        if START_GENERATOR and generator and generator in self.generator_names:
+            self.redirect(self.url(self.context, generator))
+        pass
+
+    def get_creators(self):
+        """Get all registered report generator names.
+
+        Returns a list of tuples (<TITLE>, <NAME>) with ``<TITLE>``
+        being a human readable description of the respective generator
+        and ``<NAME>`` being the registration name with the ZCA.
+        """
+        result = [(gen.title, name) for name, gen in get_generators()]
+        return result
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/actionbutton.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/actionbutton.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/actionbutton.pt	(revision 12677)
@@ -0,0 +1,7 @@
+<a class="btn btn-default" tal:condition="viewlet/target_url"
+   tal:attributes="href viewlet/target_url; onclick viewlet/onclick">
+  <img src="" alt="edit"
+   tal:attributes="src viewlet/icon_url;
+	   alt viewlet/alt"/>
+  <span tal:replace="viewlet/text">TEXT</span>
+</a>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/addaction.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/addaction.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/addaction.pt	(revision 12677)
@@ -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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/administrationpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/administrationpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/administrationpage.pt	(revision 12677)
@@ -0,0 +1,5 @@
+<p>
+  You can manage the following tasks:
+</p>
+
+<tal:admintasks tal:content="structure provider:admintasks" />
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/admintask.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/admintask.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/admintask.pt	(revision 12677)
@@ -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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/breadcrumbs.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/breadcrumbs.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/breadcrumbs.pt	(revision 12677)
@@ -0,0 +1,5 @@
+<li tal:repeat="item viewlet/getEntries">
+  <a href="#" tal:attributes="href item/url">
+    <span tal:replace="item/title">Entry</span>
+  </a>
+</li>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/changepw.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/changepw.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/changepw.pt	(revision 12677)
@@ -0,0 +1,24 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tr tal:condition="view/captcha_code">
+        <td i18n:translate="solve_captcha">
+          Please solve the captcha<br />to prevent misuse of this service:
+        </td>
+        <td>
+          <div tal:content="structure view/captcha_code"></div>
+        </td>
+      </tr>
+
+    </tbody>
+  </table>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/companypage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/companypage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/companypage.pt	(revision 12677)
@@ -0,0 +1,1 @@
+<span tal:replace="structure view/frontpage">Frontpage</span>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/companyrss20feed.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/companyrss20feed.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/companyrss20feed.pt	(revision 12677)
@@ -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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/contactform.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/contactform.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/contactform.pt	(revision 12677)
@@ -0,0 +1,32 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+
+  <tal:block repeat="widget view/widgets">
+
+    <div tal:condition="python: widget.name != 'form.body'"
+         tal:content="widget/label">Label
+    </div>
+    <br />
+    <div tal:content="structure widget">
+      Textbody
+    </div>
+    <tal:error tal:condition="widget/error">
+      <span tal:replace="structure widget/error">error</span>
+      <br />
+    </tal:error>
+    <br />
+  </tal:block>
+
+  <tal:captcha condition="view/captcha_code|nothing">
+    <p tal:content="structure view/captcha_code"></p>
+    <p i18n:translate="">
+      Please solve the captcha to prevent misuse of this service.
+    </p>
+  </tal:captcha>
+
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/customersitelayout.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/customersitelayout.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/customersitelayout.pt	(revision 12677)
@@ -0,0 +1,122 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" i18n:domain="waeup.ikoba">
+  <head>
+    <title i18n:translate="">
+      WAeUP.Ikoba - Registration and Application System
+    </title>
+    <!--base href="http://localhost:8080/app/@@page"
+     tal:attributes="href python: view.url(layout.site)" / -->
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+    <meta name="robots" content="index, follow" />
+    <link rel="stylesheet" media="only screen and (max-device-width: 480px)"
+        tal:attributes="href python: view.url(layout.site, '@@/waeup.ikoba.browser/mobile.css')"
+        type="text/css" />
+    <link rel="alternate" type="application/rss+xml" title="RSS"
+        href="http://www.example.net/feed.rss"
+        tal:attributes="title string:RSS feed of ${layout/getAppTitle};
+            href python: view.url(layout.site, 'feed.rss')" />
+  </head>
+
+  <!-- Diazo element-->
+  <body class="ikoba-body">
+
+    <!-- Diazo element-->
+    <a class="ikoba-brand-link" href="#"
+        tal:attributes="href python: view.url(layout.site)"
+        tal:content="layout/getAppAcronym">ACRONYM</a>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <ul class="nav ikoba-navbar-left">
+        <tal:primary_nav content="structure provider:primary_nav" />
+        <tal:primary_nav content="structure provider:primary_nav_customer" />
+    </ul>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <ul class="nav secondary-nav ikoba-navbar-right">
+      <li tal:condition="not: layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(layout.site, 'login')">
+          Login
+      </a>
+      </li>
+      <li tal:condition="layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(layout.site, '@@contactadmin')">
+          Contact
+      </a>
+      </li>
+      <li tal:condition="layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(context, '@@logout')">
+         Logout
+      </a>
+      </li>
+    </ul>
+    <!-- Diazo element-->
+
+    <span tal:define="langs provider:languages"
+          tal:condition="langs"
+          class="language">
+      <tal:languages tal:content="structure langs" /> |
+    </span>
+
+    <!-- Diazo element-->
+    <ul class="breadcrumb" tal:condition="layout/isAuthenticated">
+      <tal:breadcrumbs
+            tal:condition="python: layout.isAuthenticated()"
+            tal:content="structure provider:breadcrumbs" />
+    </ul>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <span tal:condition="layout/getCustomerName">
+      <div class="wfstatus">
+            <span tal:replace="python:context.customer.customer_id">
+              Id
+            </span>
+            &bull;
+            <span tal:replace="python:context.customer.translated_state">
+              State
+            </span>
+      </div>
+    </span>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <div class="ikoba-content">
+      <div class="actionbar" tal:condition="provider:actionbar">
+        <tal:actionbar content="structure provider:actionbar" />&nbsp;
+      </div>
+      <div tal:define="message context/@@messages"
+           tal:condition="python: len(message)>11">
+        <div tal:content="structure message">
+        </div>
+      </div>
+      <div class="alert alert-danger"
+        tal:define="status view/status|nothing"
+        tal:condition="status">
+        <span tal:content="view/status">
+          FORM STATUS SUMMARY
+        </span>:
+        <ul id="alert-error" tal:condition="view/errors">
+          <li tal:repeat="error view/error_views">
+             <span tal:replace="structure error">Error Type</span>
+          </li>
+        </ul>
+      </div>
+      <h1 class="ikoba-content-label"
+          tal:condition="view/label|nothing"
+          tal:content="structure view/label">Label</h1>
+      <div tal:content="structure view/content">
+        THE CONTENT
+      </div>
+      <br />
+    </div>
+    <!-- /Diazo element-->
+
+  </body>
+  <!-- /Diazo element-->
+
+</html>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterexportpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterexportpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterexportpage.pt	(revision 12677)
@@ -0,0 +1,92 @@
+<p i18n:translate="">
+  Here you can create CSV files from parts of portal data.
+  Please pick the type of objects you want to export from the
+  selection below. The file will be generated and then be
+  made available for you to download in the table below.
+  Files can only be downloaded within 24 hours after generation.
+  The download link disappears afterwards.
+</p>
+
+<br />
+
+<form  method="POST" class="form-inline">
+  <label for="exporter">
+    Exporter (Data Type):
+  </label>
+  <div class="form-group">
+    <select name="exporter" class="form-control half">
+      <span tal:repeat="items view/getExporters" tal:omit-tag="">
+        <option tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE
+        </span>
+        </option>
+      </span>
+    </select>
+  </div>
+  <div class="form-group">
+    <input i18n:translate="" type="submit" class="btn btn-primary"
+      name="CREATE" tal:attributes="value view/export_button" />
+  </div>
+  <br /><br />
+  <div class="form-group">
+  <input type="submit" name="CANCEL" class="btn btn-default"
+      tal:attributes="value view/cancel_button" />
+  </div>
+</form>
+
+<br /><br />
+
+<table i18n:domain="waeup.ikoba" class="table table-condensed table-hover">
+  <thead>
+    <tr>
+      <th i18n:translate="">Export Number</th>
+      <th i18n:translate="">Exporter</th>
+      <th i18n:translate="">Filter</th>
+      <th i18n:translate="">Creator</th>
+      <th i18n:translate="">Creation Date</th>
+      <th i18n:translate="">Status</th>
+      <th>&nbsp;</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="job view/entries">
+      <td>
+        <span tal:replace="job/job">12</span>
+      </td>
+      <td>
+        <span tal:replace="structure job/exporter">EXPORTER</span>
+      </td>
+      <td>
+        <span tal:replace="structure job/args">ARGS</span>
+      </td>
+      <td>
+        <span tal:replace="job/creator">CREATOR</span>
+      </td>
+      <td>
+        <span tal:replace="job/start_time">DATETIME</span>
+      </td>
+      <td>
+        <span tal:replace="job/status">STATUS</span>
+      </td>
+      <td nowrap>
+        <a href="" class="btn btn-default btn-xs" i18n:translate=""
+           tal:condition="job/show_refresh_button">
+          <img src="/static/img/actionicon_reload.png" />
+          Reload
+        </a>
+        <form method="POST">
+          <a href="" class="btn btn-primary btn-xs"
+                 tal:attributes="href job/download_url"
+                 tal:condition="job/show_download_button">
+            Download</a>
+          <input type="hidden" name="job_id"
+                 tal:attributes="value job/job" />
+          <input type="submit" class="btn btn-default btn-xs"
+                 name="DISCARD" value="Discard"
+                 tal:condition="job/show_discard_button" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterfinishedpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterfinishedpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterfinishedpage.pt	(revision 12677)
@@ -0,0 +1,27 @@
+<p i18n:translate="">
+  Find below the last
+  <span tal:content="view/context/max_files" i18n:name="count">50</span>
+  imports. The files contain the imported datasets.
+</p>
+<table i18n:domain="waeup.ikoba" class="ikoba-data-table dataTableFiles">
+  <thead>
+    <tr>
+      <th i18n:translate="">File</th>
+      <th i18n:translate="">Datasets</th>
+      <th i18n:translate="">Upload Date</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="file context/getFinishedFiles">
+      <td><a tal:attributes="href python:  view.url(context)  +
+                             '/download?filename=' + file.rel_path"
+             tal:content="file/name">FILENAME</a></td>
+      <td tal:content="python: file.lines-1">12 k</td>
+      <td tal:content="file/uploaddate">Mar 12, 2008</td>
+    </tr>
+  </tbody>
+</table>
+<form method="POST">
+  <input i18n:translate="" type="submit" class="btn btn-default" name="CANCEL"
+      tal:attributes="value view/cancel_button"/>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport1page.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport1page.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport1page.pt	(revision 12677)
@@ -0,0 +1,34 @@
+<h3 i18n:domain="waeup.ikoba" i18n:translate="">Step 1</h3>
+<p i18n:domain="waeup.ikoba" i18n:translate="">
+  Using batch processing you can mass-create, mass-update, or
+  mass-remove datasets from the database using CSV files.
+</p>
+<p i18n:domain="waeup.ikoba" i18n:translate="">
+  Please select a file for processing from the list below.
+</p>
+<form i18n:domain="waeup.ikoba" method="POST">
+  <table class="table">
+    <thead>
+      <th />
+      <th i18n:translate="">Filename</th>
+      <th i18n:translate="">Datasets</th>
+      <th i18n:translate="">Size</th>
+      <th i18n:translate="">Date</th>
+    </thead>
+    <tbody>
+      <tr tal:repeat="file view/getFiles">
+        <td>
+          <button i18n:translate=""
+            type="submit" name="select" class="btn btn-primary"
+            tal:attributes="value file/name">Select</button>
+        </td>
+        <td tal:content="file/name">Filename.csv</td>
+        <td tal:content="python: file.lines - 1">12</td>
+        <td tal:content="file/size">123 Bytes</td>
+        <td tal:content="file/getDate">1.1.1970</td>
+      </tr>
+    </tbody>
+  </table>
+  <input type="submit" name="cancel" class="btn btn-default"
+      tal:attributes="value view/cancel_button" />
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport2page.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport2page.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport2page.pt	(revision 12677)
@@ -0,0 +1,60 @@
+<h3 i18n:domain="waeup.ikoba" i18n:translate="">Step 2</h3>
+<form i18n:domain="waeup.ikoba" method="POST">
+  <p>
+    <b i18n:translate="">File:</b>
+    <span tal:content="view/filename">Filename.csv</span>
+    <input type="hidden" name="filename" tal:attributes="value view/filename" />
+  </p>
+  <p i18n:translate="">
+    Please select a file-processor and a processing-mode from the selections below.
+  </p>
+  <table tal:define="prev_table view/getPreviewTable;" class="table table-condensed">
+    <thead>
+      <tr>
+        <th i18n:translate="">Header</th>
+        <th i18n:translate="">Sample Record 1</th>
+        <th i18n:translate="">Sample Record 2</th>
+        <th i18n:translate="">Sample Record 3</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="row prev_table">
+        <td tal:repeat="value row"
+            tal:content="value"> value
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <div> <b i18n:translate="">Processor:</b>
+    <select name="importer" class="form-control">
+      <option tal:repeat="importer view/getImporters"
+          tal:attributes="value importer/name;
+              selected python: importer['name'] == view.importer and 'selected' or ''"
+          tal:content="importer/title"> Some Importer
+      </option>
+    </select>
+  </div>
+  <p>&nbsp;
+  </p>
+  <div> <b i18n:translate="">Mode:</b>
+    <input type="radio" name="mode" value="create" checked="checked"
+       tal:attributes="checked python: view.mode == 'create' and 'checked' or '';
+        disabled python: view.mode != 'create' and view.mode_locked and 'disabled' or ''" />
+        Create  &nbsp;&nbsp;
+    <input type="radio" name="mode" value="update"
+       tal:attributes="checked python: view.mode == 'update' and 'checked' or '';
+        disabled python: view.mode != 'update' and view.mode_locked and 'disabled' or ''" />
+        Update  &nbsp;&nbsp;
+    <input type="radio" name="mode" value="remove"
+       tal:attributes="checked python: view.mode == 'remove' and 'checked' or '';
+        disabled python: view.mode != 'remove' and view.mode_locked and 'disabled' or ''" />
+        Remove
+    <p>&nbsp;
+    </p>
+  </div>
+  <input class="btn btn-default" type="submit" name="cancel" tal:attributes="value view/cancel_button" />
+  <input class="btn btn-danger" type="submit" name="back1" tal:attributes="value view/back_button" />
+  <input class="btn btn-primary"
+    tal:attributes="disabled python: view.getWarnings() and 'disabled' or ''; value view/proceed_button"
+    type="submit" name="proceed" />
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport3page.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport3page.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport3page.pt	(revision 12677)
@@ -0,0 +1,75 @@
+<div i18n:domain="waeup.ikoba" i18n:translate=""
+  class="alert alert-success" tal:condition="not: view/getWarnings">
+  Header fields OK
+</div>
+
+<h3 i18n:domain="waeup.ikoba" i18n:translate="">Step 3</h3>
+<form i18n:domain="waeup.ikoba">
+  <p i18n:translate="">
+    Eventually modify headerfields of import file below.
+  </p>
+  <p>
+    <b i18n:translate="">File:</b>
+    <span tal:content="view/filename">Filename.csv</span>
+    <input type="hidden" name="filename"
+     tal:attributes="value view/filename" />
+  </p>
+  <p>
+    <b i18n:translate="">Processor:</b>
+    <span tal:content="view/importer/name">Importer Name</span>
+    <input type="hidden" name="importer"
+     tal:attributes="value view/importer/util_name" />
+  </p>
+  <p>
+    <b i18n:translate="">Processing mode: </b>
+    <span tal:content="view/mode">mode</span>
+    <input type="hidden" name="mode"
+     tal:attributes="value view/mode" />
+  </p>
+
+  <div>
+    <table tal:define="prev_table view/getPreviewTable;
+                       headers view/headerfields;"
+            class="table">
+      <thead>
+        <tr>
+          <th i18n:translate="">Header</th>
+          <th i18n:translate="">change to</th>
+          <th i18n:translate="">Sample Record</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="row prev_table">
+          <td tal:content="python:headers[row[0]]"> value
+          </td>
+          <td>
+            <select name="headerfield" class="form-control">
+              <option tal:repeat="opt view/getPossibleHeaders"
+                tal:attributes="value opt/value;
+                  selected python: opt['value']==headers[row[0]] and 'selected' or '';"
+                tal:content="opt/title">
+                new_header_name
+              </option>
+            </select>
+          </td>
+          <td tal:repeat="value python:row[1:]"
+              tal:content="value"> value
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+
+  <input class="btn btn-default" type="submit" name="cancel"
+    tal:attributes="value view/cancel_button" />
+  <input class="btn btn-default" type="reset" name="reset"
+    tal:attributes="value view/reset_button" />
+  <input class="btn btn-default" type="submit" name="update"
+    tal:attributes="value view/update_button" />
+  <br /><br />
+  <input class="btn btn-danger" type="submit" name="back2"
+    tal:attributes="value view/back_button" />
+  <input class="btn btn-primary"
+    tal:attributes="disabled python: view.getWarnings() and 'disabled' or ''; value view/proceed_button"
+    type="submit" name="proceed"/>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport4page.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport4page.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterimport4page.pt	(revision 12677)
@@ -0,0 +1,20 @@
+<h3 i18n:domain="waeup.ikoba" i18n:translate="">Step 4</h3>
+<p i18n:domain="waeup.ikoba" i18n:translate="">
+  Batch processing finished.
+</p>
+<p i18n:domain="waeup.ikoba">
+  <b i18n:translate="">File:</b> <span tal:content="view/filename">Filename.csv</span>
+</p>
+<p i18n:domain="waeup.ikoba">
+  <b i18n:translate="">Processor:</b>
+  <span tal:content="view/importer/name">Importer Name</span>
+</p>
+<p i18n:domain="waeup.ikoba">
+  <b i18n:translate="">Processing mode: </b>
+  <span tal:content="view/mode">mode</span>
+</p>
+<form method="POST" i18n:domain="waeup.ikoba">
+  <input class="btn btn-primary" type="submit" name="finish"
+    tal:attributes="value view/back_button" />
+</form>
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterlogspage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterlogspage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterlogspage.pt	(revision 12677)
@@ -0,0 +1,34 @@
+<div i18n:domain="waeup.ikoba" tal:define="files view/files">
+  <p i18n:translate="" tal:condition="not: files">
+  Currently no log files are available.
+  </p>
+  <table tal:condition="files" class="table">
+    <thead>
+      <tr>
+        <th>
+        </th>
+        <th i18n:translate="">File
+        </th>
+        <th i18n:translate="">Date
+        </th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="file files">
+        <td>
+          <form method="POST" action="@@show">
+            <input i18n:translate="" type="submit" class="btn btn-primary"
+                   name="show" tal:attributes="value view/show_button" />
+            <input type="hidden" name="logname"
+                   tal:attributes="value file/name" />
+          </form></td>
+        <td tal:content="file/name">file.name</td>
+        <td tal:content="file/uploaddate">Fri Jan 15 17:44:25 2010</td>
+      </tr>
+    </tbody>
+  </table>
+  <form method="POST">
+    <input i18n:translate="" type="submit" class="btn btn-default" name="back"
+        tal:attributes="value view/back_button"/>
+  </form>
+</div>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterlogsshowfilepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterlogsshowfilepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterlogsshowfilepage.pt	(revision 12677)
@@ -0,0 +1,16 @@
+<form method="post" class="form-horizontal">
+  <input type="text" name="query" class="form-control"
+	 tal:attributes="value view/query; placeholder view/placeholder" />
+  <input class="btn btn-primary" type="submit" name="search"
+	 tal:attributes="value view/search_button" />
+  <input class="btn btn-default" type="submit" name="back"
+	 tal:attributes="value view/back_button" />
+  <input type="hidden" name="logname"
+	 tal:attributes="value view/filename" />
+</form>
+
+<div tal:condition="view/result|nothing">
+  <pre tal:content="structure view/result">
+    GREPRESULT
+  </pre>
+</div>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacentermanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacentermanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacentermanagepage.pt	(revision 12677)
@@ -0,0 +1,30 @@
+<form  i18n:domain="waeup.ikoba" method="POST">
+
+  <br />
+  <div class="form-group">
+    <label i18n:translate="">Storage path</label>
+        <input type="text" name="newpath" class="form-control"
+               tal:attributes="value context/storage" />
+  </div>
+
+  <div class="checkbox">
+    <label>
+      <input type="checkbox" name="move"  checked="checked">
+      Copy existing data to new storage?
+    </label>
+  </div>
+
+  <div class="checkbox">
+    <label>
+      <input type="checkbox" name="overwrite">
+      Overwrite contents in new location?
+    </label>
+  </div>
+
+  <input class="btn btn-primary" type="submit" name="save"
+      tal:attributes="value view/save_button" />
+  <input class="btn btn-default" type="reset" name="reset"
+      tal:attributes="value view/reset_button" />
+  <input class="btn btn-default" type="submit" name="cancel"
+      tal:attributes="value view/cancel_button" />
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenterpage.pt	(revision 12677)
@@ -0,0 +1,41 @@
+<p  i18n:domain="waeup.ikoba" i18n:translate="">
+The data center helps you to manage portal data. You can upload CSV
+files here, which will be available for import afterwards.
+</p>
+
+<p i18n:domain="waeup.ikoba">
+  <b i18n:translate="">Storage path:</b>
+  <span tal:content="context/storage">/foo/bar</span>
+</p>
+
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+    <table i18n:domain="waeup.ikoba" class="ikoba-data-table dataTableManageFiles">
+      <thead>
+        <tr>
+          <th></th>
+          <th i18n:translate="">File</th>
+          <th i18n:translate="">Datasets</th>
+          <th i18n:translate="">Upload Date</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="file context/getPendingFiles">
+          <td>
+            <input type="checkbox" name="val_id"
+                         tal:attributes="value file/name" />
+          </td>
+          <td><a tal:attributes="href python: view.url(context) +
+                                 '/download?filename=' + file.name"
+                 tal:content="file/name">FILENAME</a></td>
+          <td tal:content="python: file.lines - 1">100</td>
+          <td tal:content="file/uploaddate">Mar 12, 2008</td>
+        </tr>
+      </tbody>
+    </table>
+    <div tal:condition="view/availableActions">
+      <span tal:repeat="action view/actions" tal:omit-tag="">
+        <input tal:replace="structure action/render"/>
+      </span>
+    </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenteruploadpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenteruploadpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/datacenteruploadpage.pt	(revision 12677)
@@ -0,0 +1,121 @@
+<p i18n:translate="">
+    Before uploading a file check that your file header corresponds
+    with header format of the selected processor. All available headers
+    are listed below.
+    After file upload click the 'Process data' button and proceed up to
+    import step 3. Verify that the data format
+    meets all the import criteria and requirements of the processor.
+</p>
+
+<form method="post" enctype="multipart/form-data">
+<br />
+  <table class="form-table">
+    <tr>
+      <td i18n:translate="">
+        File:
+      </td>
+      <td>
+    		<div class="input-group half">
+    			<div class="input-group-btn">
+    				<div class="btn btn-default btn-file">
+              Select&hellip;
+              <input type="file" name="uploadfile:file" />
+            </div>
+    			</div>
+    			<input type="text" class="form-control" readonly>
+        </div>
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="">
+        Processor:
+      </td>
+      <td>
+        <select name="importer" class="form-control half">
+          <option tal:repeat="importer view/getImporters"
+              tal:attributes="value importer/title"
+              tal:content="importer/title"> Some Importer
+          </option>
+        </select>
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="">
+        Import Mode:
+      </td>
+      <td>
+        <select name="import_mode" class="form-control half">
+          <option value='Create'>Create</option>
+          <option value='Update'>Update</option>
+          <option value='Remove'>Remove</option>
+        </select>
+      </td>
+    </tr>
+  </table>
+  <br />
+  <input class="btn btn-primary" type="submit" name="SUBMIT"
+      tal:attributes="value view/upload_button" />
+  <input class="btn btn-default" type="submit" name="CANCEL"
+      tal:attributes="value view/cancel_button"/>
+</form>
+<br />
+<p i18n:translate="">
+    Import managers will be automatically informed by email after file upload.
+    There is no need for assigning tickets or sending emails anymore.
+    The data will be imported according to the information given.
+</p>
+
+<br /><br />
+
+<h2 i18n:translate="">Available Processors (Importers)</h2>
+
+<table i18n:domain="waeup.ikoba" class="table table-condensed">
+  <thead>
+    <tr>
+      <th i18n:translate="">Processor</th>
+      <th i18n:translate="">Required Schema Fields</th>
+      <th i18n:translate="">Optional Schema Fields</th>
+      <th i18n:translate="">Non-Schema Fields</th>
+    </tr>
+  </thead>
+  <tr tal:repeat="importer view/getImporters">
+    <td>
+      <span tal:content="importer/title">TITLE</span><br /><br />
+      <a i18n:translate="" class="btn btn-primary btn-xs"
+         tal:attributes="href python: 'skeleton?name=' + importer['name']">
+         Download CSV Skeleton File
+      </a>
+    </td>
+    <td nowrap>
+      <span tal:repeat="field importer/fields">
+          <tal:schemafield condition="field/f_type">
+            <tal:required condition="field/f_required">
+              <span tal:replace="field/f_name"></span>
+              (<span tal:replace="field/f_type"></span>)
+              <br />
+            </tal:required>
+          </tal:schemafield>
+      </span>
+    </td>
+    <td nowrap>
+      <span tal:repeat="field importer/fields">
+          <tal:schemafield condition="field/f_type">
+            <tal:notrequired condition="not:field/f_required">
+              <span tal:replace="field/f_name"></span>
+              (<span tal:replace="field/f_type"></span>)
+              <br />
+            </tal:notrequired>
+          </tal:schemafield>
+      </span>
+    </td>
+    <td>
+      <span tal:repeat="field importer/fields">
+          <tal:extrafield condition="not:field/f_type">
+            <span tal:replace="field/f_name"></span>
+            <br />
+          </tal:extrafield>
+      </span>
+    </td>
+
+  </tr>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/default_waeup_display_form.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/default_waeup_display_form.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/default_waeup_display_form.pt	(revision 12677)
@@ -0,0 +1,12 @@
+<table class="form-table" i18n:domain="waeup.ikoba"
+  tal:define="files provider:files">
+  <tbody>
+    <tal:widgets content="structure provider:widgets" />
+    <tr tal:condition="python: files.replace('\n','')">
+      <td class="separator" colspan=2 i18n:translate="">
+        Uploaded Files
+      </td>
+    </tr>
+    <tal:files content="structure files" />
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/default_waeup_edit_form.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/default_waeup_edit_form.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/default_waeup_edit_form.pt	(revision 12677)
@@ -0,0 +1,16 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tal:files content="structure provider:files" />
+    </tbody>
+  </table>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/exception.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/exception.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/exception.pt	(revision 12677)
@@ -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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/filedisplay.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/filedisplay.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/filedisplay.pt	(revision 12677)
@@ -0,0 +1,24 @@
+<tr tal:condition = "viewlet/file_exists">
+  <td class="fieldname">
+    <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+    <a tal:attributes="href python: view.url(context, viewlet.download_name)"
+       tal:content="viewlet/download_filename"
+       target="image">
+      LINK
+    </a>
+  </td>
+</tr>
+
+<tr tal:condition = "python: viewlet.file_exists and getattr(context, 'state', None) == 'published'">
+  <td class="fieldname" i18n:translate="">
+    HTML Element:
+  </td>
+  <td>
+    &lt;a href=&quot;
+    <span tal:replace="python: view.url(context, viewlet.download_name)"></span>
+    &quot;&gt;<span tal:replace="viewlet/label">FILENAME</span>&lt;/a&gt;
+  </td>
+</tr>
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/fileupload.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/fileupload.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/fileupload.pt	(revision 12677)
@@ -0,0 +1,39 @@
+<tr i18n:domain="waeup.ikoba" tal:condition = "viewlet/show_viewlet">
+  <td class="fieldname">
+    <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+
+    <tal:file tal:condition="viewlet/file_exists">
+      <a tal:attributes="href python: view.url(context, viewlet.download_name)"
+         tal:content="viewlet/download_filename"
+         target="image">
+        LINK
+      </a>&nbsp;
+      <input type="submit"
+             tal:attributes="id python:('delete_%s' % viewlet.input_name);
+                name python:('delete_%s' % viewlet.input_name);
+                value viewlet/delete_button;
+                onclick python:'return window.confirm(\'%s\')' % view.deletion_warning"
+             class="btn btn-danger btn-xs"/>
+    </tal:file>
+
+		<div class="input-group" tal:condition="not: viewlet/file_exists">
+			<div class="input-group-btn">
+				<div class="btn btn-default btn-file">
+          Select new file&hellip;
+          <input type="file" tal:attributes="name viewlet/input_name" />
+        </div>
+			</div>
+			<input type="text" class="form-control" readonly>
+      <div class="input-group-btn">
+        <input type="submit"
+               tal:attributes="id python:('upload_%s' % viewlet.input_name);
+                name python:('upload_%s' % viewlet.input_name);
+                value viewlet/upload_button"
+               class="btn btn-primary" />
+        (<span tal:replace="viewlet/max_upload_size">10 kB</span> max.)
+      </div>
+    </div>
+  </td>
+</tr>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/jobindex.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/jobindex.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/jobindex.pt	(revision 12677)
@@ -0,0 +1,21 @@
+<html>
+  <head>
+  </head>
+  <body>
+    <div>
+      Job: <span tal:content="python:view.context.__repr__()">JOB</span>
+    </div>
+    <div>
+      Status: <span tal:content="view/context/status">status</span>
+    </div>
+    <div>
+      Begin after: <span tal:content="view/context/begin_after">after</span>
+    </div>
+    <div>
+      Begin by: <span tal:content="view/context/begin_by">by</span>
+    </div>
+    <div>
+      Percent done: <span tal:content="view/percent">percent</span>
+    </div>
+  </body>
+</html>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/loginpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/loginpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/loginpage.pt	(revision 12677)
@@ -0,0 +1,45 @@
+<form i18n:domain="waeup.ikoba" method="post" class="form-horizontal">
+  <br />
+  <div class="form-group">
+    <label i18n:translate="" class="col-sm-2 control-label">Identifier</label>
+    <div class="col-sm-10">
+      <input name="form.login" type="text" class="form-control" id="login">
+      <div class="hint" i18n:translate="identifier_hint">Customers can enter
+      either their customer id, registration number or email address. 
+      Officers must enter their user name.</div>
+    </div>
+  </div>
+  <div class="form-group">
+    <label i18n:translate="" class="col-sm-2 control-label">Password</label>
+    <div class="col-sm-10">
+      <input name="form.password" type="password" class="form-control" id="password">
+    </div>
+  </div>
+  <br />
+  <input type="hidden" name="camefrom"   tal:attributes="value view/camefrom" />
+  <input class="btn btn-primary" type="submit" name="SUBMIT"
+    tal:attributes="value view/login_button" />
+  <br /><br /><br />
+  <p>
+  All identifiers and passwords are case sensitive.
+  </p>
+  <p  i18n:translate="login_trouble1">Don't forget to logout or exit your browser when you're done.
+    If you are having trouble logging in, make sure to enable  cookies in your web browser.
+  </p>
+  <p i18n:translate="login_trouble2" tal:condition="python:False">
+    You don't have an account because you are a new customer, or your customer record has been created for you?
+    Acquire a Password Activation Code (PWD) and inititialize your customer account
+    <strong><a href ="setpassword"> here</a></strong>.
+  </p>
+  <p i18n:translate="login_trouble4">
+    You don't have an account because you are a new customer?
+    Create your customer account <strong><a href="createaccount"> here</a></strong>.
+  </p>
+  <p i18n:translate="login_trouble5">
+    You don't have an account because you are a new customer and your customer record has been created for you?
+    Inititialize your customer account <strong><a href="requestpw"> here</a></strong>.
+  </p>
+  <p i18n:translate="login_trouble3"> Or simply forgot your id or password? Then request a new password
+    <strong><a href="changepw"> here</a></strong>.
+  </p>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/logoutpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/logoutpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/logoutpage.pt	(revision 12677)
@@ -0,0 +1,2 @@
+<div>
+</div>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/messages.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/messages.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/messages.pt	(revision 12677)
@@ -0,0 +1,6 @@
+<div
+  tal:repeat="message view/messages"
+  tal:attributes="class message/type"
+  tal:content="structure message/message">Sauron will arrive soon!
+</div>
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/myrolespage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/myrolespage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/myrolespage.pt	(revision 12677)
@@ -0,0 +1,29 @@
+<h3 i18n:domain="waeup.ikoba"
+    i18n:translate="">My Portal Roles:</h3>
+<table i18n:domain="waeup.ikoba" class="form-table">
+<tr tal:repeat="role view/getSiteRoles">
+    <td>
+      <div tal:content="role" >ROLE</div>
+    </td>
+  </tr>
+</table>
+
+<h3 i18n:domain="waeup.ikoba"
+    i18n:translate="">My Local Roles:</h3>
+<table class="form-table">
+  <tr tal:repeat="role view/getLocalRoles">
+    <td valign="top" >
+      <span tal:content="role">ROLE</span>:</td>
+    <td>
+      <span tal:repeat="object python:view.getLocalRoles[role]">
+      <a tal:attributes="href python:view.url(object)"
+         tal:content="python:view.url(object)" >OBJECT
+      </a>
+      <br />
+      </span>
+    </td>
+  </tr>
+</table>
+
+
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/notfound.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/notfound.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/notfound.pt	(revision 12677)
@@ -0,0 +1,16 @@
+<h2 i18n:domain="waeup.ikoba" i18n:translate="">
+  The page you are trying to access is not available.
+</h2>
+
+<div i18n:domain="waeup.ikoba">
+  <b i18n:translate="">Please note the following:</b></div>
+<div i18n:domain="waeup.ikoba">
+  <ol class="list">
+    <li i18n:translate="">
+      You might have misspelled the URL.
+    </li>
+    <li i18n:translate="">
+        You might be trying to access a non-existing page.
+    </li>
+  </ol>
+</div>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/plainactionbutton.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/plainactionbutton.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/plainactionbutton.pt	(revision 12677)
@@ -0,0 +1,7 @@
+<span tal:condition="viewlet/target_url">
+  <span>
+    <a href=""
+       tal:attributes="href viewlet/target_url">
+    <span tal:content="viewlet/text">TEXT</span></a>
+  </span>
+</span>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/primarynavtab.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/primarynavtab.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/primarynavtab.pt	(revision 12677)
@@ -0,0 +1,7 @@
+<li tal:attributes="class viewlet/active" tal:condition="viewlet/link_target">
+  <a href="#"
+     tal:attributes="href viewlet/link_target"
+     tal:content="viewlet/tab_title">
+    Primary Tab Title
+  </a>
+</li>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/reportscontainercreate.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/reportscontainercreate.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/reportscontainercreate.pt	(revision 12677)
@@ -0,0 +1,28 @@
+<form  i18n:domain="waeup.ikoba"  method="POST">
+  <p i18n:translate="">
+    Here you can create reports representing portal data.  Please pick
+    the type of report you want to create from the selection below.
+    Afterwards you can configure the report (do the settings).  The
+    report data will be calculated and then made available for you to
+    view/download.
+  </p>
+  <br />
+  <div class="form-group">
+    <label for="generator">Report Type</label>
+    <select name="generator" class="form-control half">
+      <span tal:repeat="items view/creators" tal:omit-tag="">
+        <option
+            tal:define="name python: items[1]; title python: items[0]"
+            tal:attributes="value name">
+          <span tal:replace="title">TITLE</span>
+        </option>
+      </span>
+    </select>
+  </div>
+  <div class="form-group">
+    <input i18n:translate="" type="submit" class="btn btn-primary"
+           name="START_GENERATOR" value="Configure" />
+    <a class="btn btn-default" href=""
+       tal:attributes="href python:view.url(context)">Cancel</a>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/reportscontainerpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/reportscontainerpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/reportscontainerpage.pt	(revision 12677)
@@ -0,0 +1,54 @@
+<table i18n:domain="waeup.ikoba" class="table">
+  <thead>
+    <tr>
+      <th i18n:translate="">Report Number</th>
+      <th i18n:translate="">Description</th>
+      <th i18n:translate="">Creator</th>
+      <th i18n:translate="">Creation Date</th>
+      <th i18n:translate="">Status</th>
+      <th>&nbsp;</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="entry view/entries">
+      <td>
+        <span tal:replace="python: entry[0]">12</span>
+      </td>
+      <td>
+        <span tal:replace="python: entry[1]">REPORT DESCRIPTION</span>
+      </td>
+      <td>
+        <span tal:replace="python: entry[7]">CREATOR</span>
+      </td>
+      <td nowrap>
+        <span tal:replace="python: entry[6]">DATETIME</span>
+      </td>
+      <td>
+        <span tal:replace="python: entry[2]">STATUS</span>
+      </td>
+      <td nowrap>
+        <form method="POST">
+          <input type="hidden" name="job_id"
+                 tal:attributes="value python: entry[0]" />
+          <input type="submit" class="btn btn-sm btn-primary"
+                 name="DOWNLOAD" value="Download"
+                 tal:condition="python: entry[4]" />
+          <input type="submit" class="btn btn-sm btn-default"
+                 name="DISCARD" value="Discard"
+                 tal:condition="python: entry[3]" />
+          <a href="" class="btn btn-sm btn-default" i18n:translate=""
+             tal:condition="python: entry[5]">
+            <img src="/static/img/actionicon_reload.png" />
+            Reload
+          </a>
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
+<form method="POST" i18n:domain="waeup.ikoba">
+  <a tal:attributes="href python:view.url(context, 'create')"
+     class="btn btn-primary" i18n:translate="">
+    Create new report
+  </a>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/staffsitelayout.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/staffsitelayout.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/staffsitelayout.pt	(revision 12677)
@@ -0,0 +1,141 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" i18n:domain="waeup.ikoba">
+  <head>
+
+    <!-- Diazo element-->
+    <title i18n:translate="">
+      WAeUP.Ikoba - Registration and Application System
+    </title>  
+    <!-- /Diazo element-->
+
+    <!--base href="http://localhost:8080/app/@@page"
+     tal:attributes="href python: view.url(layout.site)" / -->
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+    <meta name="robots" content="index, follow" />
+    <link rel="alternate" type="application/rss+xml" title="RSS"
+        href="http://www.example.net/feed.rss"
+        tal:attributes="title string:RSS feed of ${layout/getAppTitle};
+            href python: view.url(layout.site, 'feed.rss')" />
+  </head>
+
+  <!-- Diazo element-->
+  <body class="ikoba-body">
+
+    <!-- Diazo element-->
+    <a class="ikoba-brand-link" href="#"
+        tal:attributes="href python: view.url(layout.site)"
+        tal:content="layout/getAppAcronym">ACRONYM
+    </a>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <ul class="nav ikoba-navbar-left">
+      <tal:primary_nav content="structure provider:primary_nav" />
+    </ul>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <ul class="nav secondary-nav ikoba-navbar-right">
+      <li tal:condition="not: layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+          tal:attributes="href python: view.url(layout.site, 'login')">
+          Login
+      </a>
+      </li>
+      <li tal:condition="layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(layout.site, '@@contactadmin')">
+         Contact
+      </a>
+      </li>
+      <li tal:condition="layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(context, '@@logout')">
+         Logout
+      </a>
+      </li>
+    </ul>
+    <!-- /Diazo element-->
+
+    <span tal:define="langs provider:languages"
+          tal:condition="langs"
+          class="language">
+      <tal:languages tal:content="structure langs" /> |
+    </span>
+
+    <!-- Diazo element-->
+    <ul class="breadcrumb">
+      <tal:breadcrumbs
+            tal:condition="python: layout.isAuthenticated()"
+            tal:content="structure provider:breadcrumbs" />
+    </ul>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <span tal:condition="layout/getCustomerName">
+      <div class="wfstatus">
+            <span tal:replace="python:context.customer.customer_id">
+              Id
+            </span>
+            &bull;
+            <span tal:replace="python:context.customer.translated_state">
+              State
+            </span>
+      </div>
+    </span>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <div class="sidebar well">
+        <div tal:condition="layout/getCustomerName">
+          <div class="ikoba-sidebar-title" tal:content="layout/getCustomerName">STUDENTNAME</div>
+          <ul class="nav">
+            <tal:left content="structure provider:left_customermanage" />
+          </ul>
+        </div>
+        <div tal:condition="layout/isAuthenticated">
+          <div class="ikoba-sidebar-title" tal:content="layout/getUserTitle">USERNAME</div>
+          <ul class="nav">
+            <tal:left content="structure provider:left_manage" />
+          </ul>
+      </div>
+    </div>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <div class="ikoba-content">
+      <div class="actionbar" tal:condition="provider:actionbar">
+        <tal:actionbar content="structure provider:actionbar" />&nbsp;
+      </div>
+      <div
+          tal:define="message context/@@messages"
+          tal:condition="python: len(message)>11">
+        <div tal:content="structure message">
+        </div>
+      </div>
+      <div class="alert alert-danger"
+        tal:define="status view/status|nothing"
+        tal:condition="status">
+        <span tal:content="view/status">
+          FORM STATUS SUMMARY
+        </span>:
+        <ul id="alert-error" tal:condition="view/errors">
+          <li tal:repeat="error view/error_views">
+             <span tal:replace="structure error">Error Type</span>
+          </li>
+        </ul>
+      </div>
+      <h1 class="ikoba-content-label"
+          tal:condition="view/label|nothing"
+          tal:content="structure view/label">LABEL</h1>
+      <div tal:content="structure view/content"> THE CONTENT
+      </div>
+      <br />
+
+    </div>
+    <!-- /Diazo element-->
+
+  </body>
+  <!-- /Diazo element-->
+
+</html>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/subobjectlist.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/subobjectlist.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/subobjectlist.pt	(revision 12677)
@@ -0,0 +1,15 @@
+<table>
+  <thead>
+    <tr tal:repeat="viewlet viewletmanager/viewlets">
+      <th tal:replace="structure viewlet/render">viewlet
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="row viewletmanager/rows">
+      <td tal:repeat="viewlet row">
+        <div tal:replace="structure viewlet/render">viewlet
+        </div></td>
+    </tr>
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/unauthorized.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/unauthorized.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/unauthorized.pt	(revision 12677)
@@ -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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/usereditformpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/usereditformpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/usereditformpage.pt	(revision 12677)
@@ -0,0 +1,27 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data"
+      autocomplete="off">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tr>
+        <td i18n:translate="" class="fieldname">Password:</td>
+        <td>
+          <input class="form-control" name="password" type="password"  />
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">Retype Password:</td>
+        <td>
+          <input class="form-control" name="control_password" type="password" />
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/userscontainerpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/userscontainerpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/userscontainerpage.pt	(revision 12677)
@@ -0,0 +1,29 @@
+<table i18n:domain="waeup.ikoba" class="ikoba-data-table dataTable">
+  <thead>
+    <tr>
+      <th i18n:translate="">Id</th>
+      <th i18n:translate="">Name</th>
+      <th i18n:translate="">Portal Roles</th>
+      <th i18n:translate="">Local Roles</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="account context/values">
+      <td tal:content="account/name">USERNAME</td>
+      <td tal:content="account/title">TITLE</td>
+      <td nowrap tal:content="structure python:view.getSiteRoles(account)">SITE ROLES</td>
+      <td tal:content="structure python:view.getLocalRoles(account)">LOCAL ROLES</td>
+      <td class="text-right">
+        <form method="post">
+          <input type="hidden" name="userid"
+           tal:attributes="value account/name"/>
+            <input class="btn btn-sm btn-primary" type="submit" name="manage"
+              tal:attributes="value view/manage_button" />
+            <input class="btn btn-sm btn-default" type="submit" name="delete"
+              tal:attributes="value view/delete_button" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/virtjobscontainerindex.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/virtjobscontainerindex.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/virtjobscontainerindex.pt	(revision 12677)
@@ -0,0 +1,45 @@
+<h1>Running and Completed Jobs</h1>
+<div>
+  <form method="POST">
+    <input type="submit" class="btn primary" name="RELOAD" value="reload" />
+    <input type="submit" class="btn secondary"
+	   name="START_NEW" value="start new test job" />
+  </form>
+</div>
+<hr />
+
+<div tal:repeat="job view/jobs">
+  <div>
+    JOB
+    <a href="" tal:attributes="href job/link">
+      <span tal:replace="job/job_id">12</span>
+    </a>
+    status: <span tal:replace="job/status">new</span>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">begin by:</span>
+      <span tal:content="job/begin_by"></span>
+    </div>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">begin after:</span>
+      <span tal:content="job/begin_after"></span>
+    </div>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">annotations:</span>
+      <span tal:content="job/annotations"></span>
+    </div>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">failure:</span>
+      <span tal:content="job/failure"></span>
+    </div>
+  </div>
+  <form tal:condition="job/removable" method="POST">
+    <input type="submit" class="btn secondary" name="REMOVE" value="Remove" />
+    <input type="hidden" name="job_id"
+	   tal:attributes="value job/job_id" />
+  </form>
+  <hr />
+</div>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/widgets.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/widgets.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/templates/widgets.pt	(revision 12677)
@@ -0,0 +1,26 @@
+<tal:block repeat="widget view/widgets">
+  <tr tal:define="separators view/separators|nothing"
+      tal:condition="python: separators and separators.get(widget.name,None)">
+    <td class="separator" colspan=2 tal:content="python: separators[widget.name]">
+      HEADLINE
+    </td>
+  </tr>
+  <tr>
+    <td class="fieldname">
+      <span tal:condition="widget/required">*</span>
+      <span tal:content="widget/label">LABEL</span>:
+    </td>
+    <td>
+      <span tal:content="structure widget">
+        <input type="text" />
+      </span>
+      <tal:error tal:condition="widget/error">
+        <div tal:content="structure widget/error">ERROR</div>
+      </tal:error>
+      <tal:hint
+        tal:condition="python: widget.hint and not getattr(view,'hide_hint',False)">
+        <div class="hint" tal:content="structure widget/hint">HINT</div>
+      </tal:hint>
+    </td>
+  </tr>
+</tal:block>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/__init__.py	(revision 12677)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/forbidden_file.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/forbidden_file.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/forbidden_file.csv	(revision 12677)
@@ -0,0 +1,3 @@
+File with forbidden chracters
+
+This is character 160:  
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_async.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_async.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_async.py	(revision 12677)
@@ -0,0 +1,174 @@
+import os
+import shutil
+import tempfile
+import transaction
+from zc.async.testing import wait_for_result
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.security.interfaces import Unauthorized
+from zope.testbrowser.testing import Browser
+from waeup.ikoba.app import Company
+from waeup.ikoba.async import AsyncJob, get_job_id
+from waeup.ikoba.interfaces import IJobManager
+from waeup.ikoba.testing import FunctionalTestCase, FunctionalLayer
+from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
+
+def fake_get_export_jobs_status(user_id=None):
+    return [('completed', 'Completed', 'my_exporter'),]
+
+def dummy_func():
+    return 42
+
+class AsyncBrowserTests(FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AsyncBrowserTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        self.storage = os.path.join(self.workdir, 'storage')
+        os.mkdir(self.storage)
+        app = Company()
+        app['datacenter'].setStoragePath(self.storage)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+        self.browser = Browser()
+        self.browser.handleErrors = False
+        self.datacenter_path = 'http://localhost/app/datacenter'
+
+    def tearDown(self):
+        super(AsyncBrowserTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+
+class JobViewTests(AsyncBrowserTests):
+    # Tests for the view of single jobs
+    overview_path = 'http://localhost/app/jobs'
+
+    def setUp(self):
+        super(JobViewTests, self).setUp()
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+
+    def start_job(self):
+        # helper that starts a job in background and returns the job
+        manager = getUtility(IJobManager)
+        job = AsyncJob(dummy_func)
+        manager.put(job, site=self.getRootFolder())
+        return job
+
+    def test_get_job_view(self):
+        # we can get a job on a view
+        self.browser.open(self.overview_path)
+        job = self.start_job()
+        wait_for_result(job)
+        self.browser.open(self.overview_path)
+        job_id = get_job_id(job)
+        link = self.browser.getLink(job_id)
+        expected_url = 'http://localhost/app/jobs/%s' % job_id
+        self.assertEqual(link.url, expected_url)
+        link.click()
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        return
+
+    def test_anonymous_forbidden(self):
+        # we need some permissions to get a job view
+        transaction.commit() # make sure the 'app' is stored in ZODB
+        job = self.start_job()
+        wait_for_result(job)
+        job_id = get_job_id(job)
+        link_url = 'http://localhost/app/jobs/%s' % job_id
+
+        # get a new browser, where we're not authenticated
+        browser = Browser()
+        browser.handleErrors = False
+        self.assertRaises(Unauthorized, browser.open, link_url)
+        return
+
+class JobOverviewTests(AsyncBrowserTests):
+    # Tests for the job overview page of universities
+    overview_path = 'http://localhost/app/jobs'
+
+    def setUp(self):
+        super(JobOverviewTests, self).setUp()
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+
+    def start_job(self):
+        # helper that start a job in background and returns the job
+        manager = getUtility(IJobManager)
+        job = AsyncJob(dummy_func)
+        manager.put(job, site=self.getRootFolder())
+        return job
+
+    def test_job_get(self):
+        # we can get the overview page
+        self.browser.open(self.overview_path)
+        self.assertTrue("Running and Completed Jobs" in self.browser.contents)
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        return
+
+    def test_anonymous_forbidden(self):
+        # anonymous user will be kept out
+        browser = Browser() # get a browser w/o credentials set
+        browser.handleErrors = False
+        self.assertRaises(Unauthorized, browser.open, self.overview_path)
+        return
+
+    def test_job_reload(self):
+        # we can reload the overview page
+        self.browser.open(self.overview_path)
+        self.browser.getControl('reload').click()
+        self.assertTrue("Running and Completed Jobs" in self.browser.contents)
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        return
+
+    def test_jobs_appear(self):
+        # any job running will appear
+        self.browser.open(self.overview_path)
+        job = self.start_job()
+        wait_for_result(job)
+        self.browser.getControl('reload').click()
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        self.assertTrue('status: Completed' in self.browser.contents)
+        return
+
+    def test_job_removal(self):
+        # we can remove jobs via the overview page
+        self.browser.open(self.overview_path)
+        job = self.start_job()
+        wait_for_result(job)
+        self.browser.getControl('reload').click()
+        self.assertEqual(len(list(getUtility(IJobManager).jobs())), 1)
+        self.browser.getControl('Remove').click()
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        self.assertTrue('Removed job' in self.browser.contents)
+        self.assertEqual(len(list(getUtility(IJobManager).jobs())), 0)
+        return
+
+class DataCenterJSONTests(AsyncBrowserTests):
+
+    def test_status_plain(self):
+        # if no job was triggered, no need to update something.
+        path = '%s/status' % self.datacenter_path
+        self.browser.open(path)
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/json')
+        self.assertEqual(self.browser.contents,
+                         '{"reload": false, "interval": 0, "html": ""}')
+        return
+
+    def test_status_completed(self):
+        # if all jobs are completed, we should reload the page
+        self.app[
+            'datacenter'].get_export_jobs_status = fake_get_export_jobs_status
+        path = '%s/status' % self.datacenter_path
+        self.browser.open(path)
+        self.assertEqual(self.browser.contents,
+                         '{"reload": true, "interval": 0, "html": ""}')
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_captcha.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_captcha.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_captcha.py	(revision 12677)
@@ -0,0 +1,289 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+from zope.component import getAdapter, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.interface import verify
+from zope.publisher.browser import TestRequest
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.browser.captcha import (
+    CaptchaResponse, CaptchaRequest, NullCaptcha, StaticCaptcha, ReCaptcha,
+    CaptchaManager)
+from waeup.ikoba.browser.interfaces import (
+    ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
+    ICaptchaManager)
+
+
+class CaptchaResponseTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces
+        obj = CaptchaResponse(True)
+        verify.verifyClass(ICaptchaResponse, CaptchaResponse)
+        verify.verifyObject(ICaptchaResponse, obj)
+        return
+
+    def test_init_vals(self):
+        # make sure the initial values are stored
+        resp = CaptchaResponse(False, 'some-msg')
+        self.assertEqual(resp.is_valid, False)
+        self.assertEqual(resp.error_code, 'some-msg')
+        return
+
+class CaptchaRequestTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfces
+        obj = CaptchaRequest(None)
+        verify.verifyClass(ICaptchaRequest, CaptchaRequest)
+        verify.verifyObject(ICaptchaRequest, obj)
+        return
+
+    def test_init_vals(self):
+        # make sure initial values are stored
+        req = CaptchaRequest('my-solution', 'my-challenge')
+        self.assertEqual(req.solution, 'my-solution')
+        self.assertEqual(req.challenge, 'my-challenge')
+        return
+
+class CaptchaTestBase(object):
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces
+        obj = self.factory()
+        verify.verifyClass(ICaptcha, self.factory)
+        verify.verifyObject(ICaptcha, obj)
+        return
+
+    def test_utility(self):
+        # the null captcha is also registered as a utility for ICaptcha
+        captcha = getUtility(ICaptcha, name=self.name)
+        self.assertTrue(isinstance(captcha, self.factory))
+        return
+
+class NullCaptchaTests(FunctionalTestCase, CaptchaTestBase):
+
+    layer = FunctionalLayer
+
+    factory = NullCaptcha
+    name = 'No captcha'
+
+    def test_verify(self):
+        # null captchas accept any input request
+        captcha = self.factory()
+        result1 = captcha.verify(TestRequest())
+        result2 = captcha.verify(
+            TestRequest(form={'solution': 'a', 'challenge': 'b'}))
+        self.assertEqual(result1.is_valid, True)
+        self.assertEqual(result2.is_valid, True)
+        return
+
+    def test_display(self):
+        # null captchas do not generate additional HTML code
+        captcha = self.factory()
+        result = captcha.display()
+        self.assertEqual(result, u'')
+        return
+
+class StaticCaptchaTests(FunctionalTestCase, CaptchaTestBase):
+
+    layer = FunctionalLayer
+
+    factory = StaticCaptcha
+    name = 'Testing captcha'
+
+    def test_verify(self):
+        # id captchas accept any solution that matches challenge and
+        # is not empty or None
+        captcha = self.factory()
+        request1 = TestRequest(form={'solution': 'my-sol',
+                                     'challenge': 'my-challenge'})
+        request2 = TestRequest()
+        request3 = TestRequest(form={'solution': '', 'challenge': ''})
+        request4 = TestRequest(form={'solution': 'my-sol',
+                                     'challenge': 'my-sol'})
+        result1 = captcha.verify(request1)
+        result2 = captcha.verify(request2)
+        result3 = captcha.verify(request3)
+        result4 = captcha.verify(request4)
+        self.assertEqual(result1.is_valid, False)
+        self.assertEqual(result2.is_valid, False)
+        self.assertEqual(result3.is_valid, False)
+        self.assertEqual(result4.is_valid, True)
+        return
+
+    def test_display(self):
+        # id captchas provide a simple HTML input
+        captcha = self.factory()
+        result = captcha.display()
+        self.assertMatches(
+            '<input type="hidden" name="challenge"'
+            '      value="..." /><br />Type: ...<br />'
+            '<input type="text" name="solution" value="the-solution" /><br />',
+            result)
+        return
+
+class ReCaptchaTests(FunctionalTestCase, CaptchaTestBase):
+
+    layer = FunctionalLayer
+
+    factory = ReCaptcha
+    name = 'ReCaptcha'
+
+    def DISABLEDtest_verify(self):
+        # recaptcha verification cannot be tested easily. As the
+        # solution and other environment parameters is only known to
+        # the remote server, we cannot guess on server side what the
+        # solution is.  Further more this test contacts a remote
+        # server so that we might want to disable this test by
+        # default.
+        captcha = self.factory()
+        request1 = TestRequest(
+            form={'recaptcha_challenge_field': 'my-challenge',
+                  'recaptcha_response_field': 'my-solution'})
+        result1 = captcha.verify(request1)
+        self.assertEqual(result1.is_valid, False)
+        self.assertEqual(result1.error_code, 'invalid-request-cookie')
+        return
+
+    def test_display(self):
+        # recaptchas provide the pieces to trigger the remote API
+        captcha = self.factory()
+        result = captcha.display()
+        self.assertMatches(
+            '<script type="text/javascript" src="..."></script>'
+            '<noscript>'
+            '<iframe src="..." height="300" width="500" '
+            '        frameborder="0"></iframe><br />'
+            '<textarea name="recaptcha_challenge_field" rows="3" '
+            '          cols="40"></textarea>'
+            '<input type="hidden" name="recaptcha_response_field" '
+            '       value="manual_challenge" /></noscript>',
+            result)
+        return
+
+class CaptchaManagerTests(FunctionalTestCase):
+    # These tests do not require a site setup
+    # See testsuite below for insite tests.
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces
+        obj = CaptchaManager()
+        verify.verifyClass(ICaptchaManager, CaptchaManager)
+        verify.verifyObject(ICaptchaManager, obj)
+        return
+
+    def test_utility(self):
+        # the global captcha chooser is registered as a global utility
+        result = getUtility(ICaptchaManager)
+        self.assertTrue(isinstance(result, CaptchaManager))
+        return
+
+    def test_get_avail_captchas(self):
+        # we can get a dict of available captchas.
+        chooser = getUtility(ICaptchaManager)
+        result = dict(chooser.getAvailCaptchas())
+        no_captcha = result.get('No captcha', None)
+        self.assertTrue(no_captcha is not None)
+        default_captcha = result.get(u'', None)
+        self.assertTrue(default_captcha is None)
+        return
+
+    def test_get_captcha_wo_site(self):
+        # if there is no site set, we get the default captcha
+        setSite(None)
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is default)
+        return
+
+class FakeSite(grok.Site, grok.Container):
+    pass
+
+class FakeConfiguration(object):
+    captcha = None
+
+    def __init__(self, captcha=None):
+        self.captcha = captcha
+
+class CaptchaManagerTestsWithSite(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CaptchaManagerTestsWithSite, self).setUp()
+        self.getRootFolder()['app'] = FakeSite()
+        self.site = self.getRootFolder()['app']
+        return
+
+    def tearDown(self):
+        super(CaptchaManagerTestsWithSite, self).tearDown()
+        clearSite(self.site)
+        return
+
+    def test_get_captcha_no_captcha_in_config(self):
+        # if a site has no configuration setting for captchas, we get
+        # the default captcha
+        setSite(self.site)
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is default)
+        return
+
+    def test_get_captcha_empty_captcha_in_config(self):
+        # if a site has None as configuration setting for captchas, we get
+        # the default captcha
+        setSite(self.site)
+        self.site['configuration'] = dict()
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is default)
+        return
+
+    def test_get_captcha_invalid_captcha_in_config(self):
+        # if a site has None as configuration setting for captchas, we get
+        # the default captcha
+        setSite(self.site)
+        self.site['configuration'] = FakeConfiguration(captcha='invalid name')
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is default)
+        return
+
+    def test_get_captcha_invalid_captcha_in_config(self):
+        # if a site has None as configuration setting for captchas, we get
+        # the default captcha
+        setSite(self.site)
+        self.site['configuration'] = FakeConfiguration(captcha='No captcha')
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        no_captcha = getUtility(ICaptcha, name='No captcha')
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is not default)
+        self.assertTrue(result is no_captcha)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_doctests.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_doctests.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_doctests.py	(revision 12677)
@@ -0,0 +1,28 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Setup doctests for browser module.
+"""
+from waeup.ikoba.testing import get_doctest_suite
+
+def test_suite():
+    suite = get_doctest_suite([
+        'browser/batchprocessing.txt',
+        'browser/breadcrumbs.txt',
+        'browser/browser.txt',
+        ])
+    return suite
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_file.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_file.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_file.csv	(revision 12677)
@@ -0,0 +1,1 @@
+empty file
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_pdf.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_pdf.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_pdf.py	(revision 12677)
@@ -0,0 +1,541 @@
+import os
+import tempfile
+import unittest
+from reportlab.lib import colors
+from reportlab.platypus.flowables import PageBreak
+from waeup.ikoba.browser.pdf import (
+    format_html, normalize_signature, format_signatures,
+    vert_signature_cell, horiz_signature_cell, signature_row, get_sig_tables,
+    PDFCreator, Paragraph, Table, NORMAL_STYLE, sig_table,
+    get_signature_tables)
+
+DRAW_SAMPLE_GRID = True
+
+class HelperTest(unittest.TestCase):
+
+    def test_format_html(self):
+        # test format_html()
+        intext = 'Two\nLines'
+        self.assertEqual(format_html(intext), 'Two<br />Lines')
+        intext = '<div>Two</div>\nLines'
+        self.assertEqual(format_html(intext), '<div>Two</div><br />Lines')
+        intext = '<div>Two</div>Lines'
+        self.assertEqual(format_html(intext), '<div>Two</div><br />Lines')
+        return
+
+class SignatureNormalizerTests(unittest.TestCase):
+    # Tests for normalize_signature()
+
+    def test_invalid_input_type(self):
+        # we accept only strings and tuples
+        self.assertRaises(
+            # we don't accept number, for instance
+            ValueError, normalize_signature, 12)
+        return
+
+    def test_invalid_number_of_items(self):
+        # a signature tuple must contain 1, 2, or 3 elements
+        self.assertRaises(
+            # empty tuple
+            ValueError, normalize_signature, ())
+        self.assertRaises(
+            # overcrowded tuple
+            ValueError, normalize_signature, ('a', 'b', 'c', 'd'))
+        return
+
+    def test_simple_string(self):
+        # we accept a simple string
+        self.assertEqual(
+            normalize_signature('sig1'),
+            ((None, 'sig1', None)))
+        # also unicode is allowed
+        self.assertEqual(
+            normalize_signature(u'sig1'),
+            ((None, u'sig1', None)))
+
+    def test_one_item_tuple(self):
+        # we accept a tuple with a single signature string
+        self.assertEqual(
+            normalize_signature(('sig1',)),
+            ((None, 'sig1', None)))
+        return
+
+    def test_two_items_tuple(self):
+        # we accept a tuple with two items
+        self.assertEqual(
+            normalize_signature(('pre-text', 'sig1')),
+            (('pre-text', 'sig1', None)))
+        return
+
+    def test_three_items_tuple(self):
+        # we accept a tuple with three items, of course
+        self.assertEqual(
+            normalize_signature(('pre-text', 'sig1', 'post-text')),
+            (('pre-text', 'sig1', 'post-text')))
+        return
+
+class SignaturesFormatterTests(unittest.TestCase):
+
+    def test_single_signature(self):
+        # we can format a single signature (w/o pre or post blocks)
+        data, style = format_signatures(['My Signature'])[0]
+        self.assertEqual(
+            data, [['Date', '', 'My Signature', '']])
+        #self.assertEqual(style, )
+        return
+
+    def test_single_sig_with_pre_text(self):
+        # we can format a single signature (with pre-text)
+        data, style = format_signatures([('Pre-Text', 'My Sig', None)])[0]
+        self.assertEqual(
+            data,
+            [['Pre-Text', '', '', ''],
+             ['Date', '', 'My Sig', '']])
+        return
+
+    def test_single_sig_with_post_text(self):
+        # we can format a single signature (with post-text)
+        data, style = format_signatures([(None, 'My Sig', 'Post-Text')])[0]
+        self.assertEqual(
+            data,
+            [['Date', '', 'My Sig', ''],
+             ['Post-Text', '', '', '']])
+        return
+
+    def test_single_sig_with_pre_and_post_text(self):
+        # we can format a single signature (with pre- and post-text)
+        data, style = format_signatures([('Pre', 'My Sig', 'Post')])[0]
+        self.assertEqual(
+            data,
+            [['Pre', '', '', ''],
+             ['Date', '', 'My Sig', ''],
+             ['Post', '', '', '']])
+        return
+
+class SignatureCellFormatterTests(unittest.TestCase):
+
+    def test_vert_signature(self):
+        # we can create vertical signatures
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'))
+        self.assertEqual(
+            data,
+            [['Pre'], ['Date:'], ['Sig'], ['Post']])
+        self.assertEqual(
+            style,
+            (('LINEABOVE', (0, 2), (0, 2), 1, colors.black),)
+            )
+        return
+
+    def test_vert_signature_custom_row_and_col(self):
+        # custom row and col nums are respected
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), start_row=5, start_col=3)
+        self.assertEqual(
+            style,
+            (('LINEABOVE', (3, 7), (3, 7), 1, colors.black),)
+            )
+        return
+
+    def test_vert_signature_no_underline(self):
+        # we can suppress the underlining of signature cell
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), underline=False)
+        self.assertEqual(
+            style, ())
+        return
+
+    def test_vert_signature_custom_date(self):
+        # we can create vertical signatures with customized date text
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_text='My Date')
+        self.assertEqual(
+            data,
+            [['Pre'], ['My Date'], ['Sig'], ['Post']])
+        return
+
+    def test_vert_signature_no_date(self):
+        # we can create vertical signatures without date
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_field=False)
+        self.assertEqual(
+            data,
+            [['Pre'], [''], ['Sig'], ['Post']])
+        return
+
+    def test_vert_signature_no_pre(self):
+        # we can create vertical signatures w/o pre-text
+        data, style, cols = vert_signature_cell(
+            (None, 'Sig', 'Post'))
+        self.assertEqual(
+            data,
+            [[''], ['Date:'], ['Sig'], ['Post']])
+        return
+
+    def test_vert_signature_no_post(self):
+        # we can create vertical signatures w/o post-text
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', None))
+        self.assertEqual(
+            data,
+            [['Pre'], ['Date:'], ['Sig'], ['']])
+        return
+
+    def test_horiz_signature_cell(self):
+        # we can create horizontal signatures
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'))
+        self.assertEqual(
+            data, [['Pre', '', ''],
+                   ['', '', ''],
+                   ['Date', '', 'Sig'],
+                   ['Post', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+             ))
+        return
+
+    def test_horiz_signature_cell_no_date(self):
+        # we can create horizontal signatures without date
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_field=False)
+        self.assertEqual(
+            data, [['Pre'], [''], ['Sig'], ['Post']])
+        self.assertEqual(
+            style, (('LINEABOVE', (0, 2), (0, 2), 1, colors.black),))
+        return
+
+    def test_horiz_signature_cell_custom_date(self):
+        # we can create horizontal signatures with custom date text
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_text='My Date')
+        self.assertEqual(
+            data, [['Pre', '', ''],
+                   ['', '', ''],
+                   ['My Date', '', 'Sig'],
+                   ['Post', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ))
+        return
+
+    def test_horiz_signature_cell_no_pre(self):
+        # we can create horizontal signatures without pre-text
+        data, style, cols = horiz_signature_cell(
+            (None, 'Sig', 'Post'))
+        self.assertEqual(
+            data, [['', '', ''],
+                   ['', '', ''],
+                   ['Date', '', 'Sig'],
+                   ['Post', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ))
+        return
+
+    def test_horiz_signature_cell_no_post(self):
+        # we can create horizontal signatures without post-text
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', None))
+        self.assertEqual(
+            data, [['Pre', '', ''],
+                   ['', '', ''],
+                   ['Date', '', 'Sig'],
+                   ['', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ))
+        return
+
+    def test_horiz_signature_cell_custom_row_and_col(self):
+        # custom col and row numbers are respected in styles
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'), start_row=3, start_col=5)
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (5, 3), (7, 3)),
+                ('SPAN', (5, 6), (7, 6)),
+                ('LINEABOVE', (5, 5), (5, 5), 1, colors.black),
+                ('LINEABOVE', (7, 5), (7, 5), 1, colors.black),
+                ))
+        return
+
+class SignatureRowFormatterTests(unittest.TestCase):
+
+    def test_vertical_row(self):
+        data, style, cols = signature_row(
+            ['Sig 1', 'Sig 2',])
+        self.assertEqual(
+            data, [['', '', ''], ['Date:', '', 'Date:'],
+                   ['Sig 1', '', 'Sig 2'], ['', '', '']]
+            )
+        self.assertEqual(
+            style, (
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ('TOPPADDING', (0, 2), (-1, 2), 0),
+                ('BOTTOMPADDING', (0, 1), (-1, 1), 36),
+                ('LEFTPADDING', (0, 0), (-1, 0), 1),
+                ('LEFTPADDING', (0, 3), (-1, 3), 1),
+                ('TOPPADDING', (0, 0), (-1, 0), -6),
+                ('BOTTOMPADDING', (0, 0), (-1, 0), -6),
+                ('TOPPADDING', (0, 3), (-1, 3), -6),
+                ('BOTTOMPADDING', (0, 3), (-1, 3), -6)
+                )
+            )
+        return
+
+def samples_dir():
+    # create a directory for sample sheets, if it doesn't exist,
+    # and return its path.
+    tmp_dir = os.path.join(
+        tempfile.gettempdir(), 'waeup.ikoba.pdf-samples')
+    if not os.path.exists(tmp_dir):
+        os.mkdir(tmp_dir)
+    return tmp_dir
+
+def draw_sample_pdf(data, name='signatures.pdf',
+                    title='Sample Title', author='Test Author'):
+    # create a pdf document with data as content
+    #data = [Paragraph('<b>Sample</b>', NORMAL_STYLE)]
+    creator = PDFCreator()
+    pdf = creator.create_pdf(
+        data, title=title, author=author)
+    path = os.path.join(samples_dir(), name)
+    open(path, 'wb').write(pdf)
+    return path
+
+
+class SampleSheet(unittest.TestCase):
+
+    def add_tables(self, data, title, tables, grid=DRAW_SAMPLE_GRID):
+        # add tables to `data`
+        mod_tables = []
+        data += [Paragraph(title, NORMAL_STYLE),]
+        for table in tables:
+            if grid:
+                table.setStyle((
+                    ('INNERGRID',(0,0),(-1,-1), 0.25, colors.blue),
+                    ('BOX', (0,0), (-1, -1), 0.25, colors.blue)),)
+            data += [table,]
+        return data
+
+
+    def test_draw_sample(self):
+        # create a sample sheet as PDF
+        # single signature
+        data = [
+            Paragraph('Signature Samples in here are created by '
+                      'calling the '
+                      '<font face="Times">get_signature_tables()</font> '
+                      'function.<br />'
+                      'Grids are drawn for better visibility of '
+                      'internal table structure only. You can set '
+                      'DRAW_SAMPLE_GRID value in browser/tests/test_pdf.py.'
+                      '<br /><br />',
+                      NORMAL_STYLE),
+            ]
+
+        tables = get_signature_tables(['Signature 1'])
+        data = self.add_tables(
+            data, '<b>Single Signature:</b> '
+            '<font face="Times">get_signature_tables(["Signature 1"])</font>',
+            tables)
+
+        # two signatures in a row
+        tables = get_signature_tables(['Signature 1', 'Signature 2'])
+        data = self.add_tables(
+            data, '<br /><b>Two Signatures:</b> '
+            '<font face="Times">get_signature_tables(["Signature 1",'
+            ' "Signature 2"])</font></b>',
+            tables)
+
+        # single signature with pre/post text
+        tables = get_signature_tables(
+            [('Pre-Text', 'Signature 1', 'Post-Text')])
+        data = self.add_tables(
+            data,
+            '<br /><b>Single Signature with Pre- and Post Text:</b> '
+            '<font face="Times">get_signature_tables('
+            '[("Pre-Text", "Signature 1", "Post-Text")])</font>',
+            tables)
+
+        # three signatures
+        tables = get_signature_tables(['Sig 1', 'Sig 2', 'Sig 3'])
+        data = self.add_tables(
+            data,
+            '<br /><b>Three Signatures:</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3"])</font>',
+            tables)
+
+        # four signatures
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'])
+        data = self.add_tables(
+            data,
+            '<br /><b>Sample: Four Signatures (each row a table)</b>',
+            tables)
+
+        # four sigs in one table
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], single_table=True)
+        data = self.add_tables(
+            data,
+            '<br /><b>Sample: Four Signatures (as one table)</b>',
+            tables)
+
+
+        data += [PageBreak(), Paragraph(
+            '<br /><br /><font size="14"><font face="Times">horizontal</font>'
+            ' Parameter (with one sig): </b><br />',
+            NORMAL_STYLE)]
+
+        # one signature (default behaviour)
+        tables = get_signature_tables(
+            ['Sig 1'], horizontal=None)
+        data = self.add_tables(
+            data, '<br /><b>One Signature (default, horizontal=None):</b> '
+            '<font face="Times">get_signature_tables(["Signature 1"], '
+            'horizontal=None)</font></b>',
+            tables)
+
+        # one signature (horizontal)
+        tables = get_signature_tables(
+            ['Sig 1'], horizontal=True)
+        data = self.add_tables(
+            data, '<br /><b>One Signature (horizontal=True):</b> '
+            '<font face="Times">get_signature_tables(["Signature 1"], '
+            'horizontal=True)</font></b>',
+            tables)
+
+        # one signature (vertical)
+        tables = get_signature_tables(
+            ['Sig 1'], horizontal=False)
+        data = self.add_tables(
+            data, '<br /><b>One Signature (horizontal=False):</b> '
+            '<font face="Times">get_signature_tables(["Signature 1"], '
+            'horizontal=False)</font></b>',
+            tables)
+
+        data += [Paragraph(
+            '<br /><br /><font size="14"><font face="Times">horizontal</font>'
+            ' Parameter (with two sigs): </b><br />',
+            NORMAL_STYLE)]
+
+        # two signatures (default behaviour)
+        tables = get_signature_tables(
+            ['Signature 1', 'Signature 2'], horizontal=None)
+        data = self.add_tables(
+            data, '<br /><b>Two Signatures (default, horizontal=None):</b> '
+            '<font face="Times">get_signature_tables(["Signature 1",'
+            ' "Signature 2"])</font></b>',
+            tables)
+
+        # two sigs, both horizontal
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2'], horizontal=True)
+        data = self.add_tables(
+            data,
+            '<br /><b>Two Signatures (horizontal=True):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2"], horizontal=True)</font>',
+            tables)
+
+        # two sigs, both vertical
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2'], horizontal=False)
+        data = self.add_tables(
+            data,
+            '<br /><b>Two Signatures (vertical, horizontal=False):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2"], horizontal=False)</font>',
+            tables)
+
+
+        data += [PageBreak(), Paragraph(
+            '<br /><br /><font size="14"><font face="Times">max_per_row</font>'
+            ' Parameter: </b><br />',
+            NORMAL_STYLE)]
+
+        # four signature (default)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'])
+        data = self.add_tables(
+            data,
+            '<br /><b>Four Sigs (default, max_per_row=3):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"])</font>',
+            tables)
+
+        # four signature (max_per_row=1)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], max_per_row=1)
+
+        data = self.add_tables(
+            data,
+            '<br /><b>Four Sigs (max_per_row=1):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"], max_per_row=1)</font>'
+            '<br /><i>Please note: mode switches from vertical to '
+            'horizontal automatically</i>',
+            tables)
+
+        # four signature (max_per_row=2)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], max_per_row=2)
+        data = self.add_tables(
+            data,
+            '<br /><b>Four Sigs (max_per_row=2):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"], max_per_row=2)</font>',
+            tables)
+
+        data += [PageBreak()]
+
+        # four signature (max_per_row=4)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], max_per_row=4)
+        data = self.add_tables(
+            data,
+            '<b>Four Sigs (max_per_row=4):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"], max_per_row=4)</font>',
+            tables)
+
+        # four signature (max_per_row=5)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], max_per_row=5)
+        data = self.add_tables(
+            data,
+            '<br /><b>Four Sigs (max_per_row=5):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"], max_per_row=5)</font>',
+            tables)
+
+
+        # create PDF from data
+        path = draw_sample_pdf(data, title=u"Signatures Sample Sheet")
+        print "Sample PDF written to %s" % path
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_permissions.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_permissions.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/tests/test_permissions.py	(revision 12677)
@@ -0,0 +1,96 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.ikoba.app import Company
+from waeup.ikoba.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',
+    '/users/@@index', '/users/@@add', '/users/alice/@@manage',
+    '/datacenter/@@index', '/datacenter/@@upload', '/datacenter/@@import1',
+    '/datacenter/@@import2', '/datacenter/@@import3', '/datacenter/@@import4',
+    '/datacenter/@@logs', '/datacenter/@@show', '/datacenter/@@manage',
+    ]
+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 company to have every page available...
+        app = Company()
+        self.getRootFolder()['app'] = app
+        setSite(self.getRootFolder()['app'])
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+        app['users'].addUser('alice', 'alice')
+
+        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.ikoba/branches/uli-payments/src/waeup/ikoba/browser/viewlets.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/viewlets.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/browser/viewlets.py	(revision 12677)
@@ -0,0 +1,602 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+from urllib import urlencode
+from zope.i18n import translate
+from zope.component import getMultiAdapter, queryAdapter, getUtility
+from zope.interface import Interface
+from zope.location.interfaces import ISite
+from zope.traversing.browser import absoluteURL
+from waeup.ikoba.browser.pages import (
+    CompanyPage, DatacenterPage,
+    UsersContainerPage, UserManageFormPage)
+from waeup.ikoba.browser.interfaces import (
+    IBreadcrumbContainer, ICompany, IUsersContainer)
+from waeup.ikoba.interfaces import (
+    IIkobaUtils, IIkobaObject, IIkobaXMLExporter,
+    IIkobaXMLImporter, IDataCenter, IUserAccount)
+from waeup.ikoba.browser.layout import IkobaPage, default_primary_nav_template
+from waeup.ikoba.utils.helpers import get_user_account
+
+from waeup.ikoba.interfaces import MessageFactory as _
+
+grok.templatedir('templates')
+grok.context(IIkobaObject) # Make IIkobaObject the default context
+
+class ManageSidebar(grok.ViewletManager):
+    grok.name('left_manage')
+
+class BreadCrumbManager(grok.ViewletManager):
+    grok.name('breadcrumbs')
+
+class LanguageManager(grok.ViewletManager):
+    grok.name('languages')
+
+class ActionBar(grok.ViewletManager):
+    grok.name('actionbar')
+
+class AdministrationTasks(grok.ViewletManager):
+    grok.name('admintasks')
+
+class WidgetsSlot(grok.ViewletManager):
+    grok.name('widgets')
+
+
+#
+# Baseclasses that give some defaults for really used viewlets.
+#
+class ActionButton(grok.Viewlet):
+    """A base for action buttons.
+
+    An action button provides an icon, some text, links to a
+    target and optionally an onclick event handler.
+    If you want to set a different text, icon or target name
+    for some active button below, just override the approriate
+    attribute in the concerned viewlet.
+
+    Action buttons provide by default dynamic attributes
+
+     * ``alt``
+          An alternative text for the icon. By default the same as
+          the text.
+
+     * ``icon_url``
+          The URL of the icon.
+
+     * ``target_url``
+          The URL of the link target.
+
+     * ``onclick``
+          An onclick Javascript event handler.
+
+    """
+    grok.baseclass()
+    grok.context(IIkobaObject)
+    grok.viewletmanager(ActionBar)
+    icon = 'actionicon_modify.png' # File must exist in static/
+    target = '@@manage' # link to this viewname.
+    text = _('Edit') # Text to display on the button
+
+    # We set the template file explicitly (instead of using
+    # ``grok.template('actionbutton')``) to stick with this template
+    # also in derived classes in other packages. If we didn't, those
+    # derived ActionButton viewlets had to provide an own template,
+    # which would not be updated automatically, when the local
+    # template ``templates/actionbutton.pt`` changes.
+    #
+    # Inheriting viewlets that wish to use their own template anyway
+    # can do so by setting their local ``grok.template(<mytemplate>)``
+    # and setting ``template`` to ``None`` for the class::
+    #
+    # class DerivedActionButton(ActionButton):
+    #   ...
+    #   grok.template('overriding_template')
+    #   template = None
+    #   ...
+    #
+    template = grok.PageTemplateFile('templates/actionbutton.pt')
+
+    @property
+    def alt(self):
+        """Alternative text for icon.
+        """
+        return self.text
+
+    @property
+    def icon_url(self):
+        """Get the icon URL.
+        """
+        return '/static/img/%s' % self.icon
+
+    @property
+    def target_url(self):
+        """Get a URL to the target...
+        """
+        if self.target:
+            return self.view.url(self.view.context, self.target)
+        return
+
+    @property
+    def onclick(self):
+        """Onclick event...
+        """
+        return
+
+class PlainActionButton(ActionButton):
+    """A base for action buttons without image
+    """
+    grok.baseclass()
+    template = grok.PageTemplateFile('templates/plainactionbutton.pt')
+
+    
+class ManageActionButton(ActionButton):
+    """A base for 'edit' buttons
+    """
+    grok.baseclass()
+    grok.order(2)
+    grok.require('waeup.manageAcademics')
+    icon = 'actionicon_modify.png'
+    target = '@@manage'
+    text = _('Edit')
+
+class AddActionButton(ActionButton):
+    """A base for 'add' buttons.
+    """
+    grok.baseclass()
+    grok.order(4)
+    grok.require('waeup.manageAcademics')
+    icon = 'actionicon_add.png'
+    target = 'add'
+    text = _('Add')
+    
+class RemoveActionButton(ActionButton):
+    """A base for 'remove' buttons.
+    """
+    grok.baseclass()
+    grok.order(4)
+    grok.require('waeup.manageAcademics')
+    icon = 'actionicon_delete.png'
+    target = 'remove'
+    text = _('Remove')
+
+class SearchActionButton(ActionButton):
+    """A base for 'search' buttons.
+    """
+    grok.baseclass()
+    grok.order(5)
+    grok.require('waeup.manageAcademics')
+    icon = 'actionicon_search.png'
+    target = 'search'
+    text = _('Search')
+
+
+#
+# General viewlets (for more than one page/context)
+#
+
+class BreadCrumbs(grok.Viewlet):
+    grok.context(IIkobaObject)
+    grok.viewletmanager(BreadCrumbManager)
+    grok.order(1)
+
+    def getEntries(self):
+        result = []
+        site = grok.getSite()
+        context = self.context
+        breadcrumbs = IBreadcrumbContainer(self.view)
+        for breadcrumb in breadcrumbs:
+            if breadcrumb.target is None:
+                yield dict(
+                    title = breadcrumb.title,
+                    url = self.view.url(breadcrumb.context)
+                    )
+            elif breadcrumb.target:
+                yield dict(
+                    title = breadcrumb.title,
+                    url = self.view.url(breadcrumb.context, breadcrumb.target)
+                    )
+
+class LanguagesLink(grok.Viewlet):
+    """ The language selector itself.
+    """
+    grok.viewletmanager(LanguageManager)
+    grok.context(IIkobaObject)
+    grok.require('waeup.Public')
+    title = u'Languages'
+
+    def render(self):
+        preferred_languages = getUtility(IIkobaUtils).PREFERRED_LANGUAGES_DICT
+        html = u''
+        for key, value in sorted(
+            preferred_languages.items(), key=lambda lang: lang[1][0]):
+            args = {'lang':key, 'view_name':self.view.__name__}
+            url = self.view.url(
+                self.context) + '/@@change_language?%s' % urlencode(args)
+            html += u'| <a href="%s" title="%s">%s</a> ' % (url, value[1], key)
+        return html
+
+# Problem with circular references. Disabled for now...
+# class ExportXMLAction(grok.Viewlet):
+#    grok.viewletmanager(ActionBar)
+#     #grok.view(Index)
+#     grok.order(98)
+#     grok.require('waeup.managePortal')
+
+#class ImportXMLAction(grok.Viewlet):
+#    grok.viewletmanager(ActionBar)
+#    #grok.view(Index)
+#    grok.order(99)
+#    grok.require('waeup.managePortal')
+#
+#    def update(self):
+#        # We cannot simply replace local sites.
+#        self.can_import = not ISite.providedBy(self.context)
+
+
+class WidgetsTableRows(grok.Viewlet):
+    """The only viewlet for the WidgetsSlot viewlet manager.
+    """
+    template = grok.PageTemplateFile('templates/widgets.pt')
+    grok.viewletmanager(WidgetsSlot)
+
+
+class Login(grok.Viewlet):
+    """This viewlet allows to login in the sidebar.
+    """
+    grok.viewletmanager(ManageSidebar)
+    grok.context(IIkobaObject)
+    grok.view(Interface)
+    grok.order(2)
+    grok.require('waeup.Anonymous')
+    text = _('Login')
+    link = 'login'
+
+    def render(self):
+        if self.request.principal.id != 'zope.anybody':
+            return ''
+        url = self.view.url(grok.getSite(), self.link)
+        return u'<li"><a href="%s">%s</a></li>' % (
+                url, self.text)
+
+
+class ManageLink(grok.Viewlet):
+    """A link displayed in the upper left box.
+
+    This viewlet renders a link to the application object's settings
+    form (the 'manage' view).
+
+    In derived classes you can create different links by setting a
+    different link and text attribute. The `link` parameter is
+    understood relative to the respective application object, so that
+    ``@@manage`` will create a link to
+    ``localhost:8080/app/@@manage``.
+
+    Links defined by descendants from this viewlet are displayed on
+    every page the user is allowed to go to, if the user has also the
+    permissions set by `grok.require()`. By default only users with
+    ``waeup.managePortal`` permission will see links defined by
+    this or derivated classes.
+    """
+    grok.baseclass()
+    grok.viewletmanager(ManageSidebar)
+    grok.context(IIkobaObject)
+    grok.view(Interface)
+    grok.order(1)
+    # This link is only displayed, if the user is
+    # allowed to use it!
+    grok.require('waeup.managePortal')
+
+    link = 'relative_link'
+    text = _(u'Any link text')
+    
+    def render(self):
+        url = self.view.url(grok.getSite(), self.link)
+        text = translate(self.text, context=self.request)
+        return u'<li><a href="%s">%s</a></li>' % (
+                url, text)
+
+class ManagePortalConfiguration(ManageLink):
+    """A link to portal configuration.
+    """
+    grok.order(1)
+    grok.require('waeup.managePortalConfiguration')
+
+    link = 'configuration'
+    text = _(u'Portal Configuration')
+
+class ManageUsersLink(ManageLink):
+    """A link to users management, placed in upper left box.
+    """
+    grok.order(2)
+    grok.require('waeup.manageUsers')
+
+    link = u'users'
+    text = _(u'Officers')
+
+class ManageDataCenter(ManageLink):
+    """A link to datacenter, placed in upper left box.
+    """
+    grok.order(3)
+    grok.require('waeup.manageDataCenter')
+
+    link = u'datacenter'
+    text = _(u'Data Center')
+
+class ManageReports(ManageLink):
+    """A link to reports, placed in upper left box.
+    """
+    grok.order(4)
+    grok.require('waeup.manageReports')
+
+    link = u'reports'
+    text = _(u'Reports')
+
+class MyPreferences(ManageLink):
+    """A link to personal preferences, placed in upper left box.
+    """
+    grok.order(6)
+    grok.require('waeup.Public')
+    text = _(u'My Preferences')
+
+    def render(self):
+        account_object = get_user_account(self.request)
+        if account_object:
+            url = self.view.url(account_object)
+            text = translate(self.text, context=self.request)
+            return u'<li><a href="%s">%s</a></li>' % (
+                    url, text)
+        return ''
+
+class MyRoles(ManageLink):
+    """A link to display site and local roles.
+    """
+    grok.order(7)
+    grok.require('waeup.Public')
+    text = _(u'My Roles')
+
+    def render(self):
+        account_object = get_user_account(self.request)
+        if account_object:
+            url = self.view.url(account_object) + '/my_roles'
+            text = translate(self.text, context=self.request)
+            return u'<li><a href="%s">%s</a></li>' % (
+                    url, text)
+        return ''
+
+class ContactActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IUserAccount)
+    grok.view(UserManageFormPage)
+    grok.require('waeup.manageUsers')
+    icon = 'actionicon_mail.png'
+    text = _('Send email')
+    target = 'contactuser'
+
+class ManageDataCenterActionButton(ManageActionButton):
+    """ 'Edit settings' button for datacenter.
+    """
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.managePortal')
+    text = _('Edit settings')
+    grok.order(1)
+
+class AddUserActionButton(AddActionButton):
+    grok.require('waeup.manageUsers')
+    grok.context(IUsersContainer)
+    grok.view(UsersContainerPage)
+    text = _('Add user')
+
+class BrowseDatacenterLogs(ActionButton):
+    grok.context(IDataCenter)
+    grok.require('waeup.manageDataCenter')
+    grok.view(DatacenterPage)
+    grok.order(2)
+    icon = 'actionicon_info.png'
+    target = '@@logs'
+    text = _('Show logs')
+
+class UploadCSVButton(ActionButton):
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.manageDataCenter')
+    grok.order(3)
+    icon = 'actionicon_up.png'
+    target = '@@upload'
+    text = _('Upload data')
+
+class BatchOpButton(ActionButton):
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.manageDataCenter')
+    grok.order(4)
+    icon = 'actionicon_gear.png'
+    target = '@@import1'
+    text = _('Process data')
+
+class ExportCSVButton(ActionButton):
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.exportData')
+    grok.order(5)
+    icon = 'actionicon_down.png'
+    target = '@@export'
+    text = _('Export data')
+
+class BrowseFinishedFiles(ActionButton):
+    grok.context(IDataCenter)
+    grok.require('waeup.manageDataCenter')
+    grok.view(DatacenterPage)
+    grok.order(6)
+    icon = 'actionicon_finished.png'
+    target = '@@processed'
+    text = _('View processed files')
+
+#
+# Primary navigation tabs (in upper left navigation bar)...
+#
+class PrimaryNavManager(grok.ViewletManager):
+    """Viewlet manager for the primary navigation tab.
+    """
+    grok.name('primary_nav')
+
+class PrimaryNavTab(grok.Viewlet):
+    """Base for primary nav tabs.
+    """
+    grok.baseclass()
+    grok.viewletmanager(PrimaryNavManager)
+    grok.order(1)
+    grok.require('waeup.Public')
+    template = default_primary_nav_template
+
+    pnav = 0 # This is a kind of id of a tab. If some page provides
+             # also a 'pnav' attribute with the same value (here: 0),
+             # then the tab will be rendered as 'active' when the page
+             # gets rendered.
+             #
+             # This way you can assign certain pages to certain
+             # primary nav tabs. Each primary tab should therefore set
+             # the 'pnav' attribute to a different value (or several
+             # tabs might be rendered as active simultanously when the
+             # page gets rendered.
+    tab_title = u'Some Text'
+    
+    @property
+    def link_target(self):
+        return self.view.application_url()
+
+    @property
+    def active(self):
+        view_pnav = getattr(self.view, 'pnav', 0)
+        if view_pnav == self.pnav:
+            return 'active'
+        return ''
+
+#class HomeTab(PrimaryNavTab):
+#    """Home-tab in primary navigation.
+#    """
+#    grok.order(1)
+#    grok.require('waeup.Public')
+
+#    pnav = 0
+#    tab_title = u'Home'
+
+
+class EnquiriesTab(PrimaryNavTab):
+    """Contact tab in primary navigation.
+
+    Display tab only for anonymous. Authenticated users can call a
+    contact form from the user navigation bar.
+    """
+    grok.order(6)
+    grok.require('waeup.Anonymous')
+    tab_title = _(u'Enquiries')
+    pnav = 2
+
+    # Also zope.manager has role Anonymous.
+    # To avoid displaying this tab, we have to check the principal id too.
+    @property
+    def link_target(self):
+        if self.request.principal.id == 'zope.anybody':
+            return self.view.application_url('enquiries')
+        return
+
+#
+# Administration tasks
+#
+class AdminTask(grok.Viewlet):
+    """The base for task entries on administration page.
+    """
+    grok.baseclass()
+    grok.order(1)
+    grok.viewletmanager(AdministrationTasks)
+    grok.require('waeup.managePortal')
+    grok.template('admintask')
+
+    link_title = 'Manage users' # How the link to the target will be titled.
+    target_viewname = 'users'   # The name of the target view.
+    
+    @property
+    def link_target(self):
+        return self.view.url(self.context[self.target_viewname])
+
+class AdminTaskPortalConfiguration(AdminTask):
+    """Entry on administration page that link to portal settings.
+    """
+    grok.order(1)
+    grok.require('waeup.managePortalConfiguration')
+
+    link_title = _('Portal Configuration')
+    def link_target(self):
+        return self.view.url(self.view.context, 'configuration')
+
+class AdminTaskUsers(AdminTask):
+    """Entry on administration page that link to user folder.
+    """
+    grok.order(2)
+    grok.require('waeup.manageUsers')
+
+    link_title = _('Officers')
+    target_viewname = 'users'
+    
+class AdminTaskDatacenter(AdminTask):
+    """Entry on administration page that link to datacenter.
+    """
+    grok.order(3)
+    grok.require('waeup.manageDataCenter')
+
+    link_title = _('Data Center')
+    target_viewname = 'datacenter'
+
+class AdminTaskReports(AdminTask):
+    """Entry on administration page that link to datacenter.
+    """
+    grok.order(3)
+    grok.require('waeup.manageReports')
+
+    link_title = _('Reports')
+    target_viewname = 'reports'
+
+# The SubobjectLister and its viewlets below are not used in Ikoba.
+
+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
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/catalog.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/catalog.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/catalog.py	(revision 12677)
@@ -0,0 +1,142 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components to help cataloging and searching objects.
+"""
+import grok
+from hurry.query.interfaces import IQuery
+from hurry.query.query import Query
+from zope.catalog.catalog import ResultSet
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility, queryUtility
+from zope.interface import implementer
+from zope.intid.interfaces import IIntIds
+from waeup.ikoba.interfaces import (
+    IQueryResultItem, IFilteredQuery, IFilteredCatalogQuery)
+
+# not yet used
+@implementer(IQuery)
+class IkobaQuery(Query):
+    """A hurry.query-like query that supports also ``apply``.
+    """
+    def apply(self, query):
+        """Get a catalog's BTree set of intids conforming to a query.
+        """
+        return query.apply()
+
+    def searchResults(self, query):
+        """Get a set of ZODB objects conforming to a query.
+        """
+        results = self.apply(query)
+        if results is not None:
+            uidutil = getUtility(IIntIds)
+            results = ResultSet(results, uidutil)
+        return results
+
+grok.global_utility(IkobaQuery)
+
+# not yet used
+@implementer(IQueryResultItem)
+class QueryResultItem(object):
+    url = None
+    title = None
+    description = None
+
+    def __init__(self, context, view):
+        self.context = context
+        self.url = view.url(context)
+        self.title = context.title
+        self.description = ''
+
+@implementer(IFilteredQuery)
+class FilteredQueryBase(object):
+    """A filter to find objects that match certain parameters.
+
+    Parameters are passed to constructor as keyword arguments. The
+    real data retrieval then happens when `query()` is called.
+
+    The `defaults` attribute, a dict, can set certain default values
+    for parameters that are used if the constructor is called without
+    any parameters.
+    """
+    defaults = dict()
+
+    def __init__(self, **kw):
+        self._kw = dict(self.defaults)
+        self._kw.update(kw)
+        return
+
+    def query(self, context=None):
+        err_msg = 'class %s does not implement the query() method.' % (
+            self.__class__.__name__, )
+        raise NotImplementedError(err_msg)
+
+@implementer(IFilteredCatalogQuery)
+class FilteredCatalogQueryBase(FilteredQueryBase):
+    """Base for filtered queries based on catalog lookups.
+
+    This type of query asks a catalog to find objects.
+
+    You would normally use this type of query like this:
+
+      >>> query = FilteredCatalogQueryBase(name='bob')
+      >>> objects = query.query()
+
+    The name of the catalog to use can be set via `cat_name`
+    attribute.
+
+    Looked up are all objects that match keywords passed to
+    constructor where the keyword names must match a certain index of
+    the chosen catalog. So, if some catalog has indexes `name` and
+    `age`, then keywords `name='bob', age='12'` would search for all
+    objects with name ``bob`` and age ``12``.
+
+    This query supports single values (exact matches) and ranges of
+    values passed in via ``(min_value, max_value)`` tuples. So,
+    constructor keyword args `name=('a', 'd')` would find objects with
+    name ``alice``, ``bob``, ``d``, but not ``donald``, ``john``, or
+    ``zak``.
+    """
+    cat_name = None
+
+    def query_catalog(self, catalog):
+        """Search ``catalog``.
+
+        Use `catalog`, some ``Catalog`` instance, to search objects
+        denoted by constructor keywords.
+        """
+        query = dict()
+        for idx_name, value in self._kw.items():
+            if idx_name == 'catalog':
+                continue
+            if not isinstance(value, tuple):
+                value = (value, value)
+            query[idx_name] = value
+        result = catalog.searchResults(**query)
+        return result
+
+    def query(self):
+        """Perform a query with parameters passed to constructor.
+
+        Returns some iterable, normally a list or a catalog result
+        set.
+        """
+        catalog = queryUtility(
+            ICatalog, name=self.cat_name, default=None)
+        if catalog is None:
+            return []
+        return self.query_catalog(catalog)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/catalog.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/catalog.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/catalog.txt	(revision 12677)
@@ -0,0 +1,235 @@
+:mod:`waeup.ikoba.catalog` -- Cataloging support Ikoba
+****************************************************
+
+.. module:: waeup.ikoba.catalog
+
+Components that support cataloging and searching objects inside a
+Ikoba site.
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+.. contents::
+
+Classes
+=======
+
+:class:`IkobaQuery`
+------------------
+
+.. class:: IkobaQuery()
+
+   .. 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, `IkobaQuery` 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 IkobaQuery 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.ikoba.catalog.IkobaQuery 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 IkobaQuery (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
+``IkobaQuery``:
+
+    >>> 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.ikoba/branches/uli-payments/src/waeup/ikoba/configuration.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/configuration.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/configuration.py	(revision 12677)
@@ -0,0 +1,34 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Components for portal configuration.
+"""
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.ikoba.interfaces import IConfigurationContainer
+from waeup.ikoba.utils.helpers import attrs_to_fields
+
+class ConfigurationContainer(grok.Container):
+    """
+    The node which may contain further configuration models (see waeup.kofa).
+    """
+
+    grok.implements(IConfigurationContainer)
+
+ConfigurationContainer = attrs_to_fields(ConfigurationContainer)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/configure.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/configure.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/configure.zcml	(revision 12677)
@@ -0,0 +1,23 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:grok="http://namespaces.zope.org/grok"
+           xmlns:i18n="http://namespaces.zope.org/i18n">
+
+  <include package="waeup.ikoba" file="locales.zcml" />
+  <include package="grok" />
+  <include package="waeup.ikoba" file="meta.zcml" />
+  <includeDependencies package="." />
+
+  <!-- Comment the following line if you don't want dolmen.beaker,
+       for instance on Windows if you were not able to compile it
+
+       We have to configure dolmen.beaker manually, because it is
+       not part of installation requirements and will therefore not
+       be found by the includeDependencies directive. -->
+  <include package="dolmen.beaker" file="configure.zcml" />
+  <grok:grok package="." />
+
+  <!-- include package="." file="async.zcml" / -->
+  <includeOverrides package="waeup.ikoba.widgets" file="overrides.zcml" />
+  <includeOverrides package="waeup.ikoba.utils" file="overrides.zcml" />
+
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/__init__.py	(revision 12677)
@@ -0,0 +1,3 @@
+"""This package contains everything regarding customers.
+"""
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/authentication.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/authentication.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/authentication.py	(revision 12677)
@@ -0,0 +1,314 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Authenticate customers.
+"""
+import grok
+import time
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from zope.password.interfaces import IPasswordManager
+from zope.pluggableauth.interfaces import (
+    IAuthenticatorPlugin, ICredentialsPlugin)
+from zope.pluggableauth.plugins.session import (
+    SessionCredentialsPlugin, SessionCredentials)
+from zope.publisher.interfaces.http import IHTTPRequest
+from zope.session.interfaces import ISession
+from waeup.ikoba.authentication import (
+    IkobaPrincipalInfo, get_principal_role_manager, FailedLoginInfo)
+from waeup.ikoba.interfaces import (
+    IAuthPluginUtility, IUserAccount, IPasswordValidator)
+from waeup.ikoba.customers.interfaces import ICustomer
+
+
+class CustomerAccount(grok.Adapter):
+    """An adapter to turn customer objects into accounts on-the-fly.
+    """
+    grok.context(ICustomer)
+    grok.implements(IUserAccount)
+
+    public_name = None
+
+    @property
+    def name(self):
+        return self.context.customer_id
+
+    @property
+    def password(self):
+        return getattr(self.context, 'password', None)
+
+    @property
+    def title(self):
+        return self.context.display_fullname
+
+    @property
+    def email(self):
+        return self.context.email
+
+    @property
+    def phone(self):
+        return self.context.phone
+
+    @property
+    def user_type(self):
+        return u'customer'
+
+    @property
+    def description(self):
+        return self.title
+
+    @property
+    def failed_logins(self):
+        if not hasattr(self.context, 'failed_logins'):
+            self.context.failed_logins = FailedLoginInfo()
+        return self.context.failed_logins
+
+    def _get_roles(self):
+        prm = get_principal_role_manager()
+        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
+                 if x[0].startswith('waeup.')]
+        return roles
+
+    def _set_roles(self, roles):
+        """Set roles for principal denoted by this account.
+        """
+        prm = get_principal_role_manager()
+        old_roles = self.roles
+        for role in old_roles:
+            # Remove old roles, not to be set now...
+            if role.startswith('waeup.') and role not in roles:
+                prm.unsetRoleForPrincipal(role, self.name)
+        for role in roles:
+            prm.assignRoleToPrincipal(role, self.name)
+        return
+
+    roles = property(_get_roles, _set_roles)
+
+    def setPassword(self, password):
+        """Set a password (LDAP-compatible) SSHA encoded.
+
+        We do not store passwords in plaintext. Encrypted password is
+        stored as unicode string.
+        """
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        self.context.password = passwordmanager.encodePassword(password)
+
+    def checkPassword(self, password):
+        """Check whether the given `password` matches the one stored by
+        customers or the temporary password set by officers.
+
+        We additionally check if customer account has been suspended.
+        """
+        if not isinstance(password, basestring):
+            return False
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        temp_password = self.context.getTempPassword()
+        if temp_password:
+            return passwordmanager.checkPassword(temp_password, password)
+        if not getattr(self.context, 'password', None):
+            # unset/empty passwords do never match
+            return False
+        if self.context.suspended == True:
+            return False
+        return passwordmanager.checkPassword(self.context.password, password)
+
+
+class CustomersAuthenticatorPlugin(grok.GlobalUtility):
+    grok.implements(IAuthenticatorPlugin)
+    grok.provides(IAuthenticatorPlugin)
+    grok.name('customers')
+
+    def authenticateCredentials(self, credentials):
+        """Authenticate `credentials`.
+
+        `credentials` is a tuple (login, password).
+
+        We look up customers to find out whether a respective customer
+        exists, then check the password and return the resulting
+        `PrincipalInfo` or ``None`` if no such customer 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 IkobaPrincipalInfo(id=account.name,
+                             title=account.title,
+                             description=account.description,
+                             email=account.email,
+                             phone=account.phone,
+                             public_name=account.public_name,
+                             user_type=account.user_type)
+
+    def principalInfo(self, id):
+        """Get a principal identified by `id`.
+
+        This one is required by IAuthenticatorPlugin but not needed here
+        (see respective docstring in applicants package).
+        """
+        return None
+
+    def getAccount(self, login):
+        """Look up a customer identified by `login`. Returns an account.
+
+        Currently, we simply look up the key under which the customer
+        is stored in the portal. That means we hit if login name and
+        name under which the customer is stored match.
+
+        Returns not a customer but an account object adapted from any
+        customer found.
+
+        If no such customer exists, ``None`` is returned.
+        """
+        site = grok.getSite()
+        if site is None:
+            return None
+        customerscontainer = site.get('customers', None)
+        if customerscontainer is None:
+            return None
+        customer = customerscontainer.get(login, None)
+        if customer is None:
+            cat = getUtility(ICatalog, name='customers_catalog')
+            results = list(
+                cat.searchResults(email=(login, login)))
+            if len(results) != 1:
+                return None
+            customer = results[0]
+        return IUserAccount(customer)
+
+
+class PasswordChangeCredentialsPlugin(grok.GlobalUtility,
+                                      SessionCredentialsPlugin):
+    """A session credentials plugin that handles the case of a user
+    changing his/her own password.
+
+    When users change their own password they might find themselves
+    logged out on next request.
+
+    To avoid this, we support to use a 'change password' page a bit
+    like a regular login page. That means, on each request we lookup
+    the sent data for a login field called 'customer_id' and a
+    password.
+
+    If both exist, this means someone sent new credentials.
+
+    We then look for the old credentials stored in the user session.
+    If the new credentials' login (the customer_id) matches the old
+    one's, we set the new credentials in session _but_ we return the
+    old credentials for the authentication plugins to check as for the
+    current request (and for the last time) the old credentials apply.
+
+    No valid credentials are returned by this plugin if one of the
+    follwing circumstances is true
+
+    - the sent request is not a regular IHTTPRequest
+
+    - the credentials to set do not match the old ones
+
+    - no credentials are sent with the request
+
+    - no credentials were set before (i.e. the user has no session
+      with credentials set before)
+
+    - no session exists already
+
+    - password and repeated password do not match
+
+    Therefore it is mandatory to put this plugin in the line of all
+    credentials plugins _before_ other plugins, so that the regular
+    credentials plugins can drop in as a 'fallback'.
+
+    This plugin was designed for customers to change their passwords,
+    but might be used to allow password resets for other types of
+    accounts as well.
+    """
+    grok.provides(ICredentialsPlugin)
+    grok.name('customer_pw_change')
+
+    loginpagename = 'login'
+    loginfield = 'customer_id'
+    passwordfield = 'change_password'
+    repeatfield = 'change_password_repeat'
+
+    def extractCredentials(self, request):
+        if not IHTTPRequest.providedBy(request):
+            return None
+        login = request.get(self.loginfield, None)
+        password = request.get(self.passwordfield, None)
+        password_repeat = request.get(self.repeatfield, None)
+
+        if not login or not password:
+            return None
+
+        validator = getUtility(IPasswordValidator)
+        errors = validator.validate_password(password, password_repeat)
+        if errors:
+            return None
+
+        session = ISession(request)
+        sessionData = session.get(
+            'zope.pluggableauth.browserplugins')
+        if not sessionData:
+            return None
+
+        old_credentials = sessionData.get('credentials', None)
+        if old_credentials is None:
+            # Password changes for already authenticated users only!
+            return None
+        if old_credentials.getLogin() != login:
+            # Special treatment only for users that change their own pw.
+            return None
+        old_credentials = {
+            'login': old_credentials.getLogin(),
+            'password': old_credentials.getPassword()}
+
+        # Set new credentials in session. These will be active on next request
+        new_credentials = SessionCredentials(login, password)
+        sessionData['credentials'] = new_credentials
+
+        # Return old credentials for this one request only
+        return old_credentials
+
+
+class CustomersAuthenticatorSetup(grok.GlobalUtility):
+    """Register or unregister customer authentication for a PAU.
+
+    This piece is called when a new site is created.
+    """
+    grok.implements(IAuthPluginUtility)
+    grok.name('customers_auth_setup')
+
+    def register(self, pau):
+        plugins = list(pau.credentialsPlugins)
+        # this plugin must come before the regular credentials plugins
+        plugins.insert(0, 'customer_pw_change')
+        pau.credentialsPlugins = tuple(plugins)
+        plugins = list(pau.authenticatorPlugins)
+        plugins.append('customers')
+        pau.authenticatorPlugins = tuple(plugins)
+        return pau
+
+    def unregister(self, pau):
+        plugins = [x for x in pau.authenticatorPlugins
+                   if x != 'customers']
+        pau.authenticatorPlugins = tuple(plugins)
+        return pau
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/batching.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/batching.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/batching.py	(revision 12677)
@@ -0,0 +1,618 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Batch processing components for customer 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 customers specific objects like
+customers and subobjects.
+"""
+import grok
+import unicodecsv as csv  # XXX: csv ops should move to dedicated module.
+from time import time
+from datetime import datetime
+from zope.i18n import translate
+from zope.interface import Interface
+from zope.schema import getFields
+from zope.formlib.interfaces import ConversionError
+from zope.component import queryUtility, getUtility, createObject
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from waeup.ikoba.interfaces import (
+    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
+    IObjectHistory, IGNORE_MARKER)
+from waeup.ikoba.interfaces import IIkobaUtils, DELETION_MARKER, IGNORE_MARKER
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.customers.utils import generate_contract_id
+from waeup.ikoba.customers.interfaces import (
+    ICustomer, ICustomerUpdateByRegNo,
+    ICustomerSampleDocument, ISampleContractProcess)
+from waeup.ikoba.customers.workflow import  (
+    IMPORTABLE_REGISTRATION_STATES, IMPORTABLE_REGISTRATION_TRANSITIONS)
+from waeup.ikoba.utils.batching import BatchProcessor
+
+DATE_FORMAT = '%Y-%m-%d'
+
+
+def date_to_field(input):
+    # In import files we can use the hash symbol at the end of a
+    # date string to avoid annoying automatic date transformation
+    # by Excel or Calc
+    input = input.strip('#')
+    try:
+        value = datetime.strptime(input, DATE_FORMAT)
+    except (ValueError, IndexError), v:
+        raise ConversionError("Invalid datetime data", v)
+    return value.date()
+
+
+class CustomerProcessor(BatchProcessor):
+    """A batch processor for ICustomer objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'customerprocessor'
+    grok.name(util_name)
+
+    name = _('Customer Processor')
+    iface = ICustomer
+    iface_byregnumber = ICustomerUpdateByRegNo
+
+    location_fields = []
+    factory_name = 'waeup.Customer'
+
+    @property
+    def available_fields(self):
+        fields = getFields(self.iface)
+        return sorted(list(set(
+            ['customer_id', 'reg_number',
+            'password', 'state', 'transition'] + fields.keys())))
+
+    def checkHeaders(self, headerfields, mode='create'):
+        if 'state' in headerfields and 'transition' in headerfields:
+            raise FatalCSVError(
+                "State and transition can't be  imported at the same time!")
+        if not 'reg_number' in headerfields and not 'customer_id' \
+            in headerfields:
+            raise FatalCSVError(
+                "Need at least columns customer_id or reg_number " +
+                "for import!")
+        if mode == 'create':
+            for field in self.required_fields:
+                if not field in headerfields:
+                    raise FatalCSVError(
+                        "Need at least columns %s for import!" %
+                        ', '.join(["'%s'" % x for x in self.required_fields]))
+        # Check for fields to be ignored...
+        not_ignored_fields = [x for x in headerfields
+                              if not x.startswith('--')]
+        if len(set(not_ignored_fields)) < len(not_ignored_fields):
+            raise FatalCSVError(
+                "Double headers: each column name may only appear once.")
+        return True
+
+    def parentsExist(self, row, site):
+        return 'customers' in site.keys()
+
+    def getLocator(self, row):
+        if row.get('customer_id', None) not in (None, IGNORE_MARKER):
+            return 'customer_id'
+        elif row.get('reg_number', None) not in (None, IGNORE_MARKER):
+            return 'reg_number'
+        else:
+            return None
+
+    # The entry never exists in create mode.
+    def entryExists(self, row, site):
+        return self.getEntry(row, site) is not None
+
+    def getParent(self, row, site):
+        return site['customers']
+
+    def getEntry(self, row, site):
+        if not 'customers' in site.keys():
+            return None
+        if self.getLocator(row) == 'customer_id':
+            if row['customer_id'] in site['customers']:
+                customer = site['customers'][row['customer_id']]
+                return customer
+        elif self.getLocator(row) == 'reg_number':
+            reg_number = row['reg_number']
+            cat = queryUtility(ICatalog, name='customers_catalog')
+            results = list(
+                cat.searchResults(reg_number=(reg_number, reg_number)))
+            if results:
+                return results[0]
+        return None
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addCustomer(obj)
+        # Reset _curr_cust_id if customer_id has been imported
+        if self.getLocator(row) == 'customer_id':
+            parent._curr_cust_id -= 1
+        # We have to log this if state is provided. If not,
+        # logging is done by the event handler handle_customer_added
+        if 'state' in row:
+            parent.logger.info('%s - Customer record created' % obj.customer_id)
+        history = IObjectHistory(obj)
+        history.addMessage(_('Customer record created'))
+        return
+
+    def delEntry(self, row, site):
+        customer = self.getEntry(row, site)
+        if customer is not None:
+            parent = self.getParent(row, site)
+            parent.logger.info('%s - Customer removed' % customer.customer_id)
+            del parent[customer.customer_id]
+        pass
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being updated.
+
+        This method is not used in case of deleting or adding objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        transition = row.get('transition', IGNORE_MARKER)
+        if transition not in (IGNORE_MARKER, ''):
+            allowed_transitions = IWorkflowInfo(obj).getManualTransitionIds()
+            if transition not in allowed_transitions:
+                return 'Transition not allowed.'
+        return None
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = ''
+
+        # Remove customer_id from row if empty
+        if 'customer_id' in row and row['customer_id'] in (None, IGNORE_MARKER):
+            row.pop('customer_id')
+
+        # Update password
+        # XXX: Take DELETION_MARKER into consideration
+        if 'password' in row:
+            passwd = row.get('password', IGNORE_MARKER)
+            if passwd not in ('', IGNORE_MARKER):
+                if passwd.startswith('{SSHA}'):
+                    # already encrypted password
+                    obj.password = passwd
+                else:
+                    # not yet encrypted password
+                    IUserAccount(obj).setPassword(passwd)
+                items_changed += ('%s=%s, ' % ('password', passwd))
+            row.pop('password')
+
+        # Update registration state
+        if 'state' in row:
+            state = row.get('state', IGNORE_MARKER)
+            if state not in (IGNORE_MARKER, ''):
+                value = row['state']
+                IWorkflowState(obj).setState(value)
+                msg = _("State '${a}' set", mapping={'a': value})
+                history = IObjectHistory(obj)
+                history.addMessage(msg)
+                items_changed += ('%s=%s, ' % ('state', state))
+            row.pop('state')
+
+        if 'transition' in row:
+            transition = row.get('transition', IGNORE_MARKER)
+            if transition not in (IGNORE_MARKER, ''):
+                value = row['transition']
+                IWorkflowInfo(obj).fireTransition(value)
+                items_changed += ('%s=%s, ' % ('transition', transition))
+            row.pop('transition')
+
+        # apply other values...
+        items_changed += super(CustomerProcessor, self).updateEntry(
+            obj, row, site, filename)
+
+        # Log actions...
+        parent = self.getParent(row, site)
+        if hasattr(obj, 'customer_id'):
+            # Update mode: the customer exists and we can get the customer_id.
+            # Create mode: the record contains the customer_id
+            parent.logger.info(
+                '%s - %s - %s - updated: %s'
+                % (self.name, filename, obj.customer_id, items_changed))
+        else:
+            # Create mode: the customer does not yet exist
+            # XXX: It seems that this never happens because customer_id
+            # is always set.
+            parent.logger.info(
+                '%s - %s - %s - imported: %s'
+                % (self.name, filename, obj.customer_id, items_changed))
+        return items_changed
+
+    def getMapping(self, path, headerfields, mode):
+        """Get a mapping from CSV file headerfields to actually used fieldnames.
+        """
+        result = dict()
+        reader = csv.reader(open(path, 'rb'))
+        raw_header = reader.next()
+        for num, field in enumerate(headerfields):
+            if field not in ['customer_id', 'reg_number'] and mode == 'remove':
+                continue
+            if field == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[raw_header[num]] = field
+        return result
+
+    def checkConversion(self, row, mode='create'):
+        """Validates all values in row.
+        """
+        iface = self.iface
+        if mode in ['update', 'remove']:
+            if self.getLocator(row) == 'reg_number':
+                iface = self.iface_byregnumber
+        converter = IObjectConverter(iface)
+        errs, inv_errs, conv_dict = converter.fromStringDict(
+            row, self.factory_name, mode=mode)
+        if 'transition' in row:
+            if row['transition'] not in IMPORTABLE_REGISTRATION_TRANSITIONS:
+                if row['transition'] not in (IGNORE_MARKER, ''):
+                    errs.append(('transition', 'not allowed'))
+        if 'state' in row:
+            if row['state'] not in IMPORTABLE_REGISTRATION_STATES:
+                if row['state'] not in (IGNORE_MARKER, ''):
+                    errs.append(('state', 'not allowed'))
+                else:
+                    # State is an attribute of Customer and must not
+                    # be changed if empty.
+                    conv_dict['state'] = IGNORE_MARKER
+        try:
+            # Correct cust_id counter. As the IConverter for customers
+            # creates customer objects that are not used afterwards, we
+            # have to fix the site-wide customer_id counter.
+            site = grok.getSite()
+            customers = site['customers']
+            customers._curr_cust_id -= 1
+        except (KeyError, TypeError, AttributeError):
+                pass
+        return errs, inv_errs, conv_dict
+
+
+class CustomerProcessorBase(BatchProcessor):
+    """A base for customer subitem processors.
+
+    Helps reducing redundancy.
+    """
+    grok.baseclass()
+
+    # additional available fields
+    # beside 'customer_id', 'reg_number' and 'matric_number'
+    additional_fields = []
+
+    #: header fields additionally required
+    additional_headers = []
+
+    @property
+    def available_fields(self):
+        fields = ['customer_id', 'reg_number'] + self.additional_fields
+        return sorted(list(set(fields + getFields(
+                self.iface).keys())))
+
+    def checkHeaders(self, headerfields, mode='ignore'):
+        if not 'reg_number' in headerfields and not 'customer_id' \
+            in headerfields:
+            raise FatalCSVError(
+                "Need at least columns customer_id " +
+                "or reg_number for import!")
+        for name in self.additional_headers:
+            if not name in headerfields:
+                raise FatalCSVError(
+                    "Need %s for import!" % name)
+
+        # Check for fields to be ignored...
+        not_ignored_fields = [x for x in headerfields
+                              if not x.startswith('--')]
+        if len(set(not_ignored_fields)) < len(not_ignored_fields):
+            raise FatalCSVError(
+                "Double headers: each column name may only appear once.")
+        return True
+
+    def _getCustomer(self, row, site):
+        NON_VALUES = ['', IGNORE_MARKER]
+        if not 'customers' in site.keys():
+            return None
+        if row.get('customer_id', '') not in NON_VALUES:
+            if row['customer_id'] in site['customers']:
+                customer = site['customers'][row['customer_id']]
+                return customer
+        elif row.get('reg_number', '') not in NON_VALUES:
+            reg_number = row['reg_number']
+            cat = queryUtility(ICatalog, name='customers_catalog')
+            results = list(
+                cat.searchResults(reg_number=(reg_number, reg_number)))
+            if results:
+                return results[0]
+        return None
+
+    def parentsExist(self, row, site):
+        return self.getParent(row, site) is not None
+
+    def entryExists(self, row, site):
+        return self.getEntry(row, site) is not None
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        converter = IObjectConverter(self.iface)
+        errs, inv_errs, conv_dict = converter.fromStringDict(
+            row, self.factory_name, mode=mode)
+        return errs, inv_errs, conv_dict
+
+    def getMapping(self, path, headerfields, mode):
+        """Get a mapping from CSV file headerfields to actually used fieldnames.
+        """
+        result = dict()
+        reader = csv.reader(open(path, 'rb'))
+        raw_header = reader.next()
+        for num, field in enumerate(headerfields):
+            if field not in ['customer_id', 'reg_number', 'document_id',
+                             'code', 'level', 'contract_id'
+                             ] and mode == 'remove':
+                continue
+            if field == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[raw_header[num]] = field
+        return result
+
+class CustomerDocumentProcessorBase(CustomerProcessorBase):
+    """A batch processor for customer documents.
+
+    This is a baseclass.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+
+    grok.baseclass()
+
+    util_name = None
+    name = None
+    iface = None
+    factory_name = None
+
+    location_fields = []
+    additional_fields = ['class_name']
+    additional_headers = ['class_name']
+
+    def checkHeaders(self, headerfields, mode='ignore'):
+        super(CustomerDocumentProcessorBase, self).checkHeaders(headerfields)
+        if mode in ('update', 'remove') and not 'document_id' in headerfields:
+            raise FatalCSVError(
+                "Need document_id for import in update and remove modes!")
+        return True
+
+    def getParent(self, row, site):
+        customer = self._getCustomer(row, site)
+        if customer is None:
+            return None
+        return customer['documents']
+
+    def getEntry(self, row, site):
+        document_id = row.get('document_id', None)
+        if document_id is None:
+            return None
+        cat = queryUtility(ICatalog, name='documents_catalog')
+        results = list(cat.searchResults(document_id=(document_id, document_id)))
+        if results:
+            return results[0]
+        return None
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        # Remove document_id from row if empty
+        if 'document_id' in row and row['document_id'] in (None, IGNORE_MARKER):
+            row.pop('document_id')
+        items_changed = super(CustomerDocumentProcessorBase, self).updateEntry(
+            obj, row, site, filename)
+        customer = self.getParent(row, site).__parent__
+        customer.__parent__.logger.info(
+            '%s - %s - %s - %s - updated: %s'
+            % (self.name, filename, customer.customer_id, obj.document_id,
+               items_changed))
+        return
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addDocument(obj)
+        return
+
+    def delEntry(self, row, site):
+        document = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if document is not None:
+            customer = self._getCustomer(row, site)
+            customer.__parent__.logger.info('%s - Document removed: %s'
+                % (customer.customer_id, document.document_id))
+            del parent[document.document_id]
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            CustomerDocumentProcessorBase, self).checkConversion(row, mode=mode)
+        # We need to check if the class_name corresponds with the
+        # processor chosen. This is to avoid accidentally wrong imports.
+        if mode != 'remove':
+            class_name = row.get('class_name', None)
+            if class_name != self.factory_name.strip('waeup.'):
+                errs.append(('class_name','wrong processor'))
+        return errs, inv_errs, conv_dict
+
+
+class CustomerSampleDocumentProcessor(CustomerDocumentProcessorBase):
+    """A batch processor for ICustomerSampleDocument objects.
+    """
+    util_name = 'customersampledocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Customer Sample Document Processor')
+    iface = ICustomerSampleDocument
+    factory_name = 'waeup.CustomerSampleDocument'
+
+
+class ContractProcessorBase(CustomerProcessorBase):
+    """A batch processor for contracts.
+
+    This is a baseclass.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+
+    grok.baseclass()
+
+    util_name = None
+    name = None
+    iface = None
+    factory_name = None
+
+    location_fields = []
+    additional_fields = ['class_name', 'title',
+                         'tc_dict', 'valid_from', 'valid_to']
+    additional_headers = ['class_name']
+
+    def checkHeaders(self, headerfields, mode='ignore'):
+        super(ContractProcessorBase, self).checkHeaders(headerfields)
+        if mode in ('update', 'remove') and not 'contract_id' in headerfields:
+            raise FatalCSVError(
+                "Need contract_id for import in update and remove modes!")
+        return True
+
+    def getParent(self, row, site):
+        customer = self._getCustomer(row, site)
+        if customer is None:
+            return None
+        return customer['contracts']
+
+    def getEntry(self, row, site):
+        contracts = self.getParent(row, site)
+        if contracts is None:
+            return None
+        contract_id = row.get('contract_id', None)
+        if contract_id is None:
+            return None
+        cat = queryUtility(ICatalog, name='contracts_catalog')
+        results = list(cat.searchResults(contract_id=(contract_id, contract_id)))
+        if results:
+            return results[0]
+        return None
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        # Remove contract_id from row if empty
+        if 'contract_id' in row and row['contract_id'] in (None, IGNORE_MARKER):
+            row.pop('contract_id')
+        items_changed = super(ContractProcessorBase, self).updateEntry(
+            obj, row, site, filename)
+        customer = self.getParent(row, site).__parent__
+        customer.__parent__.logger.info(
+            '%s - %s - %s - %s - updated: %s'
+            % (self.name, filename, customer.customer_id, obj.contract_id,
+               items_changed))
+        return
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addContract(obj)
+        return
+
+    def delEntry(self, row, site):
+        contract = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if contract is not None:
+            customer = self._getCustomer(row, site)
+            customer.__parent__.logger.info('%s - Contract removed: %s'
+                % (customer.customer_id, contract.contract_id))
+            del parent[contract.contract_id]
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            ContractProcessorBase, self).checkConversion(row, mode=mode)
+        # We need to check if the class_name corresponds with the
+        # processor chosen. This is to avoid accidentally wrong imports.
+        if mode != 'remove':
+            class_name = row.get('class_name', None)
+            if class_name != self.factory_name.strip('waeup.'):
+                errs.append(('class_name','wrong processor'))
+        tc_dict = row.get('tc_dict', None)
+        # We need to check tc_dict, valid_to and valid_from because they
+        # are not schema fields are thus not checked by the
+        # DefaultObjectConverter.
+        while True:
+            if tc_dict in (IGNORE_MARKER, None, {}, ''):
+                break
+            if tc_dict == DELETION_MARKER:
+                conv_dict['tc_dict'] = None
+                break
+            try:
+                tc_dict = eval(tc_dict)
+                conv_dict['tc_dict'] = tc_dict
+                break
+            except SyntaxError:
+                errs.append(('tc_dict','syntax error'))
+                break
+            if not isinstance(tc_dict, dict):
+                errs.append(('tc_dict','not a dictionary'))
+                break
+        valid_from = row.get('valid_from', None)
+        while True:
+            if valid_from in (IGNORE_MARKER, None, ''):
+                break
+            try:
+                date = date_to_field(valid_from)
+            except ConversionError:
+                errs.append(('valid_from','invalid date format'))
+            break
+        valid_to = row.get('valid_to', None)
+        while True:
+            if valid_to in (IGNORE_MARKER, None, ''):
+                break
+            try:
+                date = date_to_field(valid_to)
+            except ConversionError:
+                errs.append(('valid_to','invalid date format'))
+            break
+        return errs, inv_errs, conv_dict
+
+
+class SampleContractProcessor(ContractProcessorBase):
+    """A batch processor for ISampleContract objects.
+    """
+    util_name = 'samplecontractprocessor'
+    grok.name(util_name)
+
+    name = _('Customer Sample Contract Processor')
+    iface = ISampleContractProcess
+    factory_name = 'waeup.SampleContract'
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser.py	(revision 12677)
@@ -0,0 +1,1681 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""UI components for customers and related components.
+"""
+
+import sys
+import grok
+import pytz
+import os
+from urllib import urlencode
+from datetime import datetime
+from zope.event import notify
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
+from zope.formlib.textwidgets import BytesDisplayWidget
+from zope.security import checkPermission
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import (
+    IContactForm, IObjectHistory, IIkobaObject, IIkobaUtils, IExtFileStore,
+    IPasswordValidator, IUserAccount,
+    STARTED, VERIFIED, REJECTED, EXPIRED, CREATED, REQUESTED,
+    APPROVED, PROVISIONALLY)
+from waeup.ikoba.browser.layout import (
+    IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage,
+    IkobaForm, NullValidator, jsaction, action, UtilityView)
+from waeup.ikoba.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget,
+    FriendlyDatetimeDisplayWidget)
+from waeup.ikoba.browser.pages import ContactAdminForm
+from waeup.ikoba.browser.breadcrumbs import Breadcrumb
+from waeup.ikoba.browser.interfaces import ICaptchaManager
+from waeup.ikoba.mandates.mandate import PasswordMandate
+from waeup.ikoba.widgets.hrefwidget import HREFDisplayWidget
+from waeup.ikoba.utils.helpers import (
+    get_current_principal, to_timezone, now, format_date)
+from waeup.ikoba.customers.interfaces import (
+    ICustomer, ICustomersContainer, ICustomerRequestPW, ICustomersUtils,
+    ICustomerDocument, ICustomerDocumentsContainer, ICustomerCreate,
+    ICustomerPDFDocument, IContractsContainer, IContract,
+    IContractSelectProduct, ISampleContract,
+    )
+from waeup.ikoba.customers.catalog import search
+from waeup.ikoba.customers.workflow import PAYMENT_TRANSITIONS
+
+grok.context(IIkobaObject)
+
+WARNING_CUST = _('You can not edit some data after final submission.'
+            ' You really want to submit?')
+
+WARNING_DOC = _('You can not edit your document after final submission.'
+            ' You really want to submit?')
+
+WARNING_CON = _('You can not edit your contract after final submission.'
+            ' You really want to submit?')
+
+
+# Save function used for save methods in pages
+def msave(view, **data):
+    changed_fields = view.applyData(view.context, **data)
+    # Turn list of lists into single list
+    if changed_fields:
+        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+    if 'product_object' in changed_fields and data['product_object'] is not None:
+        view.context.last_product_id = data['product_object'].product_id
+        notify(grok.ObjectModifiedEvent(view.context))
+    fields_string = ' + '.join(changed_fields)
+    view.flash(_('Form has been saved.'))
+    if fields_string:
+        view.context.writeLogMessage(
+            view, '%s - saved: %s' % (view.context.__name__, fields_string))
+    return
+
+
+def emit_lock_message(view):
+    """Flash a lock message.
+    """
+    view.flash(_('The requested form is locked (read-only).'), type="warning")
+    view.redirect(view.url(view.context))
+    return
+
+def isCustomer(principal):
+    return getattr(principal, 'user_type', None) == 'customer'
+
+
+class CustomersBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customers container.
+    """
+    grok.context(ICustomersContainer)
+    title = _('Customers')
+
+    @property
+    def target(self):
+        user = get_current_principal()
+        if getattr(user, 'user_type', None) == 'customer':
+            return None
+        return self.viewname
+
+
+class CustomerBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customer container.
+    """
+    grok.context(ICustomer)
+
+    def title(self):
+        return self.context.display_fullname
+
+
+class CustomersContainerPage(IkobaPage):
+    """The standard view for customer containers.
+    """
+    grok.context(ICustomersContainer)
+    grok.name('index')
+    grok.require('waeup.viewCustomersContainer')
+    grok.template('containerpage')
+    label = _('Find customers')
+    search_button = _('Find customer(s)')
+    pnav = 4
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        self.hitlist = []
+        if form.get('searchtype', None) == 'suspended':
+            self.searchtype = form['searchtype']
+            self.searchterm = None
+        elif 'searchterm' in form and form['searchterm']:
+            self.searchterm = form['searchterm']
+            self.searchtype = form['searchtype']
+        elif 'old_searchterm' in form:
+            self.searchterm = form['old_searchterm']
+            self.searchtype = form['old_searchtype']
+        else:
+            if 'search' in form:
+                self.flash(_('Empty search string'), type="warning")
+            return
+        if self.searchtype == 'current_session':
+            try:
+                self.searchterm = int(self.searchterm)
+            except ValueError:
+                self.flash(_('Only year dates allowed (e.g. 2011).'),
+                           type="danger")
+                return
+        self.hitlist = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if not self.hitlist:
+            self.flash(_('No customer found.'), type="warning")
+        return
+
+
+class CustomersContainerManagePage(IkobaPage):
+    """The manage page for customer containers.
+    """
+    grok.context(ICustomersContainer)
+    grok.name('manage')
+    grok.require('waeup.manageCustomer')
+    grok.template('containermanagepage')
+    pnav = 4
+    label = _('Manage customer section')
+    search_button = _('Find customer(s)')
+    remove_button = _('Remove selected')
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        self.hitlist = []
+        if form.get('searchtype', None) == 'suspended':
+            self.searchtype = form['searchtype']
+            self.searchterm = None
+        elif 'searchterm' in form and form['searchterm']:
+            self.searchterm = form['searchterm']
+            self.searchtype = form['searchtype']
+        elif 'old_searchterm' in form:
+            self.searchterm = form['old_searchterm']
+            self.searchtype = form['old_searchtype']
+        else:
+            if 'search' in form:
+                self.flash(_('Empty search string'), type="warning")
+            return
+        if self.searchtype == 'current_session':
+            try:
+                self.searchterm = int(self.searchterm)
+            except ValueError:
+                self.flash(_('Only year dates allowed (e.g. 2011).'),
+                           type="danger")
+                return
+        if not 'entries' in form:
+            self.hitlist = search(query=self.searchterm,
+                searchtype=self.searchtype, view=self)
+            if not self.hitlist:
+                self.flash(_('No customer found.'), type="warning")
+            if 'remove' in form:
+                self.flash(_('No item selected.'), type="warning")
+            return
+        entries = form['entries']
+        if isinstance(entries, basestring):
+            entries = [entries]
+        deleted = []
+        for entry in entries:
+            if 'remove' in form:
+                del self.context[entry]
+                deleted.append(entry)
+        self.hitlist = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping={'a': ','.join(deleted)}))
+        return
+
+
+class CustomerAddFormPage(IkobaAddFormPage):
+    """Add-form to add a customer.
+    """
+    grok.context(ICustomersContainer)
+    grok.require('waeup.manageCustomer')
+    grok.name('addcustomer')
+    form_fields = grok.AutoFields(ICustomer).select(
+        'firstname', 'middlename', 'lastname', 'reg_number')
+    label = _('Add customer')
+    pnav = 4
+
+    @action(_('Create customer record'), style='primary')
+    def addCustomer(self, **data):
+        customer = createObject(u'waeup.Customer')
+        self.applyData(customer, **data)
+        self.context.addCustomer(customer)
+        self.flash(_('Customer created.'))
+        self.redirect(self.url(self.context[customer.customer_id], 'index'))
+        return
+
+
+class LoginAsCustomerStep1(IkobaEditFormPage):
+    """ View to temporarily set a customer password.
+    """
+    grok.context(ICustomer)
+    grok.name('loginasstep1')
+    grok.require('waeup.loginAsCustomer')
+    grok.template('loginasstep1')
+    pnav = 4
+
+    def label(self):
+        return _(u'Set temporary password for ${a}',
+            mapping={'a': self.context.display_fullname})
+
+    @action('Set password now', style='primary')
+    def setPassword(self, *args, **data):
+        ikoba_utils = getUtility(IIkobaUtils)
+        password = ikoba_utils.genPassword()
+        self.context.setTempPassword(self.request.principal.id, password)
+        self.context.writeLogMessage(
+            self, 'temp_password generated: %s' % password)
+        args = {'password': password}
+        self.redirect(self.url(self.context) +
+            '/loginasstep2?%s' % urlencode(args))
+        return
+
+
+class LoginAsCustomerStep2(IkobaPage):
+    """ View to temporarily login as customer with a temporary password.
+    """
+    grok.context(ICustomer)
+    grok.name('loginasstep2')
+    grok.require('waeup.Public')
+    grok.template('loginasstep2')
+    login_button = _('Login now')
+    pnav = 4
+
+    def label(self):
+        return _(u'Login as ${a}',
+            mapping={'a': self.context.customer_id})
+
+    def update(self, SUBMIT=None, password=None):
+        self.password = password
+        if SUBMIT is not None:
+            self.flash(_('You successfully logged in as customer.'))
+            self.redirect(self.url(self.context))
+        return
+
+
+class CustomerBaseDisplayFormPage(IkobaDisplayFormPage):
+    """ Page to display customer base data
+    """
+    grok.context(ICustomer)
+    grok.name('index')
+    grok.require('waeup.viewCustomer')
+    grok.template('basepage')
+    pnav = 4
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(
+            self.context.form_fields_interface).omit(
+            'password', 'suspended', 'suspended_comment')
+
+    @property
+    def label(self):
+        if self.context.suspended:
+            return _('${a}: Base Data (account deactivated)',
+                mapping={'a': self.context.display_fullname})
+        return  _('${a}: Base Data',
+            mapping={'a': self.context.display_fullname})
+
+    @property
+    def hasPassword(self):
+        if self.context.password:
+            return _('set')
+        return _('unset')
+
+    @property
+    def is_requestable(self):
+        if self.context.state in (REQUESTED, PROVISIONALLY, APPROVED):
+            return False
+        return True
+
+    def update(self):
+        # Fire transition if customer logs in for the first time
+        usertype = getattr(self.request.principal, 'user_type', None)
+        if usertype == 'customer' and \
+            IWorkflowState(self.context).getState() == CREATED:
+            IWorkflowInfo(self.context).fireTransition('start')
+        return
+
+
+class ContactCustomerForm(ContactAdminForm):
+    grok.context(ICustomer)
+    grok.name('contactcustomer')
+    grok.require('waeup.viewCustomer')
+    pnav = 4
+    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
+
+
+    def update(self, subject=u'', body=u''):
+        super(ContactCustomerForm, self).update()
+        self.form_fields.get('subject').field.default = subject
+        self.form_fields.get('body').field.default = body
+        return
+
+    def label(self):
+        return _(u'Send message to ${a}',
+            mapping={'a': self.context.display_fullname})
+
+    @action('Send message now', style='primary')
+    def send(self, *args, **data):
+        try:
+            email = self.request.principal.email
+        except AttributeError:
+            email = self.config.email_admin
+        usertype = getattr(self.request.principal,
+                           'user_type', 'system').title()
+        ikoba_utils = getUtility(IIkobaUtils)
+        success = ikoba_utils.sendContactForm(
+                self.request.principal.title, email,
+                self.context.display_fullname, self.context.email,
+                self.request.principal.id,usertype,
+                self.config.name,
+                data['body'], data['subject'])
+        if success:
+            self.flash(_('Your message has been sent.'))
+        else:
+            self.flash(_('An smtp server error occurred.'), type="danger")
+        return
+
+
+class CustomerBaseManageFormPage(IkobaEditFormPage):
+    """ View to manage customer base data
+    """
+    grok.context(ICustomer)
+    grok.name('manage_base')
+    grok.require('waeup.manageCustomer')
+    grok.template('basemanagepage')
+    label = _('Manage base data')
+    pnav = 4
+    deletion_warning = _('Are you sure?')
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(
+            self.context.form_fields_interface).omit(
+            'customer_id', 'suspended')
+
+    def update(self):
+        super(CustomerBaseManageFormPage, self).update()
+        self.wf_info = IWorkflowInfo(self.context)
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        form = self.request.form
+        password = form.get('password', None)
+        password_ctl = form.get('control_password', None)
+        if password:
+            validator = getUtility(IPasswordValidator)
+            errors = validator.validate_password(password, password_ctl)
+            if errors:
+                self.flash(' '.join(errors), type="danger")
+                return
+        changed_fields = self.applyData(self.context, **data)
+        # Turn list of lists into single list
+        if changed_fields:
+            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+        else:
+            changed_fields = []
+        if password:
+            # Now we know that the form has no errors and can set password
+            IUserAccount(self.context).setPassword(password)
+            changed_fields.append('password')
+        fields_string = ' + '.join(changed_fields)
+        self.flash(_('Form has been saved.'))
+        if fields_string:
+            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
+        return
+
+
+class CustomerTriggerTransitionFormPage(IkobaEditFormPage):
+    """ View to trigger customer workflow transitions
+    """
+    grok.context(ICustomer)
+    grok.name('trigtrans')
+    grok.require('waeup.triggerTransition')
+    grok.template('trigtrans')
+    label = _('Trigger registration transition')
+    pnav = 4
+
+    def getTransitions(self):
+        """Return a list of dicts of allowed transition ids and titles.
+
+        Each list entry provides keys ``name`` and ``title`` for
+        internal name and (human readable) title of a single
+        transition.
+        """
+        wf_info = IWorkflowInfo(self.context)
+        allowed_transitions = [t for t in wf_info.getManualTransitions()]
+        return [dict(name='', title=_('No transition'))] +[
+            dict(name=x, title=y) for x, y in allowed_transitions]
+
+    @action(_('Apply'), style='primary')
+    def apply(self, **data):
+        form = self.request.form
+        if 'transition' in form and form['transition']:
+            transition_id = form['transition']
+            wf_info = IWorkflowInfo(self.context)
+            wf_info.fireTransition(transition_id)
+            self.flash(_("Transition '%s' executed." % transition_id))
+            self.redirect(self.url(self.context))
+        return
+
+
+class CustomerActivatePage(UtilityView, grok.View):
+    """ Activate customer account
+    """
+    grok.context(ICustomer)
+    grok.name('activate')
+    grok.require('waeup.manageCustomer')
+
+    def update(self):
+        self.context.suspended = False
+        self.context.writeLogMessage(self, 'account activated')
+        history = IObjectHistory(self.context)
+        history.addMessage('Customer account activated')
+        self.flash(_('Customer account has been activated.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+
+class CustomerDeactivatePage(UtilityView, grok.View):
+    """ Deactivate customer account
+    """
+    grok.context(ICustomer)
+    grok.name('deactivate')
+    grok.require('waeup.manageCustomer')
+
+    def update(self):
+        self.context.suspended = True
+        self.context.writeLogMessage(self, 'account deactivated')
+        history = IObjectHistory(self.context)
+        history.addMessage('Customer account deactivated')
+        self.flash(_('Customer account has been deactivated.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+
+class CustomerHistoryPage(IkobaPage):
+    """ Page to display customer history
+    """
+    grok.context(ICustomer)
+    grok.name('history')
+    grok.require('waeup.viewCustomer')
+    grok.template('customerhistory')
+    pnav = 4
+
+    @property
+    def label(self):
+        return _('${a}: History', mapping={'a':self.context.display_fullname})
+
+
+class CustomerRequestPasswordPage(IkobaAddFormPage):
+    """Captcha'd password request page for customers.
+    """
+    grok.name('requestpw')
+    grok.require('waeup.Anonymous')
+    grok.template('requestpw')
+    form_fields = grok.AutoFields(ICustomerRequestPW)
+    label = _('Request password for first-time login')
+
+    def update(self):
+        # Handle captcha
+        self.captcha = getUtility(ICaptchaManager).getCaptcha()
+        self.captcha_result = self.captcha.verify(self.request)
+        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
+        return
+
+    def _redirect(self, email, password, customer_id):
+        # Forward only email address to landing page in base package.
+        self.redirect(self.url(self.context, 'requestpw_complete',
+            data=dict(email=email)))
+        return
+
+    def _pw_used(self):
+        # XXX: False if password has not been used. We need an extra
+        #      attribute which remembers if customer logged in.
+        return True
+
+    @action(_('Send login credentials to email address'), style='primary')
+    def get_credentials(self, **data):
+        if not self.captcha_result.is_valid:
+            # Captcha will display error messages automatically.
+            # No need to flash something.
+            return
+        number = data.get('number','')
+        firstname = data.get('firstname','')
+        cat = getUtility(ICatalog, name='customers_catalog')
+        results = list(
+            cat.searchResults(reg_number=(number, number)))
+        if results:
+            customer = results[0]
+            if getattr(customer,'firstname',None) is None:
+                self.flash(_('An error occurred.'), type="danger")
+                return
+            elif customer.firstname.lower() != firstname.lower():
+                # Don't tell the truth here. Anonymous must not
+                # know that a record was found and only the firstname
+                # verification failed.
+                self.flash(_('No customer found.'), type="warning")
+                return
+            elif customer.password is not None and self._pw_used:
+                self.flash(_('Your password has already been set and used. '
+                             'Please proceed to the login page.'),
+                           type="warning")
+                return
+            # Store email address but nothing else.
+            customer.email = data['email']
+            notify(grok.ObjectModifiedEvent(customer))
+        else:
+            # No record found, this is the truth.
+            self.flash(_('No customer found.'), type="warning")
+            return
+
+        ikoba_utils = getUtility(IIkobaUtils)
+        password = ikoba_utils.genPassword()
+        mandate = PasswordMandate()
+        mandate.params['password'] = password
+        mandate.params['user'] = customer
+        site = grok.getSite()
+        site['mandates'].addMandate(mandate)
+        # Send email with credentials
+        args = {'mandate_id':mandate.mandate_id}
+        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
+        url_info = u'Confirmation link: %s' % mandate_url
+        msg = _('You have successfully requested a password for the')
+        if ikoba_utils.sendCredentials(IUserAccount(customer),
+            password, url_info, msg):
+            email_sent = customer.email
+        else:
+            email_sent = None
+        self._redirect(email=email_sent, password=password,
+            customer_id=customer.customer_id)
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        self.context.logger.info(
+            '%s - %s (%s) - %s' % (ob_class, number, customer.customer_id, email_sent))
+        return
+
+
+class CustomerCreateAccountPage(IkobaAddFormPage):
+    """Captcha'd account creation page for customers.
+    """
+    grok.name('createaccount')
+    grok.require('waeup.Anonymous')
+    grok.template('createaccount')
+    form_fields = grok.AutoFields(ICustomerCreate)
+    label = _('Create customer account')
+
+    def update(self):
+        # Handle captcha
+        self.captcha = getUtility(ICaptchaManager).getCaptcha()
+        self.captcha_result = self.captcha.verify(self.request)
+        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
+        return
+
+    def _redirect(self, email, password, customer_id):
+        # Forward only email address to landing page in base package.
+        self.redirect(self.url(self.context, 'requestpw_complete',
+            data=dict(email=email)))
+        return
+
+    @action(_('Send login credentials to email address'), style='primary')
+    def create_account(self, **data):
+        if not self.captcha_result.is_valid:
+            # Captcha will display error messages automatically.
+            # No need to flash something.
+            return
+        customer = createObject(u'waeup.Customer')
+        customer.firstname = data.get('firstname','')
+        customer.middlename = data.get('middlename','')
+        customer.lastname = data.get('lastname','')
+        customer.email = data.get('email','')
+        self.context['customers'].addCustomer(customer)
+        ikoba_utils = getUtility(IIkobaUtils)
+        password = ikoba_utils.genPassword()
+        mandate = PasswordMandate()
+        mandate.params['password'] = password
+        mandate.params['user'] = customer
+        site = grok.getSite()
+        site['mandates'].addMandate(mandate)
+        # Send email with credentials
+        args = {'mandate_id':mandate.mandate_id}
+        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
+        url_info = u'Confirmation link: %s' % mandate_url
+        msg = _('You have successfully created a customer account for the')
+        if ikoba_utils.sendCredentials(IUserAccount(customer),
+            password, url_info, msg):
+            email_sent = customer.email
+        else:
+            email_sent = None
+        self._redirect(email=email_sent, password=password,
+            customer_id=customer.customer_id)
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        self.context.logger.info(
+            '%s - %s - %s' % (ob_class, customer.customer_id, email_sent))
+        return
+
+
+class CustomerRequestPasswordEmailSent(IkobaPage):
+    """Landing page after successful password request.
+
+    """
+    grok.name('requestpw_complete')
+    grok.require('waeup.Public')
+    grok.template('requestpwmailsent')
+    label = _('Your request was successful.')
+
+    def update(self, email=None, customer_id=None, password=None):
+        self.email = email
+        self.password = password
+        self.customer_id = customer_id
+        return
+
+# Pages for customers
+
+class CustomerFilesUploadPage(IkobaPage):
+    """ View to upload files by customer
+
+    We use this page only to upload a passport picture (portrait).
+    """
+    grok.context(ICustomer)
+    grok.name('upload_files')
+    grok.require('waeup.handleCustomer')
+    grok.template('filesuploadpage')
+    label = _('Upload files')
+    pnav = 4
+    deletion_warning = _('Are you sure?')
+
+    def update(self, CANCEL=None):
+        CUSTMANAGE_STATES = getUtility(
+            ICustomersUtils).CUSTMANAGE_CUSTOMER_STATES
+        if self.context.state not in CUSTMANAGE_STATES:
+            emit_lock_message(self)
+            return
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        super(CustomerFilesUploadPage, self).update()
+        return
+
+
+class CustomerBaseEditFormPage(IkobaEditFormPage):
+    """ View to edit customer base data
+    """
+    grok.context(ICustomer)
+    grok.name('edit_base')
+    grok.require('waeup.handleCustomer')
+    pnav = 4
+
+    def is_requestable(self, action=None):
+        if self.context.state == STARTED:
+            return True
+        return False
+
+    @property
+    def label(self):
+        if self.is_requestable():
+            return _('Edit base data and request registration')
+        return _('Edit base data')
+
+    @property
+    def form_fields(self):
+        if not self.is_requestable():
+            return grok.AutoFields(
+                self.context.form_fields_interface).select('email', 'phone')
+        return grok.AutoFields(self.context.form_fields_interface).omit(
+            'suspended', 'suspended_comment', 'reg_number', 'customer_id')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+    def dataNotComplete(self):
+        store = getUtility(IExtFileStore)
+        error = ''
+        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
+            error += _('Passport picture is missing.')
+        if error:
+            return error
+        return
+
+    @action(_('Save and request registration now'),
+            warning=WARNING_CUST, condition=is_requestable)
+    def finalsubmit(self, **data):
+        msave(self, **data)
+        if self.dataNotComplete():
+            self.flash(self.dataNotComplete(), type="warning")
+            self.redirect(self.url(self.context, 'upload_files'))
+            return
+        IWorkflowInfo(self.context).fireTransition('request')
+        self.flash(_('Registration form has been submitted.'))
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+
+class CustomerChangePasswordPage(IkobaEditFormPage):
+    """ View to edit customer passords
+    """
+    grok.context(ICustomer)
+    grok.name('changepassword')
+    grok.require('waeup.handleCustomer')
+    grok.template('changepassword')
+    label = _('Change password')
+    pnav = 4
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        form = self.request.form
+        password = form.get('change_password', None)
+        password_ctl = form.get('change_password_repeat', None)
+        if password:
+            validator = getUtility(IPasswordValidator)
+            errors = validator.validate_password(password, password_ctl)
+            if not errors:
+                IUserAccount(self.context).setPassword(password)
+                self.context.writeLogMessage(self, 'saved: password')
+                self.flash(_('Password changed.'))
+            else:
+                self.flash(' '.join(errors), type="warning")
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+
+class CustomerBasePDFFormPage(IkobaDisplayFormPage):
+    """ Page to display customer base data in pdf files.
+    """
+
+    def __init__(self, context, request, omit_fields=()):
+        self.omit_fields = omit_fields
+        super(CustomerBasePDFFormPage, self).__init__(context, request)
+
+    @property
+    def form_fields(self):
+        form_fields = grok.AutoFields(self.context.form_fields_interface)
+        for field in self.omit_fields:
+            form_fields = form_fields.omit(field)
+        return form_fields
+
+# Pages for customer documents
+
+class DocumentsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the documents container.
+    """
+    grok.context(ICustomerDocumentsContainer)
+    title = _('Documents')
+
+
+class DocumentBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customer container.
+    """
+    grok.context(ICustomerDocument)
+
+    @property
+    def title(self):
+        return "%s..." % self.context.document_id[:9]
+
+
+class DocumentsManageFormPage(IkobaEditFormPage):
+    """ Page to manage the customer documents
+
+    This manage form page is for both customers and customers officers.
+    """
+    grok.context(ICustomerDocumentsContainer)
+    grok.name('index')
+    grok.require('waeup.viewCustomer')
+    form_fields = grok.AutoFields(ICustomerDocumentsContainer)
+    grok.template('documentsmanagepage')
+    pnav = 4
+
+    @property
+    def manage_documents_allowed(self):
+        return checkPermission('waeup.editCustomerDocuments', self.context)
+
+    def unremovable(self, document):
+        usertype = getattr(self.request.principal, 'user_type', None)
+        if not usertype:
+            return False
+        if not self.manage_documents_allowed:
+            return True
+        return (self.request.principal.user_type == 'customer' and \
+            document.state in (VERIFIED, REJECTED, EXPIRED))
+
+    @property
+    def label(self):
+        return _('${a}: Documents',
+            mapping = {'a':self.context.__parent__.display_fullname})
+
+    @action(_('Add document'), validator=NullValidator, style='primary')
+    def addDocument(self, **data):
+        self.redirect(self.url(self.context, 'adddoc'))
+        return
+
+    @jsaction(_('Remove selected documents'))
+    def delDocument(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No document selected.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            # Customers are not allowed to remove used documents
+            document = self.context.get(id, None)
+            if document is not None and not self.unremovable(document):
+                del self.context[id]
+                deleted.append(id)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a': ', '.join(deleted)}))
+            self.context.writeLogMessage(
+                self,'removed: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context))
+        return
+
+
+class DocumentAddFormPage(IkobaAddFormPage):
+    """ Page to add a document
+
+    This add form page is for both customers and customers officers.
+    """
+    grok.context(ICustomerDocumentsContainer)
+    grok.name('adddoc')
+    grok.template('documentaddpage')
+    grok.require('waeup.editCustomerDocuments')
+    label = _('Add document')
+    pnav = 4
+
+    form_fields = grok.AutoFields(ICustomerDocument).omit('document_id')
+
+    @property
+    def selectable_doctypes(self):
+        doctypes = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT
+        return sorted(doctypes.items())
+
+    @property
+    def edit_documents_allowed(self):
+        right_customer_state = self.context.customer.state in getUtility(
+            ICustomersUtils).DOCMANAGE_CUSTOMER_STATES
+        if isCustomer(self.request.principal) and not right_customer_state:
+            return False
+        return True
+
+    def update(self):
+        if not self.edit_documents_allowed:
+            emit_lock_message(self)
+            return
+        return super(DocumentAddFormPage, self).update()
+
+    @action(_('Add document'), style='primary')
+    def createDocument(self, **data):
+        form = self.request.form
+        customer = self.context.__parent__
+        doctype = form.get('doctype', None)
+        # Here we can create various instances of CustomerDocument derived 
+        # classes depending on the doctype parameter given in form.
+        document = createObject('waeup.%s' % doctype)
+        self.applyData(document, **data)
+        self.context.addDocument(document)
+        doctype = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT[doctype]
+        self.flash(_('${a} added.', mapping = {'a': doctype}))
+        self.context.writeLogMessage(
+            self,'added: %s %s' % (doctype, document.document_id))
+        isCustomer = getattr(
+            self.request.principal, 'user_type', None) == 'customer'
+        if isCustomer:
+            self.redirect(self.url(document, 'edit') + '#tab2')
+        else:
+            self.redirect(self.url(document, 'manage') + '#tab2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+
+class DocumentDisplayFormPage(IkobaDisplayFormPage):
+    """ Page to view a document
+    """
+    grok.context(ICustomerDocument)
+    grok.name('index')
+    grok.require('waeup.viewCustomer')
+    grok.template('documentpage')
+    pnav = 4
+    label = None  # We render the context title in the documentpage template
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface)
+
+class DocumentManageFormPage(IkobaEditFormPage):
+    """ Page to edit a document
+    """
+    grok.context(ICustomerDocument)
+    grok.name('manage')
+    grok.require('waeup.manageCustomer')
+    grok.template('documenteditpage')
+    pnav = 4
+    deletion_warning = _('Are you sure?')
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(
+            self.context.form_fields_interface).omit('document_id')
+
+    def update(self):
+        if not self.context.is_editable_by_manager:
+            emit_lock_message(self)
+            return
+        return super(DocumentManageFormPage, self).update()
+
+    @property
+    def label(self):
+        return self.context.title
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+
+class DocumentEditFormPage(DocumentManageFormPage):
+    """ Page to edit a document
+    """
+    grok.name('edit')
+    grok.require('waeup.handleCustomer')
+
+    def update(self):
+        if not self.context.is_editable_by_customer:
+            emit_lock_message(self)
+            return
+        return super(DocumentEditFormPage, self).update()
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+    @action(_('Final Submit'), warning=WARNING_DOC)
+    def finalsubmit(self, **data):
+        msave(self, **data)
+        IWorkflowInfo(self.context).fireTransition('submit')
+        self.flash(_('Form has been submitted.'))
+        self.redirect(self.url(self.context))
+        return
+
+
+class DocumentTriggerTransitionFormPage(IkobaEditFormPage):
+    """ View to trigger customer document transitions
+    """
+    grok.context(ICustomerDocument)
+    grok.name('trigtrans')
+    grok.require('waeup.triggerTransition')
+    grok.template('trigtrans')
+    label = _('Trigger document transition')
+    pnav = 4
+
+    def update(self):
+        return super(IkobaEditFormPage, self).update()
+
+    def getTransitions(self):
+        """Return a list of dicts of allowed transition ids and titles.
+
+        Each list entry provides keys ``name`` and ``title`` for
+        internal name and (human readable) title of a single
+        transition.
+        """
+        wf_info = IWorkflowInfo(self.context)
+        allowed_transitions = [t for t in wf_info.getManualTransitions()]
+        return [dict(name='', title=_('No transition'))] +[
+            dict(name=x, title=y) for x, y in allowed_transitions]
+
+    @action(_('Apply'), style='primary')
+    def apply(self, **data):
+        form = self.request.form
+        if 'transition' in form and form['transition']:
+            transition_id = form['transition']
+            wf_info = IWorkflowInfo(self.context)
+            try:
+                wf_info.fireTransition(transition_id)
+                self.flash(_("Transition '%s' executed." % transition_id))
+            except InvalidTransitionError, error:
+                self.flash(error, type="warning")
+            self.redirect(self.url(self.context))
+        return
+
+class PDFDocumentsOverviewPage(UtilityView, grok.View):
+    """Deliver an overview slip.
+    """
+    grok.context(ICustomerDocumentsContainer)
+    grok.name('documents_overview_slip.pdf')
+    grok.require('waeup.viewCustomer')
+    prefix = 'form'
+
+    omit_fields = ('suspended', 'sex',
+                   'suspended_comment',)
+
+    form_fields = None
+
+    @property
+    def label(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        return translate(_('Documents of'),
+            'waeup.ikoba', target_language=portal_language) \
+            + ' %s' % self.context.customer.display_fullname
+
+    @property
+    def tabletitle(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        tabletitle = []
+        tabletitle.append(translate(_('Customer Documents'), 'waeup.ikoba',
+            target_language=portal_language))
+        return tabletitle
+
+    def render(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
+        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
+        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
+        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
+        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
+        tableheader = []
+        tabledata = []
+        contenttitle = []
+        for i in range(1,3):
+            tabledata.append(sorted(
+                [value for value in self.context.values()]))
+            tableheader.append([(Id, 'document_id', 2),
+                             (Title, 'title', 6),
+                             (Type, 'translated_class_name', 6),
+                             (State, 'translated_state', 2),
+                             (LT, 'formatted_transition_date', 3),
+                             ])
+        customerview = CustomerBasePDFFormPage(self.context.customer,
+            self.request, self.omit_fields)
+        customers_utils = getUtility(ICustomersUtils)
+        return customers_utils.renderPDF(
+            self, 'overview_slip.pdf',
+            self.context.customer, customerview,
+            tableheader=tableheader,
+            tabledata=tabledata,
+            omit_fields=self.omit_fields)
+
+
+class PDFDocumentSlipPage(UtilityView, grok.View):
+    """Deliver pdf file including metadata.
+    """
+    grok.context(ICustomerDocument)
+    grok.name('document_slip.pdf')
+    grok.require('waeup.viewCustomer')
+    prefix = 'form'
+
+    omit_fields = ('suspended', 'sex',
+                   'suspended_comment',)
+
+    form_fields =()
+
+    @property
+    def label(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        return '%s of %s\nTitle: %s' % (
+            self.context.translated_class_name,
+            self.context.customer.display_fullname,
+            self.context.title)
+
+    def render(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        customerview = CustomerBasePDFFormPage(self.context.customer,
+            self.request, self.omit_fields)
+        customers_utils = getUtility(ICustomersUtils)
+        return customers_utils.renderPDF(
+            self, 'document_slip.pdf',
+            self.context.customer, customerview,
+            omit_fields=self.omit_fields)
+
+
+class PDFMergeDocumentSlipPage(PDFDocumentSlipPage):
+    """Deliver pdf file including metadata.
+    """
+    grok.context(ICustomerPDFDocument)
+
+    def render(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        customerview = CustomerBasePDFFormPage(self.context.customer,
+            self.request, self.omit_fields)
+        customers_utils = getUtility(ICustomersUtils)
+        if self.context.state == VERIFIED:
+            watermark_path = os.path.join(
+                os.path.dirname(__file__), 'static', 'verified.pdf')
+        else:
+            watermark_path = os.path.join(
+                os.path.dirname(__file__), 'static', 'unverified.pdf')
+        watermark = open(watermark_path, 'rb')
+        return customers_utils.renderPDF(
+            self, 'pdfdocument_slip.pdf',
+            self.context.customer, customerview,
+            omit_fields=self.omit_fields,
+            mergefiles=self.context.connected_files,
+            watermark=watermark)
+
+# Pages for customer contracts
+
+class ContractsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the contracts container.
+    """
+    grok.context(IContractsContainer)
+    title = _('Contracts')
+
+
+class ContractBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customer container.
+    """
+    grok.context(IContract)
+
+    @property
+    def title(self):
+        return "%s..." % self.context.contract_id[:9]
+
+
+class ContractsFormPage(IkobaEditFormPage):
+    """ Page to display, edit or manage customer contracts
+
+    This form page is for both customers and officers.
+    """
+    grok.context(IContractsContainer)
+    grok.name('index')
+    grok.require('waeup.viewCustomer')
+    form_fields = grok.AutoFields(IContractsContainer)
+    grok.template('contractsmanagepage')
+    pnav = 4
+
+    @property
+    def edit_contracts_allowed(self):
+        right_customer_state = self.context.customer.state in getUtility(
+            ICustomersUtils).CONMANAGE_CUSTOMER_STATES
+        if isCustomer(self.request.principal) and not right_customer_state:
+            return False
+        return checkPermission('waeup.editContracts', self.context)
+
+    def remove_contract_allowed(self, contract):
+        if isCustomer(self.request.principal):
+            right_customer_state = self.context.customer.state in getUtility(
+                ICustomersUtils).CONMANAGE_CUSTOMER_STATES
+            if not right_customer_state:
+                return False
+            if contract.state in (APPROVED, REJECTED, EXPIRED):
+                return False
+        return checkPermission('waeup.editContracts', self.context)
+
+    @property
+    def label(self):
+        return _('${a}: Contracts',
+            mapping = {'a':self.context.__parent__.display_fullname})
+
+    @action(_('Add contract'), validator=NullValidator, style='primary')
+    def addContract(self, **data):
+        self.redirect(self.url(self.context, 'addcontract'))
+        return
+
+    @jsaction(_('Remove selected contracts'))
+    def delContract(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No contract selected.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            # Customers are not allowed to remove used contracts
+            contract = self.context.get(id, None)
+            if contract is not None and self.remove_contract_allowed(contract):
+                del self.context[id]
+                deleted.append(id)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a': ', '.join(deleted)}))
+            self.context.writeLogMessage(
+                self,'removed: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context))
+        return
+
+
+class ContractAddFormPage(IkobaAddFormPage):
+    """ Page to add an contract
+
+    This page is for both customers and officers.
+    """
+    grok.context(IContractsContainer)
+    grok.name('addcontract')
+    grok.template('contractaddpage')
+    grok.require('waeup.editContracts')
+    label = _('Add contract')
+    pnav = 4
+
+    @property
+    def edit_contracts_allowed(self):
+        right_customer_state = self.context.customer.state in getUtility(
+            ICustomersUtils).CONMANAGE_CUSTOMER_STATES
+        if right_customer_state:
+            return True
+        return False
+
+    def update(self):
+        if not self.edit_contracts_allowed:
+            emit_lock_message(self)
+            return
+        return super(ContractAddFormPage, self).update()
+
+    @property
+    def selectable_contypes(self):
+        contypes = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT
+        return sorted(contypes.items())
+
+    @action(_('Add contract'), style='primary')
+    def createContract(self, **data):
+        form = self.request.form
+        customer = self.context.__parent__
+        contype = form.get('contype', None)
+        # Here we can create various instances of Contract derived
+        # classes depending on the contype parameter given in form.
+        contract = createObject('waeup.%s' % contype)
+        self.context.addContract(contract)
+        contype = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT[contype]
+        self.flash(_('${a} added.', mapping = {'a': contype}))
+        self.context.writeLogMessage(
+            self,'added: %s %s' % (contype, contract.contract_id))
+        self.redirect(self.url(contract, 'selectproduct'))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+
+class ContractSelectProductPage(IkobaAddFormPage):
+    """ Page to select a contract product
+
+    This page is for both customers and officers.
+    """
+    grok.context(IContract)
+    grok.name('selectproduct')
+    grok.require('waeup.editContracts')
+    label = _('Select product')
+    pnav = 4
+
+    form_fields = grok.AutoFields(IContractSelectProduct)
+
+    def update(self):
+        if self.context.product_object is not None:
+            emit_lock_message(self)
+            return
+        return super(ContractSelectProductPage, self).update()
+
+    @action(_('Save and proceed'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        self.context.title = self.context.product_object.contract_autotitle
+        self.context.tc_dict = self.context.product_object.tc_dict
+        self.context.valid_from = self.context.product_object.valid_from
+        self.context.valid_to = self.context.product_object.valid_to
+        isCustomer = getattr(
+            self.request.principal, 'user_type', None) == 'customer'
+        if isCustomer:
+            self.redirect(self.url(self.context, 'edit'))
+        else:
+            self.redirect(self.url(self.context, 'manage'))
+        return
+
+
+class ContractDisplayFormPage(IkobaDisplayFormPage):
+    """ Page to view a contract
+    """
+    grok.context(IContract)
+    grok.name('index')
+    grok.require('waeup.viewCustomer')
+    grok.template('contractpage')
+    pnav = 4
+    label = None  # We render the context title in the contractpage template
+
+    @property
+    def form_fields(self):
+        form_fields = grok.AutoFields(self.context.form_fields_interface)
+        for field in form_fields:
+            if field.__name__.endswith('_object'):
+                form_fields[field.__name__].custom_widget = HREFDisplayWidget
+        return form_fields
+
+    @property
+    def terms_and_conditions(self):
+        lang = self.request.cookies.get('ikoba.language')
+        html = self.context.tc_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+            html = self.context.tc_dict.get(portal_language,'')
+        return html
+
+
+class ContractManageFormPage(IkobaEditFormPage):
+    """ Page to manage a contract
+    """
+    grok.context(IContract)
+    grok.name('manage')
+    grok.require('waeup.manageCustomer')
+    grok.template('contracteditpage')
+    pnav = 4
+    deletion_warning = _('Are you sure?')
+
+    def update(self):
+        if not self.context.is_editable:
+            emit_lock_message(self)
+            return
+        return super(ContractManageFormPage, self).update()
+
+    @property
+    def terms_and_conditions(self):
+        lang = self.request.cookies.get('ikoba.language')
+        html = self.context.tc_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+            html = self.context.tc_dict.get(portal_language,'')
+        return html
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface).omit(
+            'contract_id', 'product_object')
+
+    @property
+    def label(self):
+        return self.context.title
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+
+class ContractOfficialUsePage(IkobaEditFormPage):
+    """ Page to manage a official use data of contract
+    """
+    grok.context(IContract)
+    grok.name('official_use')
+    grok.require('waeup.manageCustomer')
+    grok.template('contracteditpage')
+    pnav = 4
+    deletion_warning = _('Are you sure?')
+    terms_and_conditions = None
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.ou_form_fields_interface)
+
+    @property
+    def label(self):
+        return self.context.title
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+
+class ContractEditFormPage(ContractManageFormPage):
+    """ Page to edit a contract by customer only
+    """
+    grok.name('edit')
+    grok.require('waeup.handleCustomer')
+
+    def submission_allowed(self, action=None):
+        if self.context.state == CREATED and not self.context.fee_based:
+            return True
+        return False
+
+    def payment_expected(self, action=None):
+        if self.context.state == CREATED and self.context.fee_based:
+            return True
+        return False
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.edit_form_fields_interface).omit(
+            'contract_id', 'product_object')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+    @action(_('Apply now (final submission)'), warning=WARNING_CON,
+            condition=submission_allowed, style='primary')
+    def submit(self, **data):
+        if self.terms_and_conditions and not self.request.form.get(
+            'confirm_tc', False):
+            self.flash(_('Please read the terms and conditions and '
+                     'confirm your acceptance of these by ticking '
+                     'the confirmation box.'), type="danger")
+            return
+        msave(self, **data)
+        IWorkflowInfo(self.context).fireTransition('submit')
+        self.flash(_('Application form has been submitted.'))
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Proceed to checkout'),
+            condition=payment_expected, style='primary')
+    def proceed_checkout(self, **data):
+        if self.terms_and_conditions and not self.request.form.get(
+            'confirm_tc', False):
+            self.flash(_('Please read the terms and conditions and '
+                     'confirm your acceptance of these by ticking '
+                     'the confirmation box.'), type="danger")
+            return
+        msave(self, **data)
+        self.redirect(self.url(self.context, 'select_payment_method'))
+        return
+
+
+class SelectPaymentMethodPage(IkobaEditFormPage):
+    """ Page to to select payment method
+    """
+    grok.context(IContract)
+    grok.name('select_payment_method')
+    grok.require('waeup.handleCustomer')
+    grok.template('selectpaymentmethodpage')
+    pnav = 4
+    label = _('Select payment method')
+
+    def update(self, CANCEL=None):
+        if self.context.state != CREATED or not self.context.fee_based:
+            emit_lock_message(self)
+            return
+        super(SelectPaymentMethodPage, self).update()
+        return
+
+    @action(_('Select payment method and proceed to payment gateway (final submission)'),
+            style='primary', warning=WARNING_CON,)
+    def confirm(self, **data):
+        IWorkflowInfo(self.context).fireTransition('await')
+        self.flash(_('Payment has been initiated.'))
+        return
+
+class ContractTriggerTransitionFormPage(IkobaEditFormPage):
+    """ View to trigger customer contract transitions
+    """
+    grok.context(IContract)
+    grok.name('trigtrans')
+    grok.require('waeup.triggerTransition')
+    grok.template('trigtrans')
+    label = _('Trigger contract transition')
+    pnav = 4
+
+    def update(self):
+        return super(IkobaEditFormPage, self).update()
+
+    def getTransitions(self):
+        """Return a list of dicts of allowed transition ids and titles.
+
+        Each list entry provides keys ``name`` and ``title`` for
+        internal name and (human readable) title of a single
+        transition.
+        """
+        wf_info = IWorkflowInfo(self.context)
+        allowed_transitions = [t for t in wf_info.getManualTransitions()]
+        # Skip payment transitions if total amount is zero
+        if not self.context.fee_based:
+            allowed_transitions = [t for t in allowed_transitions
+                                   if not t[0] in PAYMENT_TRANSITIONS]
+        return [dict(name='', title=_('No transition'))] +[
+            dict(name=x, title=y) for x, y in allowed_transitions]
+
+    @action(_('Apply'), style='primary')
+    def apply(self, **data):
+        form = self.request.form
+        if 'transition' in form and form['transition']:
+            transition_id = form['transition']
+            wf_info = IWorkflowInfo(self.context)
+            try:
+                wf_info.fireTransition(transition_id)
+                self.flash(_("Transition '%s' executed." % transition_id))
+            except InvalidTransitionError, error:
+                self.flash(error, type="warning")
+            self.redirect(self.url(self.context))
+        return
+
+class PDFContractsOverviewPage(UtilityView, grok.View):
+    """Deliver an overview slip.
+    """
+    grok.context(IContractsContainer)
+    grok.name('contracts_overview_slip.pdf')
+    grok.require('waeup.viewCustomer')
+    prefix = 'form'
+
+    omit_fields = ('suspended', 'sex',
+                   'suspended_comment',)
+
+    form_fields = None
+
+    @property
+    def label(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        return translate(_('Contracts of'),
+            'waeup.ikoba', target_language=portal_language) \
+            + ' %s' % self.context.customer.display_fullname
+
+    @property
+    def tabletitle(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        tabletitle = []
+        tabletitle.append(translate(_('Customer Contracts'), 'waeup.ikoba',
+            target_language=portal_language))
+        return tabletitle
+
+    def render(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        #Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
+        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
+        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
+        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
+        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
+        tableheader = []
+        tabledata = []
+        contenttitle = []
+        for i in range(1,3):
+            tabledata.append(sorted(
+                [value for value in self.context.values()]))
+            tableheader.append([
+                             #(Id, 'contract_id', 2),
+                             (Title, 'title', 6),
+                             (Type, 'translated_class_name', 6),
+                             (State, 'translated_state', 2),
+                             (LT, 'formatted_transition_date', 3),
+                             ])
+        customerview = CustomerBasePDFFormPage(self.context.customer,
+            self.request, self.omit_fields)
+        customers_utils = getUtility(ICustomersUtils)
+        return customers_utils.renderPDF(
+            self, 'overview_slip.pdf',
+            self.context.customer, customerview,
+            tableheader=tableheader,
+            tabledata=tabledata,
+            omit_fields=self.omit_fields)
+
+
+class PDFContractSlipPage(UtilityView, grok.View):
+    """Deliver pdf file including metadata.
+    """
+    grok.context(IContract)
+    grok.name('contract_slip.pdf')
+    grok.require('waeup.viewCustomer')
+    prefix = 'form'
+
+    omit_fields = ('suspended', 'sex',
+                   'suspended_comment',)
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface)
+
+    @property
+    def terms_and_conditions(self):
+        lang = self.request.cookies.get('ikoba.language')
+        html = self.context.tc_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+            html = self.context.tc_dict.get(portal_language,'')
+        return html
+
+    @property
+    def validity_period(self):
+        if self.context.valid_from or self.context.valid_to:
+            return "%s - %s" % (format_date(self.context.valid_from, self.request),
+                                format_date(self.context.valid_to, self.request))
+        return
+
+    @property
+    def label(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        return self.context.title
+
+    # XXX: not used in waeup.ikoba and thus not tested
+    def _signatures(self):
+        return ([_('Customer Signature')],
+                [_('Company Officer Signature')]
+                )
+
+    def _sigsInFooter(self):
+        return (_('Date, Customer Signature'),
+                _('Date, Company Officer Signature'),
+                )
+
+    def render(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        customerview = CustomerBasePDFFormPage(self.context.customer,
+            self.request, self.omit_fields)
+        customers_utils = getUtility(ICustomersUtils)
+        return customers_utils.renderPDF(
+            self, 'contract_slip.pdf',
+            self.context.customer, customerview,
+            signatures=self._signatures(),
+            sigs_in_footer=self._sigsInFooter(),
+            omit_fields=self.omit_fields)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/basemanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/basemanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/basemanagepage.pt	(revision 12677)
@@ -0,0 +1,55 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data"
+      autocomplete="off">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+      <span i18n:translate="">Base Data</span></a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">Files</span>
+      </a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+          <tr>
+            <td class="fieldname" i18n:translate="">Password:</td>
+            <td>
+              <input name="password" class="form-control" type="password"  />
+            </td>
+          </tr>
+          <tr>
+            <td class="fieldname" i18n:translate="">Retype Password:</td>
+            <td>
+              <input name="control_password" class="form-control" type="password" />
+            </td>
+          </tr>
+
+        </tbody>
+      </table>
+
+      <div tal:condition="view/availableActions">
+        <input tal:repeat="action view/actions"
+               tal:replace="structure action/render"
+               />
+      </div>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:files content="structure provider:files" />
+        </tbody>
+      </table>
+    </div>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/basepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/basepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/basepage.pt	(revision 12677)
@@ -0,0 +1,23 @@
+<table i18n:domain="waeup.ikoba" class="form-table">
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td class="fieldname">
+          <tal:block content="widget/label"/>:
+        </td>
+        <td>
+          <input tal:replace="structure widget" />
+        </td>
+      </tr>
+    </tal:block>
+    <tr>
+      <td class="fieldname" i18n:translate="">
+          Password:
+      </td>
+      <td>
+          <tal:password replace="view/hasPassword" />
+      </td>
+    </tr>
+    <tal:files content="structure provider:files" />
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/changepassword.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/changepassword.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/changepassword.pt	(revision 12677)
@@ -0,0 +1,30 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+
+  <input type="hidden" name="customer_id" value=""
+         tal:attributes="value context/customer_id" />
+
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname" i18n:translate="">Password:</td>
+        <td>
+          <input name="change_password" class="form-control" type="password"  />
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">Retype Password:</td>
+        <td>
+          <input name="change_password_repeat" class="form-control" type="password" />
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <br />
+
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/containermanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/containermanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/containermanagepage.pt	(revision 12677)
@@ -0,0 +1,63 @@
+<form method="POST" i18n:domain="waeup.ikoba" class="form-inline">
+<br />
+  <div class="form-group">
+    <input class="btn btn-primary" type="submit" name="search"
+      tal:attributes="value view/search_button" />
+  </div>
+  <div class="form-group">
+    <select name="searchtype" class="form-control">
+      <option i18n:translate="" value="customer_id">with id</option>
+      <option i18n:translate="" value="fullname">with name</option>
+      <option i18n:translate="" value="reg_number">with registration number</option>
+      <option i18n:translate="" value="suspended">
+        with deactivated account (search term omitted)
+      </option>
+    </select>
+  </div>
+  <div class="form-group">
+    <input type="text" class="form-control" name="searchterm" />
+  </div>
+
+  <p>&nbsp;</p>
+  <div tal:condition="view/hitlist">
+    <h3>Search Results</h3>
+    <input type="hidden" name="old_searchterm"
+         tal:attributes="value view/searchterm" />
+    <input type="hidden" name="old_searchtype"
+         tal:attributes="value view/searchtype" />
+
+    <br /><br />
+    <table class="ikoba-data-table dataTableManage">
+      <thead>
+    	<tr>
+    	  <th>&nbsp;</th>
+        <th i18n:translate="">Customer Id</th>
+        <th i18n:translate="">Reg. Number</th>
+        <th i18n:translate="">State</th>
+        <th i18n:translate="">Full Name</th>
+    	</tr>
+      </thead>
+      <tbody>
+    	<tr tal:repeat="item view/hitlist">
+    	  <td><input type="checkbox" name="entries"
+    	             tal:attributes="value item/context/__name__" /></td>
+    	  <td> <a tal:attributes="href item/url">
+          <span tal:content="item/customer_id">A123456</span></a>
+        </td>
+        <td class="center"> <a tal:attributes="href item/url">
+          <span tal:content="item/reg_number">9999999</span></a>
+        </td>
+        <td tal:content="item/state">admitted</td>
+    	  <td tal:content="item/display_fullname">Bob</td>
+    	</tr>
+      </tbody>
+    </table>
+    <input type="checkbox" onClick="toggle(this, 'entries')" />
+    <span  i18n:translate="">Select all</span>
+    <br /><br />
+    <input type="submit" name="remove"
+           tal:attributes="value view/remove_button" class="btn btn-default"
+           onclick="return confirmPost('Are you sure?')"/>
+
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/containerpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/containerpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/containerpage.pt	(revision 12677)
@@ -0,0 +1,51 @@
+<form method="POST" i18n:domain="waeup.ikoba" class="form-inline">
+<br />
+  <div class="form-group">
+    <input class="btn btn-primary" type="submit" name="search"
+      tal:attributes="value view/search_button" />
+  </div>
+  <div class="form-group">
+    <select name="searchtype" class="form-control">
+      <option i18n:translate="" value="customer_id">with id</option>
+      <option i18n:translate="" value="fullname">with name</option>
+      <option i18n:translate="" value="reg_number">with registration number</option>
+      <option i18n:translate="" value="suspended">
+        with deactivated account (search term omitted)
+      </option>
+    </select>
+  </div>
+  <div class="form-group">
+    <input type="text" class="form-control" name="searchterm" />
+  </div>
+</form>
+<br />
+<div tal:condition="view/hitlist">
+  <h3 i18n:translate="">Search Results</h3>
+  <input type="hidden" name="old_searchterm"
+       tal:attributes="value view/searchterm" />
+  <input type="hidden" name="old_searchtype"
+       tal:attributes="value view/searchtype" />
+  <table class="ikoba-data-table dataTable">
+    <thead>
+    <tr>
+      <th i18n:translate="">Customer Id</th>
+      <th i18n:translate="">Reg. Number</th>
+      <th i18n:translate="">State</th>
+      <th i18n:translate="">Full Name</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr tal:repeat="item view/hitlist">
+      <td> <a tal:attributes="href item/url">
+        <span tal:content="item/customer_id">A123456</span></a>
+      </td>
+      <td class="center"> <a tal:attributes="href item/url">
+        <span tal:content="item/reg_number">9999999</span></a>
+      </td>
+      <td tal:content="item/state">admitted</td>
+      <td tal:content="item/display_fullname">Bob</td>
+    </tr>
+    </tbody>
+  </table>
+</div>
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contractaddpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contractaddpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contractaddpage.pt	(revision 12677)
@@ -0,0 +1,27 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data" i18n:domain="waeup.ikoba">
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname">
+          <span i18n:translate="">Contract Type</span>:
+        </td>
+        <td>
+          <select id="contype" name="contype" size="1"
+                  class="form-control half">
+            <option tal:repeat="item view/selectable_contypes"
+                    tal:attributes="value python:item[0]">
+              <span tal:replace="python:item[1]">CONTYPE</span>
+            </option>
+          </select>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contracteditpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contracteditpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contracteditpage.pt	(revision 12677)
@@ -0,0 +1,32 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+
+  <br />
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+
+  <br />
+
+  <tal:tc condition="python: view.terms_and_conditions and view.__name__ == 'edit'">
+    <h4 i18n:domain="waeup.ikoba">
+      <span i18n:translate="">Terms and Conditions</span>
+    </h4>
+    <tal:tc content="structure view/terms_and_conditions" />
+    <br />
+    <input id="confirm_tc" name="confirm_tc" type="checkbox" value="True"/>
+    <span i18n:translate="">
+        I confirm that I have read, understood and accepted the Terms & Conditions.
+    </span>
+   <br /><br />
+  </tal:tc>
+
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contractpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contractpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contractpage.pt	(revision 12677)
@@ -0,0 +1,49 @@
+<br />
+<div class="wfstatus-sub"  i18n:domain="waeup.ikoba">
+  Contract State: <span tal:replace="context/translated_state">
+    CONTRACTSTATE</span>
+</div>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<h1 class="ikoba-content-label"
+    tal:condition="context/title|nothing"
+    tal:content="context/title">TITLE</h1>
+
+<table class="form-table" i18n:domain="waeup.ikoba">
+  <tbody>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Contract Type:
+      </td>
+      <td>
+        <span tal:content="context/translated_class_name">CLASSNAME</span>
+      </td>
+    </tr>
+    <tal:widgets content="structure provider:widgets" />
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Validity Period:
+      </td>
+      <td>
+        <span tal:content="python: layout.formatDate(context.valid_from)">
+          VALIDFROM
+        </span> -
+        <span tal:content="python: layout.formatDate(context.valid_to)">
+          VALIDTO
+        </span>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<tal:tc condition="view/terms_and_conditions">
+  <h4 i18n:domain="waeup.ikoba">
+    <span i18n:translate="">Terms and Conditions</span>
+  </h4>
+  <tal:tc content="structure view/terms_and_conditions" />
+</tal:tc>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contractsmanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contractsmanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/contractsmanagepage.pt	(revision 12677)
@@ -0,0 +1,51 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+
+  <table class="ikoba-data-table dataTableManage">
+    <thead>
+    <tr>
+      <th tal:condition="view/edit_contracts_allowed">&nbsp;</th>
+      <th i18n:translate="">Id</th>
+      <th i18n:translate="">Title</th>
+      <th i18n:translate="">Type</th>
+      <th i18n:translate="">State</th>
+      <th i18n:translate="">Last Transition</th>
+    </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="contract context/values">
+         <td tal:condition="view/edit_contracts_allowed">
+          <input type="checkbox"
+      		 name="val_id"
+      		 tal:attributes="value contract/__name__"
+           tal:condition="python: view.remove_contract_allowed(contract)" />
+        </td>
+        <td>
+          <a tal:attributes="href python: view.url(contract)">
+          <span tal:content="python: contract.contract_id[:9]">CONID</span>...</a>
+        </td>
+        <td>
+          <span tal:content="contract/title">TITLE</span>
+        </td>
+        <td>
+          <span tal:content="contract/translated_class_name">CLASSNAME</span>
+        </td>
+        <td>
+          <span tal:content="contract/translated_state">STATE</span>
+        </td>
+        <td>
+          <span tal:content="contract/formatted_transition_date">
+              LASTTRANSITIONDATE
+          </span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div tal:condition="python: view.availableActions and view.edit_contracts_allowed">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/createaccount.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/createaccount.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/createaccount.pt	(revision 12677)
@@ -0,0 +1,30 @@
+<form action="." tal:attributes="action request/URL" method="post"
+    i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tr tal:condition="view/captcha_code">
+        <td i18n:translate="">
+          Please solve the captcha<br />to prevent misuse of this service:
+        </td>
+        <td>
+          <div tal:content="structure view/captcha_code"></div>
+        </td>
+      </tr>
+
+    </tbody>
+  </table>
+  <br />
+  <p i18n:translate="">
+    A customer record will be created and
+    your login credentials will be sent to the email address given above.
+    To be able to proceed you must provide a valid email address!
+  </p>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/customerhistory.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/customerhistory.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/customerhistory.pt	(revision 12677)
@@ -0,0 +1,5 @@
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documentaddpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documentaddpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documentaddpage.pt	(revision 12677)
@@ -0,0 +1,28 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data" i18n:domain="waeup.ikoba">
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname">
+          <span i18n:translate="">Document Type</span>:
+        </td>
+        <td>
+          <select id="doctype" name="doctype" size="1"
+                  class="form-control half">
+            <option tal:repeat="item view/selectable_doctypes"
+                    tal:attributes="value python:item[0]">
+              <span tal:replace="python:item[1]">DOCTYPE</span>
+            </option>
+          </select>
+        </td>
+      </tr>
+    </tbody>
+    <tal:widgets content="structure provider:widgets" />
+  </table>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documenteditpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documenteditpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documenteditpage.pt	(revision 12677)
@@ -0,0 +1,41 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+        <span i18n:translate="">Metadata</span>
+      </a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">Files</span>
+      </a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+
+      <div tal:condition="view/availableActions">
+        <input tal:repeat="action view/actions"
+               tal:replace="structure action/render"
+               />
+      </div>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:files content="structure provider:files" />
+        </tbody>
+      </table>
+    </div>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documentpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documentpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documentpage.pt	(revision 12677)
@@ -0,0 +1,36 @@
+<br />
+<div class="wfstatus-sub"  i18n:domain="waeup.ikoba">
+  Document State: <span tal:replace="context/translated_state">
+    VERIFICATIONSTATE</span>
+</div>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<h1 class="ikoba-content-label"
+    tal:condition="context/title|nothing"
+    tal:content="context/title">TITLE</h1>
+
+<table class="form-table" i18n:domain="waeup.ikoba"
+  tal:define="files provider:files">
+  <tbody>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Document Type:
+      </td>
+      <td>
+        <span tal:content="context/translated_class_name">CLASSNAME</span>
+      </td>
+    </tr>
+    <tal:widgets content="structure provider:widgets" />
+    <tr tal:condition="python: files.replace('\n','')">
+      <td class="separator" colspan=2 i18n:translate="">
+        Connected Files
+      </td>
+    </tr>
+    <tal:files content="structure files" />
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documentsmanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documentsmanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/documentsmanagepage.pt	(revision 12677)
@@ -0,0 +1,51 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+
+  <table class="ikoba-data-table dataTableManage">
+    <thead>
+    <tr>
+      <th>&nbsp;</th>
+      <th i18n:translate="">Id</th>
+      <th i18n:translate="">Title</th>
+      <th i18n:translate="">Type</th>
+      <th i18n:translate="">State</th>
+      <th i18n:translate="">Last Transition</th>
+    </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="document context/values">
+         <td>
+          <input type="checkbox"
+    		 name="val_id"
+    		 tal:attributes="value document/__name__"
+         tal:condition="python: not view.unremovable(document)" />
+        </td>
+        <td>
+          <a tal:attributes="href python: view.url(document)">
+          <span tal:content="python: document.document_id[:9]">DOCID</span>...</a>
+        </td>
+        <td>
+          <span tal:content="document/title">TITLE</span>
+        </td>
+        <td>
+          <span tal:content="document/translated_class_name">CLASSNAME</span>
+        </td>
+        <td>
+          <span tal:content="document/translated_state">STATE</span>
+        </td>
+        <td>
+          <span tal:content="document/formatted_transition_date">
+              LASTTRANSITIONDATE
+          </span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div tal:condition="python: view.availableActions and view.manage_documents_allowed">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/filesuploadpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/filesuploadpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/filesuploadpage.pt	(revision 12677)
@@ -0,0 +1,9 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+  <table class="form-table">
+    <tbody>
+      <tal:files content="structure provider:files" />
+    </tbody>
+  </table>
+  <input class="btn btn-default" type="submit" name="CANCEL" value="Cancel" />
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/imagedisplay.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/imagedisplay.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/imagedisplay.pt	(revision 12677)
@@ -0,0 +1,10 @@
+<tr>
+  <td class="fieldname">
+        <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+      <img height="125px"
+         tal:attributes="src python: view.url(context, viewlet.download_name)" />
+      <br />
+  </td>
+</tr>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/imageupload.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/imageupload.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/imageupload.pt	(revision 12677)
@@ -0,0 +1,34 @@
+<tr i18n:domain="waeup.ikoba">
+  <td class="fieldname">
+    <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+    <tal:img condition="viewlet/file_exists">
+      <img align="middle" height="125px"
+           tal:attributes="src python: view.url(context, viewlet.download_name)" />
+      &nbsp;
+      <input type="submit"
+       tal:attributes="id python:('delete_%s' % viewlet.input_name);
+          name python:('delete_%s' % viewlet.input_name);
+          onclick python:'return window.confirm(\'%s\')' % view.deletion_warning"
+       value="Delete" class="btn btn-danger" />
+    </tal:img>
+    <div class="input-group" tal:condition="not:viewlet/file_exists">
+      <div class="input-group-btn">
+        <div class="btn btn-default btn-file">
+          Select new file&hellip;
+          <input type="file" tal:attributes="name viewlet/input_name" />
+        </div>
+      </div>
+      <input type="text" class="form-control" readonly>
+      <div class="input-group-btn">
+        <input type="submit"
+               tal:attributes="id python:('upload_%s' % viewlet.input_name);
+                name python:('upload_%s' % viewlet.input_name);
+                value viewlet/upload_button"
+               class="btn btn-primary" />
+        (<span tal:replace="viewlet/max_upload_size">10 kB</span> max.)
+      </div>
+    </div>
+  </td>
+</tr>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/loginasstep1.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/loginasstep1.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/loginasstep1.pt	(revision 12677)
@@ -0,0 +1,18 @@
+<form i18n:domain="waeup.ikoba" method="post">
+  <p i18n:translate="login_as_intro_1">
+    Ikoba is creating a temporary customer password which will replace the
+    original password chosen by the customer. The password will be valid
+    for
+    <span i18n:name="minutes"
+          tal:replace=view/context/temp_password_minutes></span>
+    minutes. The customer can't login
+    during this time period. After expiration the customer can login
+    with the original password again.
+  </p>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/loginasstep2.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/loginasstep2.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/loginasstep2.pt	(revision 12677)
@@ -0,0 +1,13 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+  <p i18n:translate="login_as_intro_2">
+    The random password
+    "<span i18n:name="password"
+          tal:replace=view/password></span>"
+    has been set. You can proceed by clicking the button below.
+  </p>
+  <input type="hidden" name="form.login" tal:attributes="value view/context/customer_id" />
+  <input type="hidden" name="form.password"   tal:attributes="value view/password" />
+  <input class="btn btn-primary" type="submit" name="SUBMIT"
+    tal:attributes="value view/login_button" />
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/mydatadropdowntabs.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/mydatadropdowntabs.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/mydatadropdowntabs.pt	(revision 12677)
@@ -0,0 +1,13 @@
+<li  tal:attributes="class viewlet/active">
+  <a href="#" class="dropdown-toggle"
+     data-toggle="dropdown">
+     <span tal:replace="viewlet/tab_title">My Data</span>
+     <b class="caret"></b>
+  </a>
+  <ul class="dropdown-menu">
+    <li tal:repeat="target viewlet/targets">
+      <a tal:attributes="href target/url"
+         tal:content="target/title">LINKTITLE</a>
+    </li>
+  </ul>
+</li>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/requestpw.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/requestpw.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/requestpw.pt	(revision 12677)
@@ -0,0 +1,30 @@
+<form action="." tal:attributes="action request/URL" method="post"
+    i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tr tal:condition="view/captcha_code">
+        <td i18n:translate="">
+          Please solve the captcha<br />to prevent misuse of this service:
+        </td>
+        <td>
+          <div tal:content="structure view/captcha_code"></div>
+        </td>
+      </tr>
+
+    </tbody>
+  </table>
+  <br />
+  <p i18n:translate="">
+    Your customer record will be looked up and
+    your login credentials will be sent to the email address given above.
+    To be able to proceed you must provide a valid email address!
+  </p>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/requestpwmailsent.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/requestpwmailsent.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/requestpwmailsent.pt	(revision 12677)
@@ -0,0 +1,47 @@
+<tal:only_email condition = "not: view/password" i18n:domain="waeup.ikoba">
+  <p tal:condition = "view/email">
+    <span i18n:translate=""> An email with your user name and password
+    has been sent to
+    </span>
+    <span tal:replace="view/email">user@foo.baz</span>.
+  </p>
+  <p tal:condition = "view/email" i18n:translate="">
+    Please check your email account to proceed.
+  </p>
+  <p tal:condition = "not: view/email" i18n:translate="">
+    Email could not been sent. Please retry later.
+  </p>
+  <p  i18n:translate="">
+    Thanks for using Ikoba!
+  </p>
+</tal:only_email>
+
+<tal:credentials condition = "view/password" i18n:domain="waeup.ikoba">
+  <h3 i18n:translate="">Your login credentials are:</h3>
+  <table class="bordered-table span6">
+    <tr>
+      <td i18n:translate="">User Name:</td>
+      <td tal:content="view/applicant_id">ID</td>
+    <tr>
+    <tr>
+      <td i18n:translate="">Password:</td>
+      <td tal:content="view/password">PASSWORD</td>
+    <tr>
+  </table>
+  <p>
+    <span i18n:translate="">Print this page and proceed to the</span>
+    <a tal:attributes="href python: view.url(layout.site, 'login')">login form</a>.
+    <span i18n:translate="">Please note that passwords are case-sensitive,
+    <br />when entering your credentials, and keep your password secret!
+    </span>
+  </p>
+  <p tal:condition = "view/email">
+    <span i18n:translate=""> An email with your user name and password
+    has been sent to
+    </span>
+    <span tal:replace="view/email">user@foo.baz</span>.
+  </p>
+  <p  i18n:translate="">
+    Thanks for using Ikoba!
+  </p>
+</tal:credentials>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/selectpaymentmethodpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/selectpaymentmethodpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/selectpaymentmethodpage.pt	(revision 12677)
@@ -0,0 +1,26 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname" i18n:translate=""></td>
+        <td>
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate=""></td>
+        <td>
+
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <br />
+
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/trigtrans.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/trigtrans.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser_templates/trigtrans.pt	(revision 12677)
@@ -0,0 +1,19 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data"
+      autocomplete="off">
+
+    <br />
+
+    <select id="transition" name="transition" class="form-control half">
+      <option tal:repeat="transition view/getTransitions"
+              tal:attributes="value transition/name">
+        <span tal:replace="transition/title">TRANSITIONTITLE</span>
+      </option>
+    </select>
+    <br /><br />
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/catalog.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/catalog.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/catalog.py	(revision 12677)
@@ -0,0 +1,133 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Cataloging and searching components for customers.
+"""
+import grok
+from grok import index
+from hurry.query import Eq, Text
+from hurry.query.query import Query
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility
+from waeup.ikoba.catalog import FilteredCatalogQueryBase
+from waeup.ikoba.interfaces import (
+    ICompany, IQueryResultItem)
+from waeup.ikoba.customers.interfaces import ICustomer, IContract
+
+
+class CustomersCatalog(grok.Indexes):
+    """A catalog for customers.
+    """
+    grok.site(ICompany)
+    grok.name('customers_catalog')
+    grok.context(ICustomer)
+
+    customer_id = index.Field(attribute='customer_id')
+    fullname = index.Text(attribute='fullname')
+    email = index.Field(attribute='email')
+    reg_number = index.Field(attribute='reg_number')
+    state = index.Field(attribute='state')
+
+
+class CustomerQueryResultItem(object):
+    grok.implements(IQueryResultItem)
+
+    title = u'Customer Query Item'
+    description = u'Some customer found in a search'
+
+    def __init__(self, context, view):
+        self.context = context
+        self.url = view.url(context)
+        self.customer_id = context.customer_id
+        self.display_fullname = context.display_fullname
+        self.reg_number = context.reg_number
+        self.state = context.state
+        self.translated_state = context.translated_state
+
+
+def search(query=None, searchtype=None, view=None):
+    hitlist = []
+    if searchtype in ('fullname',):
+        results = Query().searchResults(
+            Text(('customers_catalog', searchtype), query))
+    elif searchtype == 'suspended':
+        # 'suspended' is not indexed
+        cat = queryUtility(ICatalog, name='customers_catalog')
+        all = cat.searchResults(customer_id=(None, None))
+        for customer in all:
+            if customer.suspended:
+                hitlist.append(CustomerQueryResultItem(customer, view=view))
+        return hitlist
+    else:
+        # Temporary solution to display all customers added
+        if query == '*':
+            cat = queryUtility(ICatalog, name='customers_catalog')
+            results = cat.searchResults(customer_id=(None, None))
+        else:
+            results = Query().searchResults(
+                Eq(('customers_catalog', searchtype), query))
+    for result in results:
+        hitlist.append(CustomerQueryResultItem(result, view=view))
+    return hitlist
+
+
+class SimpleFieldSearch(object):
+    """A programmatic (no UI required) search.
+
+    Looks up a given field attribute of the customers catalog for a
+    single value. So normally you would call an instance of this
+    search like this:
+
+      >>> SimpleFieldSearch()(reg_number='somevalue')
+
+    """
+    catalog_name = 'customers_catalog'
+
+    def __call__(self, **kw):
+        """Search customers catalog programmatically.
+        """
+        if len(kw) != 1:
+            raise ValueError('must give exactly one index name to search')
+        cat = queryUtility(ICatalog, name=self.catalog_name)
+        index_name, query_term = kw.items()[0]
+        results = cat.searchResults(index_name=(query_term, query_term))
+        return results
+
+#: an instance of `SimpleFieldSearch` looking up customers catalog.
+simple_search = SimpleFieldSearch()
+
+
+class CustomersQuery(FilteredCatalogQueryBase):
+    """Query customers in a site. See waeup.ikoba.catalog for more info.
+    """
+    cat_name = 'customers_catalog'
+    defaults = dict(customer_id=None)  # make sure we get all studs by default
+
+
+# Catalog for customer contracts
+
+class ContractsCatalog(grok.Indexes):
+    """A catalog for all contracts.
+    """
+    grok.site(ICompany)
+    grok.name('contracts_catalog')
+    grok.context(IContract)
+
+    contract_id = grok.index.Field(attribute='contract_id')
+    last_product_id = grok.index.Field(attribute='last_product_id')
+    contract_category = grok.index.Field(attribute='contract_category')
+    user_id = grok.index.Field(attribute='user_id')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/container.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/container.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/container.py	(revision 12677)
@@ -0,0 +1,77 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Containers for customers.
+"""
+import grok
+from thread import allocate_lock
+from transaction import commit
+from zope.component import getUtility
+from waeup.ikoba.customers.interfaces import (
+    ICustomersContainer, ICustomer, ICustomersUtils)
+from waeup.ikoba.utils.helpers import attrs_to_fields
+from waeup.ikoba.utils.logger import Logger
+
+lock = allocate_lock()  # a lock object to lock threads.
+
+
+class CustomersContainer(grok.Container, Logger):
+    """
+    The node containing the customer models
+    """
+
+    grok.implements(ICustomersContainer)
+
+    _curr_cust_id = 10 ** 6
+
+    logger_name = 'waeup.ikoba.${sitename}.customers'
+    logger_filename = 'customers.log'
+
+    @property
+    def unique_customer_id(self):
+        """A unique customer id.
+
+        The customer id returned is guaranteed to be unique. It
+        consists of some prefix (normally a single letter) followed by
+        a number with at least 7 digits.
+
+        Once a customer id was issued, it won't be issued again.
+
+        Obtaining a customer id is currently not thread-safe but can be
+        made easily by enabling commented lines.
+        """
+        prefix = getUtility(ICustomersUtils).CUSTOMER_ID_PREFIX
+
+        # lock.acquire() # lock data
+        new_id = u'%s%s' % (prefix, self._curr_cust_id)
+        self._curr_cust_id += 1
+        # self._p_changed = True
+        # commit()
+        # lock.release() # end of lock
+        return new_id
+
+    def addCustomer(self, customer):
+        """Add a customer with subcontainers.
+        """
+        if not ICustomer.providedBy(customer):
+            raise TypeError(
+                'CustomersContainers contain only ICustomer instances')
+        self[customer.customer_id] = customer
+        return
+
+CustomersContainer = attrs_to_fields(CustomersContainer)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/contracts.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/contracts.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/contracts.py	(revision 12677)
@@ -0,0 +1,219 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Customer contract components.
+"""
+import os
+import grok
+from zope.component import queryUtility, getUtility
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.schema import getFields
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import (
+    IIkobaUtils, IObjectHistory,
+    VERIFIED, APPROVED, PROVISIONALLY,
+    IIDSource)
+from waeup.ikoba.customers.interfaces import (
+    IContractsContainer, ICustomerNavigation,
+    IContract, IContractSelectProduct, ICustomersUtils,
+    ISampleContract, ISampleContractProcess, ISampleContractEdit,
+    ISampleContractOfficialUse)
+from waeup.ikoba.customers.utils import generate_contract_id
+from waeup.ikoba.utils.helpers import attrs_to_fields
+
+class ContractsContainer(grok.Container):
+    """This is a container for customer contracts.
+    """
+    grok.implements(IContractsContainer, ICustomerNavigation)
+    grok.provides(IContractsContainer)
+
+    def addContract(self, contract):
+        if not IContract.providedBy(contract):
+            raise TypeError(
+                'ContractsContainers contain only IContract instances')
+        self[contract.contract_id] = contract
+        return
+
+    @property
+    def customer(self):
+        return self.__parent__
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.writeLogMessage(view, message)
+
+ContractsContainer = attrs_to_fields(ContractsContainer)
+
+class ContractBase(grok.Container):
+    """This is a customer contract baseclass.
+    """
+    grok.implements(IContractSelectProduct)  # Necesary for the selectproduct page
+
+    grok.baseclass()
+
+    def __init__(self):
+        super(ContractBase, self).__init__()
+        # The site doesn't exist in unit tests
+        source = getUtility(IIDSource)
+        self.contract_id = unicode(source.get_hex_uuid())
+        self.last_product_id = None
+        self.tc_dict = {}
+        self.title = None
+        self.valid_to = None
+        self.valid_from = None
+        return
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    @property
+    def state(self):
+        return IWorkflowState(self).getState()
+
+    @property
+    def translated_state(self):
+        try:
+            TRANSLATED_STATES = getUtility(
+                ICustomersUtils).TRANSLATED_CONTRACT_STATES
+            return TRANSLATED_STATES[self.state]
+        except KeyError:
+            return
+
+    @property
+    def class_name(self):
+        return self.__class__.__name__
+
+    @property
+    def formatted_transition_date(self):
+        try:
+            return self.history.messages[-1].split(' - ')[0]
+        except IndexError:
+            return
+
+    @property
+    def customer(self):
+        try:
+            return self.__parent__.__parent__
+        except AttributeError:
+            return None
+
+    @property
+    def user_id(self):
+        if self.customer is not None:
+            return self.customer.customer_id
+        return
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.__parent__.writeLogMessage(view, message)
+
+    @property
+    def is_editable(self):
+        try:
+            # Customer must have requested
+            cond1 = self.customer.state in getUtility(
+                ICustomersUtils).CONMANAGE_CUSTOMER_STATES
+            # Contract must be in state created
+            cond2 = self.state in getUtility(
+                ICustomersUtils).CONMANAGE_CONTRACT_STATES
+            if not (cond1 and cond2):
+                return False
+        except AttributeError:
+            pass
+        return True
+
+    @property
+    def is_approvable(self):
+        if self.customer and not self.customer.state in (
+            APPROVED, PROVISIONALLY):
+            return False, _("Customer has not yet been approved.")
+        for key, field in getFields(self.check_docs_interface).items():
+            if key.endswith('_object'):
+                obj = getattr(self, key, None)
+                state = getattr(obj, 'state', None)
+                if state and state != VERIFIED:
+                    return False, _("Attached documents must be verified first.")
+        return True, None
+
+    @property
+    def translated_class_name(self):
+        try:
+            CONTYPES_DICT = getUtility(ICustomersUtils).CONTYPES_DICT
+            return CONTYPES_DICT[self.class_name]
+        except KeyError:
+            return
+
+    @property
+    def fee_based(self):
+        if self.product_options:
+            amount = 0
+            for option in self.product_options:
+                amount += option.fee
+            if amount:
+                return True
+        return False
+
+
+class SampleContract(ContractBase):
+    """This is a sample contract.
+    """
+
+    grok.implements(
+        ISampleContractProcess, # must come before ISampleContract
+        ISampleContract,
+        ISampleContractEdit,
+        ICustomerNavigation)
+
+    contract_category = 'sample'
+
+    form_fields_interface = ISampleContract
+
+    edit_form_fields_interface = ISampleContractEdit
+
+    ou_form_fields_interface = ISampleContractOfficialUse
+
+    check_docs_interface = ISampleContract
+
+SampleContract = attrs_to_fields(SampleContract)
+
+
+# Contracts must be importable. So we might need a factory.
+class SampleContractFactory(grok.GlobalUtility):
+    """A factory for contracts.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.SampleContract')
+    title = u"Create a new contract.",
+    description = u"This factory instantiates new sample contract instances."
+
+    def __call__(self, *args, **kw):
+        return SampleContract(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(SampleContract)
+
+@grok.subscribe(IContract, grok.IObjectAddedEvent)
+def handle_contract_added(contract, event):
+    """If an contract is added the transition create is fired.
+    The latter produces a logging message.
+    """
+    if IWorkflowState(contract).getState() is None:
+        IWorkflowInfo(contract).fireTransition('create')
+    return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/customer.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/customer.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/customer.py	(revision 12677)
@@ -0,0 +1,400 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Container for the various objects owned by customers.
+"""
+import os
+import shutil
+import grok
+from datetime import datetime, timedelta
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from zope.password.interfaces import IPasswordManager
+from zope.component import getUtility, createObject
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.schema.interfaces import ConstraintNotSatisfied
+
+from waeup.ikoba.image import IkobaImageFile
+from waeup.ikoba.imagestorage import DefaultFileStoreHandler
+from waeup.ikoba.interfaces import (
+    IObjectHistory, IUserAccount, IFileStoreNameChooser, IFileStoreHandler,
+    IIkobaUtils, IExtFileStore,
+    CREATED, REQUESTED, APPROVED)
+from waeup.ikoba.customers.interfaces import (
+    ICustomer, ICustomerNavigation, ICSVCustomerExporter,
+    ICustomersUtils)
+from waeup.ikoba.customers.utils import generate_customer_id, path_from_custid
+from waeup.ikoba.customers.documents import CustomerDocumentsContainer
+from waeup.ikoba.customers.contracts import ContractsContainer
+from waeup.ikoba.utils.helpers import attrs_to_fields, now, copy_filesystem_tree
+
+class Customer(grok.Container):
+    """This is a customer container for the various objects
+    owned by customers.
+    """
+    grok.implements(ICustomer, ICustomerNavigation)
+    grok.provides(ICustomer)
+
+    temp_password_minutes = 10
+
+    form_fields_interface = ICustomer
+
+    def __init__(self):
+        super(Customer, self).__init__()
+        # The site doesn't exist in unit tests
+        try:
+            self.customer_id = generate_customer_id()
+        except TypeError:
+            self.customer_id = u'Z654321'
+        self.password = None
+        self.temp_password = None
+        return
+
+    def setTempPassword(self, user, password):
+        """Set a temporary password (LDAP-compatible) SSHA encoded for
+        officers.
+
+        """
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        self.temp_password = {}
+        self.temp_password[
+            'password'] = passwordmanager.encodePassword(password)
+        self.temp_password['user'] = user
+        self.temp_password['timestamp'] = datetime.utcnow()  # offset-naive datetime
+
+    def getTempPassword(self):
+        """Check if a temporary password has been set and if it
+        is not expired.
+
+        Return the temporary password if valid,
+        None otherwise. Unset the temporary password if expired.
+        """
+        temp_password_dict = getattr(self, 'temp_password', None)
+        if temp_password_dict is not None:
+            delta = timedelta(minutes=self.temp_password_minutes)
+            now = datetime.utcnow()
+            if now < temp_password_dict.get('timestamp') + delta:
+                return temp_password_dict.get('password')
+            else:
+                # Unset temporary password if expired
+                self.temp_password = None
+        return None
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
+        self.__parent__.logger.info(
+            '%s - %s - %s' % (ob_class, self.__name__, message))
+        return
+
+    @property
+    def display_fullname(self):
+        middlename = getattr(self, 'middlename', None)
+        ikoba_utils = getUtility(IIkobaUtils)
+        return ikoba_utils.fullname(self.firstname, self.lastname, middlename)
+
+    @property
+    def title(self):
+        return self.display_fullname
+
+    @property
+    def fullname(self):
+        firstname = getattr(self, 'firstname', None)
+        middlename = getattr(self, 'middlename', None)
+        lastname = getattr(self, 'lastname', None)
+        if middlename and lastname and firstname:
+            return '%s-%s-%s' % (firstname.lower(),
+                middlename.lower(), lastname.lower())
+        elif lastname and firstname:
+            return '%s-%s' % (firstname.lower(), lastname.lower())
+        return None
+
+    @property
+    def state(self):
+        state = IWorkflowState(self).getState()
+        return state
+
+    @property
+    def translated_state(self):
+        try:
+            TRANSLATED_STATES = getUtility(
+                ICustomersUtils).TRANSLATED_CUSTOMER_STATES
+            ts = TRANSLATED_STATES[self.state]
+            return ts
+        except KeyError:
+            return
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    @property
+    def customer(self):
+        return self
+
+    @property
+    def personal_data_expired(self):
+        return False
+
+# Set all attributes of Customer required in ICustomer 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.
+Customer = attrs_to_fields(Customer)
+
+
+class CustomerFactory(grok.GlobalUtility):
+    """A factory for customers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Customer')
+    title = u"Create a new customer.",
+    description = u"This factory instantiates new customer instances."
+
+    def __call__(self, *args, **kw):
+        return Customer()
+
+    def getInterfaces(self):
+        return implementedBy(Customer)
+
+
+@grok.subscribe(ICustomer, grok.IObjectAddedEvent)
+def handle_customer_added(customer, event):
+    """If a customer is added all subcontainers are automatically added
+    and the transition create is fired. The latter produces a logging
+    message.
+    """
+    # Assign global customer role for new customer
+    account = IUserAccount(customer)
+    account.roles = ['waeup.Customer']
+    # Assign local CustomerRecordOwner role
+    role_manager = IPrincipalRoleManager(customer)
+    role_manager.assignRoleToPrincipal(
+        'waeup.local.CustomerRecordOwner', customer.customer_id)
+    if customer.state is None:
+        IWorkflowInfo(customer).fireTransition('create')
+    documents = CustomerDocumentsContainer()
+    customer['documents'] = documents
+    contracts = ContractsContainer()
+    customer['contracts'] = contracts
+    return
+
+def move_customer_files(customer, del_dir):
+    """Move files belonging to `customer` to `del_dir`.
+
+    `del_dir` is expected to be the path to the site-wide directory
+    for storing backup data.
+
+    The current files of the customer are removed after backup.
+
+    If the customer has no associated files stored, nothing is done.
+    """
+    cust_id = customer.customer_id
+
+    src = getUtility(IExtFileStore).root
+    src = os.path.join(src, 'customers', path_from_custid(cust_id))
+
+    dst = os.path.join(
+        del_dir, 'media', 'customers', path_from_custid(cust_id))
+
+    if not os.path.isdir(src):
+        # Do not copy if no files were stored.
+        return
+    if not os.path.exists(dst):
+        os.makedirs(dst, 0755)
+    copy_filesystem_tree(src, dst)
+    shutil.rmtree(src)
+    return
+
+
+def update_customer_deletion_csvs(customer, del_dir):
+    """Update deletion CSV files with data from customer.
+
+    `del_dir` is expected to be the path to the site-wide directory
+    for storing backup data.
+
+    Each exporter available for customers (and their many subobjects)
+    is called in order to export CSV data of the given customer to csv
+    files in the site-wide backup directory for object data (see
+    DataCenter).
+
+    Each exported row is appended a column giving the deletion date
+    (column `del_date`) as a UTC timestamp.
+    """
+    for name in getUtility(ICustomersUtils).EXPORTER_NAMES:
+        exporter = getUtility(ICSVCustomerExporter, name=name)
+        csv_data = exporter.export_customer(customer)
+        csv_data = csv_data.split('\r\n')
+
+        # append a deletion timestamp on each data row
+        timestamp = str(now().replace(microsecond=0))  # store UTC timestamp
+        for num, row in enumerate(csv_data[1:-1]):
+            csv_data[num+1] = csv_data[num+1] + ',' + timestamp
+        csv_path = os.path.join(del_dir, '%s.csv' % name)
+
+        # write data to CSV file
+        if not os.path.exists(csv_path):
+            # create new CSV file (including header line)
+            csv_data[0] = csv_data[0] + ',del_date'
+            open(csv_path, 'wb').write('\r\n'.join(csv_data))
+        else:
+            # append existing CSV file (omitting headerline)
+            open(csv_path, 'a').write('\r\n'.join(csv_data[1:]))
+    return
+
+
+@grok.subscribe(ICustomer, grok.IObjectRemovedEvent)
+def handle_customer_removed(customer, event):
+    """If a customer is removed a message is logged and data is put
+       into a backup location.
+
+    The data of the removed customer is appended to CSV files in local
+    datacenter and any existing external files (passport images, etc.)
+    are copied over to this location as well.
+
+    Documents in the file storage refering to the given customer are
+    removed afterwards (if they exist). Please make no assumptions
+    about how the deletion takes place. Files might be deleted
+    individually (leaving the customers file directory intact) or the
+    whole customer directory might be deleted completely.
+
+    All CSV rows created/appended contain a timestamp with the
+    datetime of removal in an additional `del_date` column.
+
+    XXX: blocking of used customer_ids yet not implemented.
+    """
+    comment = 'Customer record removed'
+    target = customer.customer_id
+    try:
+        site = grok.getSite()
+        site['customers'].logger.info('%s - %s' % (
+            target, comment))
+    except KeyError:
+        # If we delete an entire university instance there won't be
+        # a customers subcontainer
+        return
+
+    del_dir = site['datacenter'].deleted_path
+
+    # save files of the customer
+    move_customer_files(customer, del_dir)
+
+    # update CSV files
+    update_customer_deletion_csvs(customer, del_dir)
+    return
+
+#: The file id marker for customer files
+CUSTOMER_FILE_STORE_NAME = 'file-customer'
+
+
+class CustomerFileNameChooser(grok.Adapter):
+    """A file id chooser for :class:`Customer` objects.
+
+    `context` is an :class:`Customer` instance.
+
+    The :class:`CustomerFileNameChooser` can build/check file ids for
+    :class:`Customer` objects suitable for use with
+    :class:`ExtFileStore` instances. The delivered file_id contains
+    the file id marker for :class:`Customer` object and the customer id
+    of the context customer.
+
+    This chooser is registered as an adapter providing
+    :class:`waeup.ikoba.interfaces.IFileStoreNameChooser`.
+
+    File store name choosers like this one are only convenience
+    components to ease the task of creating file ids for customer
+    objects. You are nevertheless encouraged to use them instead of
+    manually setting up filenames for customers.
+
+    .. seealso:: :mod:`waeup.ikoba.imagestorage`
+
+    """
+    grok.context(ICustomer)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name=None, attr=None):
+        """Check whether the given name is a valid file id for the context.
+
+        Returns ``True`` only if `name` equals the result of
+        :meth:`chooseName`.
+
+        """
+        return name == self.chooseName()
+
+    def chooseName(self, attr, name=None):
+        """Get a valid file id for customer context.
+
+        *Example:*
+
+        For a customer with customer id ``'A123456'`` and
+        with attr ``'nice_image.jpeg'`` stored in
+        the customers container this chooser would create:
+
+          ``'__file-customer__customers/A/A123456/nice_image_A123456.jpeg'``
+
+        meaning that the nice image of this customer would be
+        stored in the site-wide file storage in path:
+
+          ``customers/A/A123456/nice_image_A123456.jpeg``
+
+        """
+        basename, ext = os.path.splitext(attr)
+        cust_id = self.context.customer_id
+        marked_filename = '__%s__%s/%s_%s%s' % (
+            CUSTOMER_FILE_STORE_NAME, path_from_custid(cust_id), basename,
+            cust_id, ext)
+        return marked_filename
+
+
+class CustomerFileStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
+    """Customer specific file handling.
+
+    This handler knows in which path in a filestore to store customer
+    files and how to turn this kind of data into some (browsable)
+    file object.
+
+    It is called from the global file storage, when it wants to
+    get/store a file with a file id starting with
+    ``__file-customer__`` (the marker string for customer files).
+
+    Like each other file store handler it does not handle the files
+    really (this is done by the global file store) but only computes
+    paths and things like this.
+    """
+    grok.implements(IFileStoreHandler)
+    grok.name(CUSTOMER_FILE_STORE_NAME)
+
+    def pathFromFileID(self, store, root, file_id):
+        """All customer files are put in directory ``customers``.
+        """
+        marker, filename, basename, ext = store.extractMarker(file_id)
+        sub_root = os.path.join(root, 'customers')
+        return super(CustomerFileStoreHandler, self).pathFromFileID(
+            store, sub_root, basename)
+
+    def createFile(self, store, root, filename, file_id, file):
+        """Create a browsable file-like object.
+        """
+        # call super method to ensure that any old files with
+        # different filename extension are deleted.
+        file, path, file_obj = super(
+            CustomerFileStoreHandler, self).createFile(
+            store, root,  filename, file_id, file)
+        return file, path, IkobaImageFile(
+            file_obj.filename, file_obj.data)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/documents.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/documents.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/documents.py	(revision 12677)
@@ -0,0 +1,375 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Customer document components.
+"""
+import os
+import grok
+from hashlib import md5
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.component import queryUtility, getUtility
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.event import notify
+
+from waeup.ikoba.image import IkobaImageFile
+from waeup.ikoba.imagestorage import DefaultFileStoreHandler
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import (
+    IFileStoreNameChooser, IFileStoreHandler,
+    IIkobaUtils, IExtFileStore, IIDSource)
+from waeup.ikoba.customers.interfaces import (
+    ICustomerDocumentsContainer, ICustomerNavigation, ICustomerDocument,
+    ICustomersUtils, ICustomerPDFDocument)
+from waeup.ikoba.documents import DocumentsContainer, Document
+from waeup.ikoba.documents.interfaces import IDocumentsUtils
+from waeup.ikoba.utils.helpers import attrs_to_fields
+
+from waeup.ikoba.customers.utils import path_from_custid
+
+class CustomerDocumentsContainer(DocumentsContainer):
+    """This is a container for customer documents.
+    """
+    grok.implements(ICustomerDocumentsContainer, ICustomerNavigation)
+    grok.provides(ICustomerDocumentsContainer)
+
+    def __init__(self):
+        super(CustomerDocumentsContainer, self).__init__()
+        return
+
+    @property
+    def customer(self):
+        return self.__parent__
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.writeLogMessage(view, message)
+
+CustomerDocumentsContainer = attrs_to_fields(CustomerDocumentsContainer)
+
+class CustomerDocumentBase(Document):
+    """This is a customer document baseclass.
+    """
+    grok.baseclass()
+
+    local_roles = []
+
+    def __init__(self):
+        super(CustomerDocumentBase, self).__init__()
+        source = getUtility(IIDSource)
+        self.document_id = unicode(source.get_hex_uuid())
+        return
+
+    @property
+    def state(self):
+        state = IWorkflowState(self).getState()
+        return state
+
+    @property
+    def translated_state(self):
+        try:
+            TRANSLATED_STATES = getUtility(
+                ICustomersUtils).TRANSLATED_DOCUMENT_STATES
+            return TRANSLATED_STATES[self.state]
+        except KeyError:
+            return
+    @property
+    def customer(self):
+        try:
+            return self.__parent__.__parent__
+        except AttributeError:
+            return None
+
+    @property
+    def user_id(self):
+        if self.customer is not None:
+            return self.customer.customer_id
+        return
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.__parent__.writeLogMessage(view, message)
+
+    @property
+    def is_editable_by_customer(self):
+        try:
+            # Customer must be approved
+            cond1 = self.customer.state in getUtility(
+                ICustomersUtils).DOCMANAGE_CUSTOMER_STATES
+            # Document must be in state created
+            cond2 = self.state in getUtility(
+                ICustomersUtils).DOCMANAGE_DOCUMENT_STATES
+            if not (cond1 and cond2):
+                return False
+        except AttributeError:
+            pass
+        return True
+
+    @property
+    def is_editable_by_manager(self):
+        try:
+            # Document must be in state created
+            cond = self.state in getUtility(
+                ICustomersUtils).DOCMANAGE_DOCUMENT_STATES
+            if not cond:
+                return False
+        except AttributeError:
+            pass
+        return True
+
+    @property
+    def translated_class_name(self):
+        try:
+            DOCTYPES_DICT = getUtility(ICustomersUtils).DOCTYPES_DICT
+            return DOCTYPES_DICT[self.class_name]
+        except KeyError:
+            return
+
+    @property
+    def connected_files(self):
+        store = getUtility(IExtFileStore)
+        files = []
+        try:
+            # Usually there is only a single element in self.filenames.
+            for filename in self.filenames:
+                attrname = filename.replace('.','_')
+                file = store.getFileByContext(self, attr=filename)
+                if file:
+                    files.append((attrname, file))
+        except AttributeError:
+            # In unit tests we don't have a customer to
+            # determine the file path.
+            return
+        return files
+
+    @property
+    def is_verifiable(self):
+        if self.customer and not self.customer.state in getUtility(
+                ICustomersUtils).CONMANAGE_CUSTOMER_STATES:
+            return False, _("Customer has not yet been approved.")
+        files = self.connected_files
+        if files is not None and len(files) != len(self.filenames):
+            return False, _("No file uploaded.")
+        return True, None
+
+    def setMD5(self):
+        """Set md5 checksum attribute for files connected to this document.
+        """
+        connected_files = self.connected_files
+        if connected_files:
+            for file in self.connected_files:
+                attrname = '%s_md5' % file[0]
+                checksum = md5(file[1].read()).hexdigest()
+                setattr(self, attrname, checksum)
+        return
+
+
+class CustomerSampleDocument(CustomerDocumentBase):
+    """This is a sample customer document.
+    """
+
+    grok.implements(ICustomerDocument, ICustomerNavigation)
+    grok.provides(ICustomerDocument)
+
+    # Ikoba can store any number of files per Document object.
+    # However, we highly recommend to associate and store
+    # only one file per Document object. Thus the following
+    # tuple should contain only a single filename string.
+    filenames = ('sample',)
+
+    form_fields_interface = ICustomerDocument
+
+CustomerSampleDocument = attrs_to_fields(CustomerSampleDocument)
+
+
+class CustomerPDFDocument(CustomerDocumentBase):
+    """This is a customer document for a single pdf upload file.
+    """
+    grok.implements(ICustomerPDFDocument, ICustomerNavigation)
+    grok.provides(ICustomerPDFDocument)
+
+    # Ikoba can store any number of files per Document object.
+    # However, we highly recommend to associate and store
+    # only one file per Document object. Thus the following
+    # tuple should contain only a single filename string.
+    filenames = ('sample.pdf',)
+
+    form_fields_interface = ICustomerPDFDocument
+
+CustomerPDFDocument = attrs_to_fields(CustomerPDFDocument)
+
+
+# Customer documents must be importable. So we need a factory.
+class CustomerDocumentFactory(grok.GlobalUtility):
+    """A factory for customer documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CustomerSampleDocument')
+    title = u"Create a new document.",
+    description = u"This factory instantiates new sample document instances."
+
+    def __call__(self, *args, **kw):
+        return CustomerSampleDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CustomerSampleDocument)
+
+# Customer documents must be importable. So we might need a factory.
+class CustomerPDFDocumentFactory(grok.GlobalUtility):
+    """A factory for customer pdf documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CustomerPDFDocument')
+    title = u"Create a new document.",
+    description = u"This factory instantiates new pdf document instances."
+
+    def __call__(self, *args, **kw):
+        return CustomerPDFDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CustomerPDFDocument)
+
+#: The file id marker for customer files
+CUSTOMERDOCUMENT_FILE_STORE_NAME = 'file-customerdocument'
+
+
+class CustomerDocumentFileNameChooser(grok.Adapter):
+    """A file id chooser for :class:`CustomerDocument` objects.
+
+    `context` is an :class:`CustomerDocument` instance.
+
+    The :class:`CustomerDocumentFileNameChooser` can build/check file ids for
+    :class:`Customer` objects suitable for use with
+    :class:`ExtFileStore` instances. The delivered file_id contains
+    the file id marker for :class:`CustomerDocument` object and the customer id
+    of the context customer.
+
+    This chooser is registered as an adapter providing
+    :class:`waeup.ikoba.interfaces.IFileStoreNameChooser`.
+
+    File store name choosers like this one are only convenience
+    components to ease the task of creating file ids for customer document
+    objects. You are nevertheless encouraged to use them instead of
+    manually setting up filenames for customer documents.
+
+    .. seealso:: :mod:`waeup.ikoba.imagestorage`
+
+    """
+
+    grok.context(ICustomerDocument)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name=None, attr=None):
+        """Check whether the given name is a valid file id for the context.
+
+        Returns ``True`` only if `name` equals the result of
+        :meth:`chooseName`.
+
+        """
+        return name == self.chooseName()
+
+    def chooseName(self, attr, name=None):
+        """Get a valid file id for customer document context.
+
+        *Example:*
+
+        For a customer with customer id ``'A123456'``
+        and document with id 'd123'
+        with attr ``'nice_image.jpeg'`` stored in
+        the customers container this chooser would create:
+
+          ``'__file-customerdocument__customers/345/c999/nice_image_d123_c999.jpeg'``
+
+        meaning that the nice image of this customer document would be
+        stored in the site-wide file storage in path:
+
+          ``customers/345/c999/nice_image_d123_c999.jpeg``
+
+        """
+        basename, ext = os.path.splitext(attr)
+        cust_id = self.context.customer.customer_id
+        doc_id = self.context.document_id
+        marked_filename = '__%s__%s/%s_%s_%s%s' % (
+            CUSTOMERDOCUMENT_FILE_STORE_NAME, path_from_custid(cust_id),
+            basename, doc_id, cust_id, ext)
+        return marked_filename
+
+
+class CustomerDocumentFileStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
+    """Customer document specific file handling.
+
+    This handler knows in which path in a filestore to store customer document
+    files and how to turn this kind of data into some (browsable)
+    file object.
+
+    It is called from the global file storage, when it wants to
+    get/store a file with a file id starting with
+    ``__file-customerdocument__`` (the marker string for customer files).
+
+    Like each other file store handler it does not handle the files
+    really (this is done by the global file store) but only computes
+    paths and things like this.
+    """
+    grok.implements(IFileStoreHandler)
+    grok.name(CUSTOMERDOCUMENT_FILE_STORE_NAME)
+
+    def pathFromFileID(self, store, root, file_id):
+        """All customer document files are put in directory ``customers``.
+        """
+        marker, filename, basename, ext = store.extractMarker(file_id)
+        sub_root = os.path.join(root, 'customers')
+        return super(CustomerDocumentFileStoreHandler, self).pathFromFileID(
+            store, sub_root, basename)
+
+    def createFile(self, store, root, filename, file_id, file):
+        """Create a browsable file-like object.
+        """
+        # call super method to ensure that any old files with
+        # different filename extension are deleted.
+        file, path, file_obj = super(
+            CustomerDocumentFileStoreHandler, self).createFile(
+            store, root,  filename, file_id, file)
+        return file, path, IkobaImageFile(
+            file_obj.filename, file_obj.data)
+
+
+@grok.subscribe(ICustomerDocument, grok.IObjectRemovedEvent)
+def handle_document_removed(document, event):
+    """If a document is deleted, we make sure that also referrers to
+    customer contract objects are removed.
+    """
+    docid = document.document_id
+
+    # Find all customer contracts that refer to given document...
+    try:
+        contracts = document.customer['contracts'].values()
+    except AttributeError:
+        # customer not available. This might happen during tests.
+        return
+    for contract in contracts:
+        # Remove that referrer...
+        for key, value in contract.__dict__.items():
+            if key.endswith('_object') and \
+                getattr(value, 'document_id', None) == docid:
+                setattr(contract, key, None)
+                notify(grok.ObjectModifiedEvent(contract))
+                contract.customer.__parent__.logger.info(
+                    'ObjectRemovedEvent - %s - %s - removed: %s' % (
+                        contract.customer.customer_id,
+                        contract.contract_id,
+                        document.document_id))
+    return
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/export.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/export.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/export.py	(revision 12677)
@@ -0,0 +1,246 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Exporters for customer related stuff.
+"""
+import os
+import grok
+from datetime import datetime
+from zope.component import getUtility
+from waeup.ikoba.interfaces import (
+    IExtFileStore, IFileStoreNameChooser)
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.customers.catalog import CustomersQuery
+from waeup.ikoba.customers.interfaces import (
+    ICustomer, ICSVCustomerExporter,
+    ICustomerSampleDocument, ISampleContractProcess)
+from waeup.ikoba.utils.batching import ExporterBase
+from waeup.ikoba.utils.helpers import iface_names, to_timezone
+
+def get_customers(site, cust_filter=CustomersQuery()):
+    """Get all customers registered in catalog in `site`.
+    """
+    return cust_filter.query()
+
+def get_documents(customers, class_name):
+    """Get all documents of `customers`.
+    """
+    documents = []
+    for customer in customers:
+        for document in customer.get('documents', {}).values():
+            if document.class_name == class_name:
+                documents.append(document)
+    return documents
+
+def get_contracts(customers, class_name):
+    """Get all contracts of `customers`.
+    """
+    contracts = []
+    for customer in customers:
+        for contract in customer.get('contracts', {}).values():
+            if getattr(contract, 'class_name', None) == class_name:
+                contracts.append(contract)
+    return contracts
+
+class CustomerExporterBase(ExporterBase):
+    """Exporter for customers or related objects.
+
+    This is a baseclass.
+    """
+    grok.baseclass()
+    grok.implements(ICSVCustomerExporter)
+    grok.provides(ICSVCustomerExporter)
+
+    def filter_func(self, x, **kw):
+        return x
+
+    def get_filtered(self, site, **kw):
+        """Get customers from a catalog filtered by keywords.
+
+        customers_catalog is the default catalog. The keys must be valid
+        catalog index names.
+        Returns a simple empty list, a list with `Customer`
+        objects or a catalog result set with `Customer`
+        objects.
+
+        .. seealso:: `waeup.ikoba.customers.catalog.CustomersCatalog`
+
+        """
+        # Pass only given keywords to create FilteredCatalogQuery objects.
+        # This way we avoid
+        # trouble with `None` value ambivalences and queries are also
+        # faster (normally less indexes to ask). Drawback is, that
+        # developers must look into catalog to see what keywords are
+        # valid.
+        query = CustomersQuery(**kw)
+        return query.query()
+
+    def export(self, values, filepath=None):
+        """Export `values`, an iterable, as CSV file.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for value in values:
+            self.write_item(value, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export customers into filepath as CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        return self.export(self.filter_func(get_customers(site)), filepath)
+
+    def export_customer(self, customer, filepath=None):
+        return self.export(self.filter_func([customer]), filepath=filepath)
+
+    def export_filtered(self, site, filepath=None, **kw):
+        """Export items denoted by `kw`.
+
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        data = self.get_filtered(site, **kw)
+        return self.export(self.filter_func(data, **kw), filepath=filepath)
+
+
+class CustomerExporter(grok.GlobalUtility, CustomerExporterBase):
+    """Exporter for Customers.
+    """
+    grok.name('customers')
+
+    iface = ICustomer
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Customers')
+
+    #: Fieldnames considered by this exporter
+    @property
+    def fields(self):
+        return tuple(sorted(iface_names(self.iface))) + (
+            'password', 'state', 'history',)
+
+    def mangle_value(self, value, name, context=None):
+        if name == 'history':
+            value = value.messages
+        if name == 'phone' and value is not None:
+            # Append hash '#' to phone numbers to circumvent
+            # unwanted excel automatic
+            value = str('%s#' % value)
+        return super(
+            CustomerExporter, self).mangle_value(
+            value, name, context=context)
+
+
+class CustomerDocumentExporterBase(grok.GlobalUtility, CustomerExporterBase):
+    """Exporter for Customer Document instances.
+
+    This is a baseclass.
+    """
+    grok.baseclass()
+    iface = None
+    class_name = None
+    title = None
+
+    #: Fieldnames considered by this exporter
+    @property
+    def fields(self):
+        return tuple(
+            sorted(iface_names(
+                self.iface, exclude_attribs=False,
+                omit=['is_editable_by_customer',
+                      'is_editable_by_manager',
+                      'is_verifiable',
+                      'translated_state',
+                      'formatted_transition_date',
+                      'translated_class_name',
+                      'connected_files',   # Could be used to export file URLs
+                      ])))
+
+    def filter_func(self, x, **kw):
+        return get_documents(x, self.class_name)
+
+    def mangle_value(self, value, name, context=None):
+
+        if name == 'history':
+            value = value.messages
+        return super(
+            CustomerDocumentExporterBase, self).mangle_value(
+            value, name, context=context)
+
+
+class CustomerSampleDocumentExporter(CustomerDocumentExporterBase):
+    """Exporter for CustomerSampleDocument instances.
+    """
+    grok.name('customersampledocuments')
+
+    iface = ICustomerSampleDocument
+    class_name = 'CustomerSampleDocument'
+    title = _(u'Customer Sample Documents')
+
+
+class ContractExporterBase(grok.GlobalUtility, CustomerExporterBase):
+    """Exporter for Contract instances.
+
+    This is a baseclass.
+    """
+    grok.baseclass()
+    iface = None
+    class_name = None
+    title = None
+
+    #: Fieldnames considered by this exporter
+    @property
+    def fields(self):
+        return tuple(
+            sorted(iface_names(
+                self.iface, exclude_attribs=False,
+                omit=['translated_state',
+                      'formatted_transition_date',
+                      'translated_class_name',
+                      'is_editable',
+                      'is_approvable'])))
+
+    def filter_func(self, x, **kw):
+        return get_contracts(x, self.class_name)
+
+    def mangle_value(self, value, name, context=None):
+
+        if name == 'history':
+            value = value.messages
+        if name.endswith('_object'):
+            mangled_value = getattr(value, 'document_id', None)
+            if mangled_value:
+                return mangled_value
+            mangled_value = getattr(value, 'product_id', None)
+            if mangled_value:
+                return mangled_value
+        if name == 'product_options' and value is not None:
+            value = [eval(entry.to_string()) for entry in value]
+        return super(
+            ContractExporterBase, self).mangle_value(
+            value, name, context=context)
+
+
+class SampleContractExporter(ContractExporterBase):
+    """Exporter for sample contracts.
+    """
+    grok.name('samplecontracts')
+    iface = ISampleContractProcess
+    class_name = 'SampleContract'
+    title = _(u'Customer Sample Contracts')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/fileviewlets.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/fileviewlets.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/fileviewlets.py	(revision 12677)
@@ -0,0 +1,190 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+import grok
+from waeup.ikoba.interfaces import MessageFactory as _
+
+from waeup.ikoba.browser.fileviewlets import (
+    FileDisplay, FileUpload, Image)
+
+from waeup.ikoba.customers.interfaces import ICustomer
+
+from waeup.ikoba.customers.documents import (
+    CustomerSampleDocument, CustomerPDFDocument
+    )
+from waeup.ikoba.customers.browser import (
+    CustomerBaseDisplayFormPage, CustomerBaseManageFormPage,
+    CustomerFilesUploadPage,
+    DocumentDisplayFormPage, DocumentManageFormPage, DocumentEditFormPage,
+    PDFDocumentSlipPage,)
+
+grok.templatedir('browser_templates')
+
+# File viewlets for customer base page
+
+class PassportDisplay(FileDisplay):
+    """Passport display viewlet.
+    """
+    grok.order(1)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.viewCustomer')
+    grok.template('imagedisplay')
+    label = _(u'Passport Picture')
+    download_name = u'passport.jpg'
+
+
+class PassportManageUpload(FileUpload):
+    """Passport upload viewlet for officers.
+    """
+    grok.order(1)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseManageFormPage)
+    grok.require('waeup.manageCustomer')
+    grok.template('imageupload')
+    label = _(u'Passport Picture (jpg only)')
+    mus = 1024 * 50
+    download_name = u'passport.jpg'
+    tab_redirect = '#tab2'
+
+
+class PassportEditUpload(PassportManageUpload):
+    """Passport upload viewlet for customers.
+    """
+    grok.view(CustomerFilesUploadPage)
+    grok.require('waeup.handleCustomer')
+
+
+class Passport(Image):
+    """Renders jpeg passport picture.
+    """
+    grok.name('passport.jpg')
+    download_name = u'passport.jpg'
+    grok.context(ICustomer)
+    grok.require('waeup.viewCustomer')
+
+# File viewlets for customer documents
+
+class SampleScanManageUpload(FileUpload):
+    """Scan upload viewlet for officers.
+    """
+    grok.order(1)
+    grok.context(CustomerSampleDocument)
+    grok.view(DocumentManageFormPage)
+    grok.require('waeup.manageCustomer')
+    label = _(u'Sample Scan')
+    title = _(u'Sample Scan')
+    mus = 1024 * 200
+    download_name = u'sample'
+    download_filename = download_name
+    tab_redirect = '#tab2'
+
+
+class SampleScanEditUpload(SampleScanManageUpload):
+    """Scan upload viewlet for customer.
+
+    If a user has both permissions, waeup.manageCustomer and
+    waeup.handleCustomer, the upload viewlet appears twice.
+    """
+    grok.view(DocumentEditFormPage)
+    grok.require('waeup.handleCustomer')
+
+
+class SampleScanDisplay(FileDisplay):
+    """Scan display viewlet.
+    """
+    grok.order(1)
+    grok.context(CustomerSampleDocument)
+    grok.require('waeup.viewCustomer')
+    grok.view(DocumentDisplayFormPage)
+    label = _(u'Sample Scan')
+    title = _(u'Sample Scan')
+    download_name = u'sample'
+    download_filename = download_name
+
+
+class SampleScanImage(Image):
+    """Scan document.
+    """
+
+    grok.name('sample')
+    grok.context(CustomerSampleDocument)
+    grok.require('waeup.viewCustomer')
+    download_name = u'sample'
+    download_filename = download_name
+
+
+class SampleScanPDFSlip(SampleScanDisplay):
+    grok.view(PDFDocumentSlipPage)
+
+
+class PDFScanManageUpload(FileUpload):
+    """Scan upload viewlet for officers.
+    """
+    grok.view(DocumentManageFormPage)
+    grok.context(CustomerPDFDocument)
+    grok.require('waeup.manageCustomer')
+    label = _(u'PDF File')
+    title = _(u'PDF File')
+    mus = 1024 * 1000
+    download_name = u'sample.pdf'
+    tab_redirect = '#tab2'
+
+    @property
+    def download_filename(self):
+        return u"%s.pdf" % self.context.document_id[:9]
+
+
+class PDFScanEditUpload(PDFScanManageUpload):
+    """Scan upload viewlet for customer.
+    """
+    grok.view(DocumentEditFormPage)
+    grok.require('waeup.handleCustomer')
+
+
+class PDFScanDisplay(FileDisplay):
+    """Scan display viewlet.
+    """
+    grok.order(1)
+    grok.context(CustomerPDFDocument)
+    grok.require('waeup.viewCustomer')
+    grok.view(DocumentDisplayFormPage)
+    label = _(u'PDF Scan')
+    title = _(u'PDF Scan')
+    download_name = u'sample.pdf'
+
+    @property
+    def download_filename(self):
+        return u"%s.pdf" % self.context.document_id[:9]
+
+
+class PDFScanImage(Image):
+    """Scan document.
+    """
+    grok.name('sample.pdf')
+    grok.context(CustomerPDFDocument)
+    grok.require('waeup.viewCustomer')
+    download_name = u'sample.pdf'
+
+    @property
+    def download_filename(self):
+        return u"%s" % self.context.document_id[:9]
+
+
+class PDFScanSlip(PDFScanDisplay):
+    grok.view(PDFDocumentSlipPage)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/interfaces.py	(revision 12677)
@@ -0,0 +1,395 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+#from datetime import datetime
+from zope.component import getUtility
+from zope.interface import Attribute, Interface
+from zope import schema
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import (
+    IIkobaObject, validate_email, ICSVExporter, validate_uuid)
+from waeup.ikoba.schema import TextLineChoice, FormattedDate, PhoneNumber
+from waeup.ikoba.browser.interfaces import ICustomerNavigationBase
+from waeup.ikoba.documents.interfaces import IDocumentsContainer, IDocument
+from waeup.ikoba.products.productoptions import ProductOptionField
+
+from waeup.ikoba.customers.vocabularies import (
+    contextual_reg_num_source, contextual_email_source,
+    GenderSource, nats_vocab,
+    ConCatProductSource, ConCatActiveProductSource,
+    CustomerDocumentSource,
+    ProductOptionSourceFactory)
+
+
+class ICustomersUtils(Interface):
+    """A collection of methods which are subject to customization.
+
+    """
+
+
+class ICustomersContainer(IIkobaObject):
+    """A customers container contains company customers.
+
+    """
+    def addCustomer(customer):
+        """Add an ICustomer object and subcontainers.
+
+        """
+
+    unique_customer_id = Attribute("""A unique customer id.""")
+
+
+class ICustomerNavigation(ICustomerNavigationBase):
+    """Interface needed for custom navigation, logging, etc.
+
+    """
+    customer = Attribute('Customer object of context.')
+
+    def writeLogMessage(view, message):
+        """Write a view specific log message into custom.log.
+
+        """
+
+
+class ICustomer(IIkobaObject):
+    """Representation of a customer.
+
+    """
+
+    history = Attribute('Object history, a list of messages')
+    state = Attribute('Returns the registration state of a customer')
+    password = Attribute('Encrypted password of a customer')
+    temp_password = Attribute(
+        'Dictionary with user name, timestamp and encrypted password')
+    fullname = Attribute('All name parts separated by hyphens')
+    display_fullname = Attribute('The fullname of an applicant')
+    title = Attribute('A title used for email notifications only')
+
+    suspended = schema.Bool(
+        title = _(u'Account suspended'),
+        default = False,
+        required = False,
+        )
+
+    suspended_comment = schema.Text(
+        title = _(u"Reasons for Deactivation"),
+        required = False,
+        description = _(
+            u'This message will be shown if and only if deactivated '
+            'customers try to login.'),
+        )
+
+    customer_id = schema.TextLine(
+        title = _(u'Customer Id'),
+        required = False,
+        )
+
+    firstname = schema.TextLine(
+        title = _(u'First Name'),
+        required = True,
+        )
+
+    middlename = schema.TextLine(
+        title = _(u'Middle Name'),
+        required = False,
+        )
+
+    lastname = schema.TextLine(
+        title = _(u'Last Name (Surname)'),
+        required = True,
+        )
+
+    sex = schema.Choice(
+        title = _(u'Sex'),
+        source = GenderSource(),
+        required = True,
+        )
+
+    reg_number = TextLineChoice(
+        title = _(u'Registration Number'),
+        required = True,
+        readonly = False,
+        source = contextual_reg_num_source,
+        )
+
+    email = TextLineChoice(
+        title = _(u'Email'),
+        required = True,
+        constraint=validate_email,
+        source = contextual_email_source,
+        )
+
+    phone = PhoneNumber(
+        title = _(u'Phone'),
+        description = u'',
+        required = False,
+        )
+
+    def setTempPassword(user, password):
+        """Set a temporary password (LDAP-compatible) SSHA encoded for
+        officers.
+
+        """
+
+    def getTempPassword():
+        """Check if a temporary password has been set and if it
+        is not expired.
+
+        Return the temporary password if valid,
+        None otherwise. Unset the temporary password if expired.
+        """
+
+
+class ICustomerUpdateByRegNo(ICustomer):
+    """Representation of a customer. Skip regular reg_number validation.
+
+    """
+    reg_number = schema.TextLine(
+        title = _(u'Registration Number'),
+        required = False,
+        )
+
+
+class ICSVCustomerExporter(ICSVExporter):
+    """A regular ICSVExporter that additionally supports exporting
+      data from a given customer object.
+    """
+    def get_filtered(site, **kw):
+        """Get a filtered set of customer.
+        """
+
+    def export_customer(customer, filepath=None):
+        """Export data for a given customer.
+        """
+
+    def export_filtered(site, filepath=None, **kw):
+        """Export filtered set of customers.
+        """
+
+
+class ICustomerRequestPW(IIkobaObject):
+    """Representation of a customer for first-time password request.
+
+    This interface is used when customers use the requestpw page to
+    login for the the first time.
+    """
+    number = schema.TextLine(
+        title = _(u'Registration Number'),
+        required = True,
+        )
+
+    firstname = schema.TextLine(
+        title = _(u'First Name'),
+        required = True,
+        )
+
+    email = schema.TextLine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+
+
+class ICustomerCreate(IIkobaObject):
+    """Representation of an customer for account creation.
+
+    This interface is used when customers use the createaccount page.
+    """
+
+    firstname = schema.TextLine(
+        title = _(u'First Name'),
+        required = True,
+        )
+
+    middlename = schema.TextLine(
+        title = _(u'Middle Name'),
+        required = False,
+        )
+
+    lastname = schema.TextLine(
+        title = _(u'Last Name (Surname)'),
+        required = True,
+        )
+
+    email = TextLineChoice(
+        title = _(u'Email'),
+        required = True,
+        constraint=validate_email,
+        source = contextual_email_source,
+        )
+
+
+# Customer document interfaces
+
+class ICustomerDocumentsContainer(IDocumentsContainer):
+    """A container for customer document objects.
+
+    """
+
+
+class ICustomerDocument(IDocument):
+    """A customer document object.
+
+    """
+
+    is_editable_by_customer = Attribute('Document editable by customer')
+    is_editable_by_manager = Attribute('Document editable by manager')
+
+    document_id = schema.TextLine(
+        title = _(u'Document Id'),
+        required = False,
+        constraint=validate_uuid,
+        )
+
+
+class ICustomerSampleDocument(ICustomerDocument):
+    """A customer sample document object.
+
+    """
+
+
+class ICustomerPDFDocument(ICustomerDocument):
+    """A customer pdf document object.
+
+    """
+
+# Customer contract interfaces
+
+class IContractsContainer(IIkobaObject):
+    """A container for customer aplication objects.
+
+    """
+
+    def addContract(contract):
+        """Add an contract object.
+        """
+
+
+class IContract(IIkobaObject):
+    """A customer contract.
+
+    """
+    history = Attribute('Object history, a list of messages')
+    state = Attribute('Returns the contract state')
+    translated_state = Attribute(
+        'Returns a translated, more verbose appliction state')
+    class_name = Attribute('Name of the contract class')
+    formatted_transition_date = Attribute(
+        'Last transition formatted date string')
+    contract_category = Attribute('Category for the grouping of products')
+    last_product_id = Attribute(
+        'Id of product recently stored with the contract')
+    is_editable = Attribute('Contract editable by customer and manager')
+    is_approvable = Attribute('Contract approvable by officer')
+    translated_class_name = Attribute('Translatable class name')
+    user_id = Attribute('Id of a user, actually the id of the customer')
+    title = Attribute('Title generated by the associated product')
+    tc_dict = Attribute('Multilingual "Terms and Conditions" dict')
+    valid_from = Attribute('Contract valid from')
+    valid_to = Attribute('Contract valid to')
+    fee_based = Attribute('Fee based contract')
+
+    contract_id = schema.TextLine(
+        title = _(u'Contract Id'),
+        required = False,
+        constraint=validate_uuid,
+        )
+
+    product_object = schema.Choice(
+        title = _(u'Product'),
+        source = ConCatProductSource(),
+        required = False,
+        )
+
+    product_options = schema.List(
+        title = _(u'Options/Fees'),
+        value_type = schema.Choice(
+            source=ProductOptionSourceFactory(),
+            required = True,
+            ),
+        required = False,
+        readonly = False,
+        default = [],
+        )
+
+
+class IContractSelectProduct(Interface):
+    """Interface for for the ContractSelectProductPage.
+
+    """
+
+    product_object = schema.Choice(
+        title = _(u'Product'),
+        source = ConCatActiveProductSource(),
+        required = True,
+        )
+
+
+class ISampleContract(IContract):
+    """A customer contract sample with document attached.
+
+    """
+
+    document_object = schema.Choice(
+        title = _(u'Document'),
+        source = CustomerDocumentSource(),
+        required = False,
+        )
+
+
+class ISampleContractOfficialUse(IIkobaObject):
+    """Interface for official use only.
+    """
+
+    comment = schema.Text(
+        title = _(u'Comment'),
+        required = False,
+        )
+
+
+class ISampleContractProcess(ISampleContract, ISampleContractOfficialUse):
+    """Interface for processing contract data.
+    """
+
+    #title = schema.TextLine(
+    #    required = False,
+    #    )
+
+    #tc_dict = schema.Dict(
+    #    required = False,
+    #    default = {},
+    #    )
+
+    product_options = schema.List(
+        value_type = ProductOptionField(),
+        required = False,
+        readonly = False,
+        default = [],
+        )
+
+
+class ISampleContractEdit(ISampleContract):
+    """Interface for editing sample contract data by customers.
+
+    """
+
+    document_object = schema.Choice(
+        title = _(u'Document'),
+        source = CustomerDocumentSource(),
+        required = True,
+        )
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/permissions.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/permissions.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/permissions.py	(revision 12677)
@@ -0,0 +1,103 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Permissions for the customer section.
+"""
+import grok
+
+# Customer section permissions
+
+
+class HandleCustomer(grok.Permission):
+    grok.name('waeup.handleCustomer')
+
+
+class ViewCustomer(grok.Permission):
+    grok.name('waeup.viewCustomer')
+
+
+class ViewCustomersTab(grok.Permission):
+    grok.name('waeup.viewCustomersTab')
+
+
+class ViewMyCustomerDataTab(grok.Permission):
+    grok.name('waeup.viewMyCustomerDataTab')
+
+
+class ViewCustomersContainer(grok.Permission):
+    grok.name('waeup.viewCustomersContainer')
+
+
+class EditCustomerDocuments(grok.Permission):
+    grok.name('waeup.editCustomerDocuments')
+
+
+class EditContracts(grok.Permission):
+    grok.name('waeup.editContracts')
+
+
+class ManageCustomer(grok.Permission):
+    grok.name('waeup.manageCustomer')
+
+
+class LoginAsCustomer(grok.Permission):
+    grok.name('waeup.loginAsCustomer')
+
+
+class TriggerTransition(grok.Permission):
+    grok.name('waeup.triggerTransition')
+
+
+# Local role
+class CustomerRecordOwner(grok.Role):
+    grok.name('waeup.local.CustomerRecordOwner')
+    grok.title(u'Customer Record Owner')
+    grok.permissions('waeup.handleCustomer',
+                     'waeup.viewCustomer', 'waeup.editCustomerDocuments',
+                     'waeup.editContracts')
+
+
+# Site Roles
+class CustomerRole(grok.Role):
+    grok.name('waeup.Customer')
+    grok.title(u'Customer (do not assign)')
+    grok.permissions('waeup.viewProducts', 'waeup.viewMyCustomerDataTab',
+                     'waeup.Authenticated')
+
+
+class CustomersOfficer(grok.Role):
+    grok.name('waeup.CustomersOfficer')
+    grok.title(u'Customers Officer (view only)')
+    grok.permissions('waeup.viewCustomer','waeup.viewCustomers',
+          'waeup.viewCustomersTab', 'waeup.viewCustomersContainer')
+
+
+class CustomersManager(grok.Role):
+    grok.name('waeup.CustomersManager')
+    grok.title(u'Customers Manager')
+    grok.permissions('waeup.viewCustomer', 'waeup.viewCustomers',
+                     'waeup.manageCustomer', 'waeup.viewCustomersContainer',
+                     'waeup.editCustomerDocuments',
+                     'waeup.viewCustomersTab', 'waeup.triggerTransition',
+                     'waeup.editContracts')
+
+
+class CustomerImpersonator(grok.Role):
+    grok.name('waeup.CustomerImpersonator')
+    grok.title(u'Customer Impersonator')
+    grok.permissions('waeup.loginAsCustomer')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/__init__.py	(revision 12677)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/browser_templates/contractstatisticsreportgeneratorpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/browser_templates/contractstatisticsreportgeneratorpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/browser_templates/contractstatisticsreportgeneratorpage.pt	(revision 12677)
@@ -0,0 +1,11 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <p i18n:translate="">
+    The report data will be calculated and then made available for you
+    to view/download.
+  </p>
+  <br />
+  <input i18n:translate="" type="submit" class="btn btn-primary"
+         name="CREATE" value="Create" />
+  <a class="btn btn-default" href=""
+     tal:attributes="href view/parent_url">Cancel</a>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/contract_statistics.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/contract_statistics.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/contract_statistics.py	(revision 12677)
@@ -0,0 +1,230 @@
+## $Id$
+##
+## Copyright (C) 2015 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from zope.interface import implementer, Interface, Attribute
+from waeup.ikoba.interfaces import (
+    IIkobaUtils,
+    CREATED, APPROVED,
+    SUBMITTED, EXPIRED,
+    REJECTED, AWAITING)
+from waeup.ikoba.customers.interfaces import ICustomersUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.reports import IReport
+
+class IContractStatisticsReport(IReport):
+
+    creation_dt_string = Attribute('Human readable report creation datetime')
+
+def get_contract_stats():
+    """Get contracts
+
+    Returns a table ordered by contract type and
+    contract state (cols). The result is a 3-tuple representing
+    ((<TYPES>), (<STATES>), (<NUM_OF_CUSTOMERS>)). The
+    (<NUM_OF_CUSTOMERS>) is an n-tuple with each entry containing the
+    number of contracts found for that type and with the respective
+    state.
+
+    Sample result:
+
+      >>> ((u'Sample Contract 1', u'Sample Contract 2'),
+      ...  ('created', 'submitted for approval', 'approved'),
+      ...  ((12, 10, 1), (0, 5, 25)))
+
+    This result means: there are 5 contracts of type 'Sample Contract 2'
+    are in state 'submitted'.
+    """
+    site = grok.getSite()
+    TYPES = getUtility(ICustomersUtils).CONTYPES_DICT
+    types = TYPES.keys()
+    type_names = tuple(TYPES.values()) + (u'All',)
+    states = (CREATED, AWAITING, SUBMITTED, APPROVED, EXPIRED, REJECTED)
+    STATES = getUtility(ICustomersUtils).TRANSLATED_CONTRACT_STATES
+    state_names = tuple([STATES[state] for state in states]) + (u'Total',)
+    # find all contracts
+    cat = queryUtility(ICatalog, name="contracts_catalog")
+    contracts = cat.searchResults(contract_id=(None, None))
+    # create empty table
+    table = [[0 for x in xrange(len(STATES)+1)] for y in xrange(len(TYPES)+1)]
+    # fill table
+    for contract in contracts:
+        row = types.index(contract.class_name)
+        col = states.index(contract.state)
+        table[row][col] += 1
+        table[-1][col] += 1
+        table[row][-1] += 1
+        table[-1][-1] += 1
+    # turn lists into tuples
+    table = tuple([tuple(row) for row in table])
+    return (type_names, state_names, table)
+
+
+from reportlab.lib import colors
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, Table, Spacer
+from waeup.ikoba.reports import IReport, IReportGenerator
+from waeup.ikoba.reports import Report
+from waeup.ikoba.browser.interfaces import IPDFCreator
+
+STYLE = getSampleStyleSheet()
+
+def tbl_data_to_table(row_names, col_names, data):
+    result = []
+    new_col_names = []
+    for name in col_names:
+        new_col_names.append(name.replace(' ', '\n'))
+    head = [''] + list(new_col_names)
+    result = [head]
+    for idx, row_name in enumerate(row_names):
+        row = [row_name] + list(data[idx])
+        result.append(row)
+    return result
+
+TABLE_STYLE = [
+    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    ('FONT', (0,0), (0,-1), 'Helvetica-Bold', 8),
+    ('FONT', (0,0), (-1,0), 'Helvetica-Bold', 8),
+    ('FONT', (0,-1), (-1,-1), 'Helvetica-Bold', 8),
+    ('FONT', (-1,0), (-1,-1), 'Helvetica-Bold', 8),
+    ('ALIGN', (1,1), (-1,-1), 'RIGHT'),
+    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+    ('LINEBELOW', (0,-1), (-1,-1), 0.25, colors.black),
+    ('LINEAFTER', (-1,0), (-1,-1), 0.25, colors.black),
+    ('LINEBEFORE', (-1,0), (-1,-1), 1.0, colors.black),
+    ('LINEABOVE', (0,-1), (-1,-1), 1.0, colors.black),
+    #('LINEABOVE', (0,0), (-1,0), 0.25, colors.black),
+    ]
+
+@implementer(IContractStatisticsReport)
+class ContractStatisticsReport(Report):
+
+    data = None
+
+    def __init__(self, author='System'):
+        super(ContractStatisticsReport, self).__init__(
+            args=[], kwargs={'author':author})
+        self.author = author
+        self.creation_dt_string = self.creation_dt.astimezone(
+            getUtility(IIkobaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.data = get_contract_stats()
+
+    def create_pdf(self):
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        creator = getUtility(IPDFCreator, name='landscape')
+        table_data = tbl_data_to_table(*self.data)
+        col_widths = [None,] + [1.6*cm] * len(self.data[1]) + [None,]
+        pdf_data = [Paragraph('<b>%s</b>' % self.creation_dt_string,
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [
+            Table(table_data, style=TABLE_STYLE, colWidths=col_widths)]
+        doc_title = translate(_('Contracts'), 'waeup.ikoba',
+            target_language=portal_language)
+        footer_title = doc_title
+
+        pdf = creator.create_pdf(
+            pdf_data, None, doc_title, self.author, footer_title
+            )
+        return pdf
+
+@implementer(IReportGenerator)
+class ContractStatisticsReportGenerator(grok.GlobalUtility):
+
+    title = _('Contract Statistics')
+    grok.name('contract_stats')
+
+    def generate(self, site, author=None):
+        result = ContractStatisticsReport(
+            author=author)
+        return result
+
+###############################################################
+## Browser related stuff
+##
+## XXX: move to local browser module
+###############################################################
+from waeup.ikoba.browser.layout import IkobaPage
+from waeup.ikoba.reports import get_generators
+from waeup.ikoba.browser.breadcrumbs import Breadcrumb
+grok.templatedir('browser_templates')
+class ContractStatisticsReportGeneratorPage(IkobaPage):
+
+    grok.context(ContractStatisticsReportGenerator)
+    grok.name('index.html')
+    grok.require('waeup.manageReports')
+
+    label = _('Create contract statistics report')
+
+    @property
+    def generator_name(self):
+        for name, gen in get_generators():
+            if gen == self.context:
+                return name
+        return None
+
+    def update(self, CREATE=None):
+        self.parent_url = self.url(self.context.__parent__)
+        if CREATE:
+            # create a new report job for contracts
+            container = self.context.__parent__
+            user_id = self.request.principal.id
+            kw = dict()
+            self.flash(_('New report is being created in background'))
+            job_id = container.start_report_job(
+                self.generator_name, user_id, kw=kw)
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            grok.getSite().logger.info(
+                '%s - report %s created: %s' % (
+                ob_class, job_id, self.context.title))
+            self.redirect(self.parent_url)
+            return
+        return
+
+class ContractStatisticsReportPDFView(grok.View):
+
+    grok.context(IContractStatisticsReport)
+    grok.name('pdf')
+    grok.require('waeup.Public')
+    prefix = ''
+
+    def _filename(self):
+        return 'ContractStatisticsReport.pdf'
+
+    def render(self):
+        filename = self._filename().replace(
+            '/', '_').replace(' ','_').replace(':', '-')
+        self.response.setHeader(
+            'Content-Type', 'application/pdf')
+        self.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' % filename)
+        pdf_stream = self.context.create_pdf()
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        grok.getSite().logger.info('%s - report %s downloaded: %s' % (
+            ob_class, self.context.__name__, filename))
+        return pdf_stream
+
+class ContractStatsBreadcrumb(Breadcrumb):
+    """A breadcrumb for reports.
+    """
+    grok.context(ContractStatisticsReportGenerator)
+    title = _(u'Contract Statistics')
+    target = None
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/tests/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/tests/__init__.py	(revision 12677)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/tests/test_contract_statistics.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/tests/test_contract_statistics.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/reports/tests/test_contract_statistics.py	(revision 12677)
@@ -0,0 +1,119 @@
+# Tests for contract related reports
+import os
+from zc.async.testing import wait_for_result
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import getUtility
+from waeup.ikoba.interfaces import IJobManager
+from waeup.ikoba.customers.reports.contract_statistics import (
+    get_contract_stats, ContractStatisticsReport, IContractStatisticsReport)
+from waeup.ikoba.customers.tests.test_catalog import CatalogTestSetup
+from waeup.ikoba.customers.tests.test_browser import CustomersFullSetup
+from waeup.ikoba.testing import FunctionalLayer
+from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
+from waeup.ikoba.browser.tests.test_pdf import samples_dir
+
+class ContractStatisticsReportTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    states = ('created', 'awaiting payment',
+              'submitted for approval',
+              'approved', 'expired',
+              'rejected', 'Total')
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = ContractStatisticsReport()
+        verifyClass(IContractStatisticsReport, ContractStatisticsReport)
+        verifyObject(IContractStatisticsReport, obj)
+        return
+
+    def test_get_contract_stats(self):
+        # we can get a table with one contract
+        result1 = get_contract_stats()
+        self.assertEqual(
+            result1,
+            ((u'Sample Contract', u'All'),
+             self.states,
+             ((1, 0, 0, 0, 0, 0, 1),
+              (1, 0, 0, 0, 0, 0, 1),))
+            )
+        return
+
+    def test_create_pdf(self):
+        report = ContractStatisticsReport()
+        result = report.create_pdf()
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'contract_statistics.pdf')
+        open(path, 'wb').write(result)
+        print "Sample PDF contract_statistics.pdf written to %s" % path
+        return
+
+class ContractStatisticsReportUITests(CustomersFullSetup, FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def wait_for_report_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def stored_in_reports(self, job_id):
+        # tell whether job_id is stored in reports's running jobs list
+        for entry in list(self.app['reports'].running_report_jobs):
+            if entry[0] == job_id:
+                return True
+        return False
+
+    def trigger_report_creation(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/reports')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Create new report").click()
+        self.browser.getControl(name="generator").value = ['contract_stats']
+        self.browser.getControl("Configure").click()
+        #self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl("Create").click()
+        return
+
+    def test_report_download(self):
+        # We can download a generated report
+        self.trigger_report_creation()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_report_job_completed()
+        self.browser.open('http://localhost/app/reports')
+        # ... the pdf file can be downloaded ...
+        self.browser.getControl("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/pdf')
+        self.assertTrue(
+            'filename="ContractStatisticsReport' in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        # ... and discarded
+        self.browser.open('http://localhost/app/reports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - zope.mgr - customers.reports.contract_statistics.ContractStatisticsReportGeneratorPage - '
+            'report %s created: Contract Statistics'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - customers.reports.contract_statistics.ContractStatisticsReportPDFView - '
+            'report %s downloaded: ContractStatisticsReport'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/__init__.py	(revision 12677)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_contract_data.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_contract_data.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_contract_data.csv	(revision 12677)
@@ -0,0 +1,8 @@
+class_name,contract_id,reg_number,product_object,document_object,product_options,tc_dict,title,valid_from,valid_to
+SampleContract,c3,1,SAM,DOC1,"[('Base Fee', '800.6', 'USD')]",{'en':'Hello World'},Nice Contract,2014-06-25#,2015-06-25#
+SampleContract,c4,2,SAM,DOC1,[],,,,
+SampleContract,c5,3,SAM,DOC1,[],,,,
+SampleContract,,1,SAM,DOC1,[],,,,
+SampleContract,c5,2,SAM,DOC1,[],,,,
+SampleContract,c6,3,SAM,NONEXISTENT,[],,,,
+SampleContract,c7,3,NONEXISTENT,DOC1,[],,,,
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_customer_data.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_customer_data.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_customer_data.csv	(revision 12677)
@@ -0,0 +1,10 @@
+customer_id,firstname,lastname,reg_number,date_of_birth,email,phone,sex,state
+X666666,Aaren,Pieri,1,1990-01-02,aa@aa.ng,1234,M,requested
+Y777777,Claus,Finau,2,1990-01-03,bb@aa.ng,1234,m,requested
+,Susann,Berson,3,1990-01-04,cc@aa.ng,1234,F,requested
+,Else,Mueller,4,1990-01-05,dd@aa.ng,1234,f,approved
+X888888,Alfons,Meier,5,1990-01-02,ee@aa.ng,1234,m,requested
+X999999,Herbert,Mueller,6,1990-01-02,ff@aa.ng,1234,m,approved
+X111111,Fabian,Riester,7,1990-01-02,gg@aa.ng,1234,m,requested
+X222222,Thomas,Kinderman,8,1990-01-02,hh@aa.ng,1234,m,requested
+X333333,Paula,Schall,9,1990-01-02,ii@aa.ng,1234,m,approved
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_customer_data_duplicates.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_customer_data_duplicates.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_customer_data_duplicates.csv	(revision 12677)
@@ -0,0 +1,6 @@
+customer_id,firstname,lastname,reg_number,date_of_birth,email,phone,password,sex,state
+A123456,Aaren,Pieri,1,1990-01-02#,aa@aa.ng,1234,mypw1,m,requested
+C123456,Aaren,Berson,2,1990-01-04,bb@aa.ng,1234,mypw1,m,approved
+F123456,Frank,Meyer,1,1990-01-06,cc@aa.ng,1234,,m,
+A123456,Uli,Schulz,3,1990-01-07,dd@aa.ng,1234,,m,
+A234567,Berta,Pieri,4,1991-01-07,aa@aa.ng,1234,,m,
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_customer_data_update.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_customer_data_update.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_customer_data_update.csv	(revision 12677)
@@ -0,0 +1,4 @@
+reg_number,date_of_birth,state
+1,1980-01-02,approved
+2,1980-01-03,
+3,1980-01-04,
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_document_data.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_document_data.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/sample_document_data.csv	(revision 12677)
@@ -0,0 +1,6 @@
+class_name,document_id,reg_number,title
+CustomerSampleDocument,d3,1,My first doc
+CustomerSampleDocument,d4,2,My second doc
+CustomerSampleDocument,d5,3,My third doc
+CustomerSampleDocument,,1,My 4th doc
+CustomerSampleDocument,d5,2,My stolen doc
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_authentication.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_authentication.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_authentication.py	(revision 12677)
@@ -0,0 +1,240 @@
+## $Id: test_authentication.py 9334 2012-10-14 21:02:31Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import unittest
+from datetime import datetime, timedelta
+from zope.authentication.interfaces import IAuthentication
+from zope.component import provideUtility, queryUtility, getGlobalSiteManager
+from zope.interface.verify import verifyClass, verifyObject
+from zope.password.password import SSHAPasswordManager
+from zope.password.interfaces import IPasswordManager
+from zope.pluggableauth import PluggableAuthentication
+from zope.security.interfaces import Unauthorized
+from zope.securitypolicy.role import Role
+from zope.securitypolicy.interfaces import IRole, Allow
+from waeup.ikoba.authentication import get_principal_role_manager
+from waeup.ikoba.interfaces import IAuthPluginUtility, IUserAccount
+from waeup.ikoba.customers.authentication import (
+    CustomersAuthenticatorSetup, CustomerAccount)
+from waeup.ikoba.customers.tests.test_browser import CustomersFullSetup
+from waeup.ikoba.testing import FunctionalLayer
+
+class CustomersAuthenticatorSetupTests(unittest.TestCase):
+
+    def test_iface(self):
+        obj = CustomersAuthenticatorSetup()
+        verifyClass(IAuthPluginUtility, CustomersAuthenticatorSetup)
+        verifyObject(IAuthPluginUtility, obj)
+        return
+
+    def test_register(self):
+        # Make sure registration works.
+        setup = CustomersAuthenticatorSetup()
+        pau = PluggableAuthentication()
+        setup.register(pau)
+        self.assertTrue('customers' in pau.authenticatorPlugins)
+        return
+
+    def test_unregister(self):
+        # Make sure deregistration works.
+        setup = CustomersAuthenticatorSetup()
+        pau = PluggableAuthentication()
+        pau.authenticatorPlugins = ('customers')
+        setup.unregister(pau)
+        self.assertTrue('customers' not in pau.authenticatorPlugins)
+        return
+
+
+class FakeCustomer(object):
+    customer_id = 'test_stud'
+    display_fullname = 'Test User'
+    password = None
+    email = None
+    phone = None
+    suspended = False
+    temp_password_minutes = 10
+
+    def setTempPassword(self, user, password):
+        passwordmanager = queryUtility(IPasswordManager, 'SSHA')
+        self.temp_password = {}
+        self.temp_password[
+            'password'] = passwordmanager.encodePassword(password)
+        self.temp_password['user'] = user
+        self.temp_password['timestamp'] = datetime.utcnow()
+
+    def getTempPassword(self):
+        temp_password_dict = getattr(self, 'temp_password', None)
+        if temp_password_dict is not None:
+            delta = timedelta(minutes=self.temp_password_minutes)
+            now = datetime.utcnow()
+            if now < temp_password_dict.get('timestamp') + delta:
+                return temp_password_dict.get('password')
+            else:
+                # Unset temporary password if expired
+                self.temp_password = None
+        return None
+
+
+class MinimalPAU(PluggableAuthentication):
+    def getPrincipal(self, id):
+        return 'faked principal'
+
+class CustomerAccountTests(unittest.TestCase):
+
+    def setUp(self):
+        self.fake_stud = FakeCustomer()
+        self.account = CustomerAccount(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, CustomerAccount)
+        verifyObject(IUserAccount, self.account)
+        return
+
+    def test_set_password(self):
+        # make sure we can set a password.
+        self.account.setPassword('secret')
+        self.assertTrue(self.fake_stud.password is not None)
+        # we do not store plaintext passwords
+        self.assertTrue(self.fake_stud.password != 'secret')
+        # passwords are stored as bytestreams
+        self.assertTrue(isinstance(self.fake_stud.password, basestring))
+        self.assertFalse(isinstance(self.fake_stud.password, unicode))
+        return
+
+    def test_check_password(self):
+        # make sure we can check a password.
+        self.account.setPassword('secret')
+        result1 = self.account.checkPassword(None)
+        result2 = self.account.checkPassword('nonsense')
+        result3 = self.account.checkPassword('secret')
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        self.assertEqual(result3, True)
+        return
+
+    def test_check_temp_password(self):
+        # make sure that, if a temp password is set,
+        # this password is used for authentication
+        self.account.setPassword('secret')
+        self.fake_stud.setTempPassword(user='beate', password='temp_secret')
+        result1 = self.account.checkPassword('secret')
+        result2 = self.account.checkPassword(None)
+        result3 = self.account.checkPassword('nonsense')
+        result4 = self.account.checkPassword('temp_secret')
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        self.assertEqual(result3, False)
+        self.assertEqual(result4, True)
+        # if the temp password is expired, the original password
+        # is used again
+        delta = timedelta(minutes=11)
+        self.fake_stud.temp_password['timestamp'] = datetime.utcnow() - delta
+        result5 = self.account.checkPassword('temp_secret')
+        result6 = self.account.checkPassword('secret')
+        self.assertEqual(result5, False)
+        self.assertEqual(result6, True)
+        return
+
+    def test_check_unset_password(self):
+        # empty and unset passwords do not match anything
+        self.fake_stud.password = None
+        result1 = self.account.checkPassword('')
+        self.fake_stud.password = ''
+        result2 = self.account.checkPassword('')
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        return
+
+    def test_check_password_no_string(self):
+        # if passed in password is not a string, we gain no access
+        self.fake_stud.password = 'secret'
+        result1 = self.account.checkPassword(None)
+        result2 = self.account.checkPassword(object())
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        return
+
+    def test_role_set(self):
+        # make sure we can set roles for principals denoted by account
+        prm = get_principal_role_manager()
+        self.assertEqual(prm.getPrincipalsAndRoles(), [])
+        self.account.roles = ['waeup.test.Role']
+        self.assertEqual(
+            prm.getPrincipalsAndRoles(),
+            [('waeup.test.Role', 'test_stud', Allow)])
+        return
+
+    def test_role_get(self):
+        # make sure we can get roles set for an account
+        self.assertEqual(self.account.roles, [])
+        self.account.roles = ['waeup.test.Role',] # set a role
+        self.assertEqual(self.account.roles, ['waeup.test.Role'])
+        return
+
+
+
+class FunctionalCustomerAuthTests(CustomersFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FunctionalCustomerAuthTests, self).setUp()
+        return
+
+    def tearDown(self):
+        super(FunctionalCustomerAuthTests, self).tearDown()
+        return
+
+    def test_reset_protected_anonymous(self):
+        # anonymous users cannot reset others passwords
+        self.assertRaises(
+            Unauthorized,
+            self.browser.open, self.customer_path + '/changepassword')
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_batching.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_batching.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_batching.py	(revision 12677)
@@ -0,0 +1,563 @@
+# -*- coding: utf-8 -*-
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Unit tests for customer-related data processors.
+"""
+import os
+import shutil
+import tempfile
+import unittest
+import datetime
+import grok
+from decimal import Decimal
+from time import time
+from zope.event import notify
+from zope.component import createObject, queryUtility
+from zope.component.hooks import setSite, clearSite
+from zope.catalog.interfaces import ICatalog
+from zope.interface.verify import verifyClass, verifyObject
+from hurry.workflow.interfaces import IWorkflowState
+
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import (
+    IBatchProcessor, FatalCSVError, IUserAccount, VERIFIED)
+from waeup.ikoba.customers.batching import (
+    CustomerProcessor, CustomerSampleDocumentProcessor, SampleContractProcessor)
+from waeup.ikoba.customers.customer import Customer
+from waeup.ikoba.customers.documents import CustomerSampleDocument
+from waeup.ikoba.products.productoptions import ProductOption
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+CUSTOMER_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_customer_data.csv'),
+    'rb').read()
+
+CUSTOMER_HEADER_FIELDS = CUSTOMER_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+CUSTOMER_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_customer_data_update.csv'),
+    'rb').read()
+
+CUSTOMER_HEADER_FIELDS_UPDATE = CUSTOMER_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+CUSTOMER_SAMPLE_DATA_DUPLICATES = open(
+    os.path.join(os.path.dirname(__file__),
+                 'sample_customer_data_duplicates.csv'),
+    'rb').read()
+
+CUSTOMER_HEADER_FIELDS_DUPLICATES = CUSTOMER_SAMPLE_DATA_DUPLICATES.split(
+    '\n')[0].split(',')
+
+DOCUMENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_document_data.csv'),
+    'rb').read()
+
+DOCUMENT_HEADER_FIELDS = DOCUMENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+CONTRACT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_contract_data.csv'),
+    'rb').read()
+
+CONTRACT_HEADER_FIELDS = CONTRACT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+class CustomerImportExportSetup(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setup_customizable_params(self):
+        self._contract_category = u'sample'
+        return
+
+    def setUp(self):
+        super(CustomerImportExportSetup, self).setUp()
+        self.setup_customizable_params()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
+        app = Company()
+        app['datacenter'].setStoragePath(self.dc_root)
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(app)
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'customers.log')
+
+        #Add some products
+        self.product = createObject('waeup.Product')
+        self.product.product_id = u'SAM'
+        self.product.title = u'Our Sample Product'
+        self.product.contract_category = self._contract_category
+        self.product.valid_from = datetime.date(2015, 12, 4)
+        self.product.tc_dict = {'en': u'Hello world'}
+        option = ProductOption()
+        option.title = u'Base Fee'
+        option.fee = Decimal('800.6')
+        option.currency = 'USD'
+        self.product.options = [option,]
+        self.app['products'].addProduct(self.product)
+        return
+
+    def tearDown(self):
+        super(CustomerImportExportSetup, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def setup_for_export(self):
+        customer = Customer()
+        customer.customer_id = u'A111111'
+        self.app['customers'][customer.customer_id] = self.customer = customer
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        return
+
+    def setup_customer(self, customer):
+        customer.matric_number = u'234'
+        customer.perm_address = u'Customerroad 21\nLagos 123456\n'
+        customer.reg_number = u'123'
+        customer.firstname = u'Anna'
+        customer.lastname = u'Tester'
+        customer.middlename = u'M.'
+        customer.email = u'bb@bb.bb'
+        customer.date_of_birth = datetime.date(1981, 2, 4)
+        #customer.sex = 'f'
+        customer.email = u'anna@sample.com'
+        customer.phone = u'+234-123-12345'
+        customer.notice = u'Some notice\nin lines.'
+        customer.nationality = u'NG'
+        # Add document
+        document = createObject('waeup.CustomerSampleDocument')
+        document.title = u'My Document'
+        document.document_id = u'DOC1'
+        customer['documents'].addDocument(document)
+        IWorkflowState(document).setState(VERIFIED)
+        self.document = document
+        # Add contract
+        contract = createObject('waeup.SampleContract')
+        contract.contract_id = u'CON1'
+        contract.product_object = self.product
+        contract.document_object = self.document
+        contract.product_options = self.product.options
+        contract.tc_dict = {'en': u'Hello world'}
+        contract.valid_from = datetime.date(2014, 2, 4)
+        contract.valid_to = datetime.date(2014, 12, 4)
+
+        customer['contracts'].addContract(contract)
+        self.contract = contract
+        return customer
+
+class CustomerProcessorTest(CustomerImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomerProcessorTest, self).setUp()
+
+        # Add customer
+        customer = Customer()
+        self.app['customers'].addCustomer(customer)
+        customer = self.setup_customer(customer)
+        notify(grok.ObjectModifiedEvent(customer))
+        self.customer = self.app['customers'][customer.customer_id]
+
+        self.processor = CustomerProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_customer_data.csv')
+        self.csv_file_update = os.path.join(
+            self.workdir, 'sample_customer_data_update.csv')
+        self.csv_file_duplicates = os.path.join(
+            self.workdir, 'sample_customer_data_duplicates.csv')
+        open(self.csv_file, 'wb').write(CUSTOMER_SAMPLE_DATA)
+        open(self.csv_file_update, 'wb').write(CUSTOMER_SAMPLE_DATA_UPDATE)
+        open(self.csv_file_duplicates, 'wb').write(CUSTOMER_SAMPLE_DATA_DUPLICATES)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, CustomerProcessor) is True
+
+    def test_parentsExist(self):
+        self.assertFalse(self.processor.parentsExist(None, dict()))
+        self.assertTrue(self.processor.parentsExist(None, self.app))
+
+    def test_entryExists(self):
+        assert self.processor.entryExists(
+            dict(customer_id='ID_NONE'), self.app) is False
+        assert self.processor.entryExists(
+            dict(reg_number='123'), self.app) is True
+
+    def test_getParent(self):
+        parent = self.processor.getParent(None, self.app)
+        assert parent is self.app['customers']
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(customer_id='ID_NONE'), self.app) is None
+        assert self.processor.getEntry(
+            dict(customer_id=self.customer.customer_id), self.app) is self.customer
+
+    def test_addEntry(self):
+        new_customer = Customer()
+        self.processor.addEntry(
+            new_customer, dict(), self.app)
+        assert len(self.app['customers'].keys()) == 2
+
+    def test_checkConversion(self):
+        # Make sure we can check conversions and that the cust_id
+        # counter is not raised during such checks.
+        initial_cust_id = self.app['customers']._curr_cust_id
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', state='approved'))
+        self.assertEqual(len(errs),0)
+        # Empty state is allowed
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', state=''))
+        self.assertEqual(len(errs),0)
+        #self.assertTrue(('state', 'no value provided') in errs)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', state='nonsense'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('state', 'not allowed') in errs)
+        new_cust_id = self.app['customers']._curr_cust_id
+        self.assertEqual(initial_cust_id, new_cust_id)
+        return
+
+    def test_checkUpdateRequirements(self):
+        # Make sure that pg customers can't be updated with wrong transition.
+        err = self.processor.checkUpdateRequirements(self.customer,
+            dict(reg_number='1', state='returning'), self.app)
+        self.assertTrue(err is None)
+
+    def test_delEntry(self):
+        assert self.customer.customer_id in self.app['customers'].keys()
+        self.processor.delEntry(
+            dict(reg_number=self.customer.reg_number), self.app)
+        assert self.customer.customer_id not in self.app['customers'].keys()
+
+    def test_import(self):
+        self.assertEqual(self.app['customers']._curr_cust_id, 1000001)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, CUSTOMER_HEADER_FIELDS)
+        self.assertEqual(num_warns,0)
+        self.assertEqual(len(self.app['customers']), 10)
+        self.assertEqual(self.app['customers']['X666666'].reg_number,'1')
+        self.assertEqual(
+            self.app['customers']['X666666'].state, 'requested')
+        # Two new customer_ids have been created.
+        self.assertEqual(self.app['customers']._curr_cust_id, 1000003)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, CUSTOMER_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update, CUSTOMER_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,0)
+        # state has changed
+        self.assertEqual(self.app['customers']['X666666'].state,'approved')
+        # state has not changed
+        self.assertEqual(self.app['customers']['Y777777'].state, 'requested')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_duplicate_data(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_duplicates, CUSTOMER_HEADER_FIELDS_DUPLICATES)
+        content = open(fail_file).read()
+        self.assertEqual(num_warns,3)
+        self.assertEqual(
+            content,
+            'reg_number,password,firstname,lastname,sex,phone,state,date_of_birth,customer_id,email,--ERRORS--\r\n'
+            '1,,Frank,Meyer,m,1234,,1990-01-06,F123456,cc@aa.ng,reg_number: Invalid input\r\n'
+            '3,,Uli,Schulz,m,1234,,1990-01-07,A123456,dd@aa.ng,This object already exists. Skipping.\r\n'
+            '4,,Berta,Pieri,m,1234,,1991-01-07,A234567,aa@aa.ng,email: Invalid input\r\n'
+            )
+        shutil.rmtree(os.path.dirname(fin_file))
+
+class CustomerSampleDocumentProcessorTest(CustomerImportExportSetup):
+
+    def setUp(self):
+        super(CustomerSampleDocumentProcessorTest, self).setUp()
+
+        customer = Customer()
+        self.app['customers'].addCustomer(customer)
+        customer = self.setup_customer(customer)
+        notify(grok.ObjectModifiedEvent(customer))
+        self.customer = self.app['customers'][customer.customer_id]
+
+        # Import customers with subobjects
+        customer_file = os.path.join(self.workdir, 'sample_customer_data.csv')
+        open(customer_file, 'wb').write(CUSTOMER_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = CustomerProcessor().doImport(
+            customer_file, CUSTOMER_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.processor = CustomerSampleDocumentProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_document_data.csv')
+        open(self.csv_file, 'wb').write(DOCUMENT_SAMPLE_DATA)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, CustomerSampleDocumentProcessor) is True
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(customer_id='ID_NONE', document_id='nonsense'), self.app) is None
+        assert self.processor.getEntry(
+            dict(customer_id=self.customer.customer_id, document_id='DOC1'),
+            self.app) is self.customer['documents']['DOC1']
+
+    def test_delEntry(self):
+        assert self.processor.getEntry(
+            dict(customer_id=self.customer.customer_id, document_id='DOC1'),
+            self.app) is self.customer['documents']['DOC1']
+        self.assertEqual(len(self.customer['documents'].keys()),1)
+        self.processor.delEntry(
+            dict(customer_id=self.customer.customer_id, document_id='DOC1'),
+            self.app)
+        assert self.processor.getEntry(
+            dict(customer_id=self.customer.customer_id, document_id='DOC1'),
+            self.app) is None
+        self.assertEqual(len(self.customer['documents'].keys()),0)
+
+    def test_addEntry(self):
+        self.assertEqual(len(self.customer['documents'].keys()),1)
+        document1 = createObject(u'waeup.CustomerSampleDocument')
+        document1.document_id = u'd234'
+        self.processor.addEntry(
+            document1, dict(customer_id=self.customer.customer_id, document_id='d234'),
+            self.app)
+        self.assertEqual(len(self.customer['documents'].keys()),2)
+        self.assertEqual(self.customer['documents']['d234'].document_id, 'd234')
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='DOC', class_name='CustomerSampleDocument'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='id with spaces', class_name='CustomerSampleDocument'))
+        self.assertEqual(len(errs),1)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='DOC', class_name='WrongDocument'))
+        self.assertEqual(len(errs),1)
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS,'create')
+        fail_file = open(fail_file).read()
+        self.assertEqual(num_warns,1)
+        # document_id must be unique
+        self.assertEqual(fail_file,
+            'class_name,reg_number,document_id,title,--ERRORS--\r\n'
+            'CustomerSampleDocument,2,d5,My stolen doc,'
+            'This object already exists. Skipping.\r\n')
+        document = self.processor.getEntry(dict(reg_number='1',
+            document_id='d3'), self.app)
+        self.assertEqual(
+            self.app['customers']['X666666']['documents']['d3'],
+            document)
+        self.assertEqual(document.document_id, 'd3')
+        document = self.processor.getEntry(dict(reg_number='3',
+            document_id='d5'), self.app)
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        # Fetch uuid
+        docid = [i for i in self.app['customers']['X666666']['documents'].keys()
+            if len(i) > 10][0]
+         # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Customer Sample Document Processor - '
+            'sample_document_data - X666666 - %s - updated: title=My 4th doc'
+            % docid in logcontent)
+
+    def test_import_update(self):
+        # We perform the same import twice,
+        # the second time in update mode. The number
+        # of warnings must be the same.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS,'update')
+        # There is one record without document_id
+        self.assertEqual(num_warns,1)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove(self):
+        # We perform the same import twice,
+        # the second time in remove mode. The number
+        # of warnings must be the same.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS,'remove')
+        # There is one record without document_id and one duplicate
+        self.assertEqual(num_warns,2)
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - K1000001 - Document removed: d5'
+            in logcontent)
+
+
+class SampleContractProcessorTest(CustomerImportExportSetup):
+
+    def setUp(self):
+        super(SampleContractProcessorTest, self).setUp()
+
+        customer = Customer()
+        self.app['customers'].addCustomer(customer)
+        customer = self.setup_customer(customer)
+        notify(grok.ObjectModifiedEvent(customer))
+        self.customer = self.app['customers'][customer.customer_id]
+
+        # Import customers with subobjects
+        customer_file = os.path.join(self.workdir, 'sample_customer_data.csv')
+        open(customer_file, 'wb').write(CUSTOMER_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = CustomerProcessor().doImport(
+            customer_file, CUSTOMER_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.processor = SampleContractProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_contract_data.csv')
+        open(self.csv_file, 'wb').write(CONTRACT_SAMPLE_DATA)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, SampleContractProcessor) is True
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(customer_id='ID_NONE', contract_id='nonsense'), self.app) is None
+        assert self.processor.getEntry(
+            dict(customer_id=self.customer.customer_id, contract_id='CON1'),
+            self.app) is self.customer['contracts']['CON1']
+
+    def test_delEntry(self):
+        assert self.processor.getEntry(
+            dict(customer_id=self.customer.customer_id, contract_id='CON1'),
+            self.app) is self.customer['contracts']['CON1']
+        self.assertEqual(len(self.customer['contracts'].keys()),1)
+        self.processor.delEntry(
+            dict(customer_id=self.customer.customer_id, contract_id='CON1'),
+            self.app)
+        assert self.processor.getEntry(
+            dict(customer_id=self.customer.customer_id, contract_id='CON1'),
+            self.app) is None
+        self.assertEqual(len(self.customer['contracts'].keys()),0)
+
+    def test_addEntry(self):
+        self.assertEqual(len(self.customer['contracts'].keys()),1)
+        contract1 = createObject(u'waeup.SampleContract')
+        contract1.contract_id = u'CON2'
+        self.processor.addEntry(
+            contract1, dict(customer_id=self.customer.customer_id, contract_id='CON2'),
+            self.app)
+        self.assertEqual(len(self.customer['contracts'].keys()),2)
+        self.assertEqual(self.customer['contracts']['CON2'].contract_id, 'CON2')
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(contract_id='CON3', class_name='SampleContract',
+                 document_object='DOC1', product_object='SAM'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(contract_id='id with spaces', class_name='SampleContract'))
+        self.assertEqual(len(errs),1)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(contract_id='CON3', class_name='WrongContract'))
+        self.assertEqual(len(errs),1)
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, CONTRACT_HEADER_FIELDS,'create')
+        self.assertEqual(num_warns,3)
+        # contract_id must be unique
+        fail_file = open(fail_file).read()
+        self.assertEqual(fail_file,
+            'reg_number,valid_from,contract_id,title,class_name,valid_to,product_object,tc_dict,document_object,product_options,--ERRORS--\r\n'
+            '2,,c5,,SampleContract,,SAM,,DOC1,[],This object already exists. Skipping.\r\n'
+            '3,,c6,,SampleContract,,SAM,,NONEXISTENT,[],document_object: Invalid value\r\n'
+            '3,,c7,,SampleContract,,NONEXISTENT,,DOC1,[],product_object: Invalid value\r\n'
+            )
+        contract = self.processor.getEntry(dict(reg_number='1',
+            contract_id='c3'), self.app)
+        self.assertEqual(
+            self.app['customers']['X666666']['contracts']['c3'],
+            contract)
+        self.assertEqual(contract.product_options[0].title, 'Base Fee')
+        self.assertEqual(contract.product_options[0].fee, Decimal('800.6') )
+        self.assertEqual(contract.product_options[0].currency, 'USD')
+        self.assertEqual(contract.contract_id, 'c3')
+        self.assertEqual(contract.title, u'Nice Contract')
+        self.assertEqual(contract.tc_dict, {'en':'Hello World'})
+        contract = self.processor.getEntry(dict(reg_number='3',
+            contract_id='c5'), self.app)
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        # Fetch uuid
+        conid = [i for i in self.app['customers']['X666666']['contracts'].keys()
+            if len(i) > 10][0]
+        self.assertTrue(
+            'INFO - system - Customer Sample Contract Processor - sample_contract_data - '
+            'X666666 - %s - updated: valid_from=, title=, valid_to=, product_object=SAM, tc_dict=, document_object=DOC1, '
+            'product_options=[]' % conid
+            in logcontent)
+
+    def test_import_update(self):
+        # We perform the same import twice,
+        # the second time in update mode. The number
+        # of warnings must be the same.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, CONTRACT_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, CONTRACT_HEADER_FIELDS,'update')
+        # There are one record without contract_id and two
+        # records with wrong object identifier.
+        self.assertEqual(num_warns,3)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove(self):
+        # We perform the same import twice,
+        # the second time in remove mode. The number
+        # of warnings must be the same.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, CONTRACT_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, CONTRACT_HEADER_FIELDS,'remove')
+        # There are one record without contract_id, one duplicate and two 
+        # records with wrong object identifier.
+        self.assertEqual(num_warns,4)
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - K1000001 - Contract removed: c5'
+            in logcontent)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_browser.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_browser.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_browser.py	(revision 12677)
@@ -0,0 +1,1494 @@
+## $Id$
+## 
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the customer-related UI components.
+"""
+import shutil
+import tempfile
+import logging
+import pytz
+import base64
+from decimal import Decimal
+from datetime import datetime, timedelta, date
+from StringIO import StringIO
+import os
+import grok
+from zc.async.testing import wait_for_result
+from zope.event import notify
+from zope.component import createObject, queryUtility, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.schema.interfaces import ConstraintNotSatisfied
+from zope.catalog.interfaces import ICatalog
+from zope.security.interfaces import Unauthorized
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.testbrowser.testing import Browser
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.app import Company
+from waeup.ikoba.customers.interfaces import ICustomersUtils
+from waeup.ikoba.customers.customer import Customer
+from waeup.ikoba.interfaces import (
+    IUserAccount, IJobManager, APPROVED, SUBMITTED,
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler, NotIdValue)
+from waeup.ikoba.imagestorage import (
+    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
+    DefaultStorage)
+from waeup.ikoba.authentication import LocalRoleSetEvent
+from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
+from waeup.ikoba.interfaces import VERIFIED
+from waeup.ikoba.browser.tests.test_pdf import samples_dir
+from waeup.ikoba.products.productoptions import ProductOption
+
+PH_LEN = 15911  # Length of placeholder file
+
+SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
+SAMPLE_PDF = os.path.join(os.path.dirname(__file__), 'test_pdf.pdf')
+
+def lookup_submit_value(name, value, browser):
+    """Find a button with a certain value."""
+    for num in range(0, 100):
+        try:
+            button = browser.getControl(name=name, index=num)
+            if button.value.endswith(value):
+                return button
+        except IndexError:
+            break
+    return None
+
+class CustomersFullSetup(FunctionalTestCase):
+    # A test case that only contains a setup and teardown
+    #
+    # Complete setup for customers handlings is rather complex and
+    # requires lots of things created before we can start. This is a
+    # setup that does all this, creates a company etc.
+    # so that we do not have to bother with that in different
+    # test cases.
+
+    layer = FunctionalLayer
+
+    def setup_customizable_params(self):
+        self._contract_category = u'sample'
+        self._document_factory = 'waeup.CustomerSampleDocument'
+        self._contract_factory = 'waeup.SampleContract'
+        return
+
+    def setUp(self):
+        super(CustomersFullSetup, self).setUp()
+        self.setup_customizable_params()
+        # Setup a sample site for each test
+        app = Company()
+        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 some products
+        self.product = createObject('waeup.Product')
+        self.product.product_id = u'SAM'
+        self.product.title = u'Our Sample Product'
+        self.product.contract_category = self._contract_category
+        self.product.valid_from = date(2014, 12, 4)
+        self.product.tc_dict = {'en': u'Hello world'}
+        prodoption = ProductOption()
+        prodoption.title = u'First option'
+        prodoption.fee = Decimal('99.9')
+        prodoption.currency = 'USD'
+        self.product.options = [prodoption,]
+        self.app['products'].addProduct(self.product)
+
+        # Add customer with subobjects
+        customer = createObject('waeup.Customer')
+        customer.firstname = u'Anna'
+        customer.lastname = u'Tester'
+        customer.reg_number = u'123'
+        customer.sex = u'm'
+        customer.email = u'aa@aa.ng'
+        customer.phone = u'1234'
+        customer.date_of_birth = date(1981, 2, 4)
+        self.app['customers'].addCustomer(customer)
+        self.customer_id = customer.customer_id
+        self.customer = self.app['customers'][self.customer_id]
+        self.document = createObject(self._document_factory)
+        self.document.title = u'My first document'
+        self.document.document_id = u'DOC1'
+        self.assertRaises(
+            NotIdValue, setattr, self.document, 'document_id', u'id with spaces')
+        self.customer['documents'].addDocument(self.document)
+        self.contract = createObject(self._contract_factory)
+        self.contract.contract_id = u'CON1'
+        self.assertRaises(
+            NotIdValue, setattr, self.contract, 'contract_id', u'id with spaces')
+        self.customer['contracts'].addContract(self.contract)
+
+        # Set password
+        IUserAccount(
+            self.app['customers'][self.customer_id]).setPassword('cpwd')
+
+        self.login_path = 'http://localhost/app/login'
+        self.container_path = 'http://localhost/app/customers'
+        self.manage_container_path = self.container_path + '/@@manage'
+        self.add_customer_path = self.container_path + '/addcustomer'
+        self.customer_path = self.container_path + '/' + self.customer_id
+        self.manage_customer_path = self.customer_path + '/manage_base'
+        self.trigtrans_path = self.customer_path + '/trigtrans'
+        self.history_path = self.customer_path + '/history'
+        self.documents_path = self.customer_path + '/documents'
+        self.contracts_path = self.customer_path + '/contracts'
+
+        # Update the catalog
+        notify(grok.ObjectModifiedEvent(self.customer))
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(CustomersFullSetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+
+class CustomersContainerUITests(CustomersFullSetup):
+    # Tests for CustomersContainer class views and pages
+
+    layer = FunctionalLayer
+
+    def test_anonymous_access(self):
+        # Anonymous users can't access customers 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 customers
+        # 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 customer 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_customers(self):
+        # Managers can add search and remove customers
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_container_path)
+        self.browser.getLink("Add customer").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.add_customer_path)
+        self.browser.getControl(name="form.firstname").value = 'Bob'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.reg_number").value = '123'
+        self.browser.getControl("Create customer record").click()
+        self.assertTrue('Registration number in use.'
+            in self.browser.contents)
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl("Create customer").click()
+        self.assertTrue('Customer created' in self.browser.contents)
+
+        # Registration must be unique
+        self.browser.getLink("Manage").click()
+        self.browser.getControl(name="form.reg_number").value = '123'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Registration number in use...',
+                           self.browser.contents)
+
+        # We can find a customer with a certain customer_id
+        self.browser.open(self.container_path)
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Empty search string' in self.browser.contents)
+        self.browser.getControl(name="searchtype").value = ['customer_id']
+        self.browser.getControl(name="searchterm").value = self.customer_id
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        # We can find a customer by searching for all kind of name parts
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Empty search string' in self.browser.contents)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Anna Tester'
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Anna'
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Tester'
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'An'
+        self.browser.getControl("Find customer(s)").click()
+        self.assertFalse('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'An*'
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'tester'
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Tester Ana'
+        self.browser.getControl("Find customer(s)").click()
+        self.assertFalse('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Tester Anna'
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        # The old searchterm will be used again
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        # We can find suspended customers
+        self.customer.suspended = True
+        notify(grok.ObjectModifiedEvent(self.customer))
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['suspended']
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        # Removed customers won't be found
+        ctrl = self.browser.getControl(name='entries')
+        ctrl.getControl(value=self.customer_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 = ['customer_id']
+        self.browser.getControl(name="searchterm").value = self.customer_id
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('No customer found' in self.browser.contents)
+        return
+
+class OfficerUITests(CustomersFullSetup):
+    # Tests for Customer class views and pages
+
+
+    def setup_logging(self):
+        # setup a log-handler that catches all fake mailer output
+        self.stream = StringIO()
+        handler = logging.StreamHandler(self.stream)
+        logger = logging.getLogger('test.smtp')
+        logger.addHandler(handler)
+        logger.setLevel(logging.INFO)
+        return
+
+    def get_fake_smtp_output(self):
+        # get output generated by fake mailer
+        self.stream.flush()
+        self.stream.seek(0)
+        return self.stream.read()
+
+    def teardown_logging(self):
+        # remove the log handler for fake mailer output
+        logger = logging.getLogger('test.smtp')
+        handlers = [x for x in logger.handlers]
+        for handler in handlers:
+            logger.removeHandler(handler)
+        return
+
+    def test_basic_auth(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.browser.getLink("Logout").click()
+        self.assertTrue('You have been logged out' in self.browser.contents)
+        # But we are still logged in since we've used basic authentication here.
+        # Wikipedia says: Existing browsers retain authentication information
+        # until the tab or browser is closed or the user clears the history.
+        # HTTP does not provide a method for a server to direct clients to
+        # discard these cached credentials. This means that there is no
+        # effective way for a server to "log out" the user without closing
+        # the browser. This is a significant defect that requires browser
+        # manufacturers to support a "logout" user interface element ...
+        self.assertTrue('Manager' in self.browser.contents)
+
+    def test_basic_auth_base64(self):
+        auth_token = base64.b64encode('mgr:mgrpw')
+        self.browser.addHeader('Authorization', 'Basic %s' % auth_token)
+        self.browser.open(self.manage_container_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+
+    def test_manage_access(self):
+        # Managers can access the pages of customers
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.customer_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.customer_path)
+        self.browser.getLink("Transition").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # Managers can trigger transitions
+        self.browser.getControl(name="transition").value = ['start']
+        self.browser.getControl("Apply").click()
+        # Managers can edit base
+        self.browser.open(self.customer_path)
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.url, self.manage_customer_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getControl(name="form.firstname").value = 'John'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.reg_number").value = '345'
+        self.browser.getControl(name="password").value = 'secret'
+        self.browser.getControl(name="control_password").value = 'secret'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...',
+                           self.browser.contents)
+
+    def test_manage_contact_customer(self):
+        # Managers can contact customer
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.customer_path)
+        self.browser.getLink("Send email").click()
+        self.browser.getControl(name="form.subject").value = 'Important subject'
+        self.browser.getControl(name="form.body").value = 'Hello!'
+        self.browser.getControl("Send message now").click()
+        self.assertTrue('Your message has been sent' in self.browser.contents)
+        return
+
+    def test_manage_upload_passport(self):
+        # Managers can upload a file via the CustomerBaseManageFormPage
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_customer_path)
+        image = open(SAMPLE_IMAGE_BMP, 'rb')
+        ctrl = self.browser.getControl(name='passportmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_photo.bmp')
+        self.browser.getControl(
+            name='upload_passportmanageupload').click()
+        self.assertTrue('jpg file format expected'
+            in self.browser.contents)
+        ctrl = self.browser.getControl(name='passportmanageupload')
+        file_ctrl = ctrl.mech_control
+        image = open(SAMPLE_IMAGE, 'rb')
+        file_ctrl.add_file(image, filename='my_photo.jpg')
+        self.browser.getControl(
+            name='upload_passportmanageupload').click()
+        self.assertTrue(
+            'src="http://localhost/app/customers/K1000000/passport.jpg"'
+            in self.browser.contents)
+        # We remove the passport file again
+        self.browser.open(self.manage_customer_path)
+        self.browser.getControl('Delete').click()
+        self.assertTrue('passport.jpg deleted' in self.browser.contents)
+
+
+    def test_manage_workflow_send_transition_information(self):
+        # Managers can pass through the whole workflow
+        config = grok.getSite()['configuration']
+        config.email_notification = True
+        self.setup_logging()
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        customer = self.app['customers'][self.customer_id]
+        self.browser.open(self.trigtrans_path)
+        self.browser.getControl(name="transition").value = ['start']
+        self.browser.getControl("Apply").click()
+        self.browser.open(self.trigtrans_path)
+        self.browser.getControl(name="transition").value = ['request']
+        self.browser.getControl("Apply").click()
+        self.browser.open(self.trigtrans_path)
+        self.browser.getControl(name="transition").value = ['reject']
+        self.browser.getControl("Apply").click()
+        # The customer is being notified by email after rejection
+        expected = [
+            u'Sending email from no-reply@waeup.org to aa@aa.ng:',
+            u'Message:', u'msg: MIME-Version: 1.0',
+            u'msg: Content-Type: text/plain; charset="us-ascii"',
+            u'msg: Content-Transfer-Encoding: 7bit',
+            u'msg: From: Administrator <no-reply@waeup.org>',
+            u'msg: To: Anna Tester <aa@aa.ng>',
+            u'msg: Reply-To: Administrator <contact@waeup.org>',
+            u'msg: Subject: Ikoba status change information',
+            u'msg: ',
+            u'msg: Dear Anna Tester,',
+            u'msg: ',
+            u'msg: The status of the following object has been changed:',
+            u'msg: ', u'msg: Object Id: K1000000',
+            u'msg: Title: Anna Tester',
+            u'msg: Transition: customer registration rejected',
+            u'msg: New state: started',
+            u'msg: ',
+            u'msg: Regards,',
+            u'msg: ',
+            u'msg: Manager',
+            u'msg: ',
+            u'msg: --',
+            u'msg: Sample Company',
+            u'msg: ',
+            u'']
+        self.assertEqual(expected, self.get_fake_smtp_output().split('\n'))
+        self.browser.open(self.trigtrans_path)
+        self.browser.getControl(name="transition").value = ['request']
+        self.browser.getControl("Apply").click()
+        self.browser.open(self.trigtrans_path)
+        self.browser.getControl(name="transition").value = ['approve_provisionally']
+        self.browser.getControl("Apply").click()
+        self.browser.open(self.trigtrans_path)
+        self.browser.getControl(name="transition").value = ['approve_finally']
+        self.browser.getControl("Apply").click()
+        self.browser.open(self.trigtrans_path)
+        self.browser.getControl(name="transition").value = ['reset1']
+        self.browser.getControl("Apply").click()
+        self.teardown_logging()
+        return
+
+    def test_manage_import(self):
+        # Managers can import customer data files
+        datacenter_path = 'http://localhost/app/datacenter'
+        # Prepare a csv file for customers
+        open('customers.csv', 'wb').write(
+"""firstname,lastname,reg_number,date_of_birth,email,phone,sex,password
+Aaren,Pieri,1,1990-01-02,bb@aa.ng,1234,m,mypwd1
+Claus,Finau,2,1990-01-03,cc@aa.ng,1234,m,mypwd1
+Brit,Berson,2,1990-01-04,dd@aa.ng,1234,m,mypwd1
+""")
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(datacenter_path)
+        self.browser.getLink('Upload data').click()
+        filecontents = StringIO(open('customers.csv', 'rb').read())
+        filewidget = self.browser.getControl(name='uploadfile:file')
+        filewidget.add_file(filecontents, 'text/plain', 'customers.csv')
+        self.browser.getControl(name='SUBMIT').click()
+        self.browser.getLink('Process data').click()
+        button = lookup_submit_value(
+            'select', 'customers_zope.mgr.csv', self.browser)
+        button.click()
+        importerselect = self.browser.getControl(name='importer')
+        modeselect = self.browser.getControl(name='mode')
+        importerselect.getControl('Customer Processor').selected = True
+        modeselect.getControl(value='create').selected = True
+        self.browser.getControl('Proceed to step 3').click()
+        self.assertTrue('Header fields OK' in self.browser.contents)
+        self.browser.getControl('Perform import').click()
+        self.assertTrue('Processing of 1 rows failed' in self.browser.contents)
+        self.assertTrue('Successfully processed 2 rows' in self.browser.contents)
+        self.assertTrue('Batch processing finished' in self.browser.contents)
+
+        # The customers are properly indexed and we can
+        # thus find a customer in  the department
+        self.browser.open(self.manage_container_path)
+        # We can search for a new customer by name ...
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Claus'
+        self.browser.getControl("Find customer(s)").click()
+        self.assertTrue('Claus Finau' in self.browser.contents)
+        # ... and check if the imported password has been properly set
+        ctrl = self.browser.getControl(name='entries')
+        value = ctrl.options[0]
+        claus = self.app['customers'][value]
+        self.assertTrue(IUserAccount(claus).checkPassword('mypwd1'))
+        return
+
+    def test_activate_deactivate_buttons(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.customer_path)
+        self.browser.getLink("Deactivate").click()
+        self.assertTrue(
+            'Customer account has been deactivated.' in self.browser.contents)
+        self.assertTrue(
+            'Base Data (account deactivated)' in self.browser.contents)
+        self.assertTrue(self.customer.suspended)
+        self.browser.getLink("Activate").click()
+        self.assertTrue(
+            'Customer account has been activated.' in self.browser.contents)
+        self.assertFalse(
+            'Base Data (account deactivated)' in self.browser.contents)
+        self.assertFalse(self.customer.suspended)
+        # History messages have been added ...
+        self.browser.getLink("History").click()
+        self.assertTrue(
+            'Customer account deactivated by Manager<br />' in self.browser.contents)
+        self.assertTrue(
+            'Customer account activated by Manager<br />' in self.browser.contents)
+        # ... and actions have been logged.
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'customers.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - customers.browser.CustomerDeactivatePage - '
+                        'K1000000 - account deactivated' in logcontent)
+        self.assertTrue('zope.mgr - customers.browser.CustomerActivatePage - '
+                        'K1000000 - account activated' in logcontent)
+
+
+    def test_login_as_customer(self):
+        # CustomerImpersonators can login as customer
+        self.app['users'].addUser('mrofficer', 'mrofficersecret')
+        self.app['users']['mrofficer'].email = 'mrofficer@foo.ng'
+        self.app['users']['mrofficer'].title = 'Harry Actor'
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.CustomerImpersonator', 'mrofficer')
+        prmglobal.assignRoleToPrincipal('waeup.CustomersManager', 'mrofficer')
+        self.assertEqual(self.customer.state, 'created')
+        # Login as customer impersonator
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrofficer'
+        self.browser.getControl(name="form.password").value = 'mrofficersecret'
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        self.browser.open(self.customer_path)
+        self.browser.getLink("Login as").click()
+        self.browser.getControl("Set password now").click()
+        temp_password = self.browser.getControl(name='form.password').value
+        self.browser.getControl("Login now").click()
+        self.assertMatches(
+            '...You successfully logged in as...', self.browser.contents)
+        # Status has changed
+        self.assertEqual(self.customer.state, 'started')
+        # We are logged in as customer and can see the 'My Data' tab
+        self.assertMatches(
+            '...<a href="#" class="dropdown-toggle" data-toggle="dropdown">...',
+            self.browser.contents)
+        self.assertMatches(
+            '...My Data...',
+            self.browser.contents)
+        self.browser.getLink("Logout").click()
+        # The customer can't login with the original password ...
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...Your account has been temporarily deactivated...',
+            self.browser.contents)
+        # ... but with the temporary password
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = temp_password
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        # Creation of temp_password is properly logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'customers.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'mrofficer - customers.browser.LoginAsCustomerStep1 - K1000000 - '
+            'temp_password generated: %s' % temp_password in logcontent)
+
+
+class CustomerUITests(CustomersFullSetup):
+    # Tests for Customer class views and pages
+
+    def test_customer_login_with_email(self):
+        self.assertEqual(self.customer.state, 'created')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer.email
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.customer_path)
+        self.assertTrue('You logged in' in self.browser.contents)
+        # Status has changed
+        self.assertEqual(self.customer.state, 'started')
+        return
+
+    def test_customer_change_password(self):
+        # Customers can change the password
+        self.assertEqual(self.customer.state, 'created')
+        self.customer.personal_updated = datetime.utcnow()
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.customer_path)
+        self.assertTrue('You logged in' in self.browser.contents)
+        # Status has changed
+        self.assertEqual(self.customer.state, 'started')
+        # Change password
+        self.browser.getLink("Change password").click()
+        self.browser.getControl(name="change_password").value = 'pw'
+        self.browser.getControl(
+            name="change_password_repeat").value = 'pw'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Password must have at least' in self.browser.contents)
+        self.browser.getControl(name="change_password").value = 'new_password'
+        self.browser.getControl(
+            name="change_password_repeat").value = 'new_passssword'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Passwords do not match' in self.browser.contents)
+        self.browser.getControl(name="change_password").value = 'new_password'
+        self.browser.getControl(
+            name="change_password_repeat").value = 'new_password'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Password changed' in self.browser.contents)
+        # We are still logged in. Changing the password hasn't thrown us out.
+        self.browser.getLink("Base Data").click()
+        self.assertEqual(self.browser.url, self.customer_path)
+        # We can logout
+        self.browser.getLink("Logout").click()
+        self.assertTrue('You have been logged out' in self.browser.contents)
+        self.assertEqual(self.browser.url, 'http://localhost/app/index')
+        # We can login again with the new password
+        self.browser.getLink("Login").click()
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'new_password'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.customer_path)
+        self.assertTrue('You logged in' in self.browser.contents)
+        return
+
+    def test_customer_edit_upload_upload_and_request(self):
+        # Customer cant login if their password is not set
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        self.browser.getLink("Edit").click()
+        self.browser.getControl(name="form.email").value = 'new_email@aa.ng'
+        self.browser.getControl("Save", index=0).click()
+        self.assertMatches('...Form has been saved...',
+                           self.browser.contents)
+        self.browser.getControl("Save and request registration").click()
+        self.assertMatches('...Passport picture is missing...',
+                           self.browser.contents)
+        self.assertEqual(self.customer.state, 'started')
+        # Customer must upload a passport picture. We are already on
+        # the upload page.
+        ctrl = self.browser.getControl(name='passporteditupload')
+        file_obj = open(SAMPLE_IMAGE, 'rb')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file_obj, filename='my_photo.jpg')
+        self.browser.getControl(
+            name='upload_passporteditupload').click()
+        self.assertTrue(
+            'src="http://localhost/app/customers/K1000000/passport.jpg"'
+            in self.browser.contents)
+        self.browser.getControl(name="CANCEL").click()
+        self.assertEqual(self.browser.url, self.customer_path)
+        self.browser.getLink("Edit").click()
+        self.browser.getControl("Save and request registration").click()
+        self.assertMatches('...Registration form has been submitted...',
+                           self.browser.contents)
+        self.assertEqual(self.customer.state, 'requested')
+        # Customer can view history
+        self.browser.getLink("History").click()
+        self.assertMatches('...Customer created by system...',
+            self.browser.contents)
+
+    def test_customer_login(self):
+        # Customer cant login if their password is not set
+        self.customer.password = None
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You entered invalid credentials.' in self.browser.contents)
+        # We set the password again
+        IUserAccount(
+            self.app['customers'][self.customer_id]).setPassword('cpwd')
+        # Customers can't login if their account is suspended/deactivated
+        self.customer.suspended = True
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...<div class="alert alert-warning">'
+            'Your account has been deactivated.</div>...', self.browser.contents)
+        # If suspended_comment is set this message will be flashed instead
+        self.customer.suspended_comment = u'Aetsch baetsch!'
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...<div class="alert alert-warning">Aetsch baetsch!</div>...',
+            self.browser.contents)
+        self.customer.suspended = False
+        # Customers can't login if a temporary password has been set and
+        # is not expired
+        self.app['customers'][self.customer_id].setTempPassword(
+            'anybody', 'temp_cpwd')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...Your account has been temporarily deactivated...',
+            self.browser.contents)
+        # The customer can login with the temporary password
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'temp_cpwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        # Customer can view the base data
+        self.browser.open(self.customer_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.customer_path)
+        # When the password expires ...
+        delta = timedelta(minutes=11)
+        self.app['customers'][self.customer_id].temp_password[
+            'timestamp'] = datetime.utcnow() - delta
+        self.app['customers'][self.customer_id]._p_changed = True
+        # ... the customer will be automatically logged out
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.customer_path)
+        # Then the customer can login with the original password
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+
+    def test_change_password_request(self):
+        self.browser.open('http://localhost/app/changepw')
+        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('An email with' in self.browser.contents)
+
+class CustomerRegistrationTests(CustomersFullSetup):
+    # Tests for customer registration
+
+    layer = FunctionalLayer
+
+    def test_request_pw(self):
+        # Customer with wrong number can't be found.
+        self.browser.open('http://localhost/app/requestpw')
+        self.browser.getControl(name="form.firstname").value = 'Anna'
+        self.browser.getControl(name="form.number").value = 'anynumber'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('No customer found.'
+            in self.browser.contents)
+        # Anonymous is not informed that firstname verification failed.
+        # It seems that the record doesn't exist.
+        self.browser.open('http://localhost/app/requestpw')
+        self.browser.getControl(name="form.firstname").value = 'Johnny'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('No customer found.'
+            in self.browser.contents)
+        # Even with the correct firstname we can't register if a
+        # password has been set and used.
+        self.browser.getControl(name="form.firstname").value = 'Anna'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('Your password has already been set and used.'
+            in self.browser.contents)
+        self.browser.open('http://localhost/app/requestpw')
+        self.app['customers'][self.customer_id].password = None
+        # The firstname field, used for verification, is not case-sensitive.
+        self.browser.getControl(name="form.firstname").value = 'aNNa'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl(name="form.email").value = 'new@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        # Yeah, we succeded ...
+        self.assertTrue('Your request was successful.'
+            in self.browser.contents)
+        # ... and  customer can be found in the catalog via the email address
+        cat = queryUtility(ICatalog, name='customers_catalog')
+        results = list(
+            cat.searchResults(
+            email=('new@yy.zz', 'new@yy.zz')))
+        self.assertEqual(self.customer,results[0])
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.anybody - customers.browser.CustomerRequestPasswordPage - '
+                        '123 (K1000000) - new@yy.zz' in logcontent)
+        return
+
+    def test_create_account(self):
+        self.browser.open('http://localhost/app/createaccount')
+        self.browser.getControl(name="form.firstname").value = 'Ruben'
+        self.browser.getControl(name="form.lastname").value = 'Gonzales'
+        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
+        self.browser.getControl("Send login credentials").click()
+        # Email address exists.
+        self.assertTrue('Email address in use.' in self.browser.contents)
+        self.browser.getControl(name="form.email").value = 'newcustomer@xx.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('Your request was successful.' in self.browser.contents)
+        # Customer can be found in the catalog via the email address
+        cat = queryUtility(ICatalog, name='customers_catalog')
+        results = list(
+            cat.searchResults(
+            email=('newcustomer@xx.zz', 'newcustomer@xx.zz')))
+        self.assertEqual(self.app['customers']['K1000001'], results[0])
+        self.assertEqual(self.app['customers']['K1000001'].firstname, 'Ruben')
+        self.assertEqual(self.app['customers']['K1000001'].lastname, 'Gonzales')
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.anybody - customers.browser.CustomerCreateAccountPage - '
+                        'K1000001 - newcustomer@xx.zz' in logcontent)
+        return
+
+class CustomerDataExportTests(CustomersFullSetup, FunctionalAsyncTestCase):
+    # Tests for CustomersContainer class views and pages
+
+    layer = FunctionalLayer
+
+    def wait_for_export_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def test_datacenter_export(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/datacenter/@@export')
+        self.browser.getControl(name="exporter").value = ['customers']
+        self.browser.getControl("Create CSV file").click()
+
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        # ... the csv file can be downloaded ...
+        self.browser.open('http://localhost/app/datacenter/@@export')
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Ikoba_customers_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        # ... and discarded
+        self.browser.open('http://localhost/app/datacenter/@@export')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVPage - exported: '
+            'customers, job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVView - downloaded: '
+            'WAeUP.Ikoba_customers_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVPage - discarded: '
+            'job_id=%s' % job_id in logcontent
+            )
+
+
+class DocumentUITests(CustomersFullSetup):
+    # Tests for customer document related views and pages
+
+    def test_manage_document(self):
+        # Managers can access the pages of customer documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.customer_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.customer_path)
+        self.browser.open(self.customer_path)
+        self.browser.getLink("Documents", index=1).click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['CustomerPDFDocument']
+        self.browser.getControl(name="form.title").value = 'My PDF Document'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('PDF Document added.' in self.browser.contents)
+        docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
+        document = self.customer['documents'][docid]
+
+        # Document can be edited
+        self.browser.getLink(docid[:9]).click()
+        self.browser.getLink("Manage").click()
+        self.browser.getControl(name="form.title").value = 'My second doc'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url,
+            self.documents_path + '/' + docid + '/index')
+
+        # Transitions can be performed
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['submit']
+        self.browser.getControl("Apply").click()
+        self.browser.getLink("Transition").click()
+        # Document can only be verified if customer is approved
+        self.browser.getControl(name="transition").value = ['verify']
+        self.browser.getControl("Apply").click()
+        self.assertTrue('Customer has not yet been approved' in self.browser.contents)
+        IWorkflowState(self.customer).setState(APPROVED)
+        # Document can only be verified if files have been uploaded before
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['verify']
+        self.browser.getControl("Apply").click()
+        self.assertTrue('No file uploaded' in self.browser.contents)
+        self.assertEqual(document.state, 'submitted')
+        # We set state here manually (verification is tested in test_verify_document)
+        IWorkflowState(document).setState(VERIFIED)
+
+        # Manage button and form is no longer available
+        self.browser.open(self.documents_path + '/' + docid + '/index')
+        self.assertFalse(
+            'href="http://localhost/app/customers/K1000000/documents/%s/manage"'
+            % docid in self.browser.contents)
+        self.browser.open(self.documents_path + '/' + docid + '/manage')
+        self.assertTrue(
+            'The requested form is locked (read-only)'
+            in self.browser.contents)
+
+        # Documents can be removed
+        self.browser.getLink("Documents", index=1).click()
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=document.document_id).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+        # All actions are being logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'customers.log')
+        logcontent = open(logfile).read()
+
+        self.assertTrue(
+            'INFO - system - K1000000 - DOC1 - Document created'
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - customers.browser.DocumentAddFormPage '
+            '- K1000000 - added: PDF Document %s'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - customers.browser.DocumentManageFormPage '
+            '- K1000000 - %s - saved: title' % docid
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - K1000000 - %s - Submitted for verification'
+            % docid in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - customers.browser.DocumentsManageFormPage '
+            '- K1000000 - removed: %s' % docid
+            in logcontent)
+
+    def test_edit_sample_document(self):
+        # Customers can manage documents under certain conditions
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        self.browser.getLink("Documents").click()
+        self.browser.getControl("Add document").click()
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+        # Customer is in wrong state
+        IWorkflowState(self.customer).setState(APPROVED)
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['CustomerSampleDocument']
+        self.browser.getControl(name="form.title").value = 'My Sample Document'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('Sample Document added.' in self.browser.contents)
+        docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
+        document = self.customer['documents'][docid]
+        self.browser.getControl(name="form.title").value = 'My second doc'
+        self.browser.getControl("Save").click()
+        self.assertEqual(document.title, 'My second doc')
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(
+            self.browser.url, self.documents_path + '/%s/index' % docid)
+        # Customer can upload a document.
+        self.browser.getLink("Edit").click()
+        ctrl = self.browser.getControl(name='samplescaneditupload')
+        file_obj = open(SAMPLE_IMAGE, 'rb')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file_obj, filename='my_document.jpg')
+        self.browser.getControl(
+            name='upload_samplescaneditupload').click()
+        self.assertTrue(
+            'href="http://localhost/app/customers/K1000000/documents/%s/sample"'
+            % docid in self.browser.contents)
+        # Customer can submit the form. The form is also saved.
+        self.browser.getControl(name="form.title").value = 'My third doc'
+        self.browser.getControl("Final Submit").click()
+        self.assertEqual(document.title, 'My third doc')
+        self.assertEqual(document.state, 'submitted')
+        self.assertTrue('Document State: submitted for verification' in self.browser.contents)
+        # Customer can't edit the document once it has been submitted
+        self.browser.open(self.documents_path + '/%s/edit' % docid)
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+    def test_manage_upload_sample_file(self):
+        # Managers can upload a file via the DocumentManageFormPage
+        # The image is stored even if form has errors
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.customer_path + '/documents/DOC1/manage')
+        # Create a pseudo image file and select it to be uploaded 
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='samplescanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.jpg')
+        # The Save action does not upload files
+        self.browser.getControl("Save").click() # submit form
+        self.assertFalse(
+            'href="http://localhost/app/customers/K1000000/documents/DOC1/sample"'
+            in self.browser.contents)
+        # ... but the correct upload submit button does
+        image = open(SAMPLE_IMAGE)
+        ctrl = self.browser.getControl(name='samplescanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.jpg')
+        self.browser.getControl(
+            name='upload_samplescanmanageupload').click()
+        self.assertTrue(
+            'href="http://localhost/app/customers/K1000000/documents/DOC1/sample"'
+            in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open('sample')
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), 2787)
+        # We can't reupload a file. The existing file must be deleted first.
+        self.browser.open(self.customer_path + '/documents/DOC1/manage')
+        self.assertFalse(
+            'upload_samplescanmanageupload' in self.browser.contents)
+        # File must be deleted first
+        self.browser.getControl(name='delete_samplescanmanageupload').click()
+        self.assertTrue(
+            'sample deleted' in self.browser.contents)
+        # Uploading a file which is bigger than 150k will raise an error
+        big_image = StringIO(open(SAMPLE_IMAGE, 'rb').read() * 75)
+        ctrl = self.browser.getControl(name='samplescanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(big_image, filename='my_sample_scan.jpg')
+        self.browser.getControl(
+            name='upload_samplescanmanageupload').click()
+        self.assertTrue(
+            'Uploaded file is too big' in self.browser.contents)
+        # We do not rely on filename extensions given by uploaders
+        image = open(SAMPLE_IMAGE, 'rb') # a jpg-file
+        ctrl = self.browser.getControl(name='samplescanmanageupload')
+        file_ctrl = ctrl.mech_control
+        # Tell uploaded file is bmp
+        file_ctrl.add_file(image, filename='my_sample_scan.bmp')
+        self.browser.getControl(
+            name='upload_samplescanmanageupload').click()
+        self.assertTrue(
+            # jpg file was recognized
+            'File sample.jpg uploaded.' in self.browser.contents)
+        # Delete file again
+        self.browser.getControl(name='delete_samplescanmanageupload').click()
+        self.assertTrue(
+            'sample deleted' in self.browser.contents)
+        # File names must meet several conditions
+        bmp_image = open(SAMPLE_IMAGE_BMP, 'rb')
+        ctrl = self.browser.getControl(name='samplescanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(bmp_image, filename='my_sample_scan.bmp')
+        self.browser.getControl(
+            name='upload_samplescanmanageupload').click()
+        self.assertTrue('Only the following extensions are allowed'
+            in self.browser.contents)
+
+    def test_verify_document(self):
+        IWorkflowState(self.customer).setState('approved')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.customer_path + '/documents/DOC1/manage')
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='samplescanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.jpg')
+        self.browser.getControl(
+            name='upload_samplescanmanageupload').click()
+        IWorkflowState(self.document).setState(SUBMITTED)
+        # Only after verifying the document, sample_md5 is set
+        self.assertEqual(
+            getattr(self.document, 'sample_md5', None), None)
+        self.browser.open(self.documents_path + '/DOC1/trigtrans')
+        self.browser.getControl(name="transition").value = ['verify']
+        self.browser.getControl("Apply").click()
+        self.assertEqual(
+            getattr(self.document, 'sample_md5', None),
+                    '1d1ab893e87c240afb2104d61ddfe180')
+
+    def test_manage_upload_pdf_file(self):
+        # Managers can upload a file via the DocumentManageFormPage
+        # The image is stored even if form has errors
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.customer_path + '/documents')
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['CustomerPDFDocument']
+        self.browser.getControl(name="form.title").value = 'My PDF Document'
+        self.browser.getControl("Add document").click()
+        docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
+        self.browser.open(self.documents_path + '/%s/manage' % docid)
+        # Create a pseudo image file and select it to be uploaded
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.jpg')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'pdf file format expected' in self.browser.contents)
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.pdf')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'Could not determine file type' in self.browser.contents)
+        pdf = open(SAMPLE_PDF, 'rb')
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pdf, filename='my_sample_scan.pdf')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'href="http://localhost/app/customers/K1000000/documents/%s/sample.pdf">%s.pdf</a>'
+            % (docid, docid[:9]) in self.browser.contents)
+        # Browsing the link shows a real pdf
+        self.browser.open('sample.pdf')
+        self.assertEqual(
+            self.browser.headers['content-type'], 'application/pdf')
+        # The name of the downloaded file will be different
+        self.assertEqual(
+            self.browser.headers['Content-Disposition'],
+            'attachment; filename="%s.pdf' % docid[:9])
+
+    def test_view_slips(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # Officers can open the document overview
+        self.browser.open(self.customer_path + '/documents')
+        self.browser.getLink("Download documents overview").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'documents_overview_slip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF overview_slip.pdf written to %s" % path
+        # Officers can open document slips which shows a thumbnail of
+        # the jpeg file attached.
+        file_id = IFileStoreNameChooser(self.document).chooseName(attr='sample.jpg')
+        fs = ExtFileStore(root=self.dc_root)
+        jpegfile = open(SAMPLE_IMAGE, 'rb')
+        fs.createFile(file_id, jpegfile)
+        self.browser.open(self.customer_path + '/documents/DOC1')
+        self.browser.getLink("Download document slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'document_slip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample document_slip.pdf written to %s" % path
+        # Officers can open merged pdf document slips
+        pdfdocument = createObject('waeup.CustomerPDFDocument')
+        pdfdocument.title = u'My first document'
+        self.customer['documents'].addDocument(pdfdocument)
+        # Add pdf file
+        file_id = IFileStoreNameChooser(pdfdocument).chooseName(attr='sample.pdf')
+        fs = ExtFileStore(root=self.dc_root)
+        pdffile = open(SAMPLE_PDF, 'rb')
+        fs.createFile(file_id, pdffile)
+        docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
+        self.browser.open(self.customer_path + '/documents/' + docid)
+        self.browser.getLink("Download document slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'pdfdocument_slip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample pdfdocument_slip.pdf written to %s" % path
+
+    def test_get_setmd5_file(self):
+        # A proper file name chooser is registered for customer documents.
+        # This is not a UI test. It's just a functional test.
+        file_id = IFileStoreNameChooser(self.document).chooseName(attr='sample')
+        fs = ExtFileStore(root=self.dc_root)
+        fs.createFile(file_id, StringIO('my sample 1'))
+        result = fs.getFileByContext(self.document, attr='sample')
+        self.assertEqual(file_id, '__file-customerdocument__01000/K1000000/sample_DOC1_K1000000')
+        self.assertEqual(result.read(), 'my sample 1')
+        self.assertEqual(self.document.connected_files[0][1].read(), 'my sample 1')
+        self.document.setMD5()
+        self.assertEqual(self.document.sample_md5, 'a406995ee8eb6772bacf51aa4b0caa24')
+        return
+
+
+class ContractUITests(CustomersFullSetup):
+    # Tests for contract related views and pages
+
+    def test_manage_contract(self):
+        # Managers can access the pages of customer contractsconter
+        # and can perform actions
+        IWorkflowState(self.customer).setState(APPROVED)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.customer_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.customer_path)
+        self.browser.open(self.customer_path)
+        self.browser.getLink("Contracts").click()
+        self.browser.getControl("Add contract").click()
+        self.browser.getControl(name="contype").value = ['SampleContract']
+        self.browser.getControl("Add contract").click()
+        self.assertTrue('Sample Contract added.' in self.browser.contents)
+        conid = [i for i in self.customer['contracts'].keys() if len(i) > 10][0]
+        contract = self.customer['contracts'][conid]
+        self.assertEqual(
+            self.browser.url, self.contracts_path + '/%s/selectproduct' % conid)
+        # SAM is in the correct contract_category
+        self.assertTrue('<option value="SAM">' in self.browser.contents)
+        # So far last_product_id is None.
+        self.assertTrue(self.customer['contracts'][conid].last_product_id is None)
+        self.browser.getControl(name="form.product_object").value = ['SAM']
+        self.browser.getControl("Save and proceed").click()
+        self.assertEqual(
+            self.browser.url, self.contracts_path + '/%s/manage' % conid)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.contracts_path + '/%s/index' % conid)
+        self.assertEqual(contract.tc_dict, {'en': u'Hello world'})
+
+        # Transitions can be performed
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['submit']
+        self.browser.getControl("Apply").click()
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['approve']
+        self.browser.getControl("Apply").click()
+        self.assertEqual(contract.state, 'approved')
+
+        # Even in state approved the official use data can be edited
+        self.browser.open(self.contracts_path + '/%s/index' % conid)
+        self.browser.getLink("Manage official data").click()
+        self.browser.getControl(name="form.comment").value = u'Nice place'
+        self.browser.getControl("Save").click()
+        self.assertEqual(contract.comment, 'Nice place')
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+
+        # Contracts can be removed
+        self.browser.getLink("Contracts").click()
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=contract.contract_id).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+        # All actions are being logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'customers.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - zope.mgr - K1000000 - %s - Contract created' % conid
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - customers.browser.ContractAddFormPage '
+            '- K1000000 - added: Sample Contract %s'
+            % contract.contract_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - K1000000 - %s - Submitted for approval' % conid
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - K1000000 - %s - Approved' % conid
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - customers.browser.ContractsFormPage '
+            '- K1000000 - removed: %s' % conid
+            in logcontent)
+
+    def test_edit_sample_contract(self):
+        # We add a second product.
+        product = createObject('waeup.Product')
+        product.product_id = u'LIC'
+        product.title = u'Our License Product'
+        product.contract_category = u'license'
+        product.valid_from = date(2014, 12, 4)
+        self.app['products'].addProduct(product)
+        # Customers can manage contracts under certain conditions
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        self.browser.getLink("Contracts").click()
+        # Customer is in wrong state
+        self.assertFalse('Add contract' in self.browser.contents)
+        self.browser.open(self.contracts_path + '/addcontract')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+        IWorkflowState(self.customer).setState(APPROVED)
+        self.browser.open(self.contracts_path)
+        # Now customer can add a contract
+        self.browser.getControl("Add contract").click()
+        self.browser.getControl(name="contype").value = ['SampleContract']
+        self.browser.getControl("Add contract").click()
+        self.assertTrue('Sample Contract added.' in self.browser.contents)
+        conid = [i for i in self.customer['contracts'].keys() if len(i) > 10][0]
+        contract = self.customer['contracts'][conid]
+        self.assertEqual(
+            self.browser.url, self.contracts_path + '/%s/selectproduct' % conid)
+        # SAM is in the correct contract_category ...
+        self.assertTrue('<option value="SAM">' in self.browser.contents)
+        # ... but LIC not.
+        self.assertFalse('<option value="LIC">' in self.browser.contents)
+        # So far last_product_id is None.
+        self.assertTrue(self.customer['contracts'][conid].last_product_id is None)
+        self.browser.getControl(name="form.product_object").value = ['SAM']
+        self.browser.getControl("Save and proceed").click()
+        self.assertEqual(
+            self.browser.url, self.contracts_path + '/%s/edit' % conid)
+        # Document is a required field on edit form page.
+        self.browser.getControl("Save").click()
+        self.assertTrue('Document: <span class="error">Required input is missing.</span>'
+            in self.browser.contents)
+        # But our document can't be selected because it's not submitted
+        self.assertFalse('My first document' in self.browser.contents)
+        IWorkflowState(self.document).setState(SUBMITTED)
+        self.browser.open(self.contracts_path + '/%s/edit' % conid)
+        self.browser.getControl(name="form.document_object").value = ['DOC1']
+        self.browser.getControl("Save").click()
+        # After saving the form, last_product_id and other attributes are set
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.assertEqual(self.customer['contracts'][conid].last_product_id, 'SAM')
+        self.assertEqual(contract.title, 'Our Sample Product')
+        self.assertEqual(contract.product_object, self.product)
+        self.assertEqual(contract.document_object, self.document)
+        # Saving the form again does not unset last_product_id
+        self.browser.getControl("Save").click()
+        self.assertEqual(self.customer['contracts'][conid].last_product_id, 'SAM')
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        # So far we have not yet set product options.
+        # Unfortunately, we can't set them in test browser
+        prodoption = ProductOption()
+        prodoption.title = u'Any product option'
+        prodoption.fee = Decimal('88.8')
+        prodoption.currency = 'EUR'
+        contract.product_options = [prodoption,]
+        self.browser.open(self.contracts_path + '/%s/edit' % conid)
+        # We can see both the stored and the recent product options
+        # from the chosen product.
+        self.assertTrue('<option selected="selected" value="Any product option">'
+                        'Any product option @ 88.8 Euro</option>'
+                        in self.browser.contents)
+        self.assertTrue('<option value="First option">First option '
+                        '@ 99.9 US Dollar</option>' in self.browser.contents)
+        # In test browser we can at least replace the option
+        self.browser.getControl(
+            name="form.product_options.0.").value = ['First option']
+        self.assertEqual(contract.product_options[0].title, 'Any product option')
+        self.browser.getControl("Save").click()
+        self.assertEqual(contract.product_options[0].title, 'First option')
+        self.browser.getLink("View").click()
+        self.assertTrue('<span>First option @ 99.9 US Dollar</span>' in self.browser.contents)
+        self.assertEqual(self.browser.url, self.contracts_path + '/%s/index' % conid)
+        # An href attribute is referring to the document and product objects
+        self.assertTrue('<a href="http://localhost/app/products/SAM">SAM -'
+            in self.browser.contents)
+        self.assertTrue(
+            '<a href="http://localhost/app/customers/K1000000/documents/DOC1">DOC1 -'
+            in self.browser.contents)
+        # Customer can submit the form if confirmation box is ticket.
+        # The form is also saved.
+        self.browser.getLink("Edit").click()
+        self.browser.getControl("Proceed to checkout").click()
+        self.assertTrue('confirm your acceptance of these by ticking' in self.browser.contents)
+        self.assertEqual(contract.state, 'created')
+        self.browser.getControl(name="confirm_tc").value = True
+        self.browser.getControl("Proceed to checkout").click()
+        self.assertEqual(contract.state, 'created')
+        self.browser.getControl("Select payment method").click()
+        self.assertEqual(contract.state, 'awaiting')
+        # Customer can't edit the contract once it has been submitted
+        self.browser.open(self.contracts_path + '/%s/edit' % conid)
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+    def test_view_slips(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # Officers can open the contract overview
+        self.browser.open(self.customer_path + '/contracts')
+        self.browser.getLink("Download contracts overview").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'contracts_overview_slip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF overview_slip.pdf written to %s" % path
+        # Officers can open contract slips.
+        # First we add a submitted document and a product.
+        IWorkflowState(self.document).setState(SUBMITTED)
+        self.contract.document_object = self.document
+        self.contract.product_object = self.product
+        self.contract.tc_dict = {'en': u'<strong>Hello world</strong>'}
+        self.contract.valid_from = date(2015, 12, 4)
+        self.contract.valid_to = 'anything'
+        self.contract.title = u'Contract Title'
+        self.browser.open(self.customer_path + '/contracts/CON1')
+        self.browser.getLink("Download contract slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'contract_slip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample contract_slip.pdf written to %s" % path
+
+    def test_contract_approval(self):
+        # This is not a UI test. It's just a functional test.
+        self.assertRaises(ConstraintNotSatisfied,
+            self.contract.document_object, self.document)
+        # Document must be at least submitted
+        IWorkflowState(self.document).setState('submitted')
+        self.contract.document_object = self.document
+        IWorkflowState(self.contract).setState('submitted')
+        self.assertRaises(InvalidTransitionError,
+            IWorkflowInfo(self.contract).fireTransition, 'approve')
+        # Customer must be approved and
+        # document must be verified for the approval of contracts
+        IWorkflowState(self.document).setState('verified')
+        IWorkflowState(self.customer).setState('approved')
+        IWorkflowInfo(self.contract).fireTransition('approve')
+        self.assertEqual(IWorkflowState(self.contract).getState(), 'approved')
+
+    def test_contract_approval_in_UI(self):
+        # Now let's see what the UI says when trying to approve a contract
+        # with unverified documents.
+        IWorkflowState(self.customer).setState('approved')
+        IWorkflowState(self.document).setState('submitted')
+        self.contract.document_object = self.document
+        IWorkflowState(self.contract).setState('submitted')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.contracts_path + '/CON1/trigtrans')
+        self.browser.getControl(name="transition").value = ['approve']
+        self.browser.getControl("Apply").click()
+        # InvalidTransitionError is catched
+        self.assertTrue(
+            '<div class="alert alert-warning">Attached documents must be verified first.</div>'
+            in self.browser.contents)
+        self.browser.open(self.contracts_path + '/CON1/trigtrans')
+        IWorkflowState(self.document).setState('verified')
+        self.browser.getControl(name="transition").value = ['approve']
+        self.browser.getControl("Apply").click()
+        self.assertEqual(IWorkflowState(self.contract).getState(), 'approved')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_catalog.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_catalog.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_catalog.py	(revision 12677)
@@ -0,0 +1,217 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import os
+import grok
+import shutil
+import tempfile
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, createObject
+from zope.component.hooks import setSite
+from hurry.workflow.interfaces import IWorkflowState
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import VERIFIED
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.customers.customer import Customer
+from waeup.ikoba.customers.contracts import SampleContract
+
+
+class CatalogTestSetup(FunctionalTestCase):
+    # A setup for testing catalog related stuff.
+    #
+    # sets up a site with some customer already created.
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CatalogTestSetup, self).setUp()
+
+        # Prepopulate ZODB
+        app = Company()
+        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 customer with subobjects
+        customer = Customer()
+        customer.firstname = u'Bob'
+        customer.lastname = u'Tester'
+        customer.matric_number = u'1234'
+        self.app['customers'].addCustomer(customer)
+        self.customer_id = customer.customer_id
+        self.customer = self.app['customers'][self.customer_id]
+
+        product = createObject('waeup.Product')
+        product.product_id = u'SAM'
+        product.title = u'Our Sample Product'
+        product.contract_category = u'sample'
+        self.app['products'].addProduct(product)
+        self.product_id = product.product_id
+        self.product = self.app['products'][self.product_id]
+
+        document = createObject('waeup.CustomerSampleDocument')
+        document.title = u'My Sample Document'
+        IWorkflowState(document).setState(VERIFIED)
+        self.customer['documents'].addDocument(document)
+        self.document_id = document.document_id
+        self.document = self.customer['documents'][self.document_id]
+
+        contract = createObject('waeup.SampleContract')
+        contract.contract_id = u'CON1'
+        contract.product_object = product
+        contract.document_object = document
+        contract.last_product_id = product.product_id
+        self.customer['contracts'].addContract(contract)
+        self.contract_id = contract.contract_id
+        self.contract = self.customer['contracts'][self.contract_id]
+
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(CatalogTestSetup, self).tearDown()
+        return
+
+
+class CustomersCatalogTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_get_catalog(self):
+        # We can get an customers catalog if we wish
+        cat = queryUtility(ICatalog, name='customers_catalog')
+        assert cat is not None
+
+    def test_search_by_id(self):
+        # We can find a certain customer id
+        cat = queryUtility(ICatalog, name='customers_catalog')
+        results = cat.searchResults(customer_id=(self.customer_id,
+                                                self.customer_id))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['customers'][self.customer_id]
+
+    def test_search_by_name(self):
+        # We can find a certain name
+        cat = queryUtility(ICatalog, name='customers_catalog')
+        results = cat.searchResults(fullname='Bob Tester')
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['customers'][self.customer_id]
+
+
+class DocumentsCatalogTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_get_catalog(self):
+        # We can get an customers catalog if we wish
+        cat = queryUtility(ICatalog, name='documents_catalog')
+        assert cat is not None
+
+    def test_search_by_id(self):
+        # We can find a certain contract id
+        cat = queryUtility(ICatalog, name='documents_catalog')
+        results = cat.searchResults(document_id=(self.document_id,
+                                                self.document_id))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.customer['documents'][self.document_id]
+
+
+class ContractsCatalogTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_get_catalog(self):
+        # We can get an customers catalog if we wish
+        cat = queryUtility(ICatalog, name='contracts_catalog')
+        assert cat is not None
+
+    def test_search_by_id(self):
+        # We can find a certain contract id
+        cat = queryUtility(ICatalog, name='contracts_catalog')
+        results = cat.searchResults(contract_id=(self.contract_id,
+                                                self.contract_id))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.customer['contracts'][self.contract_id]
+
+    def test_search_by_category(self):
+        # We can find a certain name
+        cat = queryUtility(ICatalog, name='contracts_catalog')
+        results = cat.searchResults(contract_category=('sample', 'sample'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.customer['contracts'][self.contract_id]
+
+    def test_search_by_last_product_id(self):
+        # We can find a certain name
+        cat = queryUtility(ICatalog, name='contracts_catalog')
+        results = cat.searchResults(last_product_id=('SAM', 'SAM'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.customer['contracts'][self.contract_id]
+        assert results[0].product_object is self.product
+
+    def test_product_removal(self):
+        # This test does not only test catalog components, it also ensures
+        # that the product object attribute of a contract is cleared
+        # but the last_product_id is not.
+        self.assertTrue(self.contract.product_object is self.product)
+        del self.app['products'][self.product_id]
+        self.assertTrue(self.contract.product_object is None)
+        # The document referrer wasn't touched.
+        self.assertTrue(self.contract.document_object is self.document)
+        self.assertEqual(self.contract.last_product_id, 'SAM')
+        # We can still find the contract by last_product_id.
+        cat = queryUtility(ICatalog, name='contracts_catalog')
+        results = cat.searchResults(last_product_id=('SAM', 'SAM'))
+        results = [x for x in results]
+        assert len(results) == 1
+        assert results[0] is self.customer['contracts'][self.contract_id]
+        assert results[0].product_object is None
+        # Product removal is being logged in customers.log
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'customers.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - system - ObjectRemovedEvent - K1000000 - CON1 - removed: SAM\n'
+            in logcontent)
+
+    def test_document_removal(self):
+        # This test does not test catalog components at all.
+        # It only ensures that the document object attributes of a contract
+        # are cleared, when a document is removed, much like in the
+        # test_product_removal above.
+        self.assertTrue(self.contract.document_object is self.document)
+        del self.customer['documents'][self.document_id]
+        self.assertTrue(self.contract.document_object is None)
+        # The product referrer wasn't touched.
+        self.assertTrue(self.contract.product_object is self.product)
+        # Document removal is being logged in customers.log
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'customers.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - system - ObjectRemovedEvent - K1000000 - CON1 - removed: %s\n'
+            % self.document_id in logcontent)
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_container.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_container.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_container.py	(revision 12677)
@@ -0,0 +1,93 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for customers containers.
+"""
+import tempfile
+import shutil
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component.hooks import setSite, clearSite
+from waeup.ikoba.app import Company
+from waeup.ikoba.customers.interfaces import (
+    ICustomersContainer,
+    )
+from waeup.ikoba.customers.container import (
+    CustomersContainer,
+    )
+from waeup.ikoba.testing import (
+    FunctionalLayer, FunctionalTestCase, remove_logger)
+
+class CustomersContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        remove_logger('waeup.ikoba.app.customers')
+        super(CustomersContainerTestCase, self).setUp()
+        # Setup a sample site for each test
+        # Prepopulate the ZODB...
+        app = Company()
+        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(CustomersContainerTestCase, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+        return
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                ICustomersContainer, CustomersContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                ICustomersContainer, CustomersContainer())
+            )
+        return
+
+    def test_logger(self):
+        # We can get a logger from root
+        logger = self.app['customers'].logger
+        assert logger is not None
+        assert logger.name == 'waeup.ikoba.app.customers'
+        handlers = logger.handlers
+        assert len(handlers) == 1
+        filename = logger.handlers[0].baseFilename
+        assert filename.endswith('customers.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['customers'].logger
+        # At second call the already registered logger should be returned
+        logger = self.app['customers'].logger
+        assert logger is not None
+        assert logger.name == 'waeup.ikoba.app.customers'
+        handlers = logger.handlers
+        assert len(handlers) == 1
+        filename = logger.handlers[0].baseFilename
+        assert filename.endswith('customers.log')
+        assert filename.startswith(self.dc_root)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_contract.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_contract.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_contract.py	(revision 12677)
@@ -0,0 +1,90 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for contracts.
+"""
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import createObject
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.ikoba.customers.interfaces import (
+    IContractsContainer, IContract)
+from waeup.ikoba.interfaces import IObjectHistory
+from waeup.ikoba.customers.contracts import (
+    ContractsContainer, SampleContract)
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+class ContractsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IContractsContainer, ContractsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IContractsContainer, ContractsContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IContract, SampleContract)
+            )
+        self.assertTrue(
+            verifyObject(
+                IContract, SampleContract())
+            )
+        return
+
+    def test_addContract(self):
+        container = ContractsContainer()
+        contract = createObject(u'waeup.SampleContract')
+        id = contract.contract_id
+        container.addContract(contract)
+        self.assertEqual(container[id], contract)
+        self.assertRaises(TypeError, container.addContract, object())
+        self.assertEqual(contract.class_name, 'SampleContract')
+        return
+
+    def test_contract_workflow(self):
+        contract = createObject(u'waeup.SampleContract')
+        IWorkflowInfo(contract).fireTransition('create')
+        self.assertEqual(IWorkflowState(contract).getState(), 'created')
+        IWorkflowInfo(contract).fireTransition('submit')
+        self.assertEqual(IWorkflowState(contract).getState(), 'submitted')
+        IWorkflowInfo(contract).fireTransition('approve')
+        self.assertEqual(IWorkflowState(contract).getState(), 'approved')
+        IWorkflowInfo(contract).fireTransition('expire')
+        self.assertEqual(IWorkflowState(contract).getState(), 'expired')
+        IWorkflowInfo(contract).fireTransition('reset4')
+        self.assertEqual(IWorkflowState(contract).getState(), 'created')
+        self.assertRaises(InvalidTransitionError,
+            IWorkflowInfo(contract).fireTransition, 'approve')
+        IWorkflowInfo(contract).fireTransition('submit')
+        IWorkflowInfo(contract).fireTransition('reset3')
+        self.assertEqual(IWorkflowState(contract).getState(), 'created')
+        return
+
+    def test_contract_history(self):
+        doc = createObject(u'waeup.SampleContract')
+        IWorkflowInfo(doc).fireTransition('create')
+        messages = ' '.join(doc.history.messages)
+        self.assertTrue('Contract created by system' in messages)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_customer.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_customer.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_customer.py	(revision 12677)
@@ -0,0 +1,215 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Tests for customers and related.
+"""
+import os
+import re
+import unittest
+import grok
+from cStringIO import StringIO
+from datetime import tzinfo
+from zope.component import getUtility, queryUtility, createObject
+from zope.catalog.interfaces import ICatalog
+from zope.component.interfaces import IFactory
+from zope.event import notify
+from zope.interface import verify
+from zope.schema.interfaces import RequiredMissing
+from waeup.ikoba.interfaces import IExtFileStore, IFileStoreNameChooser
+from waeup.ikoba.customers.customer import (
+    Customer, CustomerFactory, handle_customer_removed, path_from_custid)
+from waeup.ikoba.customers.interfaces import (
+    ICustomer, ICustomerNavigation, ICustomersUtils)
+from waeup.ikoba.customers.tests.test_batching import CustomerImportExportSetup
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+class HelperTests(unittest.TestCase):
+    # Tests for helper functions in customer module.
+
+    def test_path_from_custid(self):
+        # make sure we get predictable paths from customer ids.
+        self.assertEqual(
+            path_from_custid('K1000000'), u'01000/K1000000')
+        self.assertEqual(
+            path_from_custid('K1234567'), u'01234/K1234567')
+        self.assertEqual(
+            path_from_custid('K12345678'), u'12345/K12345678')
+        # The algorithm works also for overlong numbers, just to be
+        # sure.
+        self.assertEqual(
+            path_from_custid('K123456789'), u'123456/K123456789')
+        # low numbers (< 10**6) are treated special: they get max. of
+        # 10,000 entries. That's mainly because of old customers
+        # migrated into our portal.
+        self.assertEqual(
+            path_from_custid('KM123456'), u'00120/KM123456')
+        return
+
+class CustomerTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomerTest, self).setUp()
+        self.customer = Customer()
+        self.customer.firstname = u'Anna'
+        self.customer.lastname = u'Tester'
+        return
+
+    def tearDown(self):
+        super(CustomerTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(ICustomer, Customer)
+        verify.verifyClass(ICustomerNavigation, Customer)
+        verify.verifyObject(ICustomer, self.customer)
+        verify.verifyObject(ICustomerNavigation, self.customer)
+        return
+
+    #def test_base(self):
+    #    department = Department()
+    #    studycourse = CustomerStudyCourse()
+    #    self.assertRaises(
+    #        TypeError, studycourse.addCustomerStudyLevel, department)
+    #    studylevel = CustomerStudyLevel()
+    #    self.assertRaises(
+    #        TypeError, studylevel.addCourseTicket, department, department)
+
+
+class CustomerRemovalTests(CustomerImportExportSetup):
+    # Test handle_customer_removed
+    #
+    # This is a complex action updating several CSV files and moving
+    # stored files to a backup location.
+    #
+    # These tests make no assumptions about the CSV files except that
+    # they contain a deletion timestamp at end of each data row
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomerRemovalTests, self).setUp()
+        self.setup_for_export()
+        return
+
+    def create_passport_img(self, customer):
+        # create some passport file for `customer`
+        storage = getUtility(IExtFileStore)
+        image_path = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+        self.image_contents = open(image_path, 'rb').read()
+        file_id = IFileStoreNameChooser(customer).chooseName(
+            attr='passport.jpg')
+        storage.createFile(file_id, StringIO(self.image_contents))
+
+    def test_backup_single_customer_data(self):
+        # when a single customer is removed, the data is backed up.
+        self.setup_customer(self.customer)
+        # Add a fake image
+        self.create_passport_img(self.customer)
+        handle_customer_removed(self.customer, None)
+        del_dir = self.app['datacenter'].deleted_path
+        del_img_path = os.path.join(
+            del_dir, 'media', 'customers', '00110', 'A111111',
+            'passport_A111111.jpg')
+
+        # The image was copied over
+        self.assertTrue(os.path.isfile(del_img_path))
+        self.assertEqual(
+            open(del_img_path, 'rb').read(),
+            self.image_contents)
+
+        # The customer data were put into CSV files
+        for name in getUtility(ICustomersUtils).EXPORTER_NAMES:
+            csv_path = os.path.join(del_dir, '%s.csv' % name)
+            self.assertTrue(os.path.isfile(csv_path))
+            contents = open(csv_path, 'rb').read().split('\r\n')
+            # We expect 3 lines output including a linebreak at end of file.
+            self.assertEqual(len(contents), 3)
+        return
+
+    def test_backup_append_csv(self):
+        # when several customers are removed, existing CSVs are appended
+        self.setup_customer(self.customer)
+        # Add a fake image
+        self.create_passport_img(self.customer)
+        del_dir = self.app['datacenter'].deleted_path
+        # put fake data into customers.csv with trailing linebreak
+        customers_csv = os.path.join(del_dir, 'customers.csv')
+        open(customers_csv, 'wb').write('line1\r\nline2\r\n')
+        handle_customer_removed(self.customer, None)
+        contents = open(customers_csv, 'rb').read().split('\r\n')
+        # there should be 4 lines in result csv (including trailing linebreak)
+        self.assertEqual(len(contents), 4)
+        return
+
+    def test_old_files_removed(self):
+        # make sure old files are not accessible any more
+        self.setup_customer(self.customer)
+        # Add a fake image
+        self.create_passport_img(self.customer)
+        # make sure we can access the image before removal
+        file_store = getUtility(IExtFileStore)
+        image = file_store.getFileByContext(self.customer, attr='passport.jpg')
+        self.assertTrue(image is not None)
+
+        # remove image (hopefully)
+        handle_customer_removed(self.customer, None)
+
+        # the is not accessible anymore
+        image = file_store.getFileByContext(self.customer, attr='passport.jpg')
+        self.assertEqual(image, None)
+        return
+
+    def test_csv_file_entries_have_timestamp(self):
+        # each row in written csv files has a ``del_date`` column to
+        # tell when the associated customer was deleted
+        self.setup_customer(self.customer)
+        del_dir = self.app['datacenter'].deleted_path
+        customers_csv = os.path.join(del_dir, 'customers.csv')
+        handle_customer_removed(self.customer, None)
+        contents = open(customers_csv, 'rb').read().split('\r\n')
+        # the CSV header ends with a ``del_date`` column
+        self.assertTrue(contents[0].endswith(',del_date'))
+        # each line ends with an UTC timestamp
+        timestamp = contents[1][-23:]
+        self.assertTrue(re.match(
+            '^\d\d-\d\d-\d\d \d\d:\d\d:\d\d\+00:00$', timestamp))
+        return
+
+class CustomerFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomerFactoryTest, self).setUp()
+        self.factory = CustomerFactory()
+
+    def tearDown(self):
+        super(CustomerFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, CustomerFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, Customer)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(ICustomer)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_document.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_document.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_document.py	(revision 12677)
@@ -0,0 +1,172 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for documents.
+"""
+import shutil
+import tempfile
+import grok
+from StringIO import StringIO
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import createObject, queryUtility
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.ikoba.interfaces import (
+    IObjectHistory, IFileStoreNameChooser, IFileStoreHandler)
+from waeup.ikoba.imagestorage import DefaultStorage
+from waeup.ikoba.customers.interfaces import (
+    ICustomerDocumentsContainer, ICustomerDocument)
+from waeup.ikoba.interfaces import IObjectHistory
+from waeup.ikoba.customers.customer import Customer
+from waeup.ikoba.customers.documents import (
+    CustomerDocumentsContainer, CustomerSampleDocument,
+    CustomerDocumentFileStoreHandler, CustomerDocumentFileNameChooser)
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+class CustomerDocumentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                ICustomerDocumentsContainer, CustomerDocumentsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                ICustomerDocumentsContainer, CustomerDocumentsContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                ICustomerDocument, CustomerSampleDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                ICustomerDocument, CustomerSampleDocument())
+            )
+        return
+
+    def test_addDocument(self):
+        container = CustomerDocumentsContainer()
+        document = createObject(u'waeup.CustomerSampleDocument')
+        id = document.document_id
+        container.addDocument(document)
+        self.assertEqual(container[id], document)
+        self.assertRaises(TypeError, container.addDocument, object())
+        self.assertEqual(document.class_name, 'CustomerSampleDocument')
+        return
+
+    def test_document_workflow(self):
+        document = createObject(u'waeup.CustomerSampleDocument')
+        IWorkflowInfo(document).fireTransition('create')
+        self.assertEqual(IWorkflowState(document).getState(), 'created')
+        IWorkflowInfo(document).fireTransition('submit')
+        self.assertEqual(IWorkflowState(document).getState(), 'submitted')
+        IWorkflowInfo(document).fireTransition('verify')
+        self.assertEqual(IWorkflowState(document).getState(), 'verified')
+        IWorkflowInfo(document).fireTransition('reset2')
+        self.assertEqual(IWorkflowState(document).getState(), 'created')
+        self.assertRaises(InvalidTransitionError,
+            IWorkflowInfo(document).fireTransition, 'verify')
+        IWorkflowInfo(document).fireTransition('submit')
+        IWorkflowInfo(document).fireTransition('reset3')
+        self.assertEqual(IWorkflowState(document).getState(), 'created')
+        return
+
+    def test_document_history(self):
+        doc = createObject(u'waeup.CustomerSampleDocument')
+        IWorkflowInfo(doc).fireTransition('create')
+        messages = ' '.join(doc.history.messages)
+        self.assertTrue('Document created by system' in messages)
+        return
+
+
+class CustomerDocumentFileTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomerDocumentFileTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(CustomerDocumentFileTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_file_store_handler_util_accessible(self):
+        # we can get an IFileStoreHandler utility for documents
+        handler = queryUtility(IFileStoreHandler, name='file-customerdocument')
+        self.assertTrue(
+            isinstance(handler, CustomerDocumentFileStoreHandler))
+        return
+
+    def test_file_store_handler(self):
+        store = DefaultStorage()
+        handler = queryUtility(IFileStoreHandler, name='file-customerdocument')
+        result = handler.pathFromFileID(
+            store, '/fake-root', '__anything__345/c999/nice_image_d123_c999.jpeg')
+        self.assertEqual(
+            result, '/fake-root/customers/345/c999/nice_image_d123_c999')
+        return
+
+    def test_file_store_handler_create(self):
+        # we can create files in image store with the document file
+        # store handler
+        store = DefaultStorage(self.workdir)
+        handler = queryUtility(IFileStoreHandler, name='file-customerdocument')
+        file_obj, path, waeup_file = handler.createFile(
+            store, store.root, 'sample.jpg', '__anything__345/c999/nice_image_d123_c999.jpeg',
+            StringIO('I am a JPG file'))
+        self.assertEqual(path[-43:], 'customers/345/c999/nice_image_d123_c999.jpg')
+        return
+
+    def test_iface(self):
+        # make sure we implement promised interfaces
+        obj = CustomerDocumentFileNameChooser(None) # needs a context normally
+        verifyClass(IFileStoreNameChooser, CustomerDocumentFileNameChooser)
+        verifyObject(IFileStoreNameChooser, obj)
+        return
+
+    def test_name_chooser_available(self):
+        # we can get a name chooser for document objects as adapter
+        doc = CustomerSampleDocument()
+        chooser = IFileStoreNameChooser(doc)
+        self.assertTrue(chooser is not None)
+        return
+
+    def test_name_chooser_document(self):
+        # we can't get an image filename for documents not owned by a customer
+        doc = CustomerSampleDocument()
+        doc.document_id = u'DOC1'
+        chooser = IFileStoreNameChooser(doc)
+        # no customer can be found
+        self.assertRaises(AttributeError, chooser.chooseName, 'sample.jpg')
+        # we add a fake customer and fake documents container and can get
+        # an image name
+        fake_customer = Customer()
+        fake_customer.customer_id = u'K1000001'
+        fake_container = grok.Container()
+        fake_container.__parent__ = fake_customer
+        doc.__parent__ = fake_container
+        result = chooser.chooseName('sample.jpg')
+        self.assertEqual(
+            result, '__file-customerdocument__01000/K1000001/sample_DOC1_K1000001.jpg')
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_export.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_export.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_export.py	(revision 12677)
@@ -0,0 +1,331 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the customer exporter.
+"""
+
+import os
+import grok
+import datetime
+from cStringIO import StringIO
+from zope.component import queryUtility, getUtility
+from zope.event import notify
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.ikoba.interfaces import (
+    ICSVExporter, IExtFileStore, IFileStoreNameChooser)
+from waeup.ikoba.customers.catalog import CustomersQuery
+from waeup.ikoba.customers.export import (
+    CustomerExporter, CustomerSampleDocumentExporter, SampleContractExporter,
+    get_customers)
+from waeup.ikoba.customers.interfaces import ICSVCustomerExporter
+from waeup.ikoba.customers.customer import Customer
+from waeup.ikoba.customers.documents import CustomerSampleDocument
+from waeup.ikoba.customers.contracts import SampleContract
+from waeup.ikoba.customers.tests.test_batching import CustomerImportExportSetup
+from waeup.ikoba.testing import FunctionalLayer
+
+curr_year = datetime.datetime.now().year
+year_range = range(curr_year - 9, curr_year + 1)
+year_range_str = ','.join([str(i) for i in year_range])
+
+class ExportHelperTests(CustomerImportExportSetup):
+    layer = FunctionalLayer
+    def setUp(self):
+        super(ExportHelperTests, self).setUp()
+        customer = Customer()
+        self.app['customers'].addCustomer(customer)
+        customer = self.setup_customer(customer)
+        notify(grok.ObjectModifiedEvent(customer))
+        self.customer = self.app['customers'][customer.customer_id]
+        return
+
+    def test_get_customers_plain(self):
+        # without a filter we get all customers
+        result = get_customers(self.app)
+        self.assertEqual(len(list(result)), 1)
+        return
+
+class CustomerExporterTest(CustomerImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomerExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = CustomerExporter()
+        verifyObject(ICSVCustomerExporter, obj)
+        verifyClass(ICSVCustomerExporter, CustomerExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an customer exporter as utility
+        result = queryUtility(ICSVExporter, name="customers")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export customers
+        # set values we can expect in export file
+        self.setup_customer(self.customer)
+        exporter = CustomerExporter()
+        exporter.export([self.customer], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'customer_id,email,firstname,lastname,middlename,phone,reg_number,'
+            'sex,suspended,suspended_comment,password,state,history\r\n'
+            'A111111,anna@sample.com,Anna,Tester,M.,+234-123-12345#,123,,0,,,'
+            'created,[u\'2014-12-04 11:25:35 UTC - Customer created '
+            'by system\']\r\n',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export customers
+        # set values we can expect in export file
+        self.setup_customer(self.customer)
+        exporter = CustomerExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'customer_id,email,firstname,lastname,middlename,phone,'
+            'reg_number,sex,suspended,suspended_comment,password,state,history\r\n'
+            'A111111,anna@sample.com,Anna,Tester,M.,+234-123-12345#,'
+            '123,,0,,,created'
+            in result
+            )
+        return
+
+    def test_export_customer(self):
+        # we can export a single customer
+        self.setup_customer(self.customer)
+        exporter = CustomerExporter()
+        exporter.export_customer(self.customer, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'customer_id,email,firstname,lastname,middlename,phone,reg_number,'
+            'sex,suspended,suspended_comment,password,state,history\r\n'
+            'A111111,anna@sample.com,Anna,Tester,M.,+234-123-12345#,'
+            '123,,0,,,created,'
+            in result
+            )
+        return
+
+class CustomerSampleDocumentExporterTest(CustomerImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomerSampleDocumentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = CustomerSampleDocumentExporter()
+        verifyObject(ICSVCustomerExporter, obj)
+        verifyClass(ICSVCustomerExporter, CustomerSampleDocumentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a documents exporter as utility
+        result = queryUtility(ICSVExporter, name="customersampledocuments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_empty(self):
+        # we can export a nearly empty document
+        document = CustomerSampleDocument()
+        document.document_id = u'DOC1'
+        exporter = CustomerSampleDocumentExporter()
+        exporter.export([document], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'class_name,document_id,history,state,title,user_id\r\n'
+            'CustomerSampleDocument,DOC1,[],,,\r\n'
+            )
+        return
+
+    def test_export(self):
+        # we can really export customer documents.
+        # set values we can expect in export file
+        self.setup_customer(self.customer)
+        document = self.customer['documents']['DOC1']
+        exporter = CustomerSampleDocumentExporter()
+        exporter.export([document], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'class_name,document_id,history,state,title,user_id\r\n'
+            in result
+            )
+        self.assertMatches(
+            '...CustomerSampleDocument,DOC1,[u\'2014-11-25 06:57:24 UTC - '
+            'Document created by system\'],'
+            'verified,My Document,A111111...',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all documents
+        # set values we can expect in export file
+        self.setup_customer(self.customer)
+        exporter = CustomerSampleDocumentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'class_name,document_id,history,state,title,user_id\r\n'
+            in result)
+        self.assertMatches(
+            '...CustomerSampleDocument,DOC1,[u\'2014-11-25 06:57:24 UTC - '
+            'Document created by system\'],'
+            'verified,My Document,A111111...',
+            result
+            )
+        return
+
+    def test_export_customer_document(self):
+        # we can really export all documents of a certain customer
+        # set values we can expect in export file
+        self.setup_customer(self.customer)
+        exporter = CustomerSampleDocumentExporter()
+        exporter.export_customer(self.customer, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'class_name,document_id,history,state,title,user_id\r\n'
+            in result)
+        self.assertMatches(
+            '...CustomerSampleDocument,DOC1,[u\'2014-11-25 06:57:24 UTC - '
+            'Document created by system\'],'
+            'verified,My Document,A111111...',
+            result
+            )
+        return
+
+
+class SampleContractExporterTest(CustomerImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(SampleContractExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = SampleContractExporter()
+        verifyObject(ICSVCustomerExporter, obj)
+        verifyClass(ICSVCustomerExporter, SampleContractExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a contracts exporter as utility
+        result = queryUtility(ICSVExporter, name="samplecontracts")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_empty(self):
+        # we can export a nearly empty contract
+        contract = SampleContract()
+        contract.contract_id = u'CON1'
+        exporter = SampleContractExporter()
+        exporter.export([contract], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'class_name,comment,contract_category,contract_id,document_object,'
+            'fee_based,history,last_product_id,'
+            'product_object,product_options,state,tc_dict,title,'
+            'user_id,valid_from,valid_to\r\n'
+
+            'SampleContract,,sample,CON1,,0,[],,,[],,{},,,,\r\n'
+            )
+        return
+
+    def test_export(self):
+        # we can really export customer contracts.
+        # set values we can expect in export file
+        self.setup_customer(self.customer)
+        exporter = SampleContractExporter()
+        exporter.export([self.contract], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,comment,contract_category,contract_id,document_object,'
+            'fee_based,history,last_product_id,'
+            'product_object,product_options,state,tc_dict,title,'
+            'user_id,valid_from,valid_to\r\n'
+
+            'SampleContract,,sample,CON1,DOC1,1,[u\'2014-12-04 12:10:46 UTC - '
+            'Contract created by system\'],,'
+            'SAM,"[(u\'Base Fee\', u\'800.6\', u\'USD\')]",'
+            'created,{\'en\': u\'Hello world\'},,A111111,'
+            '2014-02-04#,2014-12-04#\r\n',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all contracts
+        # set values we can expect in export file
+        self.setup_customer(self.customer)
+        exporter = SampleContractExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,comment,contract_category,contract_id,document_object,'
+            'fee_based,history,last_product_id,'
+            'product_object,product_options,state,tc_dict,title,'
+            'user_id,valid_from,valid_to\r\n'
+
+            'SampleContract,,sample,CON1,DOC1,1,[u\'2014-12-04 12:10:46 UTC - '
+            'Contract created by system\'],,'
+            'SAM,"[(u\'Base Fee\', u\'800.6\', u\'USD\')]",'
+            'created,{\'en\': u\'Hello world\'},,A111111,'
+            '2014-02-04#,2014-12-04#\r\n',
+            result
+            )
+        return
+
+    def test_export_contract(self):
+        # we can really export all contracts of a certain customer
+        # set values we can expect in export file
+        self.setup_customer(self.customer)
+        exporter = SampleContractExporter()
+        exporter.export_customer(self.customer, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,comment,contract_category,contract_id,document_object,'
+            'fee_based,history,last_product_id,'
+            'product_object,product_options,state,tc_dict,title,'
+            'user_id,valid_from,valid_to\r\n'
+
+            'SampleContract,,sample,CON1,DOC1,1,[u\'2014-12-04 12:10:46 UTC - '
+            'Contract created by system\'],,'
+            'SAM,"[(u\'Base Fee\', u\'800.6\', u\'USD\')]",'
+            'created,{\'en\': u\'Hello world\'},,A111111,'
+            '2014-02-04#,2014-12-04#\r\n',
+            result
+            )
+        return
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/tests/test_interfaces.py	(revision 12677)
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+# Tests for interfaces and included components.
+import grok
+import unittest
+import tempfile
+import shutil
+from datetime import date
+from zope.component.hooks import setSite
+from zope.component import queryUtility, createObject
+from hurry.workflow.interfaces import IWorkflowState
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import SUBMITTED
+from waeup.ikoba.customers.vocabularies import (
+    ConCatProductSource, ConCatActiveProductSource, CustomerDocumentSource)
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+
+class SourceTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(SourceTests, self).setUp()
+
+        # Prepopulate ZODB
+        app = Company()
+        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 customer with subobjects
+        customer = createObject('waeup.Customer')
+        customer.firstname = u'Bob'
+        customer.lastname = u'Tester'
+        customer.matric_number = u'1234'
+        self.app['customers'].addCustomer(customer)
+        self.customer_id = customer.customer_id
+        self.customer = self.app['customers'][self.customer_id]
+
+        product = createObject('waeup.Product')
+        product.product_id = u'SAM'
+        product.title = u'Our Sample Product'
+        product.contract_category = u'sample'
+        self.app['products'].addProduct(product)
+        self.product_id = product.product_id
+        self.product = self.app['products'][self.product_id]
+
+        contract = createObject('waeup.SampleContract')
+        contract.product = product
+        contract.last_product_id = product.product_id
+        self.customer['contracts'].addContract(contract)
+        self.contract_id = contract.contract_id
+        self.contract = self.customer['contracts'][self.contract_id]
+
+        document = createObject('waeup.CustomerSampleDocument')
+        document.title = u'My Sample Document'
+        document.document_id = u'DOC1'
+        self.customer['documents'].addDocument(document)
+        self.document_id = document.document_id
+        self.document = self.customer['documents'][self.document_id]
+        return
+
+    def test_getValues_ConCatProductSource(self):
+        source = ConCatProductSource()
+        self.product.contract_category = u'sample'
+        result = source.factory.getValues(self.contract)
+        self.assertTrue(self.product in result)
+        self.product.contract_category = u'license'
+        result = source.factory.getValues(self.contract)
+        self.assertEqual(len(result), 0)
+
+    def test_getTitle_ConCatProductSource(self):
+        source = ConCatProductSource()
+        result = source.factory.getTitle(object(), self.product)
+        self.assertEqual(result, u'SAM - Our Sample Product')
+
+    def test_getValues_ConCatActiveProductSource(self):
+        source = ConCatActiveProductSource()
+        self.product.valid_from = date(2030, 12, 4)
+        self.product.valid_to = None
+        self.product.contract_category = u'sample'
+        result = source.factory.getValues(self.contract)
+        self.assertFalse(self.product in result)
+        self.product.valid_from = None
+        self.product.valid_to = date(2030, 12, 4)
+        self.product.contract_category = u'sample'
+        result = source.factory.getValues(self.contract)
+        self.assertTrue(self.product in result)
+        self.product.valid_from = None
+        self.product.valid_to = date(2010, 12, 4)
+        self.product.contract_category = u'sample'
+        result = source.factory.getValues(self.contract)
+        self.assertFalse(self.product in result)
+        self.product.valid_from = date(2010, 12, 4)
+        self.product.valid_to = None
+        self.product.contract_category = u'sample'
+        result = source.factory.getValues(self.contract)
+        self.assertTrue(self.product in result)
+
+    def test_getValues_CustomerDocumentSource(self):
+        source = CustomerDocumentSource()
+        result = source.factory.getValues(self.contract)
+        self.assertFalse(self.document in result)
+        IWorkflowState(self.document).setState(SUBMITTED)
+        result = source.factory.getValues(self.contract)
+        self.assertTrue(self.document in result)
+
+    def test_getTitle_CustomerDocumentSource(self):
+        source = CustomerDocumentSource()
+        result = source.factory.getTitle(object(), self.document)
+        self.assertEqual(result, u'DOC1... - My Sample Document')
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/utils.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/utils.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/utils.py	(revision 12677)
@@ -0,0 +1,488 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""General helper functions and utilities for the customer section.
+"""
+import re
+import os
+import grok
+from copy import deepcopy
+from StringIO import StringIO
+from reportlab.lib import colors
+from reportlab.lib.units import cm
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.platypus import Paragraph, Image, Table, Spacer
+from reportlab.platypus.flowables import PageBreak
+from PyPDF2 import PdfFileMerger, PdfFileReader, PdfFileWriter
+from zope.event import notify
+from zope.schema.interfaces import ConstraintNotSatisfied
+from zope.component import getUtility, createObject
+from zope.formlib.form import setUpEditWidgets
+from zope.i18n import translate
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import (
+    STARTED, CREATED, REQUESTED, APPROVED, SUBMITTED, VERIFIED, REJECTED,
+    EXPIRED, PROVISIONALLY, AWAITING,
+    IIkobaUtils, IExtFileStore)
+from waeup.ikoba.customers.interfaces import ICustomersUtils
+from waeup.ikoba.browser.interfaces import IPDFCreator
+from waeup.ikoba.browser.pdf import (
+    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
+    get_signature_tables, get_qrcode)
+
+RE_CUSTID_NON_NUM = re.compile('[^\d]+')
+
+def generate_customer_id():
+    customers = grok.getSite()['customers']
+    new_id = customers.unique_customer_id
+    return new_id
+
+def path_from_custid(customer_id):
+    """Convert a customer_id into a predictable relative folder path.
+
+    Used for storing files.
+
+    Returns the name of folder in which files for a particular customer
+    should be stored. This is a relative path, relative to any general
+    customers folder with 5 zero-padded digits (except when customer_id
+    is overlong).
+
+    We normally map 1,000 different customer ids into one single
+    path. For instance ``K1000000`` will give ``01000/K1000000``,
+    ``K1234567`` will give ``0123/K1234567`` and ``K12345678`` will
+    result in ``1234/K12345678``.
+
+    For lower numbers < 10**6 we return the same path for up to 10,000
+    customer_ids. So for instance ``KM123456`` will result in
+    ``00120/KM123456`` (there will be no path starting with
+    ``00123``).
+
+    Works also with overlong number: here the leading zeros will be
+    missing but ``K123456789`` will give reliably
+    ``12345/K123456789`` as expected.
+    """
+    # remove all non numeric characters and turn this into an int.
+    num = int(RE_CUSTID_NON_NUM.sub('', customer_id))
+    if num < 10**6:
+        # store max. of 10000 custs per folder and correct num for 5 digits
+        num = num / 10000 * 10
+    else:
+        # store max. of 1000 custs per folder
+        num = num / 1000
+    # format folder name to have 5 zero-padded digits
+    folder_name = u'%05d' % num
+    folder_name = os.path.join(folder_name, customer_id)
+    return folder_name
+
+SLIP_STYLE = [
+    ('VALIGN',(0,0),(-1,-1),'TOP'),
+    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
+    ]
+
+CONTENT_STYLE = [
+    ('VALIGN',(0,0),(-1,-1),'TOP'),
+    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
+    #('BACKGROUND',(0,0),(-1,0),colors.black),
+    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+    ('BOX', (0,0), (-1,-1), 1, colors.black),
+
+    ]
+
+FONT_SIZE = 10
+FONT_COLOR = 'black'
+
+def trans(text, lang):
+    # shortcut
+    return translate(text, 'waeup.ikoba', target_language=lang)
+
+
+def formatted_text(text, color=FONT_COLOR, lang='en'):
+    """Turn `text`, `color` and `size` into an HTML snippet.
+
+    The snippet is suitable for use with reportlab and generating PDFs.
+    Wraps the `text` into a ``<font>`` tag with passed attributes.
+
+    Also non-strings are converted. Raw strings are expected to be
+    utf-8 encoded (usually the case for widgets etc.).
+
+    Finally, a br tag is added if widgets contain div tags
+    which are not supported by reportlab.
+
+    The returned snippet is unicode type.
+    """
+    if not isinstance(text, unicode):
+        if isinstance(text, basestring):
+            text = text.decode('utf-8')
+        else:
+            text = unicode(text)
+    if text == 'None':
+        text = ''
+    # Mainly for boolean values we need our customized
+    # localisation of the zope domain
+    text = translate(text, 'zope', target_language=lang)
+    text = text.replace('</div>', '<br /></div>')
+    tag1 = u'<font color="%s">' % (color)
+    return tag1 + u'%s</font>' % text
+
+def render_customer_data(customerview, context, omit_fields=(),
+                        lang='en', slipname=None):
+    """Render customer table for an existing frame.
+    """
+    width, height = A4
+    set_up_widgets(customerview, ignore_request=True)
+    data_left = []
+    data_middle = []
+    style = getSampleStyleSheet()
+    img = getUtility(IExtFileStore).getFileByContext(
+        customerview.context, attr='passport.jpg')
+    if img is None:
+        from waeup.ikoba.browser import DEFAULT_PASSPORT_IMAGE_PATH
+        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
+    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
+    data_left.append([doc_img])
+    #data.append([Spacer(1, 12)])
+
+    f_label = trans(_('Name:'), lang)
+    f_label = Paragraph(f_label, ENTRY1_STYLE)
+    f_text = formatted_text(customerview.context.display_fullname)
+    f_text = Paragraph(f_text, ENTRY1_STYLE)
+    data_middle.append([f_label,f_text])
+
+    for widget in customerview.widgets:
+        if 'name' in widget.name:
+            continue
+        f_label = translate(
+            widget.label.strip(), 'waeup.ikoba',
+            target_language=lang)
+        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
+        f_text = formatted_text(widget(), lang=lang)
+        f_text = Paragraph(f_text, ENTRY1_STYLE)
+        data_middle.append([f_label,f_text])
+
+    if getattr(customerview.context, 'certcode', None):
+        if not 'date_of_birth' in omit_fields:
+            f_label = trans(_('Date of Birth:'), lang)
+            f_label = Paragraph(f_label, ENTRY1_STYLE)
+            date_of_birth = customerview.context.date_of_birth
+            tz = getUtility(IIkobaUtils).tzinfo
+            date_of_birth = to_timezone(date_of_birth, tz)
+            if date_of_birth is not None:
+                date_of_birth = date_of_birth.strftime("%d/%m/%Y")
+            f_text = formatted_text(date_of_birth)
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            data_middle.append([f_label,f_text])
+
+    # append QR code to the right
+    if slipname:
+        url = customerview.url(context, slipname)
+        data_right = [[get_qrcode(url, width=70.0)]]
+        table_right = Table(data_right,style=SLIP_STYLE)
+    else:
+        table_right = None
+
+    table_left = Table(data_left,style=SLIP_STYLE)
+    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
+    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
+    return table
+
+def render_table_data(tableheader, tabledata, lang='en'):
+    """Render children table for an existing frame.
+    """
+    data = []
+    #data.append([Spacer(1, 12)])
+    line = []
+    style = getSampleStyleSheet()
+    for element in tableheader:
+        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
+        field = Paragraph(field, style["Normal"])
+        line.append(field)
+    data.append(line)
+    for ticket in tabledata:
+        line = []
+        for element in tableheader:
+              field = formatted_text(getattr(ticket,element[1],u' '))
+              field = Paragraph(field, style["Normal"])
+              line.append(field)
+        data.append(line)
+    table = Table(data,colWidths=[
+        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
+    return table
+
+def docs_as_flowables(view, lang='en'):
+    """Create reportlab flowables out of scanned docs.
+    """
+    # XXX: fix circular import problem
+    from waeup.ikoba.browser.fileviewlets import FileManager
+    from waeup.ikoba.browser import DEFAULT_IMAGE_PATH
+    style = getSampleStyleSheet()
+    data = []
+
+    # Collect viewlets
+    fm = FileManager(view.context, view.request, view)
+    fm.update()
+    if fm.viewlets:
+        sc_translation = trans(_('Connected Files'), lang)
+        data.append(Paragraph(sc_translation, HEADING_STYLE))
+        # Insert list of scanned documents
+        table_data = []
+        for viewlet in fm.viewlets:
+            if viewlet.file_exists:
+                # Show viewlet only if file exists
+                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
+                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
+                    view.context, attr=viewlet.download_name), 'name', None)
+                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
+                if img_path is None:
+                    pass
+                elif img_path[-4:] == '.pdf':
+                    f_text = Paragraph('%s (see file attached)' % (
+                        viewlet.title,), ENTRY1_STYLE)
+                elif not img_path[-4:] in ('.jpg', '.JPG'):
+                    # reportlab requires jpg images, I think.
+                    f_text = Paragraph('%s (no preview available)' % (
+                        viewlet.title,), ENTRY1_STYLE)
+                else:
+                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
+                table_data.append([f_label, f_text])
+        if table_data:
+            # safety belt; empty tables lead to problems.
+            data.append(Table(table_data, style=SLIP_STYLE))
+    return data
+
+
+def set_up_widgets(view, ignore_request=False):
+    view.adapters = {}
+    view.widgets = setUpEditWidgets(
+        view.form_fields, view.prefix, view.context, view.request,
+        adapters=view.adapters, for_display=True,
+        ignore_request=ignore_request
+        )
+
+def generate_contract_id():
+    new_id = grok.getSite().unique_contract_id
+    return new_id
+
+class CustomersUtils(grok.GlobalUtility):
+    """A collection of methods subject to customization.
+    """
+    grok.implements(ICustomersUtils)
+
+    #: A prefix used when generating new customer ids. Each customer id will
+    #: start with this string. The default is 'K'.
+    CUSTOMER_ID_PREFIX = u'K'
+
+    CUSTMANAGE_CUSTOMER_STATES = (STARTED,)
+
+    DOCMANAGE_CUSTOMER_STATES = (REQUESTED, APPROVED, PROVISIONALLY)
+
+    DOCMANAGE_DOCUMENT_STATES = (CREATED,)
+
+    CONMANAGE_CUSTOMER_STATES = deepcopy(DOCMANAGE_CUSTOMER_STATES)
+
+    CONMANAGE_CONTRACT_STATES = (CREATED,)
+
+    TRANSLATED_CUSTOMER_STATES = {
+        CREATED: _('created'),
+        STARTED: _('started'),
+        REQUESTED: _('requested'),
+        APPROVED: _('approved'),
+        PROVISIONALLY: _('provisionally approved'),
+        }
+
+    TRANSLATED_CONTRACT_STATES = {
+        CREATED: _('created'),
+        AWAITING: _('awaiting payment'),
+        SUBMITTED: _('submitted for approval'),
+        APPROVED: _('approved'),
+        REJECTED: _('rejected'),
+        EXPIRED:_('expired')
+        }
+
+    TRANSLATED_DOCUMENT_STATES = {
+        CREATED: _('created'),
+        SUBMITTED: _('submitted for verification'),
+        VERIFIED: _('verified'),
+        REJECTED: _('rejected'),
+        EXPIRED:_('expired')
+        }
+
+    DOCTYPES_DICT = {
+        'CustomerSampleDocument': _('Sample Document'),
+        'CustomerPDFDocument': _('PDF Document'),
+        }
+
+    CONTYPES_DICT = {
+        'SampleContract': _('Sample Contract'),
+        }
+
+    SELECTABLE_DOCTYPES_DICT = deepcopy(DOCTYPES_DICT)
+
+    SELECTABLE_CONTYPES_DICT = deepcopy(CONTYPES_DICT)
+
+    EXPORTER_NAMES = ('customers', 'customersampledocuments', 'samplecontracts')
+
+    def getPDFCreator(self, context=None):
+        """Get a pdf creator suitable for `context`.
+
+        The default implementation always returns the default creator.
+        """
+        return getUtility(IPDFCreator)
+
+    def renderPDF(self, view, filename='slip.pdf', customer=None,
+                  customerview=None,
+                  tableheader=[], tabledata=[],
+                  note=None, signatures=None, sigs_in_footer=(),
+                  show_scans=True, show_history=True, topMargin=1.5,
+                  omit_fields=(), mergefiles=None, watermark=None):
+        """Render pdf slips for various pages.
+        """
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        style = getSampleStyleSheet()
+        creator = self.getPDFCreator()
+        data = []
+        doc_title = view.label
+        author = '%s (%s)' % (view.request.principal.title,
+                              view.request.principal.id)
+        footer_text = view.label.split('\n')
+        if len(footer_text) > 2:
+            # We can add a department in first line
+            footer_text = footer_text[1]
+        else:
+            # Only the first line is used for the footer
+            footer_text = footer_text[0]
+        if getattr(customer, 'customer_id', None) is not None:
+            footer_text = "%s - %s - " % (customer.customer_id, footer_text)
+
+        # Insert customer data table
+        if customer is not None:
+            if view.__name__ == 'contract_slip.pdf':
+                bd_translation = trans(_('Contractor'), portal_language)
+                data.append(Paragraph(bd_translation, HEADING_STYLE))
+            if view.__name__ == 'document_slip.pdf':
+                bd_translation = trans(_('Document Owner'), portal_language)
+                data.append(Paragraph(bd_translation, HEADING_STYLE))
+            data.append(render_customer_data(
+                customerview, view.context, omit_fields, lang=portal_language,
+                slipname=filename))
+
+        # Insert widgets
+        if view.form_fields:
+            if view.__name__ == 'contract_slip.pdf':
+                data_header = trans(_('Contract Data'), portal_language)
+                data.append(Paragraph(data_header, HEADING_STYLE))
+            separators = getattr(self, 'SEPARATORS_DICT', {})
+            table = creator.getWidgetsTable(
+                view.form_fields, view.context, None, lang=portal_language,
+                separators=separators)
+            data.append(table)
+
+        # Insert scanned docs
+        if show_scans:
+            data.extend(docs_as_flowables(view, portal_language))
+
+        # Insert history
+        if show_history:
+            if getattr(view.context, 'history', None):
+                hist_translation = trans(_('Transaction History'),
+                    portal_language)
+                data.append(Paragraph(hist_translation, HEADING_STYLE))
+                data.extend(
+                    creator.fromStringList(view.context.history.messages))
+            else:
+                hist_translation = trans(_('Registration History'),
+                    portal_language)
+                data.append(Paragraph(hist_translation, HEADING_STYLE))
+                data.extend(creator.fromStringList(customer.history.messages))
+
+        # Insert validity period
+        if getattr(view, 'validity_period', None):
+            vp_translation = trans(_('Validity Period'), portal_language)
+            data.append(Paragraph(vp_translation, HEADING_STYLE))
+            vp = view.validity_period
+            data.append(Paragraph(vp, NOTE_STYLE))
+
+        # Insert content tables (optionally on second page)
+        if getattr(view, 'tabletitle', None):
+            for i in range(len(view.tabletitle)):
+                if tabledata[i] and tableheader[i]:
+                    #data.append(PageBreak())
+                    #data.append(Spacer(1, 20))
+                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
+                    data.append(Spacer(1, 8))
+                    contenttable = render_table_data(tableheader[i],tabledata[i])
+                    data.append(contenttable)
+
+        # Insert terms and conditions
+        if getattr(view, 'terms_and_conditions', None):
+            tc_translation = trans(_('Terms and Conditions'), portal_language)
+            data.append(Paragraph(tc_translation, HEADING_STYLE))
+            tc = format_html(view.terms_and_conditions)
+            data.append(Paragraph(tc, NOTE_STYLE))
+
+        # Insert signatures
+        # XXX: We are using only sigs_in_footer in waeup.ikoba, so we
+        # do not have a test for the following lines.
+        if signatures and not sigs_in_footer:
+            data.append(Spacer(1, 20))
+            # Render one signature table per signature to
+            # get date and signature in line.
+            for signature in signatures:
+                signaturetables = get_signature_tables(signature)
+                data.append(signaturetables[0])
+
+        view.response.setHeader(
+            'Content-Type', 'application/pdf')
+        try:
+            pdf_stream = creator.create_pdf(
+                data, None, doc_title, author=author, footer=footer_text,
+                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
+        except IOError:
+            view.flash('Error in image file.')
+            return view.redirect(view.url(view.context))
+
+        if mergefiles:
+            merger = PdfFileMerger()
+            merger.append(StringIO(pdf_stream))
+            if watermark:
+                watermark = PdfFileReader(watermark)
+            for file in mergefiles:
+                if watermark:
+                    # Pass through all pages of each file
+                    # and merge with watermark page.
+                    marked_file = PdfFileWriter()
+                    orig_file = PdfFileReader(file[1])
+                    num_pages = orig_file.getNumPages()
+                    for num in range(num_pages):
+                        page = orig_file.getPage(num)
+                        page.mergePage(watermark.getPage(0))
+                        marked_file.addPage(page)
+                    # Save into a file-like object
+                    tmp1 = StringIO()
+                    marked_file.write(tmp1)
+                    # Append the file-like object
+                    merger.append(tmp1)
+                else:
+                    # Just append the file object
+                    merger.append(file[1])
+            # Save into a file-like object
+            tmp2 = StringIO()
+            merger.write(tmp2)
+            return tmp2.getvalue()
+
+        return pdf_stream
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/viewlets.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/viewlets.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/viewlets.py	(revision 12677)
@@ -0,0 +1,464 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+import grok
+from zope.i18n import translate
+from zope.interface import Interface
+from zope.component import getUtility
+from waeup.ikoba.interfaces import IIkobaObject
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.browser.viewlets import (
+    PrimaryNavTab, ManageActionButton, AddActionButton)
+from waeup.ikoba.browser.layout import default_primary_nav_template
+from waeup.ikoba.customers.interfaces import (
+    ICustomer, ICustomersContainer,
+    ICustomerDocumentsContainer, ICustomerDocument,
+    IContractsContainer, IContract, ICustomersUtils)
+from waeup.ikoba.customers.browser import (
+    CustomersContainerPage, CustomersContainerManagePage,
+    CustomerBaseDisplayFormPage,
+    DocumentsManageFormPage, DocumentDisplayFormPage, DocumentManageFormPage,
+    ContractsFormPage, ContractDisplayFormPage,
+    ContractManageFormPage,
+    ContractOfficialUsePage)
+
+grok.context(IIkobaObject)  # Make IIkobaObject the default context
+grok.templatedir('browser_templates')
+
+
+class CustomersTab(PrimaryNavTab):
+    """Customers tab in primary navigation.
+    """
+
+    grok.context(IIkobaObject)
+    grok.order(4)
+    grok.require('waeup.viewCustomersTab')
+    grok.name('customerstab')
+
+    pnav = 4
+    tab_title = _(u'Customers')
+
+    @property
+    def link_target(self):
+        return self.view.application_url('customers')
+
+
+class PrimaryCustomerNavManager(grok.ViewletManager):
+    """Viewlet manager for the primary navigation tab.
+    """
+    grok.name('primary_nav_customer')
+
+
+class PrimaryCustomerNavTab(grok.Viewlet):
+    """Base for primary customer nav tabs.
+    """
+    grok.baseclass()
+    grok.viewletmanager(PrimaryCustomerNavManager)
+    template = default_primary_nav_template
+    grok.order(1)
+    grok.require('waeup.Authenticated')
+    pnav = 0
+    tab_title = u'Some Text'
+
+    @property
+    def link_target(self):
+        return self.view.application_url()
+
+    @property
+    def active(self):
+        view_pnav = getattr(self.view, 'pnav', 0)
+        if view_pnav == self.pnav:
+            return 'active'
+        return ''
+
+
+class MyCustomerDataTab(PrimaryCustomerNavTab):
+    """MyData dropdown tab in primary navigation.
+    """
+    grok.order(3)
+    grok.require('waeup.viewMyCustomerDataTab')
+    grok.template('mydatadropdowntabs')
+    grok.name('mycustomerdatatab')
+    pnav = 4
+    tab_title = _(u'My Data')
+
+    @property
+    def active(self):
+        view_pnav = getattr(self.view, 'pnav', 0)
+        if view_pnav == self.pnav:
+            return 'active dropdown'
+        return 'dropdown'
+
+    @property
+    def targets(self):
+        customer = grok.getSite()['customers'][self.request.principal.id]
+        customer_url = self.view.url(customer)
+        targets = []
+        targets += [
+            {'url':customer_url, 'title':'Base Data'},
+            {'url':customer_url + '/contracts', 'title':_('My Contracts')},
+            {'url':customer_url + '/documents', 'title':_('My Documents')},
+            {'url':customer_url + '/history', 'title':_('History')},
+            ]
+        return targets
+
+
+class CustomerManageSidebar(grok.ViewletManager):
+    grok.name('left_customermanage')
+
+
+class CustomerManageLink(grok.Viewlet):
+    """A link displayed in the customer box which shows up for CustomerNavigation
+    objects.
+
+    """
+    grok.baseclass()
+    grok.viewletmanager(CustomerManageSidebar)
+    grok.view(Interface)
+    grok.order(5)
+    grok.require('waeup.viewCustomer')
+
+    link = 'index'
+    text = _(u'Base Data')
+
+    def render(self):
+        url = self.view.url(self.context.customer, self.link)
+        # Here we know that the cookie has been set
+        lang = self.request.cookies.get('ikoba.language')
+        text = translate(self.text, 'waeup.ikoba',
+            target_language=lang)
+        if not self.link:
+            return ''
+        return u'<li><a href="%s">%s</a></li>' % (
+                url, text)
+
+
+class CustomerManageBaseLink(CustomerManageLink):
+    grok.order(2)
+    link = 'index'
+    text = _(u'Base Data')
+
+
+class CustomerManageContractsLink(CustomerManageLink):
+    grok.order(3)
+    link = 'contracts'
+    text = _(u'Contracts')
+
+
+class CustomerManageDocumentsLink(CustomerManageLink):
+    grok.order(4)
+    link = 'documents'
+    text = _(u'Documents')
+
+
+class CustomerManageHistoryLink(CustomerManageLink):
+    grok.order(8)
+    link = 'history'
+    text = _(u'History')
+
+
+class CustomersContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(ICustomersContainer)
+    grok.view(CustomersContainerPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Manage customer section')
+
+
+class CustomersContainerAddActionButton(AddActionButton):
+    grok.order(1)
+    grok.context(ICustomersContainer)
+    grok.view(CustomersContainerManagePage)
+    grok.require('waeup.manageCustomer')
+    text = _('Add customer')
+    target = 'addcustomer'
+
+
+class ContactActionButton(ManageActionButton):
+    grok.order(5)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    icon = 'actionicon_mail.png'
+    text = _('Send email')
+    target = 'contactcustomer'
+
+
+class CustomerBaseManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Manage')
+    target = 'manage_base'
+
+
+class CustomerTrigTransActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.triggerTransition')
+    icon = 'actionicon_trigtrans.png'
+    text = _(u'Transition')
+    target = 'trigtrans'
+
+
+class CustomerLoginAsActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.loginAsCustomer')
+    icon = 'actionicon_mask.png'
+    text = _(u'Login as customer')
+    target = 'loginasstep1'
+
+
+class CustomerDeactivateActionButton(ManageActionButton):
+    grok.order(7)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Deactivate account')
+    target = 'deactivate'
+    icon = 'actionicon_traffic_lights_red.png'
+
+    @property
+    def target_url(self):
+        if self.context.suspended:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'A history message will be added. Are you sure?'")
+
+
+class CustomerActivateActionButton(ManageActionButton):
+    grok.order(7)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Activate account')
+    target = 'activate'
+    icon = 'actionicon_traffic_lights_green.png'
+
+    @property
+    def target_url(self):
+        if not self.context.suspended:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'A history message will be added. Are you sure?'")
+
+
+class CustomerBaseActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.handleCustomer')
+    target = 'edit_base'
+
+    @property
+    def text(self):
+        if self.view.is_requestable:
+            return _('Edit and request registration')
+        return _('Edit')
+
+
+class CustomerPasswordActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.handleCustomer')
+    icon = 'actionicon_key.png'
+    text = _('Change password')
+    target = 'changepassword'
+
+
+class CustomerPassportActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.handleCustomer')
+    icon = 'actionicon_up.png'
+    text = _('Upload files')
+    target = 'upload_files'
+
+    @property
+    def target_url(self):
+        CUSTMANAGE_STATES = getUtility(
+            ICustomersUtils).CUSTMANAGE_CUSTOMER_STATES
+        if self.context.state not in CUSTMANAGE_STATES:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+# Viewlets for customer documents
+
+class PDFDocumentOverviewActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(ICustomerDocumentsContainer)
+    grok.view(DocumentsManageFormPage)
+    grok.require('waeup.viewCustomer')
+    icon = 'actionicon_pdf.png'
+    text = _('Download documents overview')
+    target = 'documents_overview_slip.pdf'
+
+
+class DocumentManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(ICustomerDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Manage')
+    target = 'manage'
+
+    @property
+    def target_url(self):
+        if not self.context.is_editable_by_manager:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class DocumentEditActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(ICustomerDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.handleCustomer')
+    text = _('Edit')
+    target = 'edit'
+
+    @property
+    def target_url(self):
+        if not self.context.is_editable_by_customer:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class DocumentTrigTransActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(ICustomerDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.triggerTransition')
+    icon = 'actionicon_trigtrans.png'
+    text = _(u'Transition')
+    target = 'trigtrans'
+
+
+class DocumentViewActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(ICustomerDocument)
+    grok.view(DocumentManageFormPage)
+    grok.require('waeup.handleCustomer')
+    text = _('View')
+    target = 'index'
+    icon = 'actionicon_view.png'
+
+
+class PDFDocumentSlipActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(ICustomerDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.viewCustomer')
+    icon = 'actionicon_pdf.png'
+    text = _('Download document slip')
+    target = 'document_slip.pdf'
+
+
+# Viewlets for customer contracts
+
+class PDFContractOverviewActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IContractsContainer)
+    grok.view(ContractsFormPage)
+    grok.require('waeup.viewCustomer')
+    icon = 'actionicon_pdf.png'
+    text = _('Download contracts overview')
+    target = 'contracts_overview_slip.pdf'
+
+
+class ContractManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IContract)
+    grok.view(ContractDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Manage')
+    target = 'manage'
+
+
+class ContractOUActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IContract)
+    grok.view(ContractDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Manage official data')
+    target = 'official_use'
+
+
+class ContractEditActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IContract)
+    grok.view(ContractDisplayFormPage)
+    grok.require('waeup.handleCustomer')
+    text = _('Edit')
+    target = 'edit'
+
+    @property
+    def target_url(self):
+        if not self.context.is_editable:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class ContractTrigTransActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IContract)
+    grok.view(ContractDisplayFormPage)
+    grok.require('waeup.triggerTransition')
+    icon = 'actionicon_trigtrans.png'
+    text = _(u'Transition')
+    target = 'trigtrans'
+
+
+class ContractViewActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IContract)
+    grok.view(ContractManageFormPage)
+    grok.require('waeup.handleCustomer')
+    text = _('View')
+    target = 'index'
+    icon = 'actionicon_view.png'
+
+
+class ContractViewActionButton2(ContractViewActionButton):
+    grok.view(ContractOfficialUsePage)
+
+
+class PDFContractSlipActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IContract)
+    grok.view(ContractDisplayFormPage)
+    grok.require('waeup.viewCustomer')
+    icon = 'actionicon_pdf.png'
+    text = _('Download contract slip')
+    target = 'contract_slip.pdf'
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/vocabularies.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/vocabularies.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/vocabularies.py	(revision 12677)
@@ -0,0 +1,239 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Vocabularies and sources for the customer section.
+"""
+import grok
+from zope.component import getUtility, queryUtility
+from zope.catalog.interfaces import ICatalog
+from zope.interface import implements, directlyProvides
+from zope.schema.interfaces import (
+    ISource, IContextSourceBinder, ValidationError)
+from zc.sourcefactory.basic import BasicSourceFactory
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
+from zc.sourcefactory.source import FactoredContextualSource
+from zc.sourcefactory.interfaces import IContextualSource
+from waeup.ikoba.sourcefactory import SmartBasicContextualSourceFactory
+from waeup.ikoba.interfaces import SimpleIkobaVocabulary, SUBMITTED, VERIFIED
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.utils.helpers import get_sorted_preferred
+from waeup.ikoba.utils.countries import COUNTRIES
+from waeup.ikoba.payments.currencies import ISO_4217_CURRENCIES
+
+#: a tuple of tuples (<COUNTRY-NAME>, <ISO-CODE>) with Nigeria first.
+COUNTRIES = get_sorted_preferred(COUNTRIES, ['NG'])
+nats_vocab = SimpleIkobaVocabulary(*COUNTRIES)
+
+
+class GenderSource(BasicSourceFactory):
+    """A gender source delivers basically a mapping
+       ``{'m': 'Male', 'f': 'Female'}``
+
+       Using a source, we make sure that the tokens (which are
+       stored/expected for instance from CSV files) are something one
+       can expect and not cryptic IntIDs.
+    """
+    def getValues(self):
+        return ['m', 'f']
+
+    def getToken(self, value):
+        return value[0].lower()
+
+    def getTitle(self, value):
+        if value == 'm':
+            return _('male')
+        if value == 'f':
+            return _('female')
+
+
+class RegNumNotInSource(ValidationError):
+    """Registration number in use.
+    """
+    # The docstring of ValidationErrors is used as error description
+    # by zope.formlib.
+    pass
+
+class EmailNotInSource(ValidationError):
+    """Email address in use.
+    """
+    pass
+
+
+class RegNumberSource(object):
+    """A source that accepts any entry for a certain field if not used
+    already.
+
+    Using this kind of source means a way of setting an invariant.
+
+    We accept a value iff:
+    - the value cannot be found in catalog or
+    - the value can be found as part of some item but the bound item
+      is the context object itself.
+    """
+    implements(ISource)
+    cat_name = 'customers_catalog'
+    field_name = 'reg_number'
+    validation_error = RegNumNotInSource
+    comp_field = 'customer_id'
+
+    def __init__(self, context):
+        self.context = context
+        return
+
+    def __contains__(self, value):
+        """We accept all values not already given to other customers.
+        """
+        cat = queryUtility(ICatalog, self.cat_name)
+        if cat is None:
+            return True
+        kw = {self.field_name: (value, value)}
+        results = cat.searchResults(**kw)
+        for entry in results:
+            if not hasattr(self.context, self.comp_field):
+                # we have no context with comp_field (most probably
+                # while adding a new object, where the container is
+                # the context) which means that the value was given
+                # already to another object (as _something_ was found in
+                # the catalog with that value). Fail on first round.
+                raise self.validation_error(value)
+            if getattr(entry, self.comp_field) != getattr(
+                self.context, self.comp_field):
+                # An entry already given to another customer is not in our
+                # range of acceptable values.
+                raise self.validation_error(value)
+                #return False
+        return True
+
+
+def contextual_reg_num_source(context):
+    source = RegNumberSource(context)
+    return source
+
+directlyProvides(contextual_reg_num_source, IContextSourceBinder)
+
+class EmailSource(RegNumberSource):
+    field_name = 'email'
+    validation_error = EmailNotInSource
+
+def contextual_email_source(context):
+    source = EmailSource(context)
+    return source
+directlyProvides(contextual_email_source, IContextSourceBinder)
+
+
+class ConCatProductSource(BasicContextualSourceFactory):
+    """A contract category product source delivers all products
+    which belong to a certain contract_category.
+    """
+
+    def getValues(self, context):
+        concat = getattr(context, 'contract_category', None)
+        products = grok.getSite()['products'].values()
+        if not concat:
+            return products
+        resultlist = [
+            value for value in products if value.contract_category == concat]
+        return resultlist
+
+    def getToken(self, context, value):
+        return value.product_id
+
+    def getTitle(self, context, value):
+        return "%s - %s" % (value.product_id, value.title)
+
+
+class ConCatActiveProductSource(ConCatProductSource):
+    """A contract category product source delivers all products
+    which belong to a certain contract_category.
+    """
+
+    def getValues(self, context):
+        concat = getattr(context, 'contract_category', None)
+        products = grok.getSite()['products'].values()
+        if not concat:
+            return products
+        
+        resultlist = [
+            value for value in products
+            if value.contract_category == concat and value.active
+            ]
+        return resultlist
+
+
+class CustomerDocumentSource(BasicContextualSourceFactory):
+    """A customer document source delivers all submitted and verified documents
+    of the context customer.
+    """
+
+    def getValues(self, context):
+        # When checking conversion during import, contracts do not belong to
+        # customers. Thus all portal documents must be returned.
+        user_id = getattr(getattr(context, 'customer', None), 'user_id', None)
+        catalog = getUtility(ICatalog, name='documents_catalog')
+        results = catalog.searchResults(user_id=(user_id, user_id))
+        resultlist = [
+            value for value in results if value.state in (SUBMITTED, VERIFIED)]
+        return resultlist
+
+    def getToken(self, context, value):
+        return value.document_id
+
+    def getTitle(self, context, value):
+        return "%s... - %s" % (value.document_id[:9], value.title)
+
+
+class IProductOptionSource(IContextualSource):
+    """A source operating in context.
+
+    This is a marker interface needed for the registration of the
+    IkobaSequenceWidget when using the List-Choice-ProductOptionSourceFactory
+    field combination primarily in IContract as a replacement for
+    the SourceOrderedMultiSelectWidget.
+    """
+
+
+class ProductOptionSource(FactoredContextualSource):
+
+      implements(IProductOptionSource)
+
+
+class ProductOptionSourceFactory(BasicContextualSourceFactory):
+    """A product option source delivers all options belonging to
+    a selected product.
+    """
+
+    source_class = ProductOptionSource
+
+    def getValues(self, context):
+        options = []
+        recent_options = getattr(
+            getattr(context, 'product_object', None), 'options', [])
+        # Check and add stored options first before adding recent
+        # product options.
+        if context.product_options:
+            for stored_option in context.product_options:
+                if stored_option not in recent_options:
+                    options.append(stored_option)
+        options += recent_options
+        return options
+
+    def getToken(self, context, value):
+        return value.title
+
+    def getTitle(self, context, value):
+        currency = ISO_4217_CURRENCIES[value.currency][1]
+        return "%s @ %s %s" % (value.title, value.fee, currency)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/workflow.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/workflow.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/workflow.py	(revision 12677)
@@ -0,0 +1,441 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Workflow for customers.
+"""
+import grok
+from datetime import datetime
+from zope.component import getUtility
+from zope.i18n import translate
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import (
+    IWorkflowState, IWorkflowTransitionEvent, InvalidTransitionError)
+from waeup.ikoba.interfaces import (
+    IObjectHistory, IIkobaWorkflowInfo, IIkobaUtils, IUserAccount,
+    STARTED, CREATED, REQUESTED, PROVISIONALLY, APPROVED,
+    SUBMITTED, VERIFIED, REJECTED, EXPIRED, AWAITING)
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.workflow import (
+    IkobaWorkflow, IkobaWorkflowInfo)
+from waeup.ikoba.customers.interfaces import (
+    ICustomer, ICustomersUtils,
+    IContract, ICustomerDocument)
+from waeup.ikoba.utils.helpers import get_current_principal
+
+# Customer workflow
+
+IMPORTABLE_REGISTRATION_STATES = (STARTED, REQUESTED, APPROVED)
+
+REGISTRATION_TRANSITIONS = (
+    Transition(
+        transition_id = 'create',
+        title = _('Create customer'),
+        source = None,
+        condition = NullCondition,
+        msg = _('Customer created'),
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'start',
+        title = _('Start registration'),
+        source = CREATED,
+        condition = NullCondition,
+        msg = _('Customer registration started'),
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'request',
+        title = _('Request registration'),
+        msg = _('Customer registration requested'),
+        source = STARTED,
+        destination = REQUESTED),
+
+    Transition(
+        transition_id = 'approve_provisionally',
+        title = _('Approve customer provisionally'),
+        msg = _('Customer registration provisionally approved'),
+        source = REQUESTED,
+        destination = PROVISIONALLY),
+
+    Transition(
+        transition_id = 'approve_finally',
+        title = _('Approve finally'),
+        msg = _('Customer registration finally approved'),
+        source = PROVISIONALLY,
+        destination = APPROVED),
+
+    Transition(
+        transition_id = 'approve',
+        title = _('Approve customer'),
+        msg = _('Customer registration approved'),
+        source = REQUESTED,
+        destination = APPROVED),
+
+    Transition(
+        transition_id = 'reject',
+        title = _('Reject customer'),
+        msg = _('Customer registration rejected'),
+        source = REQUESTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset1',
+        title = _('Reset customer'),
+        msg = _('Reset to initial customer state'),
+        source = APPROVED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset2',
+        title = _('Reset to requested'),
+        msg = _("Reset to 'requested'"),
+        source = APPROVED,
+        destination = REQUESTED),
+
+    Transition(
+        transition_id = 'reset3',
+        title = _('Reset customer'),
+        msg = _("Reset to initial state"),
+        source = REQUESTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset4',
+        title = _('Reset customer'),
+        msg = _('Reset to initial customer state'),
+        source = PROVISIONALLY,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset5',
+        title = _('Reset to requested'),
+        msg = _("Reset to 'requested'"),
+        source = PROVISIONALLY,
+        destination = REQUESTED),
+
+    )
+
+
+IMPORTABLE_REGISTRATION_TRANSITIONS = [i.transition_id for i in REGISTRATION_TRANSITIONS]
+
+registration_workflow = IkobaWorkflow(REGISTRATION_TRANSITIONS)
+
+
+class RegistrationWorkflowState(WorkflowState, grok.Adapter):
+    """An adapter to adapt Customer objects to workflow states.
+    """
+    grok.context(ICustomer)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.registration.state'
+    state_id = 'wf.registration.id'
+
+
+class RegistrationWorkflowInfo(IkobaWorkflowInfo, grok.Adapter):
+    """Adapter to adapt Customer objects to workflow info objects.
+    """
+    grok.context(ICustomer)
+    grok.provides(IIkobaWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = registration_workflow
+
+
+@grok.subscribe(ICustomer, IWorkflowTransitionEvent)
+def handle_customer_transition_event(obj, event):
+    """Append message to customer history and log file when transition happened.
+
+    Notify customer by email.
+    """
+
+    msg = event.transition.user_data['msg']
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    if event.transition.transition_id not in ('create', 'start', 'request'):
+        ikoba_utils = getUtility(IIkobaUtils)
+        ikoba_utils.sendTransitionInfo(IUserAccount(obj), obj, msg)
+    try:
+        customers_container = grok.getSite()['customers']
+        customers_container.logger.info('%s - %s' % (obj.customer_id,msg))
+    except (TypeError, AttributeError):
+        pass
+    return
+
+
+# Contract workflow
+
+
+CONTRACT_TRANSITIONS = (
+    Transition(
+        transition_id = 'create',
+        title = _('Create contract'),
+        source = None,
+        condition = NullCondition,
+        msg = _('Contract created'),
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'await',
+        title = _('Initiate payment'),
+        source = CREATED,
+        condition = NullCondition,
+        msg = _('Payment initiated'),
+        destination = AWAITING),
+
+    Transition(
+        transition_id = 'discard',
+        title = _('Discard payment'),
+        source = AWAITING,
+        condition = NullCondition,
+        msg = _('Payment discarded'),
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'submit',
+        title = _('Submit for approval'),
+        msg = _('Submitted for approval'),
+        source = CREATED,
+        destination = SUBMITTED),
+
+    Transition(
+        transition_id = 'confirm',
+        title = _('Confirm payment'),
+        msg = _('Payment and submission confirmed'),
+        source = AWAITING,
+        destination = SUBMITTED),
+
+    Transition(
+        transition_id = 'approve',
+        title = _('Approve'),
+        msg = _('Approved'),
+        source = SUBMITTED,
+        destination = APPROVED),
+
+    Transition(
+        transition_id = 'reject',
+        title = _('Reject'),
+        msg = _('REJECTED'),
+        source = SUBMITTED,
+        destination = REJECTED),
+
+    Transition(
+        transition_id = 'reset1',
+        title = _('Reset to initial state'),
+        msg = _('Reset to initial state'),
+        source = REJECTED,
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'reset2',
+        title = _('Reset to initial state'),
+        msg = _('Reset to initial state'),
+        source = APPROVED,
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'reset3',
+        title = _('Reset to initial state'),
+        msg = _('Reset to initial state'),
+        source = SUBMITTED,
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'expire',
+        title = _('Set to expired'),
+        msg = _('Set to expired'),
+        source = APPROVED,
+        destination = EXPIRED),
+
+    Transition(
+        transition_id = 'reset4',
+        title = _('Reset to initial state'),
+        msg = _('Reset to initial state'),
+        source = EXPIRED,
+        destination = CREATED),
+
+    )
+
+PAYMENT_TRANSITIONS = ['await', 'discard', 'confirm']
+
+contract_workflow = IkobaWorkflow(CONTRACT_TRANSITIONS)
+
+
+class ContractWorkflowState(WorkflowState, grok.Adapter):
+    """An adapter to adapt Contract objects to workflow states.
+    """
+    grok.context(IContract)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.contract.state'
+    state_id = 'wf.contract.id'
+
+
+class ContractWorkflowInfo(IkobaWorkflowInfo, grok.Adapter):
+    """Adapter to adapt Contract objects to workflow info objects.
+    """
+    grok.context(IContract)
+    grok.provides(IIkobaWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = contract_workflow
+
+@grok.subscribe(IContract, IWorkflowTransitionEvent)
+def handle_contract_transition_event(obj, event):
+    """Append message to contract history and log file.
+
+    Undo the approval of contract and raise an exception if contract
+    does not meet the requirements for approval.
+    """
+    if event.transition.destination == APPROVED:
+        approvable, error = obj.is_approvable
+        if not approvable:
+            # Undo transition and raise an exception.
+            IWorkflowState(obj).setState(event.transition.source)
+            raise InvalidTransitionError(error)
+    msg = event.transition.user_data['msg']
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    if event.transition.transition_id not in (
+        'create', 'submit', 'confirm', 'await') and obj.customer:
+        ikoba_utils = getUtility(IIkobaUtils)
+        ikoba_utils.sendTransitionInfo(IUserAccount(obj.customer), obj, msg)
+    try:
+        customers_container = grok.getSite()['customers']
+        customers_container.logger.info(
+            '%s - %s - %s' % (obj.customer.customer_id, obj.contract_id, msg))
+    except (TypeError, AttributeError):
+        pass
+    return
+
+# Customer document workflow
+
+VERIFICATION_TRANSITIONS = (
+    Transition(
+        transition_id = 'create',
+        title = _('Create document'),
+        source = None,
+        condition = NullCondition,
+        msg = _('Document created'),
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'submit',
+        title = _('Submit for verification'),
+        msg = _('Submitted for verification'),
+        source = CREATED,
+        destination = SUBMITTED),
+
+    Transition(
+        transition_id = 'verify',
+        title = _('Verify'),
+        msg = _('Verified'),
+        source = SUBMITTED,
+        destination = VERIFIED),
+
+    Transition(
+        transition_id = 'reject',
+        title = _('Reject'),
+        msg = _('REJECTED'),
+        source = SUBMITTED,
+        destination = REJECTED),
+
+    Transition(
+        transition_id = 'reset1',
+        title = _('Reset to initial state'),
+        msg = _('Reset to initial state'),
+        source = REJECTED,
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'reset2',
+        title = _('Reset to initial state'),
+        msg = _('Reset to initial state'),
+        source = VERIFIED,
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'reset3',
+        title = _('Reset to initial state'),
+        msg = _('Reset to initial state'),
+        source = SUBMITTED,
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'expire',
+        title = _('Set to expired'),
+        msg = _('Set to expired'),
+        source = VERIFIED,
+        destination = EXPIRED),
+
+    Transition(
+        transition_id = 'reset4',
+        title = _('Reset to initial state'),
+        msg = _('Reset to initial state'),
+        source = EXPIRED,
+        destination = CREATED),
+    )
+
+verification_workflow = IkobaWorkflow(VERIFICATION_TRANSITIONS)
+
+
+class VerificationWorkflowState(WorkflowState, grok.Adapter):
+    """An adapter to adapt CustomerDocument objects to workflow states.
+    """
+    grok.context(ICustomerDocument)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.verification.state'
+    state_id = 'wf.verification.id'
+
+
+class VerificationWorkflowInfo(IkobaWorkflowInfo, grok.Adapter):
+    """Adapter to adapt CustomerDocument objects to workflow info objects.
+    """
+    grok.context(ICustomerDocument)
+    grok.provides(IIkobaWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = verification_workflow
+
+@grok.subscribe(ICustomerDocument, IWorkflowTransitionEvent)
+def handle_customer_document_transition_event(obj, event):
+    """Append message to document history and log file.
+
+    Undo the verification of document and raise an exception if document
+    does not meet the requirements for verification.
+    """
+    if event.transition.destination == VERIFIED:
+        verifiable, error = obj.is_verifiable
+        if not verifiable:
+            # Undo transition and raise an exception.
+            IWorkflowState(obj).setState(event.transition.source)
+            raise InvalidTransitionError(error)
+    if event.transition.transition_id == 'verify':
+        obj.setMD5()
+    msg = event.transition.user_data['msg']
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    try:
+        customers_container = grok.getSite()['customers']
+        customers_container.logger.info(
+            '%s - %s - %s' % (obj.customer.customer_id, obj.document_id, msg))
+    except (TypeError, AttributeError):
+        pass
+    return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/datacenter.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/datacenter.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/datacenter.py	(revision 12677)
@@ -0,0 +1,427 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Ikoba data center.
+
+The waeup data center cares for management of upload data and provides
+tools for importing/exporting CSV data.
+"""
+import codecs
+import fnmatch
+import grok
+import os
+import re
+import shutil
+from datetime import datetime
+from zope.component import getUtility
+from zope.component.interfaces import ObjectEvent
+from waeup.ikoba.interfaces import (IDataCenter, IDataCenterFile,
+                                   IDataCenterStorageMovedEvent,
+                                   IDataCenterConfig)
+from waeup.ikoba.utils.batching import ExportJobContainer
+from waeup.ikoba.utils.helpers import copy_filesystem_tree, merge_csv_files
+from waeup.ikoba.utils.logger import Logger
+
+#: Regular expression describing a logfile name with backup extension
+RE_LOGFILE_BACKUP_NAME = re.compile('^.+\.\d+$')
+
+class DataCenter(grok.Container, Logger, ExportJobContainer):
+    """A data center contains CSV files.
+    """
+    grok.implements(IDataCenter)
+
+    logger_name = 'waeup.ikoba.${sitename}.datacenter'
+    logger_filename = 'datacenter.log'
+
+    max_files = 100
+
+    def __init__(self, *args, **kw):
+        super(DataCenter, self).__init__(*args, **kw)
+        self.storage = getUtility(IDataCenterConfig)['path']
+        self._createSubDirs()
+
+    def _createSubDirs(self):
+        """Create standard subdirs.
+        """
+        for name in ['finished', 'unfinished', 'logs', 'deleted']:
+            path = os.path.join(self.storage, name)
+            if os.path.exists(path):
+                continue
+            os.mkdir(path)
+        return
+
+    @property
+    def deleted_path(self):
+        """Get the path for deleted object data.
+        """
+        return os.path.join(self.storage, 'deleted')
+
+    def getPendingFiles(self, sort='name'):
+        """Get a list of files stored in `storage`.
+
+        Files are sorted by basename.
+        """
+        result = []
+        if not os.path.exists(self.storage):
+            return result
+        for filename in sorted(os.listdir(self.storage)):
+            fullpath = os.path.join(self.storage, filename)
+            if not os.path.isfile(fullpath):
+                continue
+            if not filename.endswith('.csv'):
+                continue
+            result.append(DataCenterFile(fullpath))
+        if sort == 'date':
+            # sort results in newest-first order...
+            result = sorted(result, key=lambda x: x.getTimeStamp(),
+                            reverse=True)
+        return result
+
+    def getFinishedFiles(self):
+        """Get a list of files stored in `finished` subfolder of `storage`.
+
+        Files are unsorted.
+        """
+        result = []
+        finished_dir = os.path.join(self.storage, 'finished')
+        if not os.path.exists(finished_dir):
+            return result
+        mtime = lambda f: os.stat(os.path.join(finished_dir, f)).st_mtime
+        finished_files = [f for f in
+            sorted(os.listdir(finished_dir), key=mtime, reverse=True)
+            if fnmatch.fnmatch(f, '*.finished.csv')]
+        for filename in finished_files[:self.max_files]:
+            fullpath = os.path.join(finished_dir, filename)
+            if not os.path.isfile(fullpath):
+                continue
+            if not filename.endswith('.csv'):
+                continue
+            result.append(DataCenterFile(fullpath, 'finished'))
+        return result
+
+    def getLogFiles(self, exclude_backups=True):
+        """Get the files from logs/ subdir. Files are sorted by name.
+
+        By default backup logs ('app.log.1', etc.) and payments.log
+        are excluded.
+        """
+        result = []
+        logdir = os.path.join(self.storage, 'logs')
+        if not os.path.exists(logdir):
+            os.mkdir(logdir)
+        for name in sorted(os.listdir(logdir)):
+            if not os.path.isfile(os.path.join(logdir, name)):
+                continue
+            if name == 'payments.log':
+                continue
+            if exclude_backups == True and RE_LOGFILE_BACKUP_NAME.match(name):
+                continue
+            result.append(
+                LogFile(os.path.join(self.storage, 'logs', name)))
+        return result
+
+    def setStoragePath(self, path, move=False, overwrite=False):
+        """Set the path where to store files.
+        """
+        path = os.path.abspath(path)
+        not_copied = []
+        if not os.path.exists(path):
+            raise ValueError('The path given does not exist: %s' % path)
+        if move is True:
+            not_copied = copy_filesystem_tree(self.storage, path,
+                                            overwrite=overwrite)
+        self.storage = path
+        self._createSubDirs()
+        grok.notify(DataCenterStorageMovedEvent(self))
+        return not_copied
+
+    def _moveFile(self, source, dest):
+        """Move file source to dest preserving ctime, mtime, etc.
+        """
+        if not os.path.exists(source):
+            self.logger.warn('No such source path: %s' % source)
+            return
+        if source == dest:
+            return
+        shutil.copyfile(source, dest)
+        shutil.copystat(source, dest)
+        os.unlink(source)
+
+    def _appendCSVFile(self, source, dest):
+        """Append data from CSV file `source` to data from CSV file `dest`.
+
+        The `source` file is deleted afterwards.
+        """
+        if not os.path.exists(dest):
+            return self._moveFile(source, dest)
+        if not os.path.exists(source):
+            self.logger.warn('No such source path: %s' % source)
+            return
+        if source == dest:
+            return
+        result_path = merge_csv_files(dest, source)
+        os.chmod(result_path, 0664)
+        self._moveFile(result_path, dest)
+        os.unlink(source)
+
+    def distProcessedFiles(self, successful, source_path, finished_file,
+                           pending_file, mode='create', move_orig=True):
+        """Put processed files into final locations.
+
+        ``successful`` is a boolean that tells, whether processing was
+        successful.
+
+        ``source_path``: path to file that was processed.
+
+        ``finished_file``, ``pending_file``: paths to the respective
+        generated .pending and .finished file. The .pending file path
+        may be ``None``.
+
+        If finished file is placed in a location outside the local
+        storage dir, the complete directory is removed
+        afterwards. Regular processors should put their stuff in
+        dedicated temporary dirs.
+
+        See datacenter.txt for more info about how this works.
+        """
+        basename = os.path.basename(source_path)
+        pending_name = basename
+        pending = False
+        finished_dir = os.path.join(self.storage, 'finished')
+        unfinished_dir = os.path.join(self.storage, 'unfinished')
+
+        if basename.endswith('.pending.csv'):
+            maybe_basename = "%s.csv" % basename.rsplit('.', 3)[0]
+            maybe_src = os.path.join(unfinished_dir, maybe_basename)
+            if os.path.isfile(maybe_src):
+                basename = maybe_basename
+                pending = True
+
+        base, ext = os.path.splitext(basename)
+        finished_name = "%s.%s.finished%s" % (base, mode, ext)
+        if not pending:
+            pending_name = "%s.%s.pending%s" % (base, mode, ext)
+
+        # Put .pending and .finished file into respective places...
+        pending_dest = os.path.join(self.storage, pending_name)
+        finished_dest = os.path.join(finished_dir, finished_name)
+        self._appendCSVFile(finished_file, finished_dest)
+        if pending_file is not None:
+            self._moveFile(pending_file, pending_dest)
+
+        # Put source file into final location...
+        finished_dest = os.path.join(finished_dir, basename)
+        unfinished_dest = os.path.join(unfinished_dir, basename)
+        if successful and not pending:
+            self._moveFile(source_path, finished_dest)
+        elif successful and pending:
+            self._moveFile(unfinished_dest, finished_dest)
+            os.unlink(source_path)
+        elif not successful and not pending:
+            self._moveFile(source_path, unfinished_dest)
+
+        # If finished and pending-file were created in a location
+        # outside datacenter storage, we remove it.
+        maybe_temp_dir = os.path.dirname(finished_file)
+        if os.path.commonprefix(
+            [self.storage, maybe_temp_dir]) != self.storage:
+            shutil.rmtree(maybe_temp_dir)
+        return
+
+    def _logfiles(self, basename):
+        """Get sorted logfiles starting with `basename`.
+        """
+        def numerical_suffix(name):
+            # return numerical suffix in `name` as number or 0.
+            suffix = name.rsplit('.', 1)[-1]
+            try:
+                return int(suffix)
+            except ValueError:
+                return 0
+            pass
+        files = [basename,]
+        for name in os.listdir(os.path.join(self.storage, 'logs')):
+            if RE_LOGFILE_BACKUP_NAME.match(name):
+                if name.rsplit('.', 1)[0] == basename:
+                    files.append(name)
+        return sorted(files, key=numerical_suffix, reverse=True)
+
+    def queryLogfiles(self, basename, query=None, limit=0, start=0):
+        """Search `query` in all logfiles starting with `basename`.
+
+        Returns an iterator of those lines in logfiles starting with
+        `basename` that match `query`. If you want the result as a
+        list, simply list() the iterator.
+
+        All logfiles with name `basename` and maybe some numerical
+        extension ('.1', '.2', ...) are searched for the `query` term
+        in correct chronological order. So, if you ask for a basename 'app.log',
+        then any file named 'app2.log', 'app.log.1', 'app.log',
+        etc. will be searched in that order.
+
+        The `query` is expected to be a string containing a regular
+        expression.
+
+        If `limit` is set to some numerical value, at most this number
+        of lines is returned.
+
+        With `start` you can give the number of first matched line to
+        return. `start` is zero-based, i.e. the first match has number
+        0, the scond one 1, etc.
+
+        Together with `limit` this allows some basic
+        batching. Please keep in mind that batching might give
+        unpredictable results, when logfiles change between two
+        requests. This is not a problem when only one file is searched
+        and changes include only appending new log messages.
+
+        Matches are found per line only (no multiline matches).
+
+        Result lines are returned as unicode instances decoded from
+        UTF-8 encoding. This means that logfiles must provide UTF-8
+        encoding for umlauts etc. if these should be rendered
+        properly. The returned unicode lines can be fed to page
+        templates even if they contain non-ASCII characters.
+
+        This method raises ValueError if some basic condition is not
+        met, for instance if the given query string is not a valid
+        regular expression.
+
+        Please note, that this exception will happen not before you
+        really fetch a result line.
+        """
+        try:
+            re_query = re.compile(query)
+        except:
+            raise ValueError('Invalid query string: %s' % query)
+
+        basename = basename.replace('/../', '')
+        files = self._logfiles(basename)
+
+        # Search the log files
+        num = 0
+        for name in files:
+            path = os.path.join(self.storage, 'logs', name)
+            if not os.path.isfile(path):
+                continue
+            for line in codecs.open(path, 'rb', 'utf-8'):
+                if not re_query.search(line):
+                    continue
+                num += 1
+                if (num - 1) < start:
+                    continue
+                yield line
+
+                if limit and (num - limit >= start):
+                    raise StopIteration
+        pass
+
+class DataCenterFile(object):
+    """A description of a file stored in data center.
+    """
+    grok.implements(IDataCenterFile)
+
+    def __init__(self, context, folder_name=''):
+        self.context = context
+        self.name = os.path.basename(self.context)
+        self.rel_path = os.path.join(folder_name, self.name)
+        self.size = self.getSize()
+        self.uploaddate = self.getDate()
+        self.lines = self.getLinesNumber()
+
+    def getDate(self):
+        """Get a human readable datetime representation.
+        """
+        date = datetime.fromtimestamp(os.path.getmtime(self.context))
+        return date.strftime("%Y-%m-%d %H:%M:%S")
+
+    def getTimeStamp(self):
+        """Get a (machine readable) timestamp.
+        """
+        return os.path.getmtime(self.context)
+
+    def getSize(self):
+        """Get a human readable file size.
+        """
+        bytesize = os.path.getsize(self.context)
+        size = "%s bytes" % bytesize
+        units = ['kb', 'MB', 'GB']
+        for power, unit in reversed(list(enumerate(units))):
+            power += 1
+            if bytesize >= 1024 ** power:
+                size = "%.2f %s" % (bytesize/(1024.0**power), unit)
+                break
+        return size
+
+    def getLinesNumber(self):
+        """Get number of lines.
+        """
+        num = 0
+        for line in open(self.context, 'rb'):
+            num += 1
+        return num
+
+class LogFile(DataCenterFile):
+    """A description of a log file.
+    """
+    def __init__(self, context):
+        super(LogFile, self).__init__(context)
+        self._markers = dict()
+        self._parsed = False
+        self.userid = self.getUserId()
+        self.mode = self.getMode()
+        self.stats = self.getStats()
+        self.source = self.getSourcePath()
+
+    def _parseFile(self, maxline=10):
+        """Find markers in a file.
+        """
+        if self._parsed:
+            return
+        for line in codecs.open(self.context, 'rb', 'utf-8'):
+            line = line.strip()
+            if not ':' in line:
+                continue
+            name, text = line.split(':', 1)
+            self._markers[name.lower()] = text
+        self._parsed = True
+        return
+
+    def _getMarker(self, marker):
+        marker = marker.lower()
+        if not self._parsed:
+            self._parseFile()
+        if marker in self._markers.keys():
+            return self._markers[marker]
+
+    def getUserId(self):
+        return self._getMarker('user') or '<UNKNOWN>'
+
+    def getMode(self):
+        return self._getMarker('mode') or '<NOT SET>'
+
+    def getStats(self):
+        return self._getMarker('processed') or '<Info not avail.>'
+
+    def getSourcePath(self):
+        return self._getMarker('source') or None
+
+
+class DataCenterStorageMovedEvent(ObjectEvent):
+    """An event fired, when datacenter storage moves.
+    """
+    grok.implements(IDataCenterStorageMovedEvent)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/datacenter.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/datacenter.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/datacenter.txt	(revision 12677)
@@ -0,0 +1,290 @@
+Ikoba Data Center
+****************
+
+The Ikoba data center cares for managing CSV files and importing then.
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+Creating a data center
+======================
+
+A data center can be created easily:
+
+    >>> from waeup.ikoba.datacenter import DataCenter
+    >>> mydatacenter = DataCenter()
+    >>> mydatacenter
+    <waeup.ikoba.datacenter.DataCenter object at 0x...>
+
+Each data center has a location in file system where files are stored:
+
+    >>> storagepath = mydatacenter.storage
+    >>> storagepath
+    '/tmp/tmp...'
+
+Beside other things it provides a location to put data of deleted
+items into:
+
+    >>> import os
+    >>> del_path = mydatacenter.deleted_path
+    >>> os.path.isdir(del_path)
+    True
+
+Overall it complies with the `IDataCenter` interface:
+
+    >>> from zope.interface import verify
+    >>> from waeup.ikoba.interfaces import IDataCenter
+    >>> verify.verifyObject(IDataCenter, DataCenter() )
+    True
+
+    >>> verify.verifyClass(IDataCenter, DataCenter)
+    True
+
+Managing the storage path
+-------------------------
+
+We can set another storage path:
+
+    >>> import os
+    >>> os.mkdir('newlocation')
+    >>> newpath = os.path.abspath('newlocation')
+    >>> mydatacenter.setStoragePath(newpath)
+    []
+
+The result here is a list of filenames, that could not be
+copied. Luckily, this list is empty.
+
+When we set a new storage path, we can tell to move all files in the
+old location to the new one. To see this feature in action, we first
+have to put a file into the old location:
+
+    >>> open(os.path.join(newpath, 'myfile.txt'), 'wb').write('hello')
+
+Now we can set a new location and the file will be copied:
+
+    >>> verynewpath = os.path.abspath('verynewlocation')
+    >>> os.mkdir(verynewpath)
+
+    >>> mydatacenter.setStoragePath(verynewpath, move=True)
+    []
+
+    >>> storagepath = mydatacenter.storage
+    >>> 'myfile.txt' in os.listdir(verynewpath)
+    True
+
+We remove the created file to have a clean testing environment for
+upcoming examples:
+
+    >>> os.unlink(os.path.join(storagepath, 'myfile.txt'))
+
+Uploading files
+===============
+
+We can get a list of files stored in that location:
+
+    >>> mydatacenter.getPendingFiles()
+    []
+
+Let's put some file in the storage:
+
+    >>> import os
+    >>> filepath = os.path.join(storagepath, 'data.csv')
+    >>> open(filepath, 'wb').write('Some Content\n')
+
+Now we can find a file:
+
+    >>> mydatacenter.getPendingFiles()
+    [<waeup.ikoba.datacenter.DataCenterFile object at 0x...>]
+
+As we can see, the actual file is wrapped by a convenience wrapper,
+that enables us to fetch some data about the file. The data returned
+is formatted in strings, so that it can easily be put into output
+pages:
+
+    >>> datafile = mydatacenter.getPendingFiles()[0]
+    >>> datafile.getSize()
+    '13 bytes'
+
+    >>> datafile.getDate() # Nearly current datetime...
+    '...'
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(newpath)
+    >>> shutil.rmtree(verynewpath)
+
+
+Distributing processed files
+============================
+
+When files were processed by a batch processor, we can put the
+resulting files into desired destinations.
+
+We recreate the datacenter root in case it is missing:
+
+    >>> import os
+    >>> dc_root = mydatacenter.storage
+    >>> fin_dir = os.path.join(dc_root, 'finished')
+    >>> unfin_dir = os.path.join(dc_root, 'unfinished')
+
+    >>> def recreate_dc_storage():
+    ...   if os.path.exists(dc_root):
+    ...     shutil.rmtree(dc_root)
+    ...   os.mkdir(dc_root)
+    ...   mydatacenter.setStoragePath(mydatacenter.storage)
+    >>> recreate_dc_storage()
+
+We define a function that creates a set of faked result files:
+
+    >>> import os
+    >>> import tempfile
+    >>> def create_fake_results(source_basename, create_pending=True):
+    ...   tmp_dir = tempfile.mkdtemp()
+    ...   src = os.path.join(dc_root, source_basename)
+    ...   pending_src = None
+    ...   if create_pending:
+    ...     pending_src = os.path.join(tmp_dir, 'mypendingsource.csv')
+    ...   finished_src = os.path.join(tmp_dir, 'myfinishedsource.csv')
+    ...   for path in (src, pending_src, finished_src):
+    ...     if path is not None:
+    ...       open(path, 'wb').write('blah')
+    ...   return tmp_dir, src, finished_src, pending_src
+
+Now we can create the set of result files, that typically come after a
+successful processing of a regular source:
+
+Now we can try to distribute those files. Let's start with a source
+file, that was processed successfully:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.csv', create_pending=False)
+    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
+    ...                            pending_src, mode='create')
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv', 'mysource.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    []
+
+The created dir will be removed for us by the datacenter. This way we
+can assured, that less temporary dirs are left hanging around:
+
+    >>> os.path.exists(tmp_dir)
+    False
+
+The root dir is empty, while the original file and the file containing
+all processed data were moved to'finished/'.
+
+Now we restart, but this time we fake an erranous action:
+
+    >>> recreate_dc_storage()
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'mysource.create.pending.csv', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    ['mysource.csv']
+
+While the original source was moved to the 'unfinished' dir, the
+pending file went to the root and the set of already processed items
+are stored in finished/.
+
+We fake processing the pending file and assume that everything went
+well this time:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv', create_pending=False)
+    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv', 'mysource.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    []
+
+The result is the same as in the first case shown above.
+
+We restart again, but this time we fake several non-working imports in
+a row.
+
+We start with a faulty start-import:
+
+    >>> recreate_dc_storage()
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+We try to process the pending file, which fails again:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+We try to process the new pending file:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'mysource.create.pending.csv', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    ['mysource.csv']
+
+Finally, we process the pending file and everything works:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv', create_pending=False)
+    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv', 'mysource.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    []
+
+The root dir is empty (contains no input files) and only the files in
+finished-subdirectory remain.
+
+
+We can get a list of imported files stored in the finished subfolder:
+
+    >>> mydatacenter.getFinishedFiles()
+    [<waeup.ikoba.datacenter.DataCenterFile object at ...>]
+
+    >>> datafile = mydatacenter.getFinishedFiles()[0]
+    >>> datafile.getSize()
+    '2 bytes'
+
+    >>> datafile.getDate() # Nearly current datetime...
+    '...'
+
+
+Clean up:
+
+    >>> shutil.rmtree(verynewpath)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/__init__.py	(revision 12677)
@@ -0,0 +1,10 @@
+"""This package contains everything regarding documents.
+"""
+# Make this a package.
+from waeup.ikoba.documents.container import DocumentsContainer
+from waeup.ikoba.documents.document import Document
+
+__all__ = [
+    'DocumentsContainer',
+    'Document',
+    ]
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/batching.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/batching.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/batching.py	(revision 12677)
@@ -0,0 +1,162 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Batch processing components for document objects.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+"""
+import grok
+import unicodecsv as csv  # XXX: csv ops should move to dedicated module.
+from time import time
+from datetime import datetime
+from zope.i18n import translate
+from zope.interface import Interface
+from zope.schema import getFields
+from zope.component import queryUtility, getUtility, createObject
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from waeup.ikoba.interfaces import (
+    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
+    IObjectHistory, IGNORE_MARKER)
+from waeup.ikoba.interfaces import IIkobaUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.documents.interfaces import (
+    IPDFDocument, IHTMLDocument, IRESTDocument)
+from waeup.ikoba.utils.batching import BatchProcessor
+
+
+class DocumentProcessorBase(BatchProcessor):
+    """A base for batch processors for IDocument objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    grok.baseclass()
+
+    util_name = None
+    name = None
+    iface = None
+
+    location_fields = ['document_id',]
+    additional_fields = ['class_name',]
+
+    factory_name = None
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+                    self.additional_fields +
+                    getFields(self.iface).keys())))
+
+    def parentsExist(self, row, site):
+        return 'documents' in site.keys()
+
+    def entryExists(self, row, site):
+        document_id = row.get('document_id', None)
+        cat = queryUtility(ICatalog, name='documents_catalog')
+        results = list(cat.searchResults(document_id=(document_id, document_id)))
+        if results:
+            return True
+        return False
+
+    def getParent(self, row, site):
+        return site['documents']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['document_id'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addDocument(obj)
+        return
+
+    def delEntry(self, row, site):
+        document = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if document is not None:
+            grok.getSite().logger.info(
+                '%s - Document removed' % document.document_id)
+            del parent[document.document_id]
+        return
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = super(DocumentProcessorBase, self).updateEntry(
+            obj, row, site, filename)
+        # Log actions...
+        location_field = self.location_fields[0]
+        grok.getSite().logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, row[location_field], items_changed))
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            DocumentProcessorBase, self).checkConversion(row, mode=mode)
+        # We need to check if the class_name corresponds with the
+        # processor chosen. This is to avoid accidentally wrong imports.
+        if mode == 'create':
+            class_name = row.get('class_name', None)
+            if class_name != self.factory_name.strip('waeup.'):
+                errs.append(('class_name','wrong processor'))
+        return errs, inv_errs, conv_dict
+
+
+class PDFDocumentProcessor(DocumentProcessorBase):
+    """A batch processor for IPDFDocument objects.
+    """
+    util_name = 'pdfdocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Public PDF Document Processor')
+    iface = IPDFDocument
+
+    factory_name = 'waeup.PDFDocument'
+
+
+class HTMLDocumentProcessor(DocumentProcessorBase):
+    """A batch processor for IHTMLDocument objects.
+    """
+    util_name = 'htmldocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Public HTML Document Processor')
+    iface = IHTMLDocument
+
+    factory_name = 'waeup.HTMLDocument'
+
+
+class RESTDocumentProcessor(DocumentProcessorBase):
+    """A batch processor for IRESTDocument objects.
+    """
+    util_name = 'restdocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Public REST Document Processor')
+    iface = IRESTDocument
+
+    factory_name = 'waeup.RESTDocument'
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser.py	(revision 12677)
@@ -0,0 +1,354 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""UI components for documents offered for download by the company.
+"""
+
+import sys
+import grok
+import pytz
+from urllib import urlencode
+from datetime import datetime
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from zope.event import notify
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
+from zope.formlib.textwidgets import BytesDisplayWidget
+from zope.security import checkPermission
+from waeup.ikoba.utils.helpers import html2dict, rest2dict
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import (
+    IContactForm, IObjectHistory, IIkobaObject, IIkobaUtils, PUBLISHED)
+from waeup.ikoba.browser.layout import (
+    IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage,
+    IkobaForm, NullValidator, jsaction, action, UtilityView)
+from waeup.ikoba.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget,
+    FriendlyDatetimeDisplayWidget)
+from waeup.ikoba.browser.breadcrumbs import Breadcrumb
+from waeup.ikoba.browser.pages import (
+    delSubobjects, add_local_role, del_local_roles, msave,
+    LocalRoleAssignmentUtilityView)
+
+from waeup.ikoba.documents.interfaces import (
+    IDocumentsContainer, IPublicDocument,
+    IHTMLDocument, IRESTDocument, IDocumentsUtils)
+
+grok.context(IIkobaObject) # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+
+class DocumentsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customers container.
+    """
+    grok.context(IDocumentsContainer)
+    title = _('Documents')
+
+
+class DocumentBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customer container.
+    """
+    grok.context(IPublicDocument)
+
+    def title(self):
+        return self.context.title
+
+
+class DocumentsContainerPage(IkobaDisplayFormPage):
+    """The standard view for document containers.
+    """
+    grok.context(IDocumentsContainer)
+    grok.name('index')
+    grok.require('waeup.viewDocuments')
+    grok.template('containerpage')
+    pnav = 2
+    label = _('Documents')
+
+
+class DocumentsContainerManageFormPage(IkobaEditFormPage,
+                                      LocalRoleAssignmentUtilityView):
+    """The manage page for customer containers.
+    """
+    grok.context(IDocumentsContainer)
+    grok.name('manage')
+    grok.require('waeup.manageDocuments')
+    grok.template('containermanagepage')
+    pnav = 2
+    label = _('Manage document section')
+
+    @action(_('Add document'), validator=NullValidator, style='primary')
+    def addSubunit(self, **data):
+        self.redirect(self.url(self.context, 'adddoc'))
+        return
+
+    @jsaction(_('Remove selected documents'))
+    def delDocuments(self, **data):
+        delSubobjects(self, redirect='manage', tab='2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+class DocumentAddFormPage(IkobaAddFormPage):
+    """Add-form to add a customer.
+    """
+    grok.context(IDocumentsContainer)
+    grok.require('waeup.manageDocuments')
+    grok.name('adddoc')
+    grok.template('documentaddform')
+    label = _('Add document')
+    pnav = 2
+
+    form_fields = grok.AutoFields(IPublicDocument)
+
+    @property
+    def selectable_doctypes(self):
+        doctypes = getUtility(IDocumentsUtils).SELECTABLE_DOCTYPES_DICT
+        return sorted(doctypes.items())
+
+    @action(_('Add document'), style='primary')
+    def createDocument(self, **data):
+        form = self.request.form
+        doctype = form.get('doctype', None)
+        # Here we can create various instances of PublicDocument derived
+        # classes depending on the doctype parameter given in form.
+        document = createObject('waeup.%s' % doctype)
+        self.applyData(document, **data)
+        try:
+            self.context.addDocument(document)
+        except KeyError:
+            self.flash(_('The id chosen already exists.'),
+                       type='danger')
+            return
+        doctype = getUtility(IDocumentsUtils).SELECTABLE_DOCTYPES_DICT[doctype]
+        self.flash(_('${a} added.', mapping = {'a': doctype}))
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        self.context.__parent__.logger.info(
+            '%s - added: %s %s' % (ob_class, doctype, document.document_id))
+        self.redirect(self.url(self.context) +
+            '/%s/manage' % document.document_id)
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+
+class DocumentDisplayFormPage(IkobaDisplayFormPage):
+    """ Page to display document data
+    """
+    grok.context(IPublicDocument)
+    grok.name('index')
+    grok.require('waeup.viewDocuments')
+    grok.template('documentpage')
+    pnav = 2
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface)
+
+    @property
+    def label(self):
+        return self.context.title
+
+
+class HTMLDocumentDisplayFormPage(DocumentDisplayFormPage):
+    """ Page to display html document data
+    """
+    grok.context(IHTMLDocument)
+    grok.template('htmldocumentpage')
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface).omit(
+            'html_dict', 'html_multilingual')
+
+    @property
+    def html(self):
+        lang = self.request.cookies.get('ikoba.language')
+        html = self.context.html_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+            html = self.context.html_dict.get(portal_language,'')
+        return html
+
+
+class HTMLDocumentDisplayContentPage(IkobaPage):
+    """ Page to display the html content of document
+    """
+    grok.context(IHTMLDocument)
+    grok.name('display')
+    grok.template('htmldisplaypage')
+    grok.require('waeup.Public')
+
+    @property
+    def label(self):
+        return self.context.title
+
+    def update(self):
+        if self.context.state != PUBLISHED:
+            self.flash(_('The document requested has not yet been published.'),
+                type="warning")
+            self.redirect(self.application_url())
+        super(HTMLDocumentDisplayContentPage, self).update()
+        return
+
+    @property
+    def content(self):
+        lang = self.request.cookies.get('ikoba.language')
+        html = self.context.html_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+            html = self.context.html_dict.get(portal_language,'')
+        return html
+
+
+class RESTDocumentDisplayFormPage(HTMLDocumentDisplayFormPage):
+    """ Page to display html document data
+    """
+    grok.context(IRESTDocument)
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface).omit(
+            'html_dict', 'rest_multilingual')
+
+
+class RESTDocumentDisplayContentPage(HTMLDocumentDisplayContentPage):
+    """ Page to display the html content of document
+    """
+    grok.context(IRESTDocument)
+
+    label = None
+
+
+class DocumentManageFormPage(IkobaEditFormPage,
+                            LocalRoleAssignmentUtilityView):
+    """ View to manage document data
+    """
+    grok.context(IPublicDocument)
+    grok.name('manage')
+    grok.require('waeup.manageDocuments')
+    grok.template('documentmanagepage')
+    pnav = 2
+
+    taboneactions = [_('Save'),_('Cancel')]
+    tabthreeactions1 = [_('Remove selected local roles')]
+    tabthreeactions2 = [_('Add local role')]
+
+    deletion_warning = _('Are you sure?')
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(
+            self.context.form_fields_interface).omit('document_id')
+
+    def label(self):
+        return _('Manage document ') + self.context.document_id
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        return msave(self, **data)
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,2,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,2,**data)
+
+
+class HTMLDocumentManageFormPage(DocumentManageFormPage):
+    """ View to manage htmldocument data
+    """
+    grok.context(IHTMLDocument)
+    grok.template('htmldocumentmanagepage')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        html_multilingual = getattr(self.context, 'html_multilingual', None)
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        self.context.html_dict = html2dict(html_multilingual, portal_language)
+        return
+
+
+class RESTDocumentManageFormPage(DocumentManageFormPage):
+    """ View to manage restdocument data
+    """
+    grok.context(IRESTDocument)
+    grok.template('htmldocumentmanagepage')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        html_multilingual = getattr(self.context, 'rest_multilingual', None)
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        self.context.html_dict = rest2dict(html_multilingual, portal_language)
+        return
+
+
+class DocumentTriggerTransitionFormPage(IkobaEditFormPage):
+    """ View to trigger public document transitions
+    """
+    grok.context(IPublicDocument)
+    grok.name('trigtrans')
+    grok.require('waeup.triggerTransition')
+    grok.template('trigtrans')
+    label = _('Trigger document transition')
+    pnav = 2
+
+    def update(self):
+        return super(IkobaEditFormPage, self).update()
+
+    def getTransitions(self):
+        """Return a list of dicts of allowed transition ids and titles.
+
+        Each list entry provides keys ``name`` and ``title`` for
+        internal name and (human readable) title of a single
+        transition.
+        """
+        wf_info = IWorkflowInfo(self.context)
+        allowed_transitions = [t for t in wf_info.getManualTransitions()]
+        return [dict(name='', title=_('No transition'))] +[
+            dict(name=x, title=y) for x, y in allowed_transitions]
+
+    @action(_('Apply now'), style='primary')
+    def apply(self, **data):
+        form = self.request.form
+        if 'transition' in form and form['transition']:
+            transition_id = form['transition']
+            wf_info = IWorkflowInfo(self.context)
+            try:
+                wf_info.fireTransition(transition_id)
+                self.flash(_("Transition '%s' executed." % transition_id))
+            except InvalidTransitionError, error:
+                self.flash(error, type="warning")
+            self.redirect(self.url(self.context))
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/containermanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/containermanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/containermanagepage.pt	(revision 12677)
@@ -0,0 +1,32 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.ikoba" method="POST" enctype="multipart/form-data">
+  <h3 i18n:translate="">Documents</h3>
+  <table class="ikoba-data-table dataTableManage">
+    <thead>
+      <tr>
+        <th>&nbsp;
+        </th>
+        <th i18n:translate="">Id</th>
+        <th i18n:translate="">Title</th>
+        <th i18n:translate="">Type</th>
+        <th i18n:translate="">State</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="doc context/values"> <td>
+          <input type="checkbox" name="val_id"
+                 tal:attributes="value doc/document_id" /> </td> <td>
+          <a tal:attributes="href python: view.url(doc)"
+             tal:content="doc/__name__"> ID</a> </td>
+          <td tal:content="doc/title">TITLE</td>
+          <td tal:content="doc/translated_class_name">CLASSNAME</td>
+          <td tal:content="doc/translated_state">STATE</td>
+      </tr>
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions" tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/containerpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/containerpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/containerpage.pt	(revision 12677)
@@ -0,0 +1,24 @@
+<div i18n:domain="waeup.ikoba" i18n:translate="" tal:condition="python: not len(context.keys())">
+There are no subobjects registered yet.
+</div>
+
+<table class="ikoba-data-table dataTable" i18n:domain="waeup.ikoba">
+  <thead>
+    <tr>
+      <th i18n:translate="">Id</th>
+      <th i18n:translate="">Title</th>
+      <th i18n:translate="">Type</th>
+      <th i18n:translate="">State</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value context/values">
+      <td> <a tal:attributes="href python: view.url(value)">
+          <span tal:content="value/document_id">ID</span></a>
+      </td>
+      <td tal:content="value/title">TITLE</td>
+      <td tal:content="value/translated_class_name">CLASSNAME</td>
+      <td tal:content="value/translated_state">STATE</td>
+    </tr>
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/documentaddform.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/documentaddform.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/documentaddform.pt	(revision 12677)
@@ -0,0 +1,27 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data" i18n:domain="waeup.ikoba">
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname">
+          <span i18n:translate="">Document Type</span>:
+        </td>
+        <td>
+          <select id="doctype" name="doctype" size="1"
+                  class="form-control half">
+            <option tal:repeat="item view/selectable_doctypes"
+                    tal:attributes="value python:item[0]">
+              <span tal:replace="python:item[1]">DOCTYPE</span>
+            </option>
+          </select>
+        </td>
+      </tr>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/documentmanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/documentmanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/documentmanagepage.pt	(revision 12677)
@@ -0,0 +1,101 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.ikoba">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">Metadata</a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab" i18n:translate="">Files</a>
+    </li>
+    <li>
+      <a href="#tab3" data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="tab-pane active">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:files content="structure provider:files" />
+        </tbody>
+      </table>
+    </div>
+    <div id="tab3" class="tab-pane">
+      <br />
+      <table class="ikoba-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+            <input type="checkbox" name="role_id"
+                   tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <div class="form-inline">
+        <br />
+        <div class="form-group">
+          <select id="user" name="user" class="form-control">
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+            <span tal:replace="user/val/title">USERTITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                  <input tal:condition="python:action.label in view.tabthreeactions2"
+                         tal:replace="structure action/render"/>
+                </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/documentpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/documentpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/documentpage.pt	(revision 12677)
@@ -0,0 +1,31 @@
+<div class="wfstatus-sub"  i18n:domain="waeup.ikoba">
+  Document State: <span tal:replace="context/translated_state">
+    STATE</span>
+</div>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<table class="form-table" i18n:domain="waeup.ikoba"
+  tal:define="files provider:files">
+  <tbody>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Document Type:
+      </td>
+      <td>
+        <span tal:content="context/translated_class_name">CLASSNAME</span>
+      </td>
+    </tr>
+    <tal:widgets content="structure provider:widgets" />
+    <tr tal:condition="python: files.replace('\n','')">
+      <td class="separator" colspan=2 i18n:translate="">
+        Connected Files
+      </td>
+    </tr>
+    <tal:files content="structure files" />
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt	(revision 12677)
@@ -0,0 +1,90 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.ikoba">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">Metadata</a>
+    </li>
+    <li>
+      <a href="#tab3" data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="tab-pane active">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab3" class="tab-pane">
+      <br />
+      <table class="ikoba-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+            <input type="checkbox" name="role_id"
+                   tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <div class="form-inline">
+        <br />
+        <div class="form-group">
+          <select id="user" name="user" class="form-control">
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+            <span tal:replace="user/val/title">USERTITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                  <input tal:condition="python:action.label in view.tabthreeactions2"
+                         tal:replace="structure action/render"/>
+                </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/htmldocumentpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/htmldocumentpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/htmldocumentpage.pt	(revision 12677)
@@ -0,0 +1,43 @@
+<div class="wfstatus-sub"  i18n:domain="waeup.ikoba">
+  Document State: <span tal:replace="context/translated_state">
+    STATE</span>
+</div>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<table class="form-table" i18n:domain="waeup.ikoba"
+  tal:define="files provider:files">
+  <tbody>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Document Type:
+      </td>
+      <td>
+        <span tal:content="context/translated_class_name">CLASSNAME</span>
+      </td>
+    </tr>
+    <tal:widgets content="structure provider:widgets" />
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Content:
+      </td>
+      <td>
+        <span tal:content="structure view/html">HTML</span>
+      </td>
+    </tr>
+    <tr tal:condition="python: getattr(context, 'state', None) == 'published'">
+      <td i18n:translate="" class="fieldname">
+        HTML Element:
+      </td>
+      <td>
+        &lt;a href=&quot;
+        <span tal:replace="python: view.url(context, 'display')"></span>
+        &quot;&gt;<span tal:replace="context/title">FILENAME</span>&lt;/a&gt;
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/trigtrans.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/trigtrans.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/browser_templates/trigtrans.pt	(revision 12677)
@@ -0,0 +1,19 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data"
+      autocomplete="off">
+
+    <br />
+
+    <select id="transition" name="transition" class="form-control half">
+      <option tal:repeat="transition view/getTransitions"
+              tal:attributes="value transition/name">
+        <span tal:replace="transition/title">TRANSITIONTITLE</span>
+      </option>
+    </select>
+    <br /><br />
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/catalog.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/catalog.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/catalog.py	(revision 12677)
@@ -0,0 +1,33 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components to help cataloging and searching documents.
+"""
+import grok
+from waeup.ikoba.interfaces import ICompany
+from waeup.ikoba.documents.interfaces import IDocument
+
+class DocumentsCatalog(grok.Indexes):
+    """A catalog for all documents.
+    """
+    grok.site(ICompany)
+    grok.name('documents_catalog')
+    grok.context(IDocument)
+
+    document_id = grok.index.Field(attribute='document_id')
+    user_id = grok.index.Field(attribute='user_id')
+    state = grok.index.Field(attribute='state')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/container.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/container.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/container.py	(revision 12677)
@@ -0,0 +1,60 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Containers which contain document objects.
+"""
+import grok
+from grok import index
+from waeup.ikoba.interfaces import IIkobaPluggable
+from waeup.ikoba.documents.interfaces import IDocumentsContainer, IDocument
+from waeup.ikoba.utils.helpers import attrs_to_fields
+from waeup.ikoba.utils.logger import Logger
+
+class DocumentsContainer(grok.Container, Logger):
+    """This is a container for all kind of documents.
+    """
+    grok.implements(IDocumentsContainer)
+    grok.provides(IDocumentsContainer)
+
+
+    def addDocument(self, document):
+        if not IDocument.providedBy(document):
+            raise TypeError(
+                'DocumentsContainers contain only IDocument instances')
+        self[document.document_id] = document
+        return
+
+DocumentsContainer = attrs_to_fields(DocumentsContainer)
+
+class DocumentsPlugin(grok.GlobalUtility):
+    """A plugin that creates container for documents inside a company.
+    """
+    grok.implements(IIkobaPluggable)
+    grok.name('documents')
+
+    def setup(self, site, name, logger):
+        if 'documents' in site.keys():
+            logger.warn('Could not create container for documents.')
+            return
+        site['documents'] = DocumentsContainer()
+        logger.info('Container for documents created')
+        return
+
+    def update(self, site, name, logger):
+        if not 'documents' in site.keys():
+            self.setup(site, name, logger)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/document.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/document.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/document.py	(revision 12677)
@@ -0,0 +1,333 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+These are the public documents.
+"""
+import os
+import grok
+from grok import index
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.event import notify
+from zope.component import getUtility
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.i18n import translate
+
+from waeup.ikoba.image import IkobaImageFile
+from waeup.ikoba.imagestorage import DefaultFileStoreHandler
+from waeup.ikoba.interfaces import (
+    IIkobaUtils, IObjectHistory,
+    IFileStoreNameChooser, IFileStoreHandler, IExtFileStore)
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.utils.helpers import attrs_to_fields, get_current_principal
+from waeup.ikoba.documents.interfaces import (
+    IDocument, IPublicDocument, IDocumentsUtils,
+    IPDFDocument, IHTMLDocument, IRESTDocument)
+
+
+@grok.subscribe(IDocument, grok.IObjectRemovedEvent)
+def handle_document_removed(document, event):
+    store = getUtility(IExtFileStore)
+    for filename in document.filenames:
+        store.deleteFileByContext(document, attr=filename)
+    return
+
+class Document(grok.Container):
+    """This is a document.
+    """
+    grok.implements(IDocument)
+    grok.provides(IDocument)
+    grok.baseclass()
+
+    form_fields_interface = None
+
+    # Kofa can store any number of files per Document object.
+    # However, we highly recommend to associate and store
+    # only one file per Document object. Thus the following
+    # tuple should contain only a single filename string.
+    filenames = ()
+
+    local_roles = [
+        'waeup.local.DocumentManager',
+        ]
+
+    user_id = None
+    state = None
+    translated_state = None
+    translated_class_name = None
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    @property
+    def class_name(self):
+        return self.__class__.__name__
+
+    @property
+    def formatted_transition_date(self):
+        try:
+            return self.history.messages[-1].split(' - ')[0]
+        except IndexError:
+            return
+
+    @property
+    def connected_files(self):
+        return
+
+    @property
+    def is_verifiable(self):
+        return True, None
+
+    def setMD5(self):
+        """Determine md5 checksum of all files and store checksums as
+        document attributes.
+        """
+        return
+        
+class PublicDocumentBase(Document):
+    """This is a customer document baseclass.
+    """
+    grok.implements(IPublicDocument)
+    grok.provides(IPublicDocument)
+    grok.baseclass()
+
+    @property
+    def state(self):
+        state = IWorkflowState(self).getState()
+        return state
+
+    @property
+    def translated_state(self):
+        try:
+            TRANSLATED_STATES = getUtility(
+                IDocumentsUtils).TRANSLATED_DOCUMENT_STATES
+            return TRANSLATED_STATES[self.state]
+        except KeyError:
+            return
+
+    @property
+    def translated_class_name(self):
+        try:
+            DOCTYPES_DICT = getUtility(IDocumentsUtils).DOCTYPES_DICT
+            return DOCTYPES_DICT[self.class_name]
+        except KeyError:
+            return
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
+        self.__parent__.__parent__.logger.info(
+            '%s - %s - %s' % (ob_class, self.__name__, message))
+        return
+
+
+class PDFDocument(PublicDocumentBase):
+    """This is a  document for a single pdf upload file.
+    """
+    grok.implements(IPDFDocument)
+    grok.provides(IPDFDocument)
+
+    form_fields_interface = IPDFDocument
+
+    filenames = ('file.pdf',)
+
+PDFDocument = attrs_to_fields(PDFDocument)
+
+
+class HTMLDocument(PublicDocumentBase):
+    """This is a  document to render html-coded text.
+    """
+    grok.implements(IHTMLDocument)
+    grok.provides(IHTMLDocument)
+
+    form_fields_interface = IHTMLDocument
+
+    def __init__(self, *args, **kw):
+        super(HTMLDocument, self).__init__(*args, **kw)
+        self.html_dict = {}
+
+HTMLDocument = attrs_to_fields(HTMLDocument)
+
+
+class RESTDocument(PublicDocumentBase):
+    """This is a  document to render html-coded text.
+    """
+    grok.implements(IRESTDocument)
+    grok.provides(IRESTDocument)
+
+    form_fields_interface = IRESTDocument
+
+    def __init__(self, *args, **kw):
+        super(RESTDocument, self).__init__(*args, **kw)
+        self.html_dict = {}
+
+RESTDocument = attrs_to_fields(RESTDocument)
+
+
+class PDFDocumentFactory(grok.GlobalUtility):
+    """A factory for documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.PDFDocument')
+    title = u"Create a new PDF document.",
+    description = u"This factory instantiates new PDF documents."
+
+    def __call__(self, *args, **kw):
+        return PDFDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(PDFDocument)
+
+
+class HTMLDocumentFactory(grok.GlobalUtility):
+    """A factory for HTML documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.HTMLDocument')
+    title = u"Create a new HTML document.",
+    description = u"This factory instantiates new HTML documents."
+
+    def __call__(self, *args, **kw):
+        return HTMLDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(HTMLDocument)
+
+
+class RESTDocumentFactory(grok.GlobalUtility):
+    """A factory for REST documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.RESTDocument')
+    title = u"Create a new REST document.",
+    description = u"This factory instantiates new REST documents."
+
+    def __call__(self, *args, **kw):
+        return RESTDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(RESTDocument)
+
+
+#: The file id marker for files
+DOCUMENT_FILE_STORE_NAME = 'file-document'
+
+
+class DocumentFileNameChooser(grok.Adapter):
+    """A file id chooser for :class:`Document` objects.
+
+    `context` is an :class:`Document` instance.
+
+    The delivered file_id contains the file id marker for
+    :class:`Document` objects in the central :class:`DocumentsContainer`.
+
+    This chooser is registered as an adapter providing
+    :class:`waeup.ikoba.interfaces.IFileStoreNameChooser`.
+
+    File store name choosers like this one are only convenience
+    components to ease the task of creating file ids for customer document
+    objects. You are nevertheless encouraged to use them instead of
+    manually setting up filenames for customer documents.
+
+    .. seealso:: :mod:`waeup.ikoba.imagestorage`
+
+    """
+
+    grok.context(IDocument)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name=None, attr=None):
+        """Check whether the given name is a valid file id for the context.
+
+        Returns ``True`` only if `name` equals the result of
+        :meth:`chooseName`.
+
+        """
+        return name == self.chooseName()
+
+    def chooseName(self, attr, name=None):
+        """Get a valid file id for customer document context.
+
+        *Example:*
+
+        For document with id 'd123'
+        with attr ``'nice_image.jpeg'`` this chooser would create:
+
+          ``'__file-document__nice_image_d123.jpeg'``
+
+        meaning that the nice image of this document would be
+        stored in the site-wide file storage in path:
+
+          ``nice_image_d123.jpeg``
+
+        """
+        basename, ext = os.path.splitext(attr)
+        doc_id = self.context.document_id
+        marked_filename = '__%s__%s_%s%s' % (
+            DOCUMENT_FILE_STORE_NAME,
+            basename, doc_id, ext)
+        return marked_filename
+
+
+class DocumentFileStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
+    """ Document specific file handling.
+
+    This handler knows in which path in a filestore to store document
+    files and how to turn this kind of data into some (browsable)
+    file object.
+
+    It is called from the global file storage, when it wants to
+    get/store a file with a file id starting with
+    ``__file-document__`` (the marker string for customer files).
+
+    Like each other file store handler it does not handle the files
+    really (this is done by the global file store) but only computes
+    paths and things like this.
+    """
+    grok.implements(IFileStoreHandler)
+    grok.name(DOCUMENT_FILE_STORE_NAME)
+
+    def pathFromFileID(self, store, root, file_id):
+        """All document files are put in directory ``documents``.
+        """
+        marker, filename, basename, ext = store.extractMarker(file_id)
+        sub_root = os.path.join(root, 'documents')
+        return super(DocumentFileStoreHandler, self).pathFromFileID(
+            store, sub_root, basename)
+
+    def createFile(self, store, root, filename, file_id, file):
+        """Create a browsable file-like object.
+        """
+        # call super method to ensure that any old files with
+        # different filename extension are deleted.
+        file, path, file_obj = super(
+            DocumentFileStoreHandler, self).createFile(
+            store, root,  filename, file_id, file)
+        return file, path, IkobaImageFile(
+            file_obj.filename, file_obj.data)
+
+
+@grok.subscribe(IDocument, grok.IObjectAddedEvent)
+def handle_document_added(document, event):
+    """If a document is added the transition create is fired.
+    The latter produces a logging message.
+    """
+    if IWorkflowState(document).getState() is None:
+        IWorkflowInfo(document).fireTransition('create')
+    return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/export.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/export.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/export.py	(revision 12677)
@@ -0,0 +1,110 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Exporters for documents.
+"""
+import grok
+from zope.securitypolicy.interfaces import IPrincipalRoleMap
+from waeup.ikoba.interfaces import ICSVExporter
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.utils.batching import ExporterBase
+from waeup.ikoba.utils.helpers import iface_names
+from waeup.ikoba.documents.interfaces import (
+    IPDFDocument, IHTMLDocument, IRESTDocument)
+
+
+class DocumentExporterBase(grok.GlobalUtility, ExporterBase):
+    """Exporter for documents.
+    """
+    grok.implements(ICSVExporter)
+    grok.baseclass()
+    class_name = None
+    iface = None
+    title = None
+
+    @property
+    def fields(self):
+        return tuple(sorted(iface_names(self.iface, exclude_attribs=False,
+            omit=['is_verifiable',
+                  'translated_state',
+                  'user_id',
+                  'formatted_transition_date',
+                  'translated_class_name',
+                  'connected_files',   # Could be used to export file URLs
+                  ]
+            ))) + (
+            'users_with_local_roles',)
+
+    def mangle_value(self, value, name, context=None):
+        """Hook for mangling values in derived classes
+        """
+        if name == 'users_with_local_roles':
+            value = []
+            role_map = IPrincipalRoleMap(context)
+            for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
+                value.append({'user_name':user_name,'local_role':local_role})
+        if name == 'history':
+            value = value.messages
+        return super(DocumentExporterBase, self).mangle_value(
+            value, name, context)
+
+    def export(self, documents, filepath=None):
+        """Export `documents`, an iterable, as CSV file.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for document in documents:
+            self.write_item(document, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export documents in documentscontainer into filepath as CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        documents = site.get('documents', {}).values()
+        documents = [doc for doc in documents
+                     if doc.class_name == self.class_name]
+        return self.export(documents, filepath)
+
+
+class PDFDocumentExporter(DocumentExporterBase):
+    """Exporter for documents.
+    """
+    grok.name('pdfdocuments')
+    iface = IPDFDocument
+    class_name = 'PDFDocument'
+    title = _(u'Public PDF Documents')
+
+
+class HTMLDocumentExporter(DocumentExporterBase):
+    """Exporter for documents.
+    """
+    grok.name('htmldocuments')
+    iface = IHTMLDocument
+    class_name = 'HTMLDocument'
+    title = _(u'Public HTML Documents')
+
+
+class RESTDocumentExporter(DocumentExporterBase):
+    """Exporter for documents.
+    """
+    grok.name('restdocuments')
+    iface = IRESTDocument
+    class_name = 'RESTDocument'
+    title = _(u'Public REST Documents')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/fileviewlets.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/fileviewlets.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/fileviewlets.py	(revision 12677)
@@ -0,0 +1,83 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+import grok
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import PUBLISHED
+from waeup.ikoba.browser.layout import UtilityView
+from waeup.ikoba.browser.fileviewlets import (
+    FileDisplay, FileUpload, Image)
+
+from waeup.ikoba.documents.document import PDFDocument
+from waeup.ikoba.documents.browser import (
+    DocumentManageFormPage, DocumentDisplayFormPage)
+
+
+# File viewlets for documents
+
+class PDFScanManageUpload(FileUpload):
+    """Scan upload viewlet for officers.
+    """
+    grok.view(DocumentManageFormPage)
+    grok.context(PDFDocument)
+    grok.require('waeup.manageDocuments')
+    label = _(u'PDF File')
+    title = _(u'PDF File')
+    mus = 1024 * 200
+    download_name = u'file.pdf'
+    tab_redirect = '#tab2'
+
+    @property
+    def download_filename(self):
+        return u"%s.pdf" % self.context.document_id
+
+
+class PDFScanDisplay(FileDisplay):
+    """Scan display viewlet.
+    """
+    grok.order(1)
+    grok.context(PDFDocument)
+    grok.require('waeup.viewDocuments')
+    grok.view(DocumentDisplayFormPage)
+    label = _(u'PDF Scan')
+    title = _(u'PDF Scan')
+    download_name = u'file.pdf'
+
+    @property
+    def download_filename(self):
+        return u"%s.pdf" % self.context.document_id
+
+
+class PDFScanImage(UtilityView, Image):
+    """Scan document.
+    """
+    grok.name('file.pdf')
+    grok.context(PDFDocument)
+    grok.require('waeup.Public')
+    download_name = u'file.pdf'
+
+    @property
+    def download_filename(self):
+        return u"%s" % self.context.document_id
+
+    def update(self):
+        if self.context.state != PUBLISHED:
+            self.flash(_('The document requested has not yet been published.'),
+                type="warning")
+            self.redirect(self.application_url())
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/interfaces.py	(revision 12677)
@@ -0,0 +1,119 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+from zope.interface import Attribute, Interface
+from zope import schema
+from waeup.ikoba.interfaces import (
+    IIkobaObject, validate_id,
+    ContextualDictSourceFactoryBase)
+from waeup.ikoba.interfaces import MessageFactory as _
+
+
+class IDocumentsContainer(IIkobaObject):
+    """A container for all kind of document objects.
+
+    """
+
+    def addDocument(document):
+        """Add a document.
+        """
+
+
+class IDocument(IIkobaObject):
+    """A base representation of documents.
+
+    """
+    history = Attribute('Object history, a list of messages')
+    state = Attribute('Returns the verification state of a document')
+    translated_state = Attribute(
+        'Returns a translated, more verbose verification state of a document')
+    class_name = Attribute('Name of the document class')
+    translated_class_name = Attribute('Translatable class name')
+    formatted_transition_date = Attribute('Last transition formatted date string')
+    user_id = Attribute('Id of a user')
+    connected_files = Attribute('Names of files connected to a document')
+    is_verifiable = Attribute('Contract verifiable by officer')
+
+    document_id = schema.TextLine(
+        title = _(u'Document Id'),
+        required = True,
+        constraint=validate_id,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Document Title'),
+        required = True,
+        )
+
+    def setMD5():
+        """Determine md5 checksum of selected files and store checksums as
+        document attributes.
+        """
+
+class IPublicDocument(IDocument):
+    """A base representation of public documents.
+
+    """
+
+    def writeLogMessage(view, message):
+        """Write a view specific log message into main.log.
+
+        """
+
+    description = schema.Text(
+        title = _(u'Description'),
+        required = False,
+        )
+
+
+class IPDFDocument(IPublicDocument):
+    """A base representation of PDF documents.
+
+    """
+
+
+class IHTMLDocument(IPublicDocument):
+    """A base representation of HTML documents.
+
+    """
+
+    html_dict = Attribute('Content as language dictionary with values in HTML format')
+
+    html_multilingual = schema.Text(
+        title = _(u'Multilingual content in HTML format'),
+        required = False,
+        )
+
+class IRESTDocument(IPublicDocument):
+    """A base representation of REST documents.
+
+    """
+
+    html_dict = Attribute(
+        'Content as language dictionary with values in HTML format')
+
+    rest_multilingual = schema.Text(
+        title = _(u'Multilingual content in REST (reStructuredText) format'),
+        required = False,
+        )
+
+
+class IDocumentsUtils(Interface):
+    """A collection of methods which are subject to customization.
+
+    """
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/permissions.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/permissions.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/permissions.py	(revision 12677)
@@ -0,0 +1,51 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+
+# Permissions
+
+class ViewDocuments(grok.Permission):
+    grok.name('waeup.viewDocuments')
+
+
+class ManageDocuments(grok.Permission):
+    grok.name('waeup.manageDocuments')
+
+
+# Local Roles
+
+class DocumentManager(grok.Role):
+    grok.name('waeup.local.DocumentManager')
+    grok.title(u'Document Manager')
+    grok.permissions('waeup.manageDocuments',
+                     'waeup.viewDocuments',
+                     'waeup.exportData')
+
+
+# Site Roles
+class DocumentsOfficer(grok.Role):
+    grok.name('waeup.DocumentsOfficer')
+    grok.title(u'Documents Officer (view only)')
+    grok.permissions('waeup.viewDocuments')
+
+
+class DocumentsManager(grok.Role):
+    grok.name('waeup.DocumentsManager')
+    grok.title(u'Documents Manager')
+    grok.permissions('waeup.viewDocuments',
+                     'waeup.manageDocuments')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/__init__.py	(revision 12677)
@@ -0,0 +1,3 @@
+"""This package contains everything regarding documents.
+"""
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/sample_document_data.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/sample_document_data.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/sample_document_data.csv	(revision 12677)
@@ -0,0 +1,6 @@
+document_id,title,class_name
+ABC,ABC title,PDFDocument
+CDE,CDE title,PDFDocument
+EFG,EFG title,PDFDocument
+ABC,Duplicate document,PDFDocument
+HIJ,HIJ title,HTMLDocument
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/sample_document_data_update.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/sample_document_data_update.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/sample_document_data_update.csv	(revision 12677)
@@ -0,0 +1,3 @@
+document_id,title
+ABC,ABC new title
+XYZ,Non-existing document
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_batching.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_batching.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_batching.py	(revision 12677)
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Unit tests for document data processors.
+"""
+import os
+import shutil
+import tempfile
+import unittest
+import datetime
+import grok
+from time import time
+from zope.event import notify
+from zope.component import createObject, queryUtility
+from zope.component.hooks import setSite, clearSite
+from zope.catalog.interfaces import ICatalog
+from zope.interface.verify import verifyClass, verifyObject
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import IBatchProcessor, FatalCSVError, IUserAccount
+from waeup.ikoba.documents.batching import PDFDocumentProcessor
+from waeup.ikoba.documents.document import PDFDocument, HTMLDocument
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+DOCUMENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_document_data.csv'),
+    'rb').read()
+
+DOCUMENT_HEADER_FIELDS = DOCUMENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+DOCUMENT_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_document_data_update.csv'),
+    'rb').read()
+
+DOCUMENT_HEADER_FIELDS_UPDATE = DOCUMENT_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+class DocumentImportExportSetup(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(DocumentImportExportSetup, self).setUp()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
+        app = Company()
+        app['datacenter'].setStoragePath(self.dc_root)
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(app)
+
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        return
+
+    def tearDown(self):
+        super(DocumentImportExportSetup, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def setup_for_export(self):
+        document1 = PDFDocument()
+        document1.document_id = u'DOC1'
+        self.app['documents'].addDocument(document1)
+        self.pdfdocument = document1
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        role_manager = IPrincipalRoleManager(document1)
+        role_manager.assignRoleToPrincipal(u'johnsrole', u'john')
+
+        document2 = HTMLDocument()
+        document2.document_id = u'DOC2'
+        self.app['documents'].addDocument(document2)
+        self.htmldocument = document2
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        role_manager = IPrincipalRoleManager(document2)
+        role_manager.assignRoleToPrincipal(u'johnsrole', u'john')
+        return
+
+
+class PDFDocumentProcessorTest(DocumentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PDFDocumentProcessorTest, self).setUp()
+
+        # Add document
+        document = PDFDocument()
+        document.document_id = u'DOC1'
+        self.app['documents'].addDocument(document)
+        document.title = u'Our PDF Document'
+        notify(grok.ObjectModifiedEvent(document))
+        self.document = self.app['documents'][document.document_id]
+
+        self.processor = PDFDocumentProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_document_data.csv')
+        self.csv_file_update = os.path.join(
+            self.workdir, 'sample_document_data_update.csv')
+        open(self.csv_file, 'wb').write(DOCUMENT_SAMPLE_DATA)
+        open(self.csv_file_update, 'wb').write(DOCUMENT_SAMPLE_DATA_UPDATE)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, PDFDocumentProcessor) is True
+
+    def test_parentsExist(self):
+        self.assertFalse(self.processor.parentsExist(None, dict()))
+        self.assertTrue(self.processor.parentsExist(None, self.app))
+
+    def test_entryExists(self):
+        assert self.processor.entryExists(
+            dict(document_id='ID_NONE'), self.app) is False
+        assert self.processor.entryExists(
+            dict(document_id=self.document.document_id), self.app) is True
+
+    def test_getParent(self):
+        parent = self.processor.getParent(None, self.app)
+        assert parent is self.app['documents']
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(document_id='ID_NONE'), self.app) is None
+        assert self.processor.getEntry(
+            dict(document_id=self.document.document_id), self.app) is self.document
+
+    def test_addEntry(self):
+        new_document = PDFDocument()
+        new_document.document_id = u'DOC2'
+        self.processor.addEntry(
+            new_document, {}, self.app)
+        assert len(self.app['documents'].keys()) == 2
+        self.assertEqual(self.app['documents']['DOC2'].document_id, 'DOC2')
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='DOC4', class_name='PDFDocument'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='id with spaces', class_name='PDFDocument'))
+        self.assertEqual(len(errs),1)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='DOC4', class_name='WrongDocument'), mode='create')
+        self.assertEqual(len(errs),1)
+
+    def test_delEntry(self):
+        assert self.document.document_id in self.app['documents'].keys()
+        self.processor.delEntry(
+            dict(document_id=self.document.document_id), self.app)
+        assert self.document.document_id not in self.app['documents'].keys()
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS)
+        self.assertEqual(num_warns,2)
+        self.assertEqual(len(self.app['documents']), 4)
+        self.assertEqual(self.app['documents']['ABC'].title,'ABC title')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Public PDF Document Processor - sample_document_data - '
+            'EFG - updated: document_id=EFG, title=EFG title\n'
+            in logcontent)
+        failcontent = open(fail_file).read()
+        self.assertTrue(
+            'PDFDocument,ABC,Duplicate document,This object already exists.'
+            in failcontent)
+        self.assertTrue(
+            'HTMLDocument,HIJ,HIJ title,class_name: wrong processor'
+            in failcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update, DOCUMENT_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,1)
+        # title has changed
+        self.assertEqual(self.app['documents']['ABC'].title,'ABC new title')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Public PDF Document Processor - sample_document_data_update - '
+            'ABC - updated: document_id=ABC, title=ABC new title\n'
+            in logcontent)
+        failcontent = open(fail_file).read()
+        self.assertTrue(
+            'XYZ,Non-existing document,Cannot update: no such entry'
+            in failcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_browser.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_browser.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_browser.py	(revision 12677)
@@ -0,0 +1,363 @@
+## $Id$
+## 
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the customer-related UI components.
+"""
+import shutil
+import tempfile
+from datetime import datetime, timedelta, date
+from StringIO import StringIO
+import os
+from zope.event import notify
+from zope.component import createObject, queryUtility, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.schema.interfaces import ConstraintNotSatisfied
+from zope.catalog.interfaces import ICatalog
+from zope.security.interfaces import Unauthorized
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.testbrowser.testing import Browser
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import (
+    IUserAccount, IJobManager, APPROVED, SUBMITTED, PUBLISHED,
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler)
+from waeup.ikoba.imagestorage import (
+    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
+    DefaultStorage)
+from waeup.ikoba.authentication import LocalRoleSetEvent
+from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
+from waeup.ikoba.interfaces import VERIFIED
+from waeup.ikoba.browser.tests.test_pdf import samples_dir
+
+PH_LEN = 15911  # Length of placeholder file
+
+SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+#SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
+SAMPLE_PDF = os.path.join(os.path.dirname(__file__), 'test_pdf.pdf')
+
+class FullSetup(FunctionalTestCase):
+    # A test case that only contains a setup and teardown
+    #
+    # Complete setup for customers handlings is rather complex and
+    # requires lots of things created before we can start. This is a
+    # setup that does all this, creates a company etc.
+    # so that we do not have to bother with that in different
+    # test cases.
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FullSetup, self).setUp()
+
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        self.login_path = 'http://localhost/app/login'
+        self.container_path = 'http://localhost/app/documents'
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(FullSetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+
+class DocumentUITests(FullSetup):
+    # Tests for document related views and pages
+
+    def test_manage_file_document(self):
+        # Managers can access the pages of documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Documents").click()
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['PDFDocument']
+        self.browser.getControl(name="form.title").value = 'My PDF Document'
+        self.browser.getControl(name="form.document_id").value = 'DOC1'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('PDF Document added.' in self.browser.contents)
+        document = self.app['documents']['DOC1']
+
+        # Document can be edited
+        self.browser.getControl(name="form.title").value = 'My first doc'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.container_path + '/DOC1/index')
+
+        # File can be uploaded
+        self.browser.getLink("Manage").click()
+        # Create a pseudo image file and select it to be uploaded
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.jpg')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'pdf file format expected' in self.browser.contents)
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.pdf')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'Could not determine file type' in self.browser.contents)
+        pdf = open(SAMPLE_PDF, 'rb')
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pdf, filename='my_sample_scan.pdf')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'href="http://localhost/app/documents/DOC1/file.pdf">DOC1.pdf</a>'
+            in self.browser.contents)
+        # The file can be found in the file system
+        file = getUtility(IExtFileStore).getFileByContext(
+            document, attr='file.pdf')
+        file_content = file.read()
+        pdf.seek(0)
+        pdf_content = pdf.read()
+        self.assertEqual(file_content, pdf_content)
+        # Browsing the link shows a real pdf only if the document
+        # has been published
+        self.browser.getLink("DOC1.pdf").click()
+        self.assertTrue(
+            'The document requested has not yet been published'
+            in self.browser.contents)
+        IWorkflowState(document).setState(PUBLISHED)
+        self.browser.open(self.container_path + '/DOC1/file.pdf')
+        self.assertEqual(
+            self.browser.headers['content-type'], 'application/pdf')
+        # The name of the downloaded file will be different
+        self.assertEqual(
+            self.browser.headers['Content-Disposition'],
+            'attachment; filename="DOC1.pdf')
+
+        # Transitions can be performed
+        self.assertEqual(document.state, 'published')
+        self.browser.open(self.container_path + '/DOC1')
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['retract']
+        self.browser.getControl("Apply now").click()
+        self.assertEqual(document.state, 'created')
+
+        # Documents can be removed
+        self.browser.getLink("Documents").click()
+        self.browser.getLink("Manage").click()
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=document.document_id).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+        # File has been removed too
+        file = getUtility(IExtFileStore).getFileByContext(
+            document, attr='file.pdf')
+        self.assertTrue(file is None)
+
+        # All actions are being logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document created' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentAddFormPage - added: PDF Document %s'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentManageFormPage - %s - saved: title'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentManageFormPage - %s - uploaded: file.pdf (my_sample_scan.pdf)'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document retracted' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentsContainerManageFormPage - removed: %s'
+            % document.document_id in logcontent)
+
+    def test_manage_html_document(self):
+        # Managers can access the pages of documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Documents").click()
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['HTMLDocument']
+        self.browser.getControl(name="form.document_id").value = 'DOC2'
+        self.browser.getControl(name="form.title").value = 'My HTML Document'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('HTML Document added.' in self.browser.contents)
+        document = self.app['documents']['DOC2']
+
+        # Document can be edited
+        self.browser.getControl(name="form.title").value = 'My second doc'
+        self.browser.getControl(name="form.html_multilingual").value = """
+<h1>Hello World</h1>
+>>de<<
+<h1>Hallo Welt</h1>
+"""
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.container_path + '/DOC2/index')
+        self.assertTrue(
+            '<h1>Hello World</h1>' in self.browser.contents)
+        self.assertFalse(
+            '<h1>Hallo Welt</h1>' in self.browser.contents)
+        self.browser.getLink("de").click()
+        self.assertFalse(
+            '<h1>Hello World</h1>' in self.browser.contents)
+        self.assertTrue(
+            '<h1>Hallo Welt</h1>' in self.browser.contents)
+        # The content can't be rendered yet
+        self.browser.open(self.container_path + '/DOC2/display')
+        self.assertTrue(
+            'The document requested has not yet been published'
+            in self.browser.contents)
+        # We have been redirected to the portal root
+        self.assertEqual(self.browser.url, 'http://localhost/app')
+
+        # Transitions can be performed
+        self.browser.open(self.container_path + '/DOC2')
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['publish']
+        self.browser.getControl("Jetzt anwenden").click()
+        self.assertEqual(document.state, 'published')
+
+        # The content can be rendered
+        self.browser.open(self.container_path + '/DOC2/display')
+        self.assertTrue(
+            '<h1>Hallo Welt</h1>' in self.browser.contents)
+
+        # Documents can be removed
+        self.browser.getLink("en", index=2).click()
+        self.browser.getLink("Documents").click()
+        self.browser.getLink("Manage").click()
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=document.document_id).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+        # All actions are being logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document created' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentAddFormPage - added: HTML Document %s'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.HTMLDocumentManageFormPage - %s - saved: title + html_multilingual'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document published' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentsContainerManageFormPage - removed: %s'
+            % document.document_id in logcontent)
+
+    def test_manage_rest_document(self):
+        # Managers can access the pages of documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Documents").click()
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['RESTDocument']
+        self.browser.getControl(name="form.document_id").value = 'DOC3'
+        self.browser.getControl(name="form.title").value = 'My REST Document'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('REST Document added.' in self.browser.contents)
+        document = self.app['documents']['DOC3']
+
+        # Document can be edited
+        self.browser.getControl(name="form.rest_multilingual").value = """
+----------
+Main Title
+----------
+
+Subtitle
+========
+>>de<<
+----------
+Haupttitel
+----------
+
+Untertitel
+==========
+"""
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.container_path + '/DOC3/index')
+        self.assertTrue(
+            '<h1 class="title">Main Title</h1>' in self.browser.contents)
+        self.assertTrue(
+            '<h2 class="subtitle" id="subtitle">Subtitle</h2>'
+            in self.browser.contents)
+        self.assertFalse(
+            '<h1 class="title">Haupttitel</h1>' in self.browser.contents)
+        self.browser.getLink("de").click()
+        self.assertFalse(
+            '<h1 class="title">Main Title</h1>' in self.browser.contents)
+        self.assertTrue(
+            '<h1 class="title">Haupttitel</h1>' in self.browser.contents)
+        # The content can be rendered
+        IWorkflowState(document).setState(PUBLISHED)
+        self.browser.open(self.container_path + '/DOC3/display')
+        self.assertTrue(
+            '<h1 class="title">Haupttitel</h1>' in self.browser.contents)
+        # The page label (object title) is not displayed
+        self.assertFalse(
+            '<h1 class="ikoba-content-label">My REST Document</h1>'
+            in self.browser.contents)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_document.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_document.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_document.py	(revision 12677)
@@ -0,0 +1,168 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for documents.
+"""
+import shutil
+import tempfile
+from StringIO import StringIO
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import createObject, queryUtility
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.ikoba.interfaces import (
+    IObjectHistory, IFileStoreNameChooser, IFileStoreHandler)
+from waeup.ikoba.imagestorage import DefaultStorage
+from waeup.ikoba.documents.interfaces import (
+    IDocumentsContainer, IPublicDocument, IPDFDocument,
+    IHTMLDocument, IRESTDocument)
+from waeup.ikoba.documents.container import DocumentsContainer
+from waeup.ikoba.documents.document import (
+    PDFDocument, HTMLDocument, RESTDocument,
+    DocumentFileNameChooser, DocumentFileStoreHandler)
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+class DocumentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IDocumentsContainer, DocumentsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IDocumentsContainer, DocumentsContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IPDFDocument, PDFDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                IPDFDocument, PDFDocument())
+            )
+        self.assertTrue(
+            verifyClass(
+                IHTMLDocument, HTMLDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                IHTMLDocument, HTMLDocument())
+            )
+        self.assertTrue(
+            verifyClass(
+                IRESTDocument, RESTDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                IRESTDocument, RESTDocument())
+            )
+        return
+
+    def test_addDocument(self):
+        container = DocumentsContainer()
+        document = createObject(u'waeup.HTMLDocument')
+        document.document_id = u'DOC'
+        container.addDocument(document)
+        self.assertEqual(container[document.document_id], document)
+        self.assertRaises(TypeError, container.addDocument, object())
+        self.assertEqual(document.document_id, 'DOC')
+        return
+
+    def test_document_workflow(self):
+        document = createObject(u'waeup.HTMLDocument')
+        IWorkflowInfo(document).fireTransition('create')
+        self.assertEqual(IWorkflowState(document).getState(), 'created')
+        return
+
+    def test_document_history(self):
+        doc = createObject(u'waeup.HTMLDocument')
+        IWorkflowInfo(doc).fireTransition('create')
+        messages = ' '.join(doc.history.messages)
+        self.assertTrue('Document created by system' in messages)
+        return
+
+
+class DocumentFileTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(DocumentFileTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(DocumentFileTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_file_store_handler_util_accessible(self):
+        # we can get an IFileStoreHandler utility for documents
+        handler = queryUtility(IFileStoreHandler, name='file-document')
+        self.assertTrue(
+            isinstance(handler, DocumentFileStoreHandler))
+        return
+
+    def test_file_store_handler(self):
+        store = DefaultStorage()
+        handler = queryUtility(IFileStoreHandler, name='file-document')
+        result = handler.pathFromFileID(
+            store, '/fake-root', '__any_marker__sample.jpg')
+        self.assertEqual(
+            result, '/fake-root/documents/sample')
+        return
+
+    def test_file_store_handler_create(self):
+        # we can create files in image store with the document file
+        # store handler
+        store = DefaultStorage(self.workdir)
+        handler = queryUtility(IFileStoreHandler, name='file-document')
+        file_obj, path, waeup_file = handler.createFile(
+            store, store.root, 'sample.jpg', '__file_document__sample',
+            StringIO('I am a JPG file'))
+        self.assertEqual(path[-20:], 'documents/sample.jpg')
+        return
+
+    def test_iface(self):
+        # make sure we implement promised interfaces
+        obj = DocumentFileNameChooser(None) # needs a context normally
+        verifyClass(IFileStoreNameChooser, DocumentFileNameChooser)
+        verifyObject(IFileStoreNameChooser, obj)
+        return
+
+    def test_name_chooser_available(self):
+        # we can get a name chooser for document objects as adapter
+        doc = PDFDocument()
+        chooser = IFileStoreNameChooser(doc)
+        self.assertTrue(chooser is not None)
+        return
+
+    def test_name_chooser_document(self):
+        # we can get an image filename for documents not in a container
+        doc = PDFDocument()
+        doc.document_id = u'DOC'
+        chooser = IFileStoreNameChooser(doc)
+        result = chooser.chooseName('sample.jpg')
+        # the file would be stored in a ``_default`` directory.
+        self.assertEqual(
+            result, '__file-document__sample_DOC.jpg')
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_export.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_export.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/tests/test_export.py	(revision 12677)
@@ -0,0 +1,138 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the document exporter.
+"""
+import os
+import grok
+import datetime
+from cStringIO import StringIO
+from zope.component import queryUtility, getUtility
+from zope.event import notify
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.ikoba.interfaces import (
+    ICSVExporter, IExtFileStore, IFileStoreNameChooser)
+from waeup.ikoba.documents.export import (
+    PDFDocumentExporter, HTMLDocumentExporter)
+from waeup.ikoba.documents.document import PDFDocument, HTMLDocument
+from waeup.ikoba.documents.tests.test_batching import DocumentImportExportSetup
+from waeup.ikoba.testing import FunctionalLayer
+
+
+class PDFDocumentExporterTest(DocumentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PDFDocumentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = PDFDocumentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, PDFDocumentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an document exporter as utility
+        result = queryUtility(ICSVExporter, name="pdfdocuments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export a document
+        exporter = PDFDocumentExporter()
+        exporter.export([self.pdfdocument], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,description,document_id,history,state,title,users_with_local_roles\r\n'
+            'PDFDocument,,DOC1,'
+            '[u\'2014-12-21 16:55:16 UTC - Document created by system\'],'
+            'created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all documents
+        exporter = PDFDocumentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,description,document_id,history,state,title,users_with_local_roles\r\n'
+            'PDFDocument,,DOC1,'
+            '[u\'2014-12-21 16:55:16 UTC - Document created by system\'],'
+            'created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
+
+
+class HTMLDocumentExporterTest(DocumentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(HTMLDocumentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = HTMLDocumentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, HTMLDocumentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an document exporter as utility
+        result = queryUtility(ICSVExporter, name="htmldocuments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export a document
+        exporter = HTMLDocumentExporter()
+        exporter.export([self.htmldocument], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,description,document_id,history,html_dict,'
+            'html_multilingual,state,title,users_with_local_roles\r\n'
+            'HTMLDocument,,DOC2,'
+            '[u\'2014-12-21 16:50:28 UTC - Document created by system\'],'
+            '{},,created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all documents
+        exporter = HTMLDocumentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,description,document_id,history,html_dict,'
+            'html_multilingual,state,title,users_with_local_roles\r\n'
+            'HTMLDocument,,DOC2,'
+            '[u\'2014-12-21 16:50:28 UTC - Document created by system\'],'
+            '{},,created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/utils.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/utils.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/utils.py	(revision 12677)
@@ -0,0 +1,42 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""General helper functions and utilities for the documents.
+"""
+import grok
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import CREATED, PUBLISHED
+from waeup.ikoba.documents.interfaces import IDocumentsUtils
+
+
+class DocumentsUtils(grok.GlobalUtility):
+    """A collection of methods subject to customization.
+    """
+    grok.implements(IDocumentsUtils)
+
+    TRANSLATED_DOCUMENT_STATES = {
+        CREATED: _('created'),
+        PUBLISHED:_('published')
+        }
+
+    DOCTYPES_DICT = {
+        'PDFDocument': _('PDF Document'),
+        'HTMLDocument': _('HTML Document'),
+        'RESTDocument': _('REST Document'),
+        }
+
+    SELECTABLE_DOCTYPES_DICT = DOCTYPES_DICT
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/viewlets.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/viewlets.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/viewlets.py	(revision 12677)
@@ -0,0 +1,92 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Viewlet components for documents offered for download by the company.
+"""
+
+import grok
+from waeup.ikoba.browser.viewlets import (
+    PrimaryNavTab, ManageActionButton, AddActionButton)
+from waeup.ikoba.interfaces import IIkobaObject
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.browser.viewlets import PrimaryNavTab
+
+from waeup.ikoba.documents.interfaces import (
+    IDocumentsContainer, IPublicDocument)
+from waeup.ikoba.documents.browser import (
+    DocumentsContainerManageFormPage, DocumentsContainerPage,
+    DocumentManageFormPage, DocumentDisplayFormPage)
+
+
+grok.context(IIkobaObject)  # Make IIkobaObject the default context
+grok.templatedir('browser_templates')
+
+
+class DocumentsTab(PrimaryNavTab):
+    """Documents tab in primary navigation.
+    """
+
+    grok.context(IIkobaObject)
+    grok.order(1)
+    grok.require('waeup.viewDocuments')
+    grok.name('documentstab')
+
+    pnav = 2
+    tab_title = _(u'Documents')
+
+    @property
+    def link_target(self):
+        return self.view.application_url('documents')
+
+class DocumentsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IDocumentsContainer)
+    grok.view(DocumentsContainerPage)
+    grok.require('waeup.manageDocuments')
+    text = _('Manage')
+    target = 'manage'
+
+
+class DocumentViewActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IPublicDocument)
+    grok.view(DocumentManageFormPage)
+    grok.require('waeup.manageDocuments')
+    text = _('View')
+    target = 'index'
+    icon = 'actionicon_view.png'
+
+
+class DocumentManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IPublicDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.manageDocuments')
+    text = _('Manage')
+    target = 'manage'
+
+
+class DocumentTrigTransActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IPublicDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.manageDocuments')
+    icon = 'actionicon_trigtrans.png'
+    text = _(u'Transition')
+    target = 'trigtrans'
+
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/workflow.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/workflow.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/documents/workflow.py	(revision 12677)
@@ -0,0 +1,95 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Workflow for documents.
+"""
+import grok
+from zope.component import getUtility
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import (
+    IWorkflowState, IWorkflowTransitionEvent, InvalidTransitionError)
+from waeup.ikoba.interfaces import (
+    IObjectHistory, IIkobaWorkflowInfo,
+    SimpleIkobaVocabulary,
+    CREATED, PUBLISHED)
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.workflow import IkobaWorkflow, IkobaWorkflowInfo
+from waeup.ikoba.documents.interfaces import IPublicDocument
+
+
+PUBLISHING_TRANSITIONS = (
+    Transition(
+        transition_id = 'create',
+        title = _('Create document'),
+        source = None,
+        condition = NullCondition,
+        msg = _('Document created'),
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'publish',
+        title = _('Publish document'),
+        source = CREATED,
+        condition = NullCondition,
+        msg = _('Document published'),
+        destination = PUBLISHED),
+
+    Transition(
+        transition_id = 'retract',
+        title = _('Retract document'),
+        source = PUBLISHED,
+        condition = NullCondition,
+        msg = _('Document retracted'),
+        destination = CREATED),
+    )
+
+publishing_workflow = IkobaWorkflow(PUBLISHING_TRANSITIONS)
+
+class PublishingWorkflowState(WorkflowState, grok.Adapter):
+    """An adapter to adapt Document objects to workflow states.
+    """
+    grok.context(IPublicDocument)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.publishing.state'
+    state_id = 'wf.publishing.id'
+
+
+class PublishingWorkflowInfo(IkobaWorkflowInfo, grok.Adapter):
+    """Adapter to adapt CustomerDocument objects to workflow info objects.
+    """
+    grok.context(IPublicDocument)
+    grok.provides(IIkobaWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = publishing_workflow
+
+
+@grok.subscribe(IPublicDocument, IWorkflowTransitionEvent)
+def handle_public_document_transition_event(obj, event):
+    """Append message to document history and log file.
+
+    """
+    msg = event.transition.user_data['msg']
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    try:
+        grok.getSite().logger.info('%s - %s' % (obj.document_id, msg))
+    except (TypeError, AttributeError):
+        pass
+    return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/fees.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/fees.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/fees.py	(revision 12677)
@@ -0,0 +1,259 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components for fee management.
+
+Fees here are considered as a pair <PARAMS, VALUES> with both being a
+nested tuple. This is a way to describe a table with nested columns
+and its values.
+
+For example let's have a look at the following table:
+
+  +------+------------------+----------------------+
+  |      | locals           | non-locals           |
+  +------+--------+---------+------------+---------+
+  |      | art    | science | art        | science |
+  +------+--------+---------+------------+---------+
+  |fac1  |    1.1 |     1.2 |        1.3 |     1.4 |
+  +------+--------+---------+------------+---------+
+  |fac2  |    2.1 |     2.2 |        2.3 |     2.4 |
+  +------+--------+---------+------------+---------+
+
+This table contains parameters (all the headings) and numbers. The
+numbers are our VALUES while the headings are our PARAMS.
+
+Now the parameters are organized in a specific way: each value
+represents exactly one heading out of the following sets: {fac1,
+fac2}, {locals, non-locals}, {art, science}.
+
+The value 1.2, for instance, stands for 'fac1' and 'locals' and
+'science' (and it is the only value for this combination of
+parameters).
+
+We can therefore write the set of parameters as a tuple of tuples like
+this::
+
+  (('locals', 'non-locals'), ('art', 'science'), ('fac1', 'fac2'))
+
+This way we define how parameters must be used with an instance of the
+:class:`FeeTable` class below and its methods. Please note that order
+of elements matters in tuples!
+
+We can set whatever headers we like and the order is not important,
+until we want to set/get the single values.
+
+The values then must be set accordingly. If the above tuple defines
+the parameters, then the following nested tuple represents respective
+values that can be assigned to a fixed set of parameters::
+
+  (
+   ((1.1, 2.1),   # locals, art, fac1/fac2
+    (1.2, 2.2)),  # locals, science, fac1/fac2
+   ((1.3, 2.3),   # non-locals, art, fac1/fac2
+    (1.4, 2.4)),  # non-locals, science, fac1/fac2
+  )
+
+This is complicated. Let's have a closer look. If we have N param
+tuples, then the values must be delivered with a nesting level of N as
+well:
+
+  params := (A, B, C)  ->  N=3 -> (((...)))  [tuples with N levels]
+
+Each inner tuple must contain as much elements as the last param. So,
+if C contains three headings, then all inner tuples must be triples:
+
+  params := (A, B, C), C:= ('f1', 'f2', 'f3') -> (((v1, v2, v3), ...))
+
+For practical purposes you can imagine the above values as a table as
+well:
+
+  +-----------+---------+-------+--------+
+  |           |         |  fac1 |   fac2 |
+  +-----------+---------+-------+--------+
+  |locals     | art     |   1.1 |    2.1 |
+  |           | science |   1.2 |    2.2 |
+  |non-locals | art     |   1.3 |    2.3 |
+  |           | science |   1.4 |    2.4 |
+  +-----------+---------+-------+--------+
+
+Now, if you look closely and read the table from left to right
+column-wise and from top to bottom, you will notice that the order of
+appearances of headings is exactly like in the `params` tuple defined
+above:
+
+  ('locals', 'non-locals'), ('art', 'science'), ('fac1', 'fac2')
+
+The only condition is, that column headings are not nested. If they
+are, but row headings are not, then simply 'turn' the table by 90
+degrees and you have a table that can be used as a pattern for input.
+
+This should help to create fee tables out of tuples.
+
+We'll try to provide more convenient input methods in future.
+
+"""
+import grok
+import itertools
+from zope import schema
+from zope.interface import Interface, implementer, Attribute
+from waeup.ikoba.utils.helpers import product
+
+class IFees(Interface):
+    """A utility storing fee tables for different academic sessions.
+    """
+    tables = Attribute("""A dict of fee tables with session as key.""")
+
+class IFeeTable(Interface):
+
+    values = Attribute("""Nested tuples containing cell values.""")
+    params = Attribute("""Parameter names required to get"""
+                            """or set the fee table values.""")
+
+    def as_table():
+        """Return fees as a tuple of tuples (of tuples).
+        """
+
+    def as_dict():
+        """Return fees as dict.
+        """
+
+    def import_values(params, values):
+        """Set fees with parameter names and value dictionary.
+        """
+
+    def set_fee(params_tuple, value):
+        """Set a single entry in fee table.
+        """
+
+    def get_fee(params_tuple):
+        """Get single value in fee table denoted by `params_tuple`.
+        """
+
+def nested_list(nested_tuple):
+    """Turn nested tuples in nested lists: ((A,B),(C,)) -> [[A,B],[C,]]
+    """
+    if isinstance(nested_tuple, tuple):
+        return [nested_list(x) for x in nested_tuple]
+    return nested_tuple
+
+def nested_tuple(nested_list):
+    """Turn nested lists in nested tuples: [[A,B],[C,]] -> ((A,B),(C,))
+    """
+    if isinstance(nested_list, list):
+        return tuple([nested_tuple(x) for x in nested_list])
+    return nested_list
+
+@implementer(IFeeTable)
+class FeeTable(grok.Model):
+
+    params = ()
+    values = ()
+
+    def __init__(self, params=(), values=None):
+        if params and values is not None:
+            self.import_values(params=params, values=values)
+        return
+
+    def as_table(self, row_index=None):
+        pass
+
+    def as_dict(self):
+        """Get all fees as dict with param tuples as keys.
+
+        Example:
+
+          {
+            ('locals', 'art'): 1,
+            ('locals', 'science'): 2,
+            ('non-locals', 'art'): 3,
+            ('non-locals', 'science'): 4,
+          }
+        """
+        result = dict()
+        combinations = list(itertools.product(*self.params))
+        for item in combinations:
+            result[item] = self.get_fee(item)
+        return result
+
+    def import_values(self, params=(), values=()):
+        if params and values:
+            self._check(params, values)
+        self.values = values
+        self.params = params
+        return
+
+    def set_fee(self, params_tuple, value):
+        """Set a single entry in fee table.
+
+        The `params_tuple` is described more thoroughly in
+        :meth:`get_fee()`.
+        """
+        values_list = nested_list(self.values)
+        sublist = values_list
+        for num, param in enumerate(params_tuple):
+            col_name_tuple = self.params[num]
+            if param not in col_name_tuple:
+                raise KeyError("%s not in %s" % (param, col_name_tuple))
+            if num == len(self.params) - 1:
+                sublist[col_name_tuple.index(param)] = value
+            else:
+                sublist = sublist[col_name_tuple.index(param)]
+        self.values = nested_tuple(values_list)
+        pass
+
+    def get_fee(self, params_tuple):
+        """Get value in fee table denoted by `param_tuple`.
+
+        The `params_tuple` must contain exactly one value for each
+        tuple in instance `params`. For instance, if we have instance
+        tuples (row and col names)
+
+          (('locals', 'non-locals'), ('art', 'science')),
+
+        then `params_tuple` must be a 2-length tuple with
+        exactly one of `locals`/`non-locals` and exactly one of
+        `art`/`science` in exactly that order (locals/non-locals
+        before art/science).
+        """
+        result = self.values
+        for num, param in enumerate(params_tuple):
+            col_name_tuple = self.params[num]
+            if param not in col_name_tuple:
+                raise KeyError("%s not in %s" % (param, col_name_tuple))
+            result = result[col_name_tuple.index(param)]
+        return result
+
+    def _check(self, params, values):
+        # XXX: something useful here, please
+        return
+
+DEFAULT_FEE_TABLE = FeeTable(
+    params = (('locals', 'non-locals'), ('art', 'science'),
+              ('fac1', 'fac2')),
+    values = (((1.1, 2.1), (1.2, 2.2)), ((1.3, 2.3), (1.4, 2.4)))
+    )
+
+
+@implementer(IFees)
+class Fees(grok.GlobalUtility):
+
+    grok.name('default')
+
+    def __init__(self):
+        self.tables = {
+            '2012': DEFAULT_FEE_TABLE,
+            }
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/frontpage.html
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/frontpage.html	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/frontpage.html	(revision 12677)
@@ -0,0 +1,102 @@
+<h1>Welcome to WAeUP.Ikoba
+<br>
+<p class="ikoba-content-desc">
+the Registration and Application Portal
+</p>
+</h1>
+
+<p>
+This is the default frontpage written in HTML.
+Ikoba's CSS design is based on Twitter's Bootstrap. Visit the
+<a href="http://getbootstrap.com/">Bootstrap documentation</a>
+for further information.
+</p>
+
+<p>
+Ikoba is part of the
+<a href="https://waeup.org/">West African eUniversity Project (WAeUP)</a>.
+It is a powerful and comprehensive Online Registration and Application System.
+The software is written in Python and built upon the Open Source
+<a href="http://grok.zope.org/">Grok Web Framework</a>.
+</p>
+
+<h2>1. Heading</h2>
+
+<p>Text</p>
+
+<h3>1.2 Heading</h3>
+
+Text
+
+<h2>2. Heading</h2>
+
+<p>Text</p>
+
+<h3>2.1 Heading</h3>
+
+<ol>
+<li>List item</li>
+<li>List item</li>
+<li>List item</li>
+</ol>
+
+<h3>2.2 Heading</h3>
+
+<ul>
+<li>List item</li>
+<li>List item</li>
+<li>List item</li>
+</ul>
+
+
+>>de<<
+<h1>Willkommen auf WAeUP.Ikoba,
+<br>
+<small>
+dem Anmelde- und Registrierungs-Portal
+</small>
+</h1>
+
+<p>
+Dies ist die Standard-Startseite des Portals, die in HTML geschrieben wurde.
+Ikobas css-Design basiert auf Twitters Bootstrap. Besuchen Sie die
+<a href="http://twitter.github.com/bootstrap/">Bootstrap-Dokumentation</a>
+auf GitHub, um weitere Informationen zu erhalten.
+</p>
+
+<p>
+Ikoba ist Teil des
+<a href="https://waeup.org/">West African eUniversity Project (WAeUP)</a>.
+Es ist ein leistungsstarkes and umfassendes Beantragungs- und Registrierungssystem, welches
+in Python geschrieben wurde und auf dem Open Source Web Framework
+<a href="http://grok.zope.org/">Grok</a>
+basiert.
+</p>
+
+<h2>1. Überschrift</h2>
+
+<p>Text</p>
+
+<h3>1.2 Überschrift</h3>
+
+Text
+
+<h2>2. Überschrift</h2>
+
+<p>Text</p>
+
+<h3>2.1 Überschrift</h3>
+
+<ol>
+<li>Listeneintrag</li>
+<li>Listeneintrag</li>
+<li>Listeneintrag</li>
+</ol>
+
+<h3>2.2 Überschrift</h3>
+
+<ul>
+<li>Listeneintrag</li>
+<li>Listeneintrag</li>
+<li>Listeneintrag</li>
+</ul>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/frontpage.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/frontpage.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/frontpage.rst	(revision 12677)
@@ -0,0 +1,69 @@
+Welcome to Ikoba
+===============
+
+This is the default frontpage of the portal written
+in `reStructuredText (reST)
+<http://docutils.sourceforge.net/docs/user/rst/quickref.html>`_
+markup language.
+
+Some more reST examples for getting started:
+
+1. Heading
+==========
+
+Text
+
+1.2 Heading
+-----------
+
+Text
+
+2. Heading
+==========
+
+Text
+
+2.1 Heading
+-----------
+
+Text
+
+2.2 Heading
+-----------
+
+Text
+
+>>de<<
+Willkommen in Ikoba
+==================
+
+Dies ist die Standard-Startseite des Portals. Sie wurde in der Markup-Sprache
+`reStructuredText (reST) <http://docutils.sourceforge.net/docs/user/rst/quickref.html>`_
+geschrieben.
+
+Weitere reST-Beispiele zur Einführung:
+
+1. Überschrift
+==============
+
+Text
+
+1.2 Überschrift
+---------------
+
+Text
+
+2. Überschrift
+==============
+
+Text
+
+2.1 Überschrift
+---------------
+
+Text
+
+2.2 Überschrift
+---------------
+
+Text
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/ftesting.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/ftesting.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/ftesting.zcml	(revision 12677)
@@ -0,0 +1,78 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:ikoba="http://namespaces.waeup.org/ikoba"
+   i18n_domain="waeup.ikoba"
+   package="waeup.ikoba"
+   >
+
+  <include package="grok" />
+  <includeOverrides package="waeup.ikoba" />
+  <include package="waeup.ikoba" file="mail.zcml" />
+  <include package="waeup.ikoba" file="async_single.zcml" />
+
+  <!-- Where should the datacenter reside by default? -->
+  <ikoba:datacenter
+      path="../../../parts/test/datacenter" />
+
+  <!-- Where can the Paypal config be found? -->
+  <!--
+  <ikoba:paypalconf
+      path="../../../etc/paypal-testing.conf" />
+  -->
+
+  <!-- Typical functional testing security setup -->
+  <securityPolicy
+      component="zope.securitypolicy.zopepolicy.ZopeSecurityPolicy"
+      />
+
+  <unauthenticatedPrincipal
+      id="zope.anybody"
+      title="Unauthenticated User"
+      />
+  <unauthenticatedGroup id="zope.Anybody"
+      title="Unauthenticated Users" />
+  <authenticatedGroup id="zope.Authenticated"
+      title="Authenticated Users" />
+  <everybodyGroup id="zope.Everybody"
+      title="All Users" />
+
+  <!-- grant
+      permission="zope.View"
+      principal="zope.Anybody"
+      / -->
+  <grant
+      permission="zope.app.dublincore.view"
+      principal="zope.Anybody" />
+  <grant
+      permission="zope.View"
+      principal="zope.Authenticated"
+      />
+  <grant
+      permission="zope.app.dublincore.view"
+      principal="zope.Authenticated"
+      />
+  <grant
+      permission="waeup.Public"
+      principal="zope.Everybody"
+      />
+  <grant 
+      permission="waeup.Anonymous"
+      principal="zope.Anybody"
+      />
+  <grant
+      permission="waeup.Authenticated"
+      principal="zope.Authenticated" />
+
+
+  <principal
+      id="zope.mgr"
+      title="Manager"
+      login="mgr"
+      password="mgrpw"
+      />
+
+  <role id="zope.Manager" title="Site Manager" />
+  <grantAll role="zope.Manager" />
+  <grant role="zope.Manager" principal="zope.mgr" />
+
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/README.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/README.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/README.txt	(revision 12677)
@@ -0,0 +1,216 @@
+waeup.ikoba.image -- handling image files
+========================================
+
+The image file widget is built on top of the :class:`IkobaImageFile` object::
+
+  >>> from waeup.ikoba.image import IkobaImageFile
+  >>> file = IkobaImageFile('foo.jpg', 'mydata')
+  >>> file.filename
+  'foo.jpg'
+  >>> file.data
+  'mydata'
+  >>> file.size
+  6
+  >>> f = file.file
+  >>> f.read()
+  'mydata'
+
+We can also create IkobaImageFile 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 IkobaImageFile 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 = IkobaImageFile('foo.jpg', '1')
+
+The data is now '1', referring to the real file::
+
+  >>> file.data
+  '1'
+
+Retrieving the file results in the real file::
+
+  >>> f = file.file
+  >>> f.read()
+  'test data'
+
+We can also retrieve its size::
+
+  >>> int(file.size)
+  9
+
+Now let's disable tramline in our utility::
+
+  >>> class TramlineFileRetrieval(TramlineFileRetrievalBase):
+  ...     def getTramlinePath(self):
+  ...        return dirpath
+  ...     def isTramlineEnabled(self):
+  ...        return False
+  >>> component.provideUtility(TramlineFileRetrieval(), IFileRetrieval)
+
+We expect the same behavior as when tramline is not installed::
+
+  >>> file = IkobaImageFile('foo.jpg', 'data')
+  >>> f = file.file
+  >>> f.read()
+  'data'
+  >>> file.size
+  4
+
+Clean up:
+
+  >>> import shutil
+  >>> shutil.rmtree(dirpath)
+
+Support for :mod:`waeup.ikoba.imagestorage`
+------------------------------------------
+
+The behaviour shown above can be used for any Zope3 application. With
+:mod:`waeup.ikoba` we use a special file retrieval utility defined in
+:mod:`waeup.ikoba.imagestorage`. As this utility is based on
+:mod:`waeup.ikoba` 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.ikoba.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.ikoba.image.image.IkobaImageFile'>, 'foo')
+
+which means: `ImageFile` fields should better contain
+:class:`IkobaImageFile` instances.
+
+We can store normal :class:`IkobaImageFile` instances:
+
+  >>> field.validate(IkobaImageFile('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(
+  ...     IkobaImageFile('bar.jpg', '123456789012')) is None
+  True
+
+  >>> field.validate(
+  ...     IkobaImageFile('bar.jpg', '12345')) is None
+  True
+
+the following are not:
+
+  >>> field.validate(
+  ...     IkobaImageFile('bar.jpg', '1234567890123'))
+  Traceback (most recent call last):
+  ...
+  TooBig: ('bar.jpg', '13 bytes (max: 12 bytes)')
+
+  >>> field.validate(
+  ...     IkobaImageFile('bar.jpg', '1234'))
+  Traceback (most recent call last):
+  ...
+  TooSmall: ('bar.jpg', '4 bytes (min: 5 bytes)')
+
+
+Broken Unequality Comparison
+----------------------------
+
+IkobaImageFile does not reproduce the broken unequal comparison from
+its base:
+
+  >>> f1 = IkobaImageFile('bar.jpg', '123456789')
+  >>> f2 = IkobaImageFile('bar.jpg', '123456789')
+  >>> f3 = IkobaImageFile('baz.jpg', '1234')
+  >>> f1 == f2
+  True
+
+  >>> f1 != f2
+  False
+
+  >>> f1 == f3
+  False
+
+  >>> f1 != f3
+  True
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/__init__.py	(revision 12677)
@@ -0,0 +1,10 @@
+"""Image file components.
+
+Including schemas, widgets and the content components.
+"""
+from waeup.ikoba.image.image import IkobaImageFile, createIkobaImageFile
+
+__all__ = [
+    "IkobaImageFile",
+    "createIkobaImageFile",
+    ]
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/__init__.py	(revision 12677)
@@ -0,0 +1,10 @@
+# The public API part
+# Contains the widgets defined for image files.
+from waeup.ikoba.image.browser.widget import (
+    EncodingImageFileWidget, ThumbnailWidget,
+    )
+
+__all__ = (
+    EncodingImageFileWidget,
+    ThumbnailWidget,
+    )
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/__init__.py	(revision 12677)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/image.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/image.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/image.txt	(revision 12677)
@@ -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.ikoba.image import IkobaImageFile
+  >>> testimage = os.path.join(os.path.dirname(__file__), 'sample.jpg')
+  >>> testimage2 = os.path.join(os.path.dirname(__file__), 'sample2.jpg')
+  >>> some_file = IkobaImageFile('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.ikoba.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.ikoba.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.ikoba.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 = IkobaImageFile('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.ikoba.image.IkobaImageFile` 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 = IkobaImageFile(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 IkobaImageFile object with that
+file_id stored:
+
+  >>> from waeup.ikoba.image import createIkobaImageFile
+  >>> image = createIkobaImageFile(
+  ...     '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 createIkobaImageFile 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.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/test_image.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/test_image.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/test_image.py	(revision 12677)
@@ -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.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/test_views.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/test_views.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/tests/test_views.py	(revision 12677)
@@ -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.ikoba.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.ikoba.image.browser.views` module.
+    """
+    @classmethod
+    def setUp(cls):
+        grok.testing.grok('waeup.ikoba.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.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/views.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/views.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/views.py	(revision 12677)
@@ -0,0 +1,48 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Views for images and the like.
+"""
+import mimetypes
+import grok
+from hurry.file.interfaces import IHurryFile
+
+class HurryFileView(grok.View):
+    """A view for hurry files stored in the ZODB.
+
+    Normally these files cannot be 'browsed' but with a view we can
+    stream them to the browser. We only have to set the appropriate
+    MIME type, which happens in the `render` method.
+    """
+
+    grok.name('index.html')
+    grok.context(IHurryFile)
+
+    def render(self):
+        """Output the data of context.
+
+        Beside this we set the output MIME type according to the
+        context's filename.
+        """
+        content_type, encoding = mimetypes.guess_type(
+            self.context.filename.lower())
+        if content_type is not None:
+            self.response.setHeader('Content-Type', content_type)
+        fd = self.context.file
+        contents = fd.read()
+        fd.close()
+        return contents
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/widget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/widget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/browser/widget.py	(revision 12677)
@@ -0,0 +1,166 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Image file widgets.
+"""
+import os
+from waeup.ikoba.image import IkobaImageFile
+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 IkobaImageFile(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 IkobaImageFile(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.ikoba/branches/uli-payments/src/waeup/ikoba/image/image.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/image.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/image.py	(revision 12677)
@@ -0,0 +1,49 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Components for handling image files.
+"""
+from hurry.file import HurryFile
+from hurry.file.interfaces import IFileRetrieval
+from zope.component import getUtility
+from zope.interface import implements
+from waeup.ikoba.image.interfaces import IIkobaImageFile
+
+class IkobaImageFile(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:`IkobaImageFile` you should use
+    :func:`createIkobaImageFile`.
+    """
+    implements(IIkobaImageFile)
+
+    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 createIkobaImageFile(filename, f):
+    retrieval = getUtility(IFileRetrieval)
+    return retrieval.createFile(filename, f)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/interfaces.py	(revision 12677)
@@ -0,0 +1,32 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Interfaces for waeup.ikoba.image
+"""
+from hurry.file.interfaces import IFile, IFileRetrieval, IHurryFile
+
+class IImageFile(IFile):
+    """Image file field.
+    """
+
+class IIkobaImageFile(IHurryFile):
+    """Image file.
+    """
+
+class IImageFileRetrieval(IFileRetrieval):
+    """A file retrieval for images.
+    """
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/schema.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/schema.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/schema.py	(revision 12677)
@@ -0,0 +1,84 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Image file schemas.
+"""
+from zope.interface import implements
+from zope.schema import MinMaxLen
+from zope.schema.interfaces import TooBig, TooSmall
+from hurry.file.schema import File
+from waeup.ikoba.image.interfaces import IImageFile
+from waeup.ikoba.image.image import IkobaImageFile
+
+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 = IkobaImageFile
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/tests.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/tests.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/image/tests.py	(revision 12677)
@@ -0,0 +1,37 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import unittest
+
+from zope.testing import doctest
+from zope.app.testing import placelesssetup
+from zope import component
+
+from hurry.file.file import IdFileRetrieval
+from hurry.file.interfaces import IFileRetrieval
+
+def fileSetUp(doctest):
+    placelesssetup.setUp()
+    component.provideUtility(IdFileRetrieval(), IFileRetrieval)
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite(
+            'README.txt',
+            setUp=fileSetUp, tearDown=placelesssetup.tearDown,
+            ),
+        ))
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/imagestorage.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/imagestorage.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/imagestorage.py	(revision 12677)
@@ -0,0 +1,580 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""A storage for image (and other) files.
+
+A few words about storing files with ``waeup.ikoba``. The need for this
+feature arised initially from the need to store passport files for
+customers. These files are dynamic (can be changed
+anytime), mean a lot of traffic and cost a lot of memory/disk space.
+
+**Design Basics**
+
+While one *can* store images and similar 'large binary objects' aka
+blobs in the ZODB, this approach quickly becomes cumbersome and
+difficult to understand. The worst approach here would be to store
+images as regular byte-stream objects. ZODB supports this but
+obviously access is slow (data must be looked up in the one
+``Data.fs`` file, each file has to be sent to the ZEO server and back,
+etc.).
+
+A bit less worse is the approach to store images in the ZODB but as
+Blobs. ZODB supports storing blobs in separate files in order to
+accelerate lookup/retrieval of these files. The files, however, have
+to be sent to the ZEO server (and back on lookups) which means a
+bottleneck and will easily result in an increased number of
+``ConflictErrors`` even on simple reads.
+
+The advantage of both ZODB-geared approaches is, of course, complete
+database consistency. ZODB will guarantee that your files are
+available under some object name and can be handled as any other
+Python object.
+
+Another approach is to leave the ZODB behind and to store images and
+other files in filesystem directly. This is faster (no ZEO contacts,
+etc.), reduces probability of `ConflictErrors`, keeps the ZODB
+smaller, and enables direct access (over filesystem) to the
+files. Furthermore steps might be better understandable for
+third-party developers. We opted for this last option.
+
+**External File Store**
+
+Our implementation for storing-files-API is defined in
+:class:`ExtFileStore`. An instance of this file storage (which is also
+able to store non-image files) is available at runtime as a global
+utility implementing :class:`waeup.ikoba.interfaces.IExtFileStore`.
+
+The main task of this central component is to maintain a filesystem
+root path for all files to be stored. It also provides methods to
+store/get files under certain file ids which identify certain files
+locally.
+
+So, to store a file away, you can do something like this:
+
+  >>> from StringIO import StringIO
+  >>> from zope.component import getUtility
+  >>> from waeup.ikoba.interfaces import IExtFileStore
+  >>> store = getUtility(IExtFileStore)
+  >>> store.createFile('myfile.txt', StringIO('some file content'))
+
+All you need is a filename and the file-like object containing the
+real file data.
+
+This will store the file somewhere (you shouldn't make too much
+assumptions about the real filesystem path here).
+
+Later, we can get the file back like this:
+
+  >>> store.getFile('myfile')
+  <open file ...>
+
+Please note, that we ask for ``myfile`` instead of ``myfile.jpg`` as
+the file id should not make a difference for different filename
+extensions. The file id for ``sample.jpg`` thus could simply be
+``sample``.
+
+What we get back is a file or file-like object already opened for
+reading:
+
+  >>> store.getFile('myfile').read()
+  'some file content'
+
+**Handlers: Special Places for Special Files**
+
+The file store supports special handling for certain files. For
+example we want some customer images to be stored in a different directory
+than other customer images, etc. Because the file store cannot know all
+details about these special treatment of certain files, it looks up
+helpers (handlers) to provide the information it needs for really
+storing the files at the correct location.
+
+That a file stored in filestore needs special handling can be
+indicated by special filenames. These filenames start with a marker like
+this::
+
+  __<MARKER-STRING>__real-filename
+
+Please note the double underscores before and after the marker
+string. They indicate that all in between is a marker.
+
+If you store a file in file store with such a filename (we call this a
+`file_id` to distuingish it from real world filenames), the file store
+will look up a handler for ``<MARKER-STRING>`` and pass it the file to
+store. The handler then will return the internal path to store the
+file and possibly do additional things as well like validating the
+file or similar.
+
+Examples for such a file store handler can be found in the
+:mod:`waeup.ikoba.customers.customer` module. Please see also the
+:class:`DefaultFileStoreHandler` class below for more details.
+
+The file store looks up handlers by utility lookups: it looks for a
+named utiliy providing
+:class:`waeup.ikoba.interfaces.IFileStoreHandler` and named like the
+marker string (without leading/trailing underscores) in lower
+case. For example if the file id would be
+
+  ``__IMG_USER__manfred``
+
+then the looked up utility should be registered under name
+
+  ``img_user``
+
+and provide :class:`waeup.ikoba.interfaces.IFileStoreHandler`. If no
+such utility can be found, a default handler is used instead
+(see :class:`DefaultFileStoreHandler`).
+
+**About File IDs and Filenames**
+
+In the waeup.ikoba package we want to store documents like CVs,
+photographs, and similar. Each of this documents might come into the
+system with different filename extensions. This could be a problem as
+the browser components might have to set different response headers
+for different filetypes and we nevertheless want to make sure that
+only one file is stored per document. For instance we don't want
+``passport.jpg`` *and* ``passport.png`` but only one of them.
+
+The default components like :class:`DefaultFileStoreHandler` take care
+of this by searching the filesystem for already existing files with
+same file id and eventually removing them.
+
+Therefore file ids should never include filename extensions (except if
+you only support exactly one filename extension for a certain
+document). The only part where you should add an extension (and it is
+important to do so) is when creating new files: when a file was
+uploaded you can pass in the filename (including the filename
+extension) and the file stored in external file store will (most
+probably) have a different name but the same extension as the original
+file.
+
+When looking for the file, you however only have to give the file id
+and the handlers should find the right file for you, regardless of the
+filename extension it has.
+
+**Context Adapters: Knowing Your Family**
+
+Often the internal filename or file id of a file depends on a
+context. For example when we store passport photographs of customers,
+then each image belongs to a certain customer instance. It is not
+difficult to maintain such a connection manually: Say every customer
+had an id, then we could put this id into the filename as well and
+would build the filename to store/get the connected file by using that
+filename. You then would create filenames of a format like this::
+
+  __<MARKER-STRING>__customer0001
+
+where ``customer0001`` would tell exactly which customer you can see
+on the photograph. You notice that the internal file id might have
+nothing to do with once uploaded filenames. The id above could have
+been uploaded with filename ``manfred.jpg`` but with the new file id
+we are able to find the file again later.
+
+Unfortunately it might soon get boring or cumbersome to retype this
+building of filenames for a certain type of context, especially if
+your filenames take more of the context into account than only a
+simple id.
+
+Therefore you can define filename building for a context as an adapter
+that then could be looked up by other components simply by doing
+something like:
+
+  >>> from waeup.ikoba.interfaces import IFileStoreNameChooser
+  >>> file_id = IFileStoreNameChooser(my_context_obj)
+
+If you later want to change the way file ids are created from a
+certain context, you only have to change the adapter implementation
+accordingly.
+
+Note, that this is only a convenience component. You don't have to
+define context adapters but it makes things easier for others if you
+do, as you don't have to remember the exact file id creation method
+all the time and can change things quick and in only one location if
+you need to do so.
+
+Please see the :class:`FileStoreNameChooser` default implementation
+below for details.
+
+"""
+import glob
+import grok
+import os
+import tempfile
+from hurry.file import HurryFile
+from hurry.file.interfaces import IFileRetrieval
+from zope.component import queryUtility
+from zope.interface import Interface
+from waeup.ikoba.interfaces import (
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,)
+
+class FileStoreNameChooser(grok.Adapter):
+    """Default file store name chooser.
+
+    File store name choosers pick a file id, a string, for a certain
+    context object. They are normally registered as adapters for a
+    certain content type and know how to build the file id for this
+    special type of context.
+
+    Provides the :class:`waeup.ikoba.interfaces.IFileStoreNameChooser`
+    interface.
+
+    This default file name chosser accepts almost every name as long
+    as it is a string or unicode object.
+    """
+    grok.context(Interface)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name, attr=None):
+        """Check whether a given name (file id) is valid.
+
+        Raises a user error if the name is not valid.
+
+        For the default file store name chooser any name is valid as
+        long as it is a string.
+
+        The `attr` is not taken into account here.
+        """
+        if isinstance(name, basestring):
+            return True
+        return False
+
+    def chooseName(self, name, attr=None):
+        """Choose a unique valid file id for the object.
+
+        The given name may be taken into account when choosing the
+        name (file id).
+
+        chooseName is expected to always choose a valid name (that
+        would pass the checkName test) and never raise an error.
+
+        For this default name chooser we return the given name if it
+        is valid or ``unknown_file`` else. The `attr` param is not
+        taken into account here.
+        """
+        if self.checkName(name):
+            return name
+        return u'unknown_file'
+
+class ExtFileStore(object):
+    """External file store.
+
+    External file stores are meant to store files 'externally' of the
+    ZODB, i.e. in filesystem.
+
+    Most important attribute of the external file store is the `root`
+    path which gives the path to the location where files will be
+    stored within.
+
+    By default `root` is a ``'media/'`` directory in the root of the
+    datacenter root of a site.
+
+    The `root` attribute is 'read-only' because you normally don't
+    want to change this path -- it is dynamic. That means, if you call
+    the file store from 'within' a site, the root path will be located
+    inside this site (a :class:`waeup.ikoba.Company` instance). If
+    you call it from 'outside' a site some temporary dir (always the
+    same during lifetime of the file store instance) will be used. The
+    term 'temporary' tells what you can expect from this path
+    persistence-wise.
+
+    If you insist, you can pass a root path on initialization to the
+    constructor but when calling from within a site afterwards, the
+    site will override your setting for security measures. This way
+    you can safely use one file store for different sites in a Zope
+    instance simultanously and files from one site won't show up in
+    another.
+
+    An ExtFileStore instance is available as a global utility
+    implementing :class:`waeup.ikoba.interfaces.IExtFileStore`.
+
+    To add and retrieve files from the storage, use the appropriate
+    methods below.
+    """
+
+    grok.implements(IExtFileStore)
+
+    _root = None
+
+    @property
+    def root(self):
+        """Root dir of this storage.
+
+        The root dir is a readonly value determined dynamically. It
+        holds media files for sites or other components.
+
+        If a site is available we return a ``media/`` dir in the
+        datacenter storage dir.
+
+        Otherwise we create a temporary dir which will be remembered
+        on next call.
+
+        If a site exists and has a datacenter, it has always
+        precedence over temporary dirs, also after a temporary
+        directory was created.
+
+        Please note that retrieving `root` is expensive. You might
+        want to store a copy once retrieved in order to minimize the
+        number of calls to `root`.
+
+        """
+        site = grok.getSite()
+        if site is not None:
+            root = os.path.join(site['datacenter'].storage, 'media')
+            return root
+        if self._root is None:
+            self._root = tempfile.mkdtemp()
+        return self._root
+
+    def __init__(self, root=None):
+        self._root = root
+        return
+
+    def _pathFromFileID(self, file_id):
+        """Helper method to create filesystem path from FileID.
+
+        Used class-internally. Do not rely on this method when working
+        with an :class:`ExtFileStore` instance from other components.
+        """
+        marker, filename, base, ext = self.extractMarker(file_id)
+        handler = queryUtility(IFileStoreHandler, name=marker,
+                               default=DefaultFileStoreHandler())
+        path = handler.pathFromFileID(self, self.root, file_id)
+        return path
+
+    def getFile(self, file_id):
+        """Get a file stored under file ID `file_id`.
+
+        Returns a file already opened for reading.
+
+        If the file cannot be found ``None`` is returned.
+
+        This methods takes into account registered handlers for any
+        marker put into the file_id.
+
+        .. seealso:: :class:`DefaultFileStoreHandler`
+        """
+        path = self._pathFromFileID(file_id)
+        if not os.path.exists(path):
+            return None
+        fd = open(path, 'rb')
+        return fd
+
+    def getFileByContext(self, context, attr=None):
+        """Get a file for given context.
+
+        Returns a file already opened for reading.
+
+        If the file cannot be found ``None`` is returned.
+
+        This method takes into account registered handlers and file
+        name choosers for context types to build an intermediate file
+        id for the context and `attr` given.
+
+        Both, `context` and `attr` are used to find (`context`)
+        and feed (`attr`) an appropriate file name chooser.
+
+        This is a convenience method that internally calls
+        :meth:`getFile`.
+
+        .. seealso:: :class:`FileStoreNameChooser`,
+                     :class:`DefaultFileStoreHandler`.
+        """
+        file_id = IFileStoreNameChooser(context).chooseName(attr=attr)
+        return self.getFile(file_id)
+
+    def deleteFile(self, file_id):
+        """Delete file stored under `file_id` in storage.
+
+        The file is physically removed from filesystem.
+        """
+        path = self._pathFromFileID(file_id)
+        if not os.path.exists(path) or not os.path.isfile(path):
+            return
+        os.unlink(path)
+        return
+
+    def deleteFileByContext(self, context, attr=None):
+        """Remove file identified by `context` and `attr` if it exists.
+
+        This method takes into account registered handlers and file
+        name choosers for context types to build an intermediate file
+        id for the context and `attr` given.
+
+        Both, `context` and `attr` are used to find (`context`)
+        and feed (`attr`) an appropriate file name chooser.
+
+        This is a convenience method that internally calls
+        :meth:`getFile`.
+
+        .. seealso:: :class:`FileStoreNameChooser`,
+                     :class:`DefaultFileStoreHandler`.
+
+        """
+        file_id = IFileStoreNameChooser(context).chooseName(attr=attr)
+        return self.deleteFile(file_id)
+
+    def createFile(self, filename, f):
+        """Store a file.
+        """
+        root = self.root # Calls to self.root are expensive
+        file_id = os.path.splitext(filename)[0]
+        marker, filename, base, ext = self.extractMarker(filename)
+        handler = queryUtility(IFileStoreHandler, name=marker,
+                               default=DefaultFileStoreHandler())
+        f, path, file_obj = handler.createFile(
+            self, root, filename, file_id, f)
+        dirname = os.path.dirname(path)
+        if not os.path.exists(dirname):
+            os.makedirs(dirname, 0755)
+        open(path, 'wb').write(f.read())
+        return file_obj
+
+    def extractMarker(self, file_id):
+        """split filename into marker, filename, basename, and extension.
+
+        A marker is a leading part of a string of form
+        ``__MARKERNAME__`` followed by the real filename. This way we
+        can put markers into a filename to request special processing.
+
+        Returns a quadruple
+
+          ``(marker, filename, basename, extension)``
+
+        where ``marker`` is the marker in lowercase, filename is the
+        complete trailing real filename, ``basename`` is the basename
+        of the filename and ``extension`` the filename extension of
+        the trailing filename. See examples below.
+
+        Example:
+
+           >>> extractMarker('__MaRkEr__sample.jpg')
+           ('marker', 'sample.jpg', 'sample', '.jpg')
+
+        If no marker is contained, we assume the whole string to be a
+        real filename:
+
+           >>> extractMarker('no-marker.txt')
+           ('', 'no-marker.txt', 'no-marker', '.txt')
+
+        Filenames without extension give an empty extension string:
+
+           >>> extractMarker('no-marker')
+           ('', 'no-marker', 'no-marker', '')
+
+        """
+        if not isinstance(file_id, basestring) or not file_id:
+            return ('', '', '', '')
+        parts = file_id.split('__', 2)
+        marker = ''
+        if len(parts) == 3 and parts[0] == '':
+            marker = parts[1].lower()
+            file_id = parts[2]
+        basename, ext = os.path.splitext(file_id)
+        return (marker, file_id, basename, ext)
+
+grok.global_utility(ExtFileStore, provides=IExtFileStore)
+
+class DefaultStorage(ExtFileStore):
+    """Default storage for files.
+
+    Registered globally as utility for
+    :class:`hurry.file.interfaces.IFileRetrieval`.
+    """
+    grok.provides(IFileRetrieval)
+
+grok.global_utility(DefaultStorage, provides=IFileRetrieval)
+
+class DefaultFileStoreHandler(grok.GlobalUtility):
+    """A default handler for external file store.
+
+    This handler is the fallback called by external file stores when
+    there is no or an unknown marker in the file id.
+
+    Registered globally as utility for
+    :class:`waeup.ikoba.interfaces.IFileStoreHandler`.
+    """
+    grok.implements(IFileStoreHandler)
+
+    def _searchInPath(self, path):
+        """Get complete path of any existing file starting with `path`.
+
+        If no such file can be found, return input path.
+
+        If multiple such files exist, return the first one.
+
+        **Example:**
+
+        Looking for a `path`::
+
+          '/tmp/myfile'
+
+        will find any file like ``'/tmp/myfile.txt'``,
+        ``'/tmp/myfile.jpg'`` but also ``'/tmp/myfile_any_attribute.pdf'``,
+        if it exists. Therefore we must be careful. File attributes
+        must come first: ``'/tmp/any_attribute_myfile.pdf'``
+        """
+        result = path
+        if os.path.isdir(os.path.dirname(path)):
+            file_iter = glob.iglob('%s*' % (path,))
+            try:
+                result = file_iter.next()
+            except StopIteration:
+                pass
+        return result
+
+    def pathFromFileID(self, store, root, file_id):
+        """Return a path for getting/storing a file with given file id.
+
+        If there is already a file stored for the given file id, the
+        path to this file is returned.
+
+        If no such file exists yet (or the the only file existing has
+        no filename extension at all) a path to store the file but
+        without any filename extension is returned.
+        """
+        path = os.path.join(root, file_id)
+        return self._searchInPath(path)
+
+    def createFile(self, store, root, filename, file_id, f):
+        """Infos about what to store exactly and where.
+
+        When a file should be handled by an external file storage, it
+        looks up any handlers (like this one), passes runtime infos
+        like the storage object, root path, filename, file_id, and the
+        raw file object itself.
+
+        The handler can then change the file, raise exceptions or
+        whatever and return the result.
+
+        This handler returns the input file as-is, a path returned by
+        :meth:`pathFromFileID` and an instance of
+        :class:`hurry.file.HurryFile` for further operations.
+
+        Please note: although a handler has enough infos to store the
+        file itself, it should leave that task to the calling file
+        store.
+
+        This method does, however, remove any existing files stored
+        under the given file id.
+        """
+        ext = os.path.splitext(filename)[1]
+        path = self.pathFromFileID(store, root, file_id)
+        base, old_ext = os.path.splitext(path)
+        if old_ext != ext:
+            if os.path.exists(path):
+                os.unlink(path)
+            path = base + ext
+        return f, path, HurryFile(filename, file_id + ext)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/index.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/index.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/index.txt	(revision 12677)
@@ -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
+   company/api.txt
+   utils/api.txt
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/interfaces.py	(revision 12677)
@@ -0,0 +1,1289 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import os
+import re
+import codecs
+import zc.async.interfaces
+import zope.i18nmessageid
+from datetime import datetime
+from hurry.file.interfaces import IFileRetrieval
+from hurry.workflow.interfaces import IWorkflowInfo
+from zc.sourcefactory.basic import BasicSourceFactory
+from zope import schema
+from zope.pluggableauth.interfaces import IPrincipalInfo
+from zope.security.interfaces import IGroupClosureAwarePrincipal as IPrincipal
+from zope.component import getUtility
+from zope.component.interfaces import IObjectEvent
+from zope.configuration.fields import Path
+from zope.container.interfaces import INameChooser, IContainer
+from zope.interface import Interface, Attribute
+from zope.schema.interfaces import IObject
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+from waeup.ikoba.schema import PhoneNumber
+from waeup.ikoba.sourcefactory import SmartBasicContextualSourceFactory
+
+_ = MessageFactory = zope.i18nmessageid.MessageFactory('waeup.ikoba')
+
+DELETION_MARKER = 'XXX'
+IGNORE_MARKER = '<IGNORE>'
+WAEUP_KEY = 'waeup.ikoba'
+VIRT_JOBS_CONTAINER_NAME = 'jobs'
+
+# States for various workflows
+
+CREATED = 'created'
+
+STARTED = 'started'
+REQUESTED = 'requested'
+PROVISIONALLY = 'provisionally'
+APPROVED = 'approved'
+
+SUBMITTED = 'submitted'
+AWAITING = 'awaiting'
+REJECTED = 'rejected'
+EXPIRED = 'expired'
+
+VERIFIED = 'verified'
+PUBLISHED = 'published'
+
+
+#: A dict giving job status as tuple (<STRING>, <TRANSLATED_STRING>),
+#: the latter for UI purposes.
+JOB_STATUS_MAP = {
+    zc.async.interfaces.NEW: ('new', _('new')),
+    zc.async.interfaces.COMPLETED: ('completed', _('completed')),
+    zc.async.interfaces.PENDING: ('pending', _('pending')),
+    zc.async.interfaces.ACTIVE: ('active', _('active')),
+    zc.async.interfaces.ASSIGNED: ('assigned', _('assigned')),
+    zc.async.interfaces.CALLBACKS: ('callbacks', _('callbacks')),
+    }
+
+#default_rest_frontpage = u'' + codecs.open(os.path.join(
+#        os.path.dirname(__file__), 'frontpage.rst'),
+#        encoding='utf-8', mode='rb').read()
+
+default_html_frontpage = u'' + codecs.open(os.path.join(
+        os.path.dirname(__file__), 'frontpage.html'),
+        encoding='utf-8', mode='rb').read()
+
+def SimpleIkobaVocabulary(*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 ContextualDictSourceFactoryBase(SmartBasicContextualSourceFactory):
+    """A base for contextual sources based on IkobaUtils dicts.
+
+    To create a real source, you have to set the `DICT_NAME` attribute
+    which should be the name of a dictionary in IkobaUtils.
+    """
+    def getValues(self, context):
+        utils = getUtility(IIkobaUtils)
+        sorted_items = sorted(getattr(utils, self.DICT_NAME).items(),
+                              key=lambda item: item[1])
+        return [item[0] for item in sorted_items]
+
+    def getToken(self, context, value):
+        return str(value)
+
+    def getTitle(self, context, value):
+        utils = getUtility(IIkobaUtils)
+        return getattr(utils, self.DICT_NAME)[value]
+
+# Define a validation method for email addresses
+class NotAnEmailAddress(schema.ValidationError):
+    __doc__ = u"Invalid email address"
+
+#: Regular expression to check email-address formats. As these can
+#: become rather complex (nearly everything is allowed by RFCs), we only
+#: forbid whitespaces, commas and dots following onto each other.
+check_email = re.compile(
+    r"^[^@\s,]+@[^@\.\s,]+(\.[^@\.\s,]+)*$").match
+
+def validate_email(value):
+    if not check_email(value):
+        raise NotAnEmailAddress(value)
+    return True
+
+# Define a validation method for ids
+class NotIdValue(schema.ValidationError):
+    __doc__ = u"Invalid id"
+
+#: Regular expressions to check id formats.
+check_id = re.compile(r"^[a-zA-Z0-9_-]{2,10}$").match
+check_uuid = re.compile( r"^[a-zA-Z0-9]{32}$").match
+
+def validate_id(value):
+    if not check_id(value):
+        raise NotIdValue(value)
+    return True
+
+def validate_uuid(value):
+    # We are using also short ids in tests
+    if not check_uuid(value) and not check_id(value):
+        raise NotIdValue(value)
+    return True
+
+# Define a validation method for international phone numbers
+class InvalidPhoneNumber(schema.ValidationError):
+    __doc__ = u"Invalid phone number"
+
+# represent format +NNN-NNNN-NNNN
+RE_INT_PHONE = re.compile(r"^\+?\d+\-\d+\-[\d\-]+$")
+
+def validate_phone(value):
+    if not RE_INT_PHONE.match(value):
+        raise InvalidPhoneNumber(value)
+    return True
+
+class FatalCSVError(Exception):
+    """Some row could not be processed.
+    """
+    pass
+
+class DuplicationError(Exception):
+    """An exception that can be raised when duplicates are found.
+
+    When raising :exc:`DuplicationError` you can, beside the usual
+    message, specify a list of objects which are duplicates. These
+    values can be used by catching code to print something helpful or
+    similar.
+    """
+    def __init__(self, msg, entries=[]):
+        self.msg = msg
+        self.entries = entries
+
+    def __str__(self):
+        return '%r' % self.msg
+
+class RoleSource(BasicSourceFactory):
+    """A source for site roles.
+    """
+    def getValues(self):
+        # late import: in interfaces we should not import local modules
+        from waeup.ikoba.permissions import get_waeup_role_names
+        return get_waeup_role_names()
+
+    def getTitle(self, value):
+        # late import: in interfaces we should not import local modules
+        from waeup.ikoba.permissions import get_all_roles
+        roles = dict(get_all_roles())
+        if value in roles.keys():
+            title = roles[value].title
+            if '.' in title:
+                title = title.split('.', 2)[1]
+        return title
+
+class CaptchaSource(BasicSourceFactory):
+    """A source for captchas.
+    """
+    def getValues(self):
+        captchas = ['No captcha', 'Testing captcha', 'ReCaptcha']
+        try:
+            # we have to 'try' because IConfiguration can only handle
+            # interfaces from w.k.interface.
+            from waeup.ikoba.browser.interfaces import ICaptchaManager
+        except:
+            return captchas
+        return sorted(getUtility(ICaptchaManager).getAvailCaptchas().keys())
+
+    def getTitle(self, value):
+        return value
+
+class CurrencySource(BasicSourceFactory):
+    """A source for currencies.
+    """
+    def getValues(self):
+        currencies = getUtility(IIkobaUtils).CURRENCIES
+        return currencies.keys()
+
+    def getToken(self, value):
+        return value
+
+    def getTitle(self, value):
+        currencies = getUtility(IIkobaUtils).CURRENCIES
+        return currencies[value][1]
+
+class IIkobaUtils(Interface):
+    """A collection of methods which are subject to customization.
+    """
+
+    PORTAL_LANGUAGE = Attribute("Dict of global language setting")
+    PREFERRED_LANGUAGES_DICT = Attribute("Dict of preferred languages")
+    SYSTEM_MAX_LOAD = Attribute("Dict of maximum system loads.")
+
+    def sendContactForm(
+          from_name,from_addr,rcpt_name,rcpt_addr,
+          from_username,usertype,portal,body,subject):
+        """Send an email with data provided by forms.
+        """
+
+    def fullname(firstname,lastname,middlename):
+        """Full name constructor.
+        """
+
+    def sendCredentials(user, password, url_info, msg):
+        """Send credentials as email.
+
+        Input is the customer for which credentials are sent and the
+        password.
+
+        Returns True or False to indicate successful operation.
+        """
+
+    def genPassword(length, chars):
+        """Generate a random password.
+        """
+
+class IIkobaObject(Interface):
+    """A Ikoba object.
+
+    This is merely a marker interface.
+    """
+
+class ICompany(IIkobaObject):
+    """Representation of an company.
+    """
+
+
+class IIkobaContainer(IIkobaObject):
+    """A container for Ikoba objects.
+    """
+
+class IIkobaContained(IIkobaObject):
+    """An item contained in an IIkobaContainer.
+    """
+
+class ICSVExporter(Interface):
+    """A CSV file exporter for objects.
+    """
+    fields = Attribute("""List of fieldnames in resulting CSV""")
+
+    title = schema.TextLine(
+        title = u'Title',
+        description = u'Description to be displayed in selections.',
+        )
+    def mangle_value(value, name, obj):
+        """Mangle `value` extracted from `obj` or suobjects thereof.
+
+        This is called by export before actually writing to the result
+        file.
+        """
+
+    def get_filtered(site, **kw):
+        """Get datasets in `site` to be exported.
+
+        The set of data is specified by keywords, which might be
+        different for any implementaion of exporter.
+
+        Returns an iterable.
+        """
+
+    def export(iterable, filepath=None):
+        """Export iterables as rows in a CSV file.
+
+        If `filepath` is not given, a string with the data should be
+        returned.
+
+        What kind of iterables are acceptable depends on the specific
+        exporter implementation.
+        """
+
+    def export_all(site, filepath=None):
+        """Export all items in `site` as CSV file.
+
+        if `filepath` is not given, a string with the data should be
+        returned.
+        """
+
+    def export_filtered(site, filepath=None, **kw):
+        """Export those items in `site` specified by `args` and `kw`.
+
+        If `filepath` is not given, a string with the data should be
+        returned.
+
+        Which special keywords are supported is up to the respective
+        exporter.
+        """
+
+class IIkobaExporter(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 IIkobaXMLExporter(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 IIkobaXMLImporter(Interface):
+    """An XML import for objects.
+    """
+    def doImport(filepath):
+        """Create Python object from XML.
+
+        Returns a Python object.
+        """
+
+class IBatchProcessor(Interface):
+    """A batch processor that handles mass-operations.
+    """
+    name = schema.TextLine(
+        title = _(u'Processor name')
+        )
+
+    def doImport(path, headerfields, mode='create', user='Unknown',
+                 logger=None, ignore_empty=True):
+        """Read data from ``path`` and update connected object.
+
+        `headerfields` is a list of headerfields as read from the file
+        to import.
+
+        `mode` gives the import mode to use (``'create'``,
+        ``'update'``, or ``'remove'``.
+
+        `user` is a string describing the user performing the
+        import. Normally fetched from current principal.
+
+        `logger` is the logger to use during import.
+
+        `ignore_emtpy` in update mode ignores empty fields if true.
+        """
+
+class IContactForm(IIkobaObject):
+    """A contact form.
+    """
+
+    email_from = schema.TextLine(
+        title = _(u'Email Address:'),
+        default = None,
+        required = True,
+        constraint=validate_email,
+        )
+
+    email_to = schema.TextLine(
+        title = _(u'Email to:'),
+        default = None,
+        required = True,
+        constraint=validate_email,
+        )
+
+    subject = schema.TextLine(
+        title = _(u'Subject:'),
+        required = True,)
+
+    fullname = schema.TextLine(
+        title = _(u'Full Name:'),
+        required = True,)
+
+    body = schema.Text(
+        title = _(u'Text:'),
+        required = True,)
+
+class IIkobaPrincipalInfo(IPrincipalInfo):
+    """Infos about principals that are users of Ikoba Ikoba.
+    """
+    email = Attribute("The email address of a user")
+    phone = Attribute("The phone number of a user")
+    public_name = Attribute("The public name of a user")
+
+
+class IIkobaPrincipal(IPrincipal):
+    """A principle for Ikoba Ikoba.
+
+    This interface extends zope.security.interfaces.IPrincipal and
+    requires also an `id` and other attributes defined there.
+    """
+
+    email = schema.TextLine(
+        title = _(u'Email Address'),
+        description = u'',
+        required=False,)
+
+    phone = PhoneNumber(
+        title = _(u'Phone'),
+        description = u'',
+        required=False,)
+
+    public_name = schema.TextLine(
+        title = _(u'Public Name'),
+        required = False,)
+
+class IFailedLoginInfo(IIkobaObject):
+    """Info about failed logins.
+
+    Timestamps are supposed to be stored as floats using time.time()
+    or similar.
+    """
+    num = schema.Int(
+        title = _(u'Number of failed logins'),
+        description = _(u'Number of failed logins'),
+        required = True,
+        default = 0,
+        )
+
+    last = schema.Float(
+        title = _(u'Timestamp'),
+        description = _(u'Timestamp of last failed login or `None`'),
+        required = False,
+        default = None,
+        )
+
+    def as_tuple():
+        """Get login info as tuple ``<NUM>, <TIMESTAMP>``.
+        """
+
+    def set_values(num=0, last=None):
+        """Set number of failed logins and timestamp of last one.
+        """
+
+    def increase():
+        """Increase the current number of failed logins and set timestamp.
+        """
+
+    def reset():
+        """Set failed login counters back to zero.
+        """
+
+
+class IUserAccount(IIkobaObject):
+    """A user account.
+    """
+
+    failed_logins = Attribute("""FailedLoginInfo for this account""")
+
+    name = schema.TextLine(
+        title = _(u'User Id'),
+        description = u'Login name of user',
+        required = True,)
+
+    title = schema.TextLine(
+        title = _(u'Full Name'),
+        required = True,)
+
+    public_name = schema.TextLine(
+        title = _(u'Public Name'),
+        description = u"Substitute for officer's real name "
+                       "in object histories.",
+        required = False,)
+
+    description = schema.Text(
+        title = _(u'Description/Notice'),
+        required = False,)
+
+    email = schema.TextLine(
+        title = _(u'Email Address'),
+        default = None,
+        required = True,
+        constraint=validate_email,
+        )
+
+    phone = PhoneNumber(
+        title = _(u'Phone'),
+        default = None,
+        required = False,
+        )
+
+    roles = schema.List(
+        title = _(u'Portal Roles'),
+        value_type = schema.Choice(source=RoleSource()),
+        required = False,
+        )
+
+
+
+class IPasswordValidator(Interface):
+    """A password validator utility.
+    """
+
+    def validate_password(password, password_repeat):
+        """Validates a password by comparing it with
+        control password and checking some other requirements.
+        """
+
+
+class IUsersContainer(IIkobaObject):
+    """A container for users (principals).
+
+    These users are used for authentication purposes.
+    """
+
+    def addUser(name, password, title=None, description=None):
+        """Add a user.
+        """
+
+    def delUser(name):
+        """Delete a user if it exists.
+        """
+
+class ILocalRolesAssignable(Interface):
+    """The local roles assignable to an object.
+    """
+    def __call__():
+        """Returns a list of dicts.
+
+        Each dict contains a ``name`` referring to the role assignable
+        for the specified object and a `title` to describe the range
+        of users to which this role can be assigned.
+        """
+
+class IConfigurationContainer(IIkobaObject):
+    """A container for session configuration objects.
+    """
+
+    name = schema.TextLine(
+        title = _(u'Name of Company'),
+        default = _(u'Sample Company'),
+        required = True,
+        )
+
+    acronym = schema.TextLine(
+        title = _(u'Abbreviated Title of Company'),
+        default = u'WAeUP.Ikoba',
+        required = True,
+        )
+
+    frontpage = schema.Text(
+        title = _(u'Content in HTML format'),
+        required = False,
+        default = default_html_frontpage,
+        )
+
+    frontpage_dict = schema.Dict(
+        title = u'Content as language dictionary with values in html format',
+        required = False,
+        default = {},
+        )
+
+    name_admin = schema.TextLine(
+        title = _(u'Name of Administrator'),
+        default = u'Administrator',
+        required = True,
+        )
+
+    email_admin = schema.TextLine(
+        title = _(u'Email Address of Administrator'),
+        default = u'contact@waeup.org',
+        required = True,
+        constraint=validate_email,
+        )
+
+    email_subject = schema.TextLine(
+        title = _(u'Subject of Email to Administrator'),
+        default = _(u'Ikoba Contact'),
+        required = True,
+        )
+
+    smtp_mailer = schema.Choice(
+        title = _(u'SMTP mailer to use when sending mail'),
+        vocabulary = 'Mail Delivery Names',
+        default = 'No email service',
+        required = True,
+        )
+
+    email_notification = schema.Bool(
+        title = _(u'Notify customers by email'),
+        default = False,
+        )
+
+    captcha = schema.Choice(
+        title = _(u'Captcha used for public registration pages'),
+        source = CaptchaSource(),
+        default = u'No captcha',
+        required = True,
+        )
+
+
+class IDataCenter(IIkobaObject):
+    """A data center.
+
+    A data center manages files (uploads, downloads, etc.).
+
+    Beside providing the bare paths needed to keep files, it also
+    provides some helpers to put results of batch processing into
+    well-defined final locations (with well-defined filenames).
+
+    The main use-case is managing of site-related files, i.e. files
+    for import, export etc.
+
+    DataCenters are _not_ meant as storages for object-specific files
+    like passport photographs and similar.
+
+    It is up to the datacenter implementation how to organize data
+    (paths) inside its storage path.
+    """
+    storage = schema.Bytes(
+        title = u'Path to directory where everything is kept.'
+        )
+
+    deleted_path = schema.Bytes(
+        title = u'Path were data about deleted objects should be stored.'
+        )
+
+    def getPendingFiles(sort='name'):
+        """Get a list of files stored in `storage` sorted by basename.
+        """
+
+    def getFinishedFiles():
+        """Get a list of files stored in `finished` subfolder of `storage`.
+        """
+
+    def setStoragePath(path, move=False, overwrite=False):
+        """Set the path where to store files.
+
+        If `move` is True, move over files from the current location
+        to the new one.
+
+        If `overwrite` is also True, overwrite any already existing
+        files of same name in target location.
+
+        Triggers a DataCenterStorageMovedEvent.
+        """
+
+    def distProcessedFiles(successful, source_path, finished_file,
+                           pending_file, mode='create', move_orig=True):
+        """Distribute processed files over final locations.
+        """
+
+
+class IDataCenterFile(Interface):
+    """A data center file.
+    """
+
+    name = schema.TextLine(
+        title = u'Filename')
+
+    size = schema.TextLine(
+        title = u'Human readable file size')
+
+    uploaddate = schema.TextLine(
+        title = u'Human readable upload datetime')
+
+    lines = schema.Int(
+        title = u'Number of lines in file')
+
+    def getDate():
+        """Get creation timestamp from file in human readable form.
+        """
+
+    def getSize():
+        """Get human readable size of file.
+        """
+
+    def getLinesNumber():
+        """Get number of lines of file.
+        """
+
+class IDataCenterStorageMovedEvent(IObjectEvent):
+    """Emitted, when the storage of a datacenter changes.
+    """
+
+class IObjectUpgradeEvent(IObjectEvent):
+    """Can be fired, when an object shall be upgraded.
+    """
+
+class ILocalRoleSetEvent(IObjectEvent):
+    """A local role was granted/revoked for a principal on an object.
+    """
+    role_id = Attribute(
+        "The role id that was set.")
+    principal_id = Attribute(
+        "The principal id for which the role was granted/revoked.")
+    granted = Attribute(
+        "Boolean. If false, then the role was revoked.")
+
+class IQueryResultItem(Interface):
+    """An item in a search result.
+    """
+    url = schema.TextLine(
+        title = u'URL that links to the found item')
+    title = schema.TextLine(
+        title = u'Title displayed in search results.')
+    description = schema.Text(
+        title = u'Longer description of the item found.')
+
+class IIkobaPluggable(Interface):
+    """A component that might be plugged into a Ikoba Ikoba app.
+
+    Components implementing this interface are referred to as
+    'plugins'. They are normally called when a new
+    :class:`waeup.ikoba.app.Company` instance is created.
+
+    Plugins can setup and update parts of the central site without the
+    site object (normally a :class:`waeup.ikoba.app.Company` 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 cutomers 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.ikoba.app.Company` instance is created and put
+    into ZODB.
+
+    The setup-code then calls the `register` method of the utility and
+    expects a modified (or unmodified) version of the PAU back.
+
+    This allows to define any authentication setup modifications by
+    submodules or third-party modules/packages.
+    """
+
+    def register(pau):
+        """Register any plugins wanted to be in the PAU.
+        """
+
+    def unregister(pau):
+        """Unregister any plugins not wanted to be in the PAU.
+        """
+
+class IObjectConverter(Interface):
+    """Object converters are available as simple adapters, adapting
+       interfaces (not regular instances).
+
+    """
+
+    def fromStringDict(self, data_dict, context, form_fields=None):
+        """Convert values in `data_dict`.
+
+        Converts data in `data_dict` into real values based on
+        `context` and `form_fields`.
+
+        `data_dict` is a mapping (dict) from field names to values
+        represented as strings.
+
+        The fields (keys) to convert can be given in optional
+        `form_fields`. If given, form_fields should be an instance of
+        :class:`zope.formlib.form.Fields`. Suitable instances are for
+        example created by :class:`grok.AutoFields`.
+
+        If no `form_fields` are given, a default is computed from the
+        associated interface.
+
+        The `context` can be an existing object (implementing the
+        associated interface) or a factory name. If it is a string, we
+        try to create an object using
+        :func:`zope.component.createObject`.
+
+        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
+        <DATA_DICT>)`` where
+
+        ``<FIELD_ERRORS>``
+           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
+           error that happened when validating the input data in
+           `data_dict`
+
+        ``<INVARIANT_ERRORS>``
+           is a list of invariant errors concerning several fields
+
+        ``<DATA_DICT>``
+           is a dict with the values from input dict converted.
+
+        If errors happen, i.e. the error lists are not empty, always
+        an empty ``<DATA_DICT>`` is returned.
+
+        If ``<DATA_DICT>` is non-empty, there were no errors.
+        """
+
+class IFieldConverter(Interface):
+    def request_data(name, value, schema_field, prefix='', mode='create'):
+        """Create a dict with key-value mapping as created by a request.
+
+        `name` and `value` are expected to be parsed from CSV or a
+        similar input and represent an attribute to be set to a
+        representation of value.
+
+        `mode` gives the mode of import.
+
+        :meth:`update_request_data` is then requested to turn this
+        name and value into vars as they would be sent by a regular
+        form submit. This means we do not create the real values to be
+        set but we only define the values that would be sent in a
+        browser request to request the creation of those values.
+
+        The returned dict should contain names and values of a faked
+        browser request for the given `schema_field`.
+
+        Field converters are normally registered as adapters to some
+        specific zope.schema field.
+        """
+
+class IObjectHistory(Interface):
+
+    messages = schema.List(
+        title = u'List of messages stored',
+        required = True,
+        )
+
+    def addMessage(message):
+        """Add a message.
+        """
+
+class IIkobaWorkflowInfo(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 IkobaLoggers")
+
+    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 Ikoba logger instance.
+        """
+
+    def setup():
+        """Create a Python :class:`logging.Logger` instance.
+
+        The created logger is based on the params given by constructor.
+        """
+
+    def update(**options):
+        """Update the logger.
+
+        Updates the logger respecting modified `options` and changed
+        paths.
+        """
+
+class ILoggerCollector(Interface):
+
+    def getLoggers(site):
+        """Return all loggers registered for `site`.
+        """
+
+    def registerLogger(site, logging_component):
+        """Register a logging component residing in `site`.
+        """
+
+    def unregisterLogger(site, logging_component):
+        """Unregister a logger.
+        """
+
+#
+# External File Storage and relatives
+#
+class IFileStoreNameChooser(INameChooser):
+    """See zope.container.interfaces.INameChooser for base methods.
+    """
+    def checkName(name, attr=None):
+        """Check whether an object name is valid.
+
+        Raises a user error if the name is not valid.
+        """
+
+    def chooseName(name, attr=None):
+        """Choose a unique valid file id for the object.
+
+        The given name may be taken into account when choosing the
+        name (file id).
+
+        chooseName is expected to always choose a valid file id (that
+        would pass the checkName test) and never raise an error.
+
+        If `attr` is not ``None`` it might been taken into account as
+        well when generating the file id. Usual behaviour is to
+        interpret `attr` as a hint for what type of file for a given
+        context should be stored if there are several types
+        possible. For instance for a certain customer some file could
+        be the connected passport photograph or some certificate scan
+        or whatever. Each of them has to be stored in a different
+        location so setting `attr` to a sensible value should give
+        different file ids returned.
+        """
+
+class IExtFileStore(IFileRetrieval):
+    """A file storage that stores files in filesystem (not as blobs).
+    """
+    root = schema.TextLine(
+        title = u'Root path of file store.',
+        )
+
+    def getFile(file_id):
+        """Get raw file data stored under file with `file_id`.
+
+        Returns a file descriptor open for reading or ``None`` if the
+        file cannot be found.
+        """
+
+    def getFileByContext(context, attr=None):
+        """Get raw file data stored for the given context.
+
+        Returns a file descriptor open for reading or ``None`` if no
+        such file can be found.
+
+        Both, `context` and `attr` might be used to find (`context`)
+        and feed (`attr`) an appropriate file name chooser.
+
+        This is a convenience method.
+        """
+
+    def deleteFile(file_id):
+        """Delete file stored under `file_id`.
+
+        Remove file from filestore so, that it is not available
+        anymore on next call to getFile for the same file_id.
+
+        Should not complain if no such file exists.
+        """
+
+    def deleteFileByContext(context, attr=None):
+        """Delete file for given `context` and `attr`.
+
+        Both, `context` and `attr` might be used to find (`context`)
+        and feed (`attr`) an appropriate file name chooser.
+
+        This is a convenience method.
+        """
+
+    def createFile(filename, f):
+        """Create file given by f with filename `filename`
+
+        Returns a hurry.file.File-based object.
+        """
+
+class IFileStoreHandler(Interface):
+    """Filestore handlers handle specific files for file stores.
+
+    If a file to store/get provides a specific filename, a file store
+    looks up special handlers for that type of file.
+
+    """
+    def pathFromFileID(store, root, filename):
+        """Turn file id into path to store.
+
+        Returned path should be absolute.
+        """
+
+    def createFile(store, root, filename, file_id, file):
+        """Return some hurry.file based on `store` and `file_id`.
+
+        Some kind of callback method called by file stores to create
+        file objects from file_id.
+
+        Returns a tuple ``(raw_file, path, file_like_obj)`` where the
+        ``file_like_obj`` should be a HurryFile, a IkobaImageFile or
+        similar. ``raw_file`` is the (maybe changed) input file and
+        ``path`` the relative internal path to store the file at.
+
+        Please make sure the ``raw_file`` is opened for reading and
+        the file descriptor set at position 0 when returned.
+
+        This method also gets the raw input file object that is about
+        to be stored and is expected to raise any exceptions if some
+        kind of validation or similar fails.
+        """
+
+class IPDF(Interface):
+    """A PDF representation of some context.
+    """
+
+    def __call__(view=None, note=None):
+        """Create a bytestream representing a PDF from context.
+
+        If `view` is passed in additional infos might be rendered into
+        the document.
+
+        `note` is optional HTML rendered at bottom of the created
+        PDF. Please consider the limited reportlab support for HTML,
+        but using font-tags and friends you certainly can get the
+        desired look.
+        """
+
+class IMailService(Interface):
+    """A mail service.
+    """
+
+    def __call__():
+        """Get the default mail delivery.
+        """
+
+
+class IDataCenterConfig(Interface):
+    path = Path(
+        title = u'Path',
+        description = u"Directory where the datacenter should store "
+                      u"files by default (adjustable in web UI).",
+        required = True,
+        )
+
+
+class IPayPalConfig(Interface):
+    path = Path(
+        title = u'Path',
+        description = u"Path to config file for PayPal REST API.",
+        required = True,
+        )
+
+
+#
+# Asynchronous job handling and related
+#
+class IJobManager(IIkobaObject):
+    """A manager for asynchronous running jobs (tasks).
+    """
+    def put(job, site=None):
+        """Put a job into task queue.
+
+        If no `site` is given, queue job in context of current local
+        site.
+
+        Returns a job_id to identify the put job. This job_id is
+        needed for further references to the job.
+        """
+
+    def jobs(site=None):
+        """Get an iterable of jobs stored.
+        """
+
+    def get(job_id, site=None):
+        """Get the job with id `job_id`.
+
+        For the `site` parameter see :meth:`put`.
+        """
+
+    def remove(job_id, site=None):
+        """Remove job with `job_id` from stored jobs.
+        """
+
+    def start_test_job(site=None):
+        """Start a test job.
+        """
+
+class IProgressable(Interface):
+    """A component that can indicate its progress status.
+    """
+    percent = schema.Float(
+        title = u'Percent of job done already.',
+        )
+
+class IJobContainer(IContainer):
+    """A job container contains IJob objects.
+    """
+
+class IExportJob(zc.async.interfaces.IJob):
+    def __init__(site, exporter_name):
+        pass
+
+    finished = schema.Bool(
+        title = u'`True` if the job finished.`',
+        default = False,
+        )
+
+    failed = schema.Bool(
+        title = u"`True` iff the job finished and didn't provide a file.",
+        default = None,
+        )
+
+class IExportJobContainer(IIkobaObject):
+    """A component that contains (maybe virtually) export jobs.
+    """
+    def start_export_job(exporter_name, user_id, *args, **kwargs):
+        """Start asynchronous export job.
+
+        `exporter_name` is the name of an exporter utility to be used.
+
+        `user_id` is the ID of the user that triggers the export.
+
+        `args` positional arguments passed to the export job created.
+
+        `kwargs` keyword arguments passed to the export job.
+
+        The job_id is stored along with exporter name and user id in a
+        persistent list.
+
+        Returns the job ID of the job started.
+        """
+
+    def get_running_export_jobs(user_id=None):
+        """Get export jobs for user with `user_id` as list of tuples.
+
+        Each tuples holds ``<job_id>, <exporter_name>, <user_id>`` in
+        that order. The ``<exporter_name>`` is the utility name of the
+        used exporter.
+
+        If `user_id` is ``None``, all running jobs are returned.
+        """
+
+    def get_export_jobs_status(user_id=None):
+        """Get running/completed export jobs for `user_id` as list of tuples.
+
+        Each tuple holds ``<raw status>, <status translated>,
+        <exporter title>`` in that order, where ``<status
+        translated>`` and ``<exporter title>`` are translated strings
+        representing the status of the job and the human readable
+        title of the exporter used.
+        """
+
+    def delete_export_entry(entry):
+        """Delete the export denoted by `entry`.
+
+        Removes `entry` from the local `running_exports` list and also
+        removes the regarding job via the local job manager.
+
+        `entry` is a tuple ``(<job id>, <exporter name>, <user id>)``
+        as created by :meth:`start_export_job` or returned by
+        :meth:`get_running_export_jobs`.
+        """
+
+    def entry_from_job_id(job_id):
+        """Get entry tuple for `job_id`.
+
+        Returns ``None`` if no such entry can be found.
+        """
+
+class IExportContainerFinder(Interface):
+    """A finder for the central export container.
+    """
+    def __call__():
+        """Return the currently used global or site-wide IExportContainer.
+        """
+
+class IFilteredQuery(IIkobaObject):
+    """A query for objects.
+    """
+
+    defaults = schema.Dict(
+        title = u'Default Parameters',
+        required = True,
+        )
+
+    def __init__(**parameters):
+        """Instantiate a filtered query by passing in parameters.
+        """
+
+    def query():
+        """Get an iterable of objects denoted by the set parameters.
+
+        The search should be applied to objects inside current
+        site. It's the caller's duty to set the correct site before.
+
+        Result can be any iterable like a catalog result set, a list,
+        or similar.
+        """
+
+class IFilteredCatalogQuery(IFilteredQuery):
+    """A catalog-based query for objects.
+    """
+
+    cat_name = schema.TextLine(
+        title = u'Registered name of the catalog to search.',
+        required = True,
+        )
+
+    def query_catalog(catalog):
+        """Query catalog with the parameters passed to constructor.
+        """
+
+
+class IIDSource(Interface):
+    """A source for unique IDs, according to RFC 4122.
+    """
+    def get_uuid():
+        """Get a random unique ID.
+
+        Get a 'Universially Unique IDentifier'. The result string
+        might contain any printable characters.
+        """
+
+    def get_hex_uuid():
+        """Get a random unique ID as a string representing a hex number.
+        """
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/language.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/language.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/language.py	(revision 12677)
@@ -0,0 +1,44 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Handle language as requested by users.
+"""
+import grokcore.component
+from zope.component import getUtility
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.session.interfaces import ISession
+from zope.i18n.interfaces import IUserPreferredLanguages
+from waeup.ikoba.configuration import ConfigurationContainer
+from waeup.ikoba.interfaces import IIkobaUtils
+
+class IkobaLanguage(grokcore.component.Adapter):
+    """Set preferred languages"""
+    grokcore.component.context(IBrowserRequest)
+    grokcore.component.implements(IUserPreferredLanguages)
+
+    def getPreferredLanguages(self):
+        # This an adapter for the request, so self.context is the request.
+        request = self.context
+
+        # Extract the preferred language from a cookie:
+        lang = request.cookies.get('ikoba.language', None)
+        if lang is None:
+            lang = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+            request.response.setCookie('ikoba.language', lang, path='/')
+
+        # According to IUserPreferredLanguages, we must return a list.
+        return [lang]
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales.zcml	(revision 12677)
@@ -0,0 +1,7 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:i18n="http://namespaces.zope.org/i18n">
+
+  <include package="zope.i18n" file="meta.zcml" />
+  <i18n:registerTranslations directory="locales" />
+
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/de/LC_MESSAGES/waeup.ikoba.po
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/de/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/de/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
@@ -0,0 +1,4955 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.Ikoba\n"
+"POT-Creation-Date: Mon Feb  2 08:48:41 2015\n"
+"PO-Revision-Date: 2015-02-02 08:31+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Germany <henrik@waeup.org>\n"
+"Language: de_DE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-SourceCharset: utf-8\n"
+"X-Poedit-Basepath: \\\\Salsa\\ikoba\\trunk\\src\n"
+"X-Generator: Poedit 1.6.5\n"
+
+#: waeup/ikoba/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:106
+msgid "Portal Settings"
+msgstr "Portaleinstellungen"
+
+#: waeup/ikoba/browser/breadcrumbs.py:119 waeup/ikoba/browser/pages.py:470
+msgid "Administration"
+msgstr "Verwaltung"
+
+#: waeup/ikoba/browser/breadcrumbs.py:126 waeup/ikoba/browser/viewlets.py:320
+#: waeup/ikoba/browser/viewlets.py:544
+msgid "Portal Configuration"
+msgstr "Portalkonfiguration"
+
+#: waeup/ikoba/browser/breadcrumbs.py:133 waeup/ikoba/browser/pages.py:517
+#: waeup/ikoba/browser/viewlets.py:329 waeup/ikoba/browser/viewlets.py:554
+msgid "Officers"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:140 waeup/ikoba/browser/pages.py:763
+#: waeup/ikoba/browser/pages.py:1363 waeup/ikoba/browser/viewlets.py:338
+#: waeup/ikoba/browser/viewlets.py:563
+msgid "Data Center"
+msgstr "Datenzentrum"
+
+#: waeup/ikoba/browser/breadcrumbs.py:147 waeup/ikoba/browser/reports.py:37
+#: waeup/ikoba/browser/viewlets.py:347 waeup/ikoba/browser/viewlets.py:572
+msgid "Reports"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:92
+msgid "Home"
+msgstr "Startseite"
+
+#: waeup/ikoba/browser/fileviewlets.py:108
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/ikoba/browser/templates/datacenterpage.pt:17
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr "Datei"
+
+#: waeup/ikoba/browser/fileviewlets.py:109
+msgid "Scan"
+msgstr "Scan"
+
+#: waeup/ikoba/browser/fileviewlets.py:129
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:130
+msgid "Delete"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:164
+msgid "No local file selected."
+msgstr "Keine lokale Datei ausgewählt."
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:44
+msgid "${a} deleted."
+msgstr "${a} gelöscht."
+
+#: waeup/ikoba/browser/fileviewlets.py:59
+msgid "Uploaded file is too big."
+msgstr "Das hochgeladene Bild ist zu groß!"
+
+#: waeup/ikoba/browser/fileviewlets.py:71
+msgid "Could not determine file type."
+msgstr "Dateityp konnte nicht ermittelt werden."
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:76
+msgid "${a} file format expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:82
+msgid "Only the following extensions are allowed: ${a}"
+msgstr "Nur die folgenden Erweiterungen sind erlaubt: ${a}"
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:92
+msgid "File ${a} uploaded."
+msgstr "Datei ${a} hochgeladen."
+
+#: waeup/ikoba/browser/interfaces.py:173
+#: waeup/ikoba/customers/interfaces.py:200 waeup/ikoba/interfaces.py:428
+#: waeup/ikoba/interfaces.py:504
+msgid "Email Address"
+msgstr "Emailadresse"
+
+#: waeup/ikoba/browser/interfaces.py:176
+msgid "Enter your email address stored in Ikoba."
+msgstr ""
+
+#: waeup/ikoba/browser/layout.py:102 waeup/ikoba/customers/browser.py:387
+#: waeup/ikoba/customers/browser.py:690 waeup/ikoba/customers/browser.py:973
+#: waeup/ikoba/customers/browser.py:1385 waeup/ikoba/customers/browser.py:1416
+#: waeup/ikoba/documents/browser.py:259
+msgid "Are you sure?"
+msgstr "Sind Sie sicher?"
+
+#: waeup/ikoba/browser/pages.py:1039 waeup/ikoba/browser/pages.py:1068
+#: waeup/ikoba/browser/pages.py:1173 waeup/ikoba/browser/pages.py:1285
+msgid "Process CSV file"
+msgstr "Verarbeite CSF-Datei"
+
+#: waeup/ikoba/browser/pages.py:1071
+msgid "Back to step 1"
+msgstr "Zurück zu Schritt 1"
+
+#: waeup/ikoba/browser/pages.py:1072
+msgid "Proceed to step 3"
+msgstr "Gehe zu Schritt 3"
+
+#: waeup/ikoba/browser/pages.py:1117
+msgid "Double headers: each column name may only appear once. "
+msgstr "Doppelte Spaltenüberschrift"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:112
+msgid "Local role successfully removed: ${a}"
+msgstr "Lokale Rolle erfolgreich entfernt: ${a}"
+
+#: waeup/ikoba/browser/pages.py:1125
+msgid "Replace imported file!"
+msgstr "Tauschen Sie die Importdatei aus!"
+
+#: waeup/ikoba/browser/pages.py:1138 waeup/ikoba/browser/pages.py:1261
+msgid "Import aborted."
+msgstr "Import abgebrochen."
+
+#: waeup/ikoba/browser/pages.py:1150
+msgid "Update mode only!"
+msgstr "Nur Aktualisierungsmodus!"
+
+#: waeup/ikoba/browser/pages.py:1176 waeup/ikoba/browser/pages.py:1404
+msgid "Reset"
+msgstr "Zurücksetzen"
+
+#: waeup/ikoba/browser/pages.py:1177
+msgid "Set headerfields"
+msgstr "Spaltenüberschrift setzen"
+
+#: waeup/ikoba/browser/pages.py:1178
+msgid "Back to step 2"
+msgstr "Zurück zu Schritt 2"
+
+#: waeup/ikoba/browser/pages.py:1179
+msgid "Perform import"
+msgstr "Import durchführen"
+
+#: waeup/ikoba/browser/pages.py:1244
+msgid "Edit headers or replace imported file!"
+msgstr ""
+"Bearbeiten Sie die Spaltenüberschrift oder tauschen Sie die Importdatei aus."
+
+#: waeup/ikoba/browser/pages.py:125 waeup/ikoba/browser/pages.py:772
+#: waeup/ikoba/customers/browser.py:207
+msgid "No item selected."
+msgstr "Nichts ausgewählt."
+
+#: waeup/ikoba/browser/pages.py:1287
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1337
+msgid "Processing of ${a} rows failed."
+msgstr "Bearbeitung von ${a} Zeilen fehlgeschlagen."
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1339
+msgid "Successfully processed ${a} rows."
+msgstr "${a} Zeilen erfolgreich verarbeitet."
+
+#: waeup/ikoba/browser/pages.py:1347
+msgid "Show logfiles"
+msgstr "Logdateien anzeigen"
+
+#: waeup/ikoba/browser/pages.py:1350
+msgid "Show"
+msgstr "Zeigen"
+
+#: waeup/ikoba/browser/pages.py:1365 waeup/ikoba/browser/viewlets.py:185
+msgid "Search"
+msgstr "Suchen"
+
+#: waeup/ikoba/browser/pages.py:1367
+msgid "Enter a regular expression here..."
+msgstr "Geben Sie einen regulären Ausdruck ein ..."
+
+#: waeup/ikoba/browser/pages.py:1375
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+"Logdateien können nur auf Unix-basierten Betriebsystem durchsucht werden."
+
+#: waeup/ikoba/browser/pages.py:1390
+msgid "Invalid search expression."
+msgstr "Ungültiger Suchausdruck"
+
+#: waeup/ikoba/browser/pages.py:1393
+msgid "No search results found."
+msgstr "Keinen Datensatz gefunden."
+
+#: waeup/ikoba/browser/pages.py:1401
+msgid "Edit data center settings"
+msgstr "Einstellungen des Datenzentrums bearbeiten"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1421
+msgid "File already existed (not copied): ${a}"
+msgstr "Datei existiert (nicht kopiert): ${a}"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1424
+msgid "Given storage path cannot be used: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1428
+msgid "New storage path succefully set."
+msgstr "Neuen Datenpfad erfolgreich gesetzt."
+
+#: waeup/ikoba/browser/pages.py:1440
+msgid "Download portal data as CSV file"
+msgstr "Portaldaten als CSV-Datei herunterladen"
+
+#: waeup/ikoba/browser/pages.py:1442
+msgid "Create CSV file"
+msgstr "CSV-Datei erzeugen"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:146 waeup/ikoba/customers/browser.py:220
+#: waeup/ikoba/customers/browser.py:883 waeup/ikoba/customers/browser.py:1253
+msgid "Successfully removed: ${a}"
+msgstr "Erfolgreich entfernt: ${a}"
+
+#: waeup/ikoba/browser/pages.py:1470
+msgid "Discarded export"
+msgstr "Verwerfe Export"
+
+#: waeup/ikoba/browser/pages.py:1514
+msgid "Send me a new password"
+msgstr "Neues Passwort zusenden"
+
+#: waeup/ikoba/browser/pages.py:1540 waeup/ikoba/customers/browser.py:543
+#: waeup/ikoba/customers/browser.py:625
+msgid "Send login credentials to email address"
+msgstr "Zugangsdaten an Email-Adresse senden"
+
+#: waeup/ikoba/browser/pages.py:1550
+msgid "No record found."
+msgstr "Keinen Datensatz gefunden."
+
+#: waeup/ikoba/browser/pages.py:1564 waeup/ikoba/customers/browser.py:589
+msgid "You have successfully requested a password for the"
+msgstr "Sie haben erfolgreich ein Passwort angefordert für das"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1568
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+"Eine Email mit Ihren Benutzernamen und Ihrem Passwort wurde versendet an "
+"${a}."
+
+#: waeup/ikoba/browser/pages.py:188 waeup/ikoba/customers/browser.py:84
+#: waeup/ikoba/customers/browser.py:422
+msgid "Form has been saved."
+msgstr "Das Formular wurde gespeichert."
+
+#: waeup/ikoba/browser/pages.py:259 waeup/ikoba/browser/viewlets.py:266
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:40
+#: waeup/ikoba/browser/templates/customersitelayout.pt:39
+msgid "Login"
+msgstr "Einloggen"
+
+#: waeup/ikoba/browser/pages.py:270
+msgid "You logged in."
+msgstr "Sie sind jetzt eingeloggt."
+
+#: waeup/ikoba/browser/pages.py:307
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:315
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:318
+msgid "You entered invalid credentials."
+msgstr "Sie haben falsche Zugangsdaten eingegeben."
+
+#: waeup/ikoba/browser/pages.py:333
+msgid "You have been logged out. Thanks for using WAeUP Ikoba!"
+msgstr "Sie wurden ausgeloggt. Danke dass Sie WAeUP Ikoba benutzt haben."
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:375
+msgid "Contact ${a}"
+msgstr "Kontaktiere ${a}"
+
+#: waeup/ikoba/browser/pages.py:381 waeup/ikoba/browser/pages.py:661
+msgid "Send message now"
+msgstr "Jetzt senden"
+
+#: waeup/ikoba/browser/pages.py:400 waeup/ikoba/browser/pages.py:433
+#: waeup/ikoba/browser/pages.py:678 waeup/ikoba/customers/browser.py:372
+msgid "Your message has been sent."
+msgstr "Ihre Nachricht wurde verschickt."
+
+#: waeup/ikoba/browser/pages.py:420
+msgid "Send now"
+msgstr "Sende jetzt"
+
+#: waeup/ikoba/browser/pages.py:435
+msgid "A smtp server error occurred."
+msgstr "Es trat ein smtp-Serverfehler auf."
+
+#: waeup/ikoba/browser/pages.py:460
+msgid "<h1>Welcome to WAeUP.Ikoba</h1>"
+msgstr "<h1>Willkommen bei WAeUP.Ikoba</h1>"
+
+#: waeup/ikoba/browser/pages.py:518 waeup/ikoba/customers/viewlets.py:207
+#: waeup/ikoba/customers/viewlets.py:332 waeup/ikoba/customers/viewlets.py:404
+#: waeup/ikoba/documents/viewlets.py:60 waeup/ikoba/documents/viewlets.py:79
+#: waeup/ikoba/products/viewlets.py:60 waeup/ikoba/products/viewlets.py:79
+msgid "Manage"
+msgstr "Verwalten"
+
+#: waeup/ikoba/browser/pages.py:519 waeup/ikoba/browser/viewlets.py:175
+msgid "Remove"
+msgstr "Entfernen"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:526
+msgid "User account ${a} successfully deleted."
+msgstr "Benutzerkonto ${a} erfolgreich gelöscht."
+
+#: waeup/ikoba/browser/pages.py:563 waeup/ikoba/browser/pages.py:565
+#: waeup/ikoba/browser/viewlets.py:403
+msgid "Add user"
+msgstr "Benutzer hinzufügen"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:587
+msgid "User account ${a} successfully added."
+msgstr "Benutzerkonto ${a} erfolgreich hinzugefügt."
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:608
+msgid "Edit user ${a}"
+msgstr "Benutzer bearbeiten"
+
+#: waeup/ikoba/browser/pages.py:616 waeup/ikoba/browser/pages.py:737
+#: waeup/ikoba/browser/pages.py:1403 waeup/ikoba/customers/browser.py:400
+#: waeup/ikoba/customers/browser.py:732 waeup/ikoba/customers/browser.py:774
+#: waeup/ikoba/customers/browser.py:990 waeup/ikoba/customers/browser.py:1008
+#: waeup/ikoba/customers/browser.py:1402 waeup/ikoba/customers/browser.py:1426
+#: waeup/ikoba/customers/browser.py:1443 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:269 waeup/ikoba/documents/browser.py:293
+#: waeup/ikoba/documents/browser.py:308 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:106 waeup/ikoba/products/browser.py:184
+#: waeup/ikoba/products/browser.py:197
+msgid "Save"
+msgstr "Speichern"
+
+#: waeup/ikoba/browser/pages.py:642
+msgid "User settings have been saved."
+msgstr "Benutzereinstellungen wurden gespeichert."
+
+#: waeup/ikoba/browser/pages.py:645 waeup/ikoba/browser/pages.py:1070
+#: waeup/ikoba/browser/pages.py:1175 waeup/ikoba/customers/browser.py:759
+#: waeup/ikoba/customers/browser.py:790 waeup/ikoba/customers/browser.py:946
+#: waeup/ikoba/customers/browser.py:1312 waeup/ikoba/documents/browser.py:106
+#: waeup/ikoba/documents/browser.py:152 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:273 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:110
+#: waeup/ikoba/products/browser.py:184 waeup/ikoba/products/browser.py:206
+msgid "Cancel"
+msgstr "Abbruch"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:659 waeup/ikoba/customers/browser.py:353
+msgid "Send message to ${a}"
+msgstr "Sende Nachricht an ${a}"
+
+#: waeup/ikoba/browser/pages.py:688 waeup/ikoba/browser/viewlets.py:354
+msgid "My Preferences"
+msgstr "Meine Präferenzen"
+
+#: waeup/ikoba/browser/pages.py:701 waeup/ikoba/browser/viewlets.py:370
+msgid "My Roles"
+msgstr "Meine Rollen"
+
+#: waeup/ikoba/browser/pages.py:733
+msgid "Edit portal configuration"
+msgstr "Portalkonfiguration bearbeiten"
+
+#: waeup/ikoba/browser/pages.py:745
+msgid "Update plugins"
+msgstr "Plugins aktualisieren"
+
+#: waeup/ikoba/browser/pages.py:746
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:747
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:752
+msgid "Plugins were updated. See log file for details."
+msgstr "Plugins wurden aktualisiert, siehe Logdatei."
+
+#: waeup/ikoba/browser/pages.py:766 waeup/ikoba/customers/browser.py:176
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:101
+msgid "Remove selected"
+msgstr "Ausgewählte Objekte löschen"
+
+#: waeup/ikoba/browser/pages.py:783
+msgid "OSError: The file could not be deleted."
+msgstr "OSError: Die Datei konnte nicht gelöscht werden."
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:787
+msgid "Successfully deleted: ${a}"
+msgstr "Successfully deleted: ${a}"
+
+#: waeup/ikoba/browser/pages.py:798
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:800 waeup/ikoba/browser/pages.py:816
+#: waeup/ikoba/browser/pages.py:1041 waeup/ikoba/browser/pages.py:1349
+#: waeup/ikoba/browser/pages.py:1366 waeup/ikoba/browser/pages.py:1405
+#: waeup/ikoba/browser/pages.py:1443
+msgid "Back to Data Center"
+msgstr "Zurück zum Datenzentrum"
+
+#: waeup/ikoba/browser/pages.py:812
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:815
+msgid "Upload"
+msgstr "Hochladen"
+
+#: waeup/ikoba/browser/pages.py:855
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:857
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:861
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:880
+msgid "All import managers have been notified by email."
+msgstr "Alle Import-Manager wurden per Email benachrichtigt."
+
+#: waeup/ikoba/browser/pages.py:882 waeup/ikoba/browser/pages.py:1571
+#: waeup/ikoba/customers/browser.py:374
+msgid "An smtp server error occurred."
+msgstr "Es trat ein smtp-Serverfehler auf."
+
+#: waeup/ikoba/browser/pages.py:890
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:905
+msgid "Only csv files are allowed."
+msgstr "Es sind nur csv-Dateien erlaubt."
+
+#: waeup/ikoba/browser/pages.py:91
+msgid "No local role selected."
+msgstr "Keine lokalen Rollen ausgewählt."
+
+#: waeup/ikoba/browser/pages.py:913
+msgid "File with same name was uploaded earlier."
+msgstr "Eine Datei gleichen Namens wurde bereits hochgeladen."
+
+#: waeup/ikoba/browser/pages.py:924
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:207 waeup/ikoba/browser/pdf.py:472
+#: waeup/ikoba/browser/pdf.py:482
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:16
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr "Datum"
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr "${footer_text} Seite ${num1} of ${num2}"
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:754
+msgid "Date: ${a}"
+msgstr "Datum: ${a}"
+
+#: waeup/ikoba/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/ikoba/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+"Bitte helfen Sie uns, Missbrauch des Dienstes zu vermeiden, indem Sie das "
+"folgende Captcha lösen:"
+
+#: waeup/ikoba/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+"Bitte helfen Sie uns, Missbrauch des Dienstes zu vermeiden, indem Sie das "
+"folgende Captcha lösen."
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data. Please pick the "
+"type of objects you want to export from the selection below. The file will "
+"be generated and then be made available for you to download in the table "
+"below. Files can only be downloaded within 24 hours after generation. The "
+"download link disappears afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:42
+msgid "Export Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:43
+msgid "Exporter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:44
+msgid "Filter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:45
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:6
+msgid "Creator"
+msgstr "Erzeuger"
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:46
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:7
+msgid "Creation Date"
+msgstr "Erstellungsdatum"
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:47
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:8
+msgid "Status"
+msgstr "Status"
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:72
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:39
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/ikoba/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr "Hochladedatum"
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr "Schritt 1"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr "Dateiname"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:14
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/ikoba/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr "Datensätze"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr "Dateigröße"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+"Im Datenzentrum können Sie durch den Import von CSV-Dateien Daten des "
+"Portals massenhaft erzeugen aktualisieren oder entfernen."
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr "Auswählen"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr "Wählen Sie eine Datei aus der Liste unten zum Import aus."
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr "Schritt 2"
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:14
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr "Überschrift"
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr "Beispielsdatensatz 1"
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr "Beispielsdatensatz 2"
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr "Beispielsdatensatz 3"
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr "Modus:"
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+"Wählen Sie einen Datei-Prozessor und einen Verarbeitungsmodus aus der Liste "
+"unten aus."
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr "Spaltenüberschriften in Ordnung"
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr "ändern zu:"
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr "Beispielsdatensatz"
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr "Schritt 3"
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr "Ändern Sie die Spaltenüberschriften der Importdateien."
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr "Schritt 4"
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:13
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr "Verarbeitungsmodus:"
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr "Stapelverarbeitung beendet."
+
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr "Zur Zeit sind keine Logdateien vorhanden."
+
+#: waeup/ikoba/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr "Das Datenzentrum dient dem Hochladen und Importieren von CSV-Daten."
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr "Pfad des Speicherorts:"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr "Datei:"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr "Prozessor:"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr "Zur Verfügung Prozessoren"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr "Erforderliche Schema-Felder"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr "Optionale Schema-Felder"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr "Nicht-Schema-Felder"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr "Hochgeladene Dateien"
+
+#: waeup/ikoba/browser/templates/filedisplay.pt:15
+msgid "HTML Element:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:13
+msgid "Password"
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/ikoba/browser/templates/loginpage.pt:26
+msgid "login_trouble1"
+msgstr ""
+"Vergessen Sie nicht, sich vor dem Verlassen des Portals auszuloggen. Wenn "
+"Sie Probleme beim Einloggen haben, stellen Sie zunächst sicher, dass Cookies "
+"im Browser aktiviert sind."
+
+#. Default: "You don't have an account because you are a new customer, or your customer record has been created for you? Acquire a Password Activation Code (PWD) and inititialize your customer account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:29
+msgid "login_trouble2"
+msgstr ""
+"Wenn Sie ein neuer Student sind und noch kein Benutzerkonto haben, oder Ihr "
+"Datensatz gerade frisch  erzeugt wurde, erwerben Sie bitte einen PWD-"
+"Zugangscode und initialisieren Sie Ihr Benutzerkonto <strong><a href="
+"\"setpassword\"> hier</a></strong>."
+
+#. Default: "You don't have an account because you are a new customer? Create your customer account <strong><a href=\"createaccount\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:34
+msgid "login_trouble4"
+msgstr ""
+"Wenn Sie ein neuer Student sind und noch kein Benutzerkonto haben, oder Ihr "
+"Datensatz gerade frisch erzeugt wurde, initialisieren Sie Ihr Benutzerkonto "
+"<strong><a href=\"requestpw\"> hier</a></strong>."
+
+#. Default: "You don't have an account because you are a new customer and your customer record has been created for you? Inititialize your customer account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:38
+msgid "login_trouble5"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:4
+msgid "Identifier"
+msgstr ""
+
+#. Default: "Or simply forgot your id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:42
+msgid "login_trouble3"
+msgstr ""
+"Oder haben Sie Ihre Sudenten Id und/oder Ihr Passwort vergessen? Dann "
+"beantragen Sie <strong><a href=\"changepw\">hier</a></strong> ein neues "
+"Passwort."
+
+#. Default: "Customers can enter either their customer id, registration number or email address. Officers must enter their user name."
+#: waeup/ikoba/browser/templates/loginpage.pt:7
+msgid "identifier_hint"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr "Meine portalweiten Rollen:"
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr "Meine lokalen Rollen:"
+
+#: waeup/ikoba/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr "Diese Seite steht nicht zur Verfügung."
+
+#: waeup/ikoba/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr "Sie versuchen eine nicht existierende Seite aufzurufen."
+
+#: waeup/ikoba/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr "Bitte beachten Sie:"
+
+#: waeup/ikoba/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr "Sie haben möglicherweise die URL falsch eingetragen."
+
+#: waeup/ikoba/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:46
+#: waeup/ikoba/browser/templates/customersitelayout.pt:45
+msgid "Contact"
+msgstr "Kontakt"
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:52
+#: waeup/ikoba/browser/templates/customersitelayout.pt:51
+msgid "Logout"
+msgstr "Ausloggen"
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:6
+#: waeup/ikoba/browser/templates/customersitelayout.pt:4
+msgid "WAeUP.Ikoba - Registration and Application System"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:15
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:16
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:30
+msgid "Retype Password:"
+msgstr "Wiederhole Passwort:"
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:9
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:27
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:10
+#: waeup/ikoba/customers/browser_templates/basepage.pt:14
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:24
+msgid "Password:"
+msgstr "Passwort:"
+
+#: waeup/ikoba/browser/viewlets.py:165
+msgid "Add"
+msgstr "Hinzufügen"
+
+#: waeup/ikoba/browser/viewlets.py:305
+msgid "Any link text"
+msgstr "Any link text"
+
+#: waeup/ikoba/browser/viewlets.py:387 waeup/ikoba/customers/viewlets.py:198
+msgid "Send email"
+msgstr "Email versenden"
+
+#: waeup/ikoba/browser/viewlets.py:396
+msgid "Edit settings"
+msgstr "Einstellungen bearbeiten"
+
+#: waeup/ikoba/browser/viewlets.py:412
+msgid "Show logs"
+msgstr "Logdateien anzeigen"
+
+#: waeup/ikoba/browser/viewlets.py:421
+msgid "Upload data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:430
+msgid "Process data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:439
+msgid "Export data"
+msgstr "Exportiere Daten"
+
+#: waeup/ikoba/browser/viewlets.py:448
+msgid "View processed files"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:508
+msgid "Enquiries"
+msgstr "Anfragen"
+
+#: waeup/ikoba/browser/viewlets.py:93 waeup/ikoba/browser/viewlets.py:155
+#: waeup/ikoba/customers/viewlets.py:284 waeup/ikoba/customers/viewlets.py:347
+#: waeup/ikoba/customers/viewlets.py:422
+msgid "Edit"
+msgstr "Bearbeiten"
+
+#: waeup/ikoba/customers/batching.py:142
+msgid "Customer record created"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/batching.py:197
+msgid "State '${a}' set"
+msgstr "Zustand '${a}' gesetzt"
+
+#: waeup/ikoba/customers/batching.py:461
+msgid "Customer Sample Document Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:562
+msgid "Customer Sample Contract Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:60
+msgid "Customer Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1013
+msgid "Final Submit"
+msgstr "Endgültig einreichen"
+
+#: waeup/ikoba/customers/browser.py:1017 waeup/ikoba/customers/browser.py:1452
+msgid "Form has been submitted."
+msgstr "Das Formular wurde eingereicht."
+
+#: waeup/ikoba/customers/browser.py:1029 waeup/ikoba/documents/browser.py:324
+msgid "Trigger document transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:106 waeup/ikoba/customers/export.py:130
+#: waeup/ikoba/customers/viewlets.py:54
+msgid "Customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1077
+msgid "Documents of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1085
+msgid "Customer Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1091
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:8
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:9
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:4
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:8
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:8
+msgid "Id"
+msgstr "Id"
+
+#: waeup/ikoba/customers/browser.py:1092 waeup/ikoba/customers/browser.py:1527
+#: waeup/ikoba/products/productoptions.py:44
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:9
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:9
+#: waeup/ikoba/products/browser_templates/containerpage.pt:9
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:39
+msgid "Title"
+msgstr "Titel"
+
+#: waeup/ikoba/customers/browser.py:1093 waeup/ikoba/customers/browser.py:1528
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:10
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:10
+msgid "Type"
+msgstr "Typ"
+
+#: waeup/ikoba/customers/browser.py:1094 waeup/ikoba/customers/browser.py:1529
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:33
+msgid "State"
+msgstr "Status"
+
+#: waeup/ikoba/customers/browser.py:1095 waeup/ikoba/customers/browser.py:1530
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:12
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:12
+msgid "Last Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1181 waeup/ikoba/customers/viewlets.py:160
+msgid "Contracts"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:1226
+msgid "${a}: Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1229 waeup/ikoba/customers/browser.py:1270
+#: waeup/ikoba/customers/browser.py:1295
+msgid "Add contract"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1234
+msgid "Remove selected contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1240
+msgid "No contract selected."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:132
+msgid "Find customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1325
+msgid "Select product"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:133 waeup/ikoba/customers/browser.py:175
+msgid "Find customer(s)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1336
+msgid "Save and proceed"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1448
+msgid "Apply now (final submit)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1464
+msgid "Trigger contract transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:150 waeup/ikoba/customers/browser.py:192
+msgid "Empty search string"
+msgstr "Leerer Suchtext"
+
+#: waeup/ikoba/customers/browser.py:1512
+msgid "Contracts of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1520
+msgid "Customer Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:156 waeup/ikoba/customers/browser.py:198
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr "Nur Jafreszahlen erlaubt (z.B. 2011)"
+
+#: waeup/ikoba/customers/browser.py:162 waeup/ikoba/customers/browser.py:205
+#: waeup/ikoba/customers/browser.py:563 waeup/ikoba/customers/browser.py:575
+msgid "No customer found."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:174 waeup/ikoba/customers/viewlets.py:180
+msgid "Manage customer section"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:233 waeup/ikoba/customers/viewlets.py:188
+msgid "Add customer"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:236
+msgid "Create customer record"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:241
+msgid "Customer created."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:256
+msgid "Set temporary password for ${a}"
+msgstr "Setze temporäres Passwort für ${a}"
+
+#: waeup/ikoba/customers/browser.py:279
+msgid "Login now"
+msgstr "Jetzt einloggen"
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:283
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:289
+msgid "You successfully logged in as customer."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:312
+msgid "${a}: Base Data (account deactivated)"
+msgstr "${a}: Basisdaten (Konto gesperrt)"
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:314
+msgid "${a}: Base Data"
+msgstr "${a}: Basisdaten"
+
+#: waeup/ikoba/customers/browser.py:320
+msgid "set"
+msgstr "gesetzt"
+
+#: waeup/ikoba/customers/browser.py:321
+msgid "unset"
+msgstr "ungesetzt"
+
+#: waeup/ikoba/customers/browser.py:385
+msgid "Manage base data"
+msgstr "Basisdaten verwalten"
+
+#: waeup/ikoba/customers/browser.py:435
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:447 waeup/ikoba/customers/browser.py:1044
+#: waeup/ikoba/customers/browser.py:1479 waeup/ikoba/documents/browser.py:339
+msgid "No transition"
+msgstr "Keine Änderung"
+
+#: waeup/ikoba/customers/browser.py:450 waeup/ikoba/customers/browser.py:1047
+#: waeup/ikoba/customers/browser.py:1482
+msgid "Apply"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:457 waeup/ikoba/customers/browser.py:1055
+#: waeup/ikoba/customers/browser.py:1490 waeup/ikoba/documents/browser.py:350
+msgid "Transition '%s' executed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:474
+msgid "Customer account has been activated."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:494
+msgid "Customer account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:513
+msgid "${a}: History"
+msgstr "${a}: Historie"
+
+#: waeup/ikoba/customers/browser.py:523
+msgid "Request password for first-time login"
+msgstr "Passwort zum erstmaligen Einloggen beantragen"
+
+#: waeup/ikoba/customers/browser.py:557
+msgid "An error occurred."
+msgstr "Es ein Fehler auf."
+
+#: waeup/ikoba/customers/browser.py:566
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+"Das Passwort wurde bereits gesetzt und benutzt. Bitte gehen Sie zu Login-"
+"Seite."
+
+#: waeup/ikoba/customers/browser.py:610
+msgid "Create customer account"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:64
+msgid ""
+"You can not edit some data after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:648
+msgid "You have successfully created a customer account for the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:669
+msgid "Your request was successful."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:67
+msgid ""
+"You can not edit your document after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:688 waeup/ikoba/customers/viewlets.py:303
+msgid "Upload files"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:70
+msgid ""
+"You can not edit your contract after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:721
+msgid "Edit base data and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:722
+msgid "Edit base data"
+msgstr "Basisdaten editieren"
+
+#: waeup/ikoba/customers/browser.py:741
+msgid "Passport picture is missing."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:746
+msgid "Save and request registration now"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:755
+msgid "Registration form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:771 waeup/ikoba/customers/viewlets.py:293
+msgid "Change password"
+msgstr "Passwort ändern"
+
+#: waeup/ikoba/customers/browser.py:785
+msgid "Password changed."
+msgstr "Passwort geändert"
+
+#: waeup/ikoba/customers/browser.py:816 waeup/ikoba/customers/viewlets.py:166
+#: waeup/ikoba/documents/browser.py:62 waeup/ikoba/documents/browser.py:82
+#: waeup/ikoba/documents/viewlets.py:49
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:3
+msgid "Documents"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:856
+msgid "${a}: Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:859 waeup/ikoba/customers/browser.py:900
+#: waeup/ikoba/customers/browser.py:924 waeup/ikoba/documents/browser.py:96
+#: waeup/ikoba/documents/browser.py:119 waeup/ikoba/documents/browser.py:129
+msgid "Add document"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:864 waeup/ikoba/documents/browser.py:101
+msgid "Remove selected documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:870
+msgid "No document selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:935 waeup/ikoba/customers/browser.py:1306
+#: waeup/ikoba/documents/browser.py:144
+msgid "${a} added."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:94
+msgid "The requested form is locked (read-only)."
+msgstr "Dieses Formular ist gesperrt."
+
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:56
+msgid "Select all"
+msgstr "Alle auswählen"
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:10
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:10
+msgid "with name"
+msgstr "mit Name"
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:11
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:11
+msgid "with registration number"
+msgstr "mit Regsistrierungsnummer"
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:12
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:12
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:23
+msgid "Search Results"
+msgstr "Suchergebnisse"
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:32
+msgid "Reg. Number"
+msgstr "Registrierungsnummer"
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:9
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:9
+msgid "with id"
+msgstr "mit Id"
+
+#: waeup/ikoba/customers/browser_templates/contractaddpage.pt:7
+msgid "Contract Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:20
+msgid "Contract Type:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:19
+msgid ""
+"A customer record will be created and your login credentials will be sent to "
+"the email address given above. To be able to proceed you must provide a "
+"valid email address!"
+msgstr ""
+
+#. Default: "Ikoba is creating a temporary customer password which will replace the original password chosen by the customer. The password will be valid for ${minutes} minutes. The customer can't login during this time period. After expiration the customer can login with the original password again."
+#: waeup/ikoba/customers/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+"Kofa erzeugt automatisch ein temporäres Passwort, welches das originale "
+"Passwort des Studenten ersetzt. Das Passwort bleibt für ${minutes} Minuten "
+"gültig. Während dieser Zeit kann sich der Student nicht einloggen. Nach "
+"Verfall des temporären Passworts kann das originale Passwort wieder "
+"verwendet werden. "
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/ikoba/customers/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+"Das temporäre Passwort \"${password}\" wurde gesetzt. Sie können sich durch "
+"Anklicken des Buttons unten jetzt einloggen."
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:19
+msgid ""
+"Your customer record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:8
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+"Bitte helfen Sie uns, Missbrauch des Dienstes zu vermeiden, indem Sie das "
+"folgende Captcha lösen:"
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+"Email konnte nicht versendet werden. Bitte versuchen Sie es später noch "
+"einmal."
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:14
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:44
+msgid "Thanks for using Ikoba!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:20
+msgid "Your login credentials are:"
+msgstr "Ihre Zugangsdaten sind:"
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:23
+msgid "User Name:"
+msgstr "Benutzername:"
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:3
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+"EIne Email mit Ihren Benutzernamen und Ihrem Passwort wurde versendet an"
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr "Drucken Sie diese Seite und gehen Sie zu "
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+"Bitte achten Sie auf Groß- und Kleinschreibung bei Passwörtern und halten "
+"Sie Ihr Passwort geheim."
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr "Bitte prüfen Sie den Eingang dieser Email und fahren Sie dann fort."
+
+#: waeup/ikoba/customers/contracts.py:147
+#: waeup/ikoba/customers/documents.py:162
+msgid "Customer has not yet been approved."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:153
+msgid "Attached documents must be verified first."
+msgstr ""
+
+#: waeup/ikoba/customers/documents.py:165
+msgid "No file uploaded."
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:194
+msgid "Customer Sample Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:246
+msgid "Customer Sample Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:142
+#: waeup/ikoba/customers/fileviewlets.py:143
+#: waeup/ikoba/documents/fileviewlets.py:39
+#: waeup/ikoba/documents/fileviewlets.py:40
+msgid "PDF File"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:167
+#: waeup/ikoba/customers/fileviewlets.py:168
+#: waeup/ikoba/documents/fileviewlets.py:57
+#: waeup/ikoba/documents/fileviewlets.py:58
+msgid "PDF Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:48
+msgid "Passport Picture"
+msgstr "Passbild"
+
+#: waeup/ikoba/customers/fileviewlets.py:60
+msgid "Passport Picture (jpg only)"
+msgstr "Passbild (nur jpg)"
+
+#: waeup/ikoba/customers/fileviewlets.py:90
+#: waeup/ikoba/customers/fileviewlets.py:91
+#: waeup/ikoba/customers/fileviewlets.py:115
+#: waeup/ikoba/customers/fileviewlets.py:116
+msgid "Sample Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:101
+#: waeup/ikoba/customers/interfaces.py:195
+#: waeup/ikoba/customers/interfaces.py:213
+msgid "First Name"
+msgstr "Vorname"
+
+#: waeup/ikoba/customers/interfaces.py:106
+#: waeup/ikoba/customers/interfaces.py:218
+msgid "Middle Name"
+msgstr "Zweiter Vorname"
+
+#: waeup/ikoba/customers/interfaces.py:111
+#: waeup/ikoba/customers/interfaces.py:223
+msgid "Last Name (Surname)"
+msgstr "Nachname"
+
+#: waeup/ikoba/customers/interfaces.py:116
+msgid "Sex"
+msgstr "Geschlecht"
+
+#: waeup/ikoba/customers/interfaces.py:122
+#: waeup/ikoba/customers/interfaces.py:161
+#: waeup/ikoba/customers/interfaces.py:190
+msgid "Registration Number"
+msgstr "Registrierungsnummer"
+
+#: waeup/ikoba/customers/interfaces.py:129
+#: waeup/ikoba/customers/interfaces.py:228
+msgid "Email"
+msgstr "Emailadresse"
+
+#: waeup/ikoba/customers/interfaces.py:136 waeup/ikoba/interfaces.py:433
+#: waeup/ikoba/interfaces.py:511
+msgid "Phone"
+msgstr "Telefon"
+
+#: waeup/ikoba/customers/interfaces.py:252
+#: waeup/ikoba/documents/interfaces.py:52
+msgid "Document Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:303
+msgid "Contract Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:309
+#: waeup/ikoba/customers/interfaces.py:332
+msgid "Product"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:315
+#: waeup/ikoba/customers/interfaces.py:365
+#: waeup/ikoba/products/interfaces.py:73
+msgid "Options/Fees"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:344
+#: waeup/ikoba/customers/interfaces.py:379
+msgid "Document"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:355
+msgid "Comment"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:82
+msgid "Account suspended"
+msgstr "Konto gesperrt"
+
+#: waeup/ikoba/customers/interfaces.py:88
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:90
+msgid ""
+"This message will be shown if and only if deactivated customers try to login."
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:96
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:31
+msgid "Customer Id"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:161
+msgid "Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:180
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:240
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:25
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:30
+msgid "Connected Files"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:301 waeup/ikoba/customers/utils.py:309
+#: waeup/ikoba/customers/utils.py:317 waeup/ikoba/documents/utils.py:32
+msgid "created"
+msgstr "neu"
+
+#: waeup/ikoba/customers/utils.py:302
+msgid "started"
+msgstr "begonnen"
+
+#: waeup/ikoba/customers/utils.py:303
+msgid "requested"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:304 waeup/ikoba/customers/utils.py:311
+msgid "approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:305
+msgid "provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:310
+msgid "submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:312 waeup/ikoba/customers/utils.py:320
+msgid "rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:313 waeup/ikoba/customers/utils.py:321
+msgid "expired"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:318
+msgid "submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:319
+msgid "verified"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:325
+msgid "Sample Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:326 waeup/ikoba/documents/utils.py:37
+msgid "PDF Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:330
+msgid "Sample Contract"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:374
+msgid "Contractor"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:377
+msgid "Document Owner"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:386
+msgid "Contract Data"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:401
+msgid "Transaction History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:407
+msgid "Registration History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:425 waeup/ikoba/products/interfaces.py:95
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:32
+msgid "Terms and Conditions"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:114
+msgid "My Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:115
+msgid "My Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:116 waeup/ikoba/customers/viewlets.py:172
+msgid "History"
+msgstr "Historie"
+
+#: waeup/ikoba/customers/viewlets.py:137 waeup/ikoba/customers/viewlets.py:154
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr "Basisdaten"
+
+#: waeup/ikoba/customers/viewlets.py:217 waeup/ikoba/customers/viewlets.py:363
+#: waeup/ikoba/customers/viewlets.py:438 waeup/ikoba/documents/viewlets.py:89
+msgid "Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:227
+msgid "Login as customer"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:236
+msgid "Deactivate account"
+msgstr "Deaktiviere Konto"
+
+#: waeup/ikoba/customers/viewlets.py:248 waeup/ikoba/customers/viewlets.py:269
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:257
+msgid "Activate account"
+msgstr "Aktiviere Konto"
+
+#: waeup/ikoba/customers/viewlets.py:283
+msgid "Edit and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:323
+msgid "Download documents overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:372 waeup/ikoba/customers/viewlets.py:447
+#: waeup/ikoba/documents/viewlets.py:69 waeup/ikoba/products/viewlets.py:69
+msgid "View"
+msgstr "Ansehen"
+
+#: waeup/ikoba/customers/viewlets.py:383
+msgid "Download document slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:395
+msgid "Download contracts overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:413
+msgid "Manage official data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:462
+msgid "Download contract slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:98
+msgid "My Data"
+msgstr "Meine Daten"
+
+#: waeup/ikoba/customers/vocabularies.py:58
+msgid "male"
+msgstr "männlich"
+
+#: waeup/ikoba/customers/vocabularies.py:60
+msgid "female"
+msgstr "weiblich"
+
+#: waeup/ikoba/customers/workflow.py:103 waeup/ikoba/customers/workflow.py:124
+msgid "Reset to requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:104 waeup/ikoba/customers/workflow.py:125
+msgid "Reset to 'requested'"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:111 waeup/ikoba/customers/workflow.py:209
+#: waeup/ikoba/customers/workflow.py:210 waeup/ikoba/customers/workflow.py:216
+#: waeup/ikoba/customers/workflow.py:217 waeup/ikoba/customers/workflow.py:223
+#: waeup/ikoba/customers/workflow.py:224 waeup/ikoba/customers/workflow.py:237
+#: waeup/ikoba/customers/workflow.py:238 waeup/ikoba/customers/workflow.py:324
+#: waeup/ikoba/customers/workflow.py:325 waeup/ikoba/customers/workflow.py:331
+#: waeup/ikoba/customers/workflow.py:332 waeup/ikoba/customers/workflow.py:338
+#: waeup/ikoba/customers/workflow.py:339 waeup/ikoba/customers/workflow.py:352
+#: waeup/ikoba/customers/workflow.py:353
+msgid "Reset to initial state"
+msgstr "Auf 'initialisiert' zurücksetzen"
+
+#: waeup/ikoba/customers/workflow.py:180
+msgid "Create contract"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:183
+msgid "Contract created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:188
+msgid "Submit for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:189
+msgid "Submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:195
+msgid "Approve"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:196
+msgid "Approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:202 waeup/ikoba/customers/workflow.py:317
+msgid "Reject"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:203 waeup/ikoba/customers/workflow.py:318
+msgid "REJECTED"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:230 waeup/ikoba/customers/workflow.py:231
+#: waeup/ikoba/customers/workflow.py:345 waeup/ikoba/customers/workflow.py:346
+msgid "Set to expired"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:295 waeup/ikoba/documents/workflow.py:37
+msgid "Create document"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:298 waeup/ikoba/documents/workflow.py:40
+msgid "Document created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:303
+msgid "Submit for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:304
+msgid "Submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:310
+msgid "Verify"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:311
+msgid "Verified"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:45
+msgid "Create customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:48
+msgid "Customer created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:53
+msgid "Start registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:56
+msgid "Customer registration started"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:61
+msgid "Request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:62
+msgid "Customer registration requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:68
+msgid "Approve customer provisionally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:69
+msgid "Customer registration provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:75
+msgid "Approve finally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:76
+msgid "Customer registration finally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:82
+msgid "Approve customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:83
+msgid "Customer registration approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:89
+msgid "Reject customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:90
+msgid "Customer registration rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:96 waeup/ikoba/customers/workflow.py:110
+#: waeup/ikoba/customers/workflow.py:117
+msgid "Reset customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:97 waeup/ikoba/customers/workflow.py:118
+msgid "Reset to initial customer state"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:135
+msgid "Public PDF Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:147
+msgid "Public HTML Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:159
+msgid "Public REST Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:140 waeup/ikoba/products/browser.py:133
+msgid "The id chosen already exists."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:210
+#: waeup/ikoba/documents/fileviewlets.py:80
+msgid "The document requested has not yet been published."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:256 waeup/ikoba/documents/browser.py:282
+#: waeup/ikoba/products/browser.py:185 waeup/ikoba/products/browser.py:215
+msgid "Remove selected local roles"
+msgstr "Ausgewählte lokale Rollen löschen"
+
+#: waeup/ikoba/documents/browser.py:257 waeup/ikoba/documents/browser.py:278
+#: waeup/ikoba/products/browser.py:186 waeup/ikoba/products/browser.py:211
+msgid "Add local role"
+msgstr "Lokale Rolle hinzufügen"
+
+#: waeup/ikoba/documents/browser.py:267
+msgid "Manage document "
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:342
+msgid "Apply now"
+msgstr "Jetzt anwenden"
+
+#: waeup/ikoba/documents/browser.py:94
+msgid "Manage document section"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:1
+#: waeup/ikoba/products/browser_templates/containerpage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr "Es existieren bisher keine Unterobjekte."
+
+#: waeup/ikoba/documents/browser_templates/documentaddform.pt:7
+#: waeup/ikoba/customers/browser_templates/documentaddpage.pt:7
+msgid "Document Type"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:12
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:12
+msgid "Files"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:16
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:16
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:21
+msgid "Document Type:"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:37
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:48
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:5
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:37
+msgid "Name"
+msgstr "Name"
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:39
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:50
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:39
+msgid "Local Role"
+msgstr "Lokale Rolle"
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:6
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:6
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:7
+msgid "Metadata"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:9
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:12
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:7
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:9
+msgid "Local Roles"
+msgstr "Lokale Rollen"
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:25
+msgid "Content:"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:101
+msgid "Public HTML Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:110
+msgid "Public REST Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:92
+msgid "Public PDF Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:110
+msgid "Multilingual content in REST (reStructuredText) format"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:58
+msgid "Document Title"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:78
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:97
+#: waeup/ikoba/products/interfaces.py:97
+msgid "Multilingual content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:33
+msgid "published"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:38
+msgid "HTML Document"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:39
+msgid "REST Document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:45
+msgid "Publish document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:48
+msgid "Document published"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:53
+msgid "Retract documet"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:56
+msgid "Document retracted"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:361
+msgid "Processor name"
+msgstr "Prozessorname"
+
+#: waeup/ikoba/interfaces.py:387
+msgid "Email Address:"
+msgstr "Emailadresse:"
+
+#: waeup/ikoba/interfaces.py:394
+msgid "Email to:"
+msgstr "Email an:"
+
+#: waeup/ikoba/interfaces.py:401
+msgid "Subject:"
+msgstr "Betreff:"
+
+#: waeup/ikoba/interfaces.py:405
+msgid "Full Name:"
+msgstr "Vollständiger Name:"
+
+#: waeup/ikoba/interfaces.py:409
+msgid "Text:"
+msgstr "Text:"
+
+#: waeup/ikoba/interfaces.py:438 waeup/ikoba/interfaces.py:494
+msgid "Public Name"
+msgstr "Öffentlicher Name"
+
+#: waeup/ikoba/interfaces.py:448 waeup/ikoba/interfaces.py:449
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:455
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:456
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:485
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:35
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:46
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:35
+msgid "User Id"
+msgstr "Benutzer Id"
+
+#: waeup/ikoba/interfaces.py:490
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:34
+msgid "Full Name"
+msgstr "Vollständiger Name"
+
+#: waeup/ikoba/interfaces.py:500
+msgid "Description/Notice"
+msgstr "Beschreibung/Nachricht"
+
+#: waeup/ikoba/interfaces.py:517
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr "Portalweite Rollen:"
+
+#: waeup/ikoba/interfaces.py:564
+msgid "Name of Company"
+msgstr "Name der Universität"
+
+#: waeup/ikoba/interfaces.py:565
+msgid "Sample Company"
+msgstr "Deomo-Universität"
+
+#: waeup/ikoba/interfaces.py:570
+msgid "Abbreviated Title of Company"
+msgstr "Abkürzung"
+
+#: waeup/ikoba/interfaces.py:576
+msgid "Content in HTML format"
+msgstr "Inhalt im HTML-Format"
+
+#: waeup/ikoba/interfaces.py:588
+msgid "Name of Administrator"
+msgstr "Name des Administrators"
+
+#: waeup/ikoba/interfaces.py:594
+msgid "Email Address of Administrator"
+msgstr "Emailadresse des Administrators"
+
+#: waeup/ikoba/interfaces.py:601
+msgid "Subject of Email to Administrator"
+msgstr "Betreff der Email an Administrator"
+
+#: waeup/ikoba/interfaces.py:602
+msgid "Ikoba Contact"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:607
+msgid "SMTP mailer to use when sending mail"
+msgstr "SMTP Dienst"
+
+#: waeup/ikoba/interfaces.py:614
+msgid "Captcha used for public registration pages"
+msgstr "Captcha für öffentliche Registrierung"
+
+#: waeup/ikoba/interfaces.py:66
+msgid "new"
+msgstr "neu"
+
+#: waeup/ikoba/interfaces.py:67
+msgid "completed"
+msgstr "abgeschlossen"
+
+#: waeup/ikoba/interfaces.py:68
+msgid "pending"
+msgstr "unerledigt"
+
+#: waeup/ikoba/interfaces.py:69
+msgid "active"
+msgstr "aktiv"
+
+#: waeup/ikoba/interfaces.py:70
+msgid "assigned"
+msgstr "zugewiesen"
+
+#: waeup/ikoba/interfaces.py:71
+msgid "callbacks"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:36
+msgid "Misuse"
+msgstr "Missbrauch"
+
+#: waeup/ikoba/mandates/browser.py:41
+msgid "No mandate."
+msgstr "Kein Mandat."
+
+#: waeup/ikoba/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr "Nichts zu tun."
+
+#: waeup/ikoba/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr "Falsche Mandatsparameter."
+
+#: waeup/ikoba/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr "Mandat abgelaufen."
+
+#: waeup/ikoba/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/ikoba/objecthistory.py:66
+msgid "by"
+msgstr "von"
+
+#: waeup/ikoba/payments/currencies.py:100
+msgid "Iranian Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:101
+msgid "Iceland Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:102
+msgid "Jamaican Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:103
+msgid "Jordanian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:104
+msgid "Yen"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:105
+msgid "Kenyan Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:106
+msgid "Som"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:107
+msgid "Riel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:108
+msgid "Comoro Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:110
+msgid "North Korean Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:113
+msgid "Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:114
+msgid "Kuwaiti Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:115
+msgid "Cayman Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:116
+msgid "Tenge"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:117
+msgid "Kip"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:118
+msgid "Lebanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:119
+msgid "Sri Lanka Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:120
+msgid "Liberian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:121
+msgid "Loti"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:122
+msgid "Lithuanian Litas"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:123
+msgid "Libyan Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:124
+msgid "Moroccan Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:125
+msgid "Moldovan Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:126
+msgid "Malagasy Ariary"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:128
+msgid "Denar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:131
+msgid "Kyat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:132
+msgid "Tugrik"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:133
+msgid "Pataca"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:134
+msgid "Ouguiya"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:135
+msgid "Mauritius Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:136
+msgid "Rufiyaa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:137
+msgid "Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:138
+msgid "Mexican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:139
+msgid "Mexican Unidad de Inversion (UDI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:140
+msgid "Malaysian Ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:141
+msgid "Mozambique Metical"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:142
+msgid "Namibia Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:143
+msgid "Naira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:144
+msgid "Cordoba Oro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:145
+msgid "Norwegian Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:146
+msgid "Nepalese Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:147
+msgid "New Zealand Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:148
+msgid "Rial Omani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:149
+msgid "Balboa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:150
+msgid "Nuevo Sol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:151
+msgid "Kina"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:152
+msgid "Philippine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:153
+msgid "Pakistan Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:154
+msgid "Zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:155
+msgid "Guarani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:156
+msgid "Qatari Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:157
+msgid "New Romanian Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:158
+msgid "Serbian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:159
+msgid "Russian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:160
+msgid "Rwanda Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:161
+msgid "Saudi Riyal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:162
+msgid "Solomon Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:163
+msgid "Seychelles Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:164
+msgid "Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:165
+msgid "Swedish Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:166
+msgid "Singapore Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:168
+msgid "Saint Helena Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:171
+msgid "Leone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:172
+msgid "Somali Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:173
+msgid "Surinam Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:174
+msgid "South Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:175
+msgid "Dobra"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:176
+msgid "El Salvador Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:177
+msgid "Syrian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:178
+msgid "Lilangeni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:179
+msgid "Baht"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:180
+msgid "Somoni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:181
+msgid "Turkmenistan New Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:182
+msgid "Tunisian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:183
+msgid "Pa'anga"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:184
+msgid "Turkish Lira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:185
+msgid "Trinidad and Tobago Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:186
+msgid "New Taiwan Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:187
+msgid "Tanzanian Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:188
+msgid "Hryvnia"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:189
+msgid "Uganda Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:190
+msgid "US Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:191
+msgid "US Dollar (Next day)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:193
+msgid "Uruguay Peso en Unidades Indexadas (URUIURUI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:196
+msgid "Peso Uruguayo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:197
+msgid "Uzbekistan Sum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:198
+msgid "Bolivar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:199
+msgid "Dong"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:200
+msgid "Vatu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:201
+msgid "Tala"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:202
+msgid "CFA Franc BEAC"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:204
+msgid "East Caribbean Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:207
+msgid "CFA Franc BCEAO"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:208
+msgid "CFP Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:209
+msgid "Yemeni Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:210
+msgid "Rand"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:211
+msgid "Zambian Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:212
+msgid "Zimbabwe Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:23
+msgid "UAE Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:24
+msgid "Afghani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:25
+msgid "Lek"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:26
+msgid "Armenian Dram"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:28
+msgid "Netherlands Antillean Guilder"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:31
+msgid "Kwanza"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:32
+msgid "Argentine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:33
+msgid "Australian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:34
+msgid "Aruban Florin"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:35
+msgid "Azerbaijanian Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:36
+msgid "Convertible Mark"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:37
+msgid "Barbados Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:38
+msgid "Taka"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:39
+msgid "Bulgarian Lev"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:40
+msgid "Bahraini Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:41
+msgid "Burundi Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:42
+msgid "Bermudian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:43
+msgid "Brunei Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:44
+msgid "Boliviano"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:45
+msgid "Mvdol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:46
+msgid "Brazilian Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:47
+msgid "Bahamian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:48
+msgid "Ngultrum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:49
+msgid "Pula"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:50
+msgid "Belarussian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:51
+msgid "Belize Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:52
+msgid "Canadian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:54
+msgid "Congolese Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:57
+msgid "WIR Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:58
+msgid "Swiss Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:59
+msgid "WIR Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:60
+msgid "Unidad de Fomento"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:61
+msgid "Chilean Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:62
+msgid "Yuan Renminbi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:63
+msgid "Colombian Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:64
+msgid "Unidad de Valor Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:65
+msgid "Costa Rican Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:66
+msgid "Peso Convertible"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:67
+msgid "Cuban Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:68
+msgid "Cabo Verde Escudo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:69
+msgid "Czech Koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:70
+msgid "Djibouti Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:71
+msgid "Danish Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:72
+msgid "Dominican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:73
+msgid "Algerian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:74
+msgid "Egyptian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:75
+msgid "-Nakfa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:76
+msgid "Ethiopian Birr"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:77
+#: waeup/ikoba/payments/paypal_currencies.py:40
+msgid "Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:78
+msgid "Fiji Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:80
+msgid "Falkland Islands Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:83
+msgid "Pound Sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:84
+msgid "Lari"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:85
+msgid "Ghana Cedi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:86
+msgid "Gibraltar Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:87
+msgid "Dalasi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:88
+msgid "Guinea Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:89
+msgid "Quetzal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:90
+msgid "Guyana Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:91
+msgid "Hong Kong Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:92
+msgid "Lempira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:93
+msgid "Croatian Kuna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:94
+msgid "Gourde"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:95
+msgid "Forint"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:96
+msgid "Rupiah"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:97
+msgid "New Israeli Sheqel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:98
+msgid "Indian Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:99
+msgid "Iraqi Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:168
+msgid "Payment State"
+msgstr "Status der Zahlung"
+
+#: waeup/ikoba/payments/interfaces.py:175
+msgid "Creation Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:181
+msgid "Payment Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:187
+msgid "Amount"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:188
+msgid "The overall sum payed, including all taxes fees, etc."
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:217
+msgid "Activation Code"
+msgstr "Aktivierungs-Code"
+
+#: waeup/ikoba/payments/interfaces.py:224
+msgid "Response Amount Approved"
+msgstr "Response Amount Approved"
+
+#: waeup/ikoba/payments/interfaces.py:231
+msgid "Response Code"
+msgstr "Response-Code"
+
+#: waeup/ikoba/payments/interfaces.py:238
+msgid "Response Description"
+msgstr "Response-Beschreibung"
+
+#: waeup/ikoba/payments/interfaces.py:34
+msgid "Not yet paid"
+msgstr "Noch nicht bezahlt"
+
+#: waeup/ikoba/payments/interfaces.py:35
+msgid "Paid"
+msgstr "Bezahlt"
+
+#: waeup/ikoba/payments/interfaces.py:36
+msgid "Failed"
+msgstr "Fehler"
+
+#: waeup/ikoba/payments/paypal.py:1147
+msgid "Credit Card (PayPal)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1223
+msgid "PayPal Payment"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:73
+msgid "pieces"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:74 waeup/ikoba/payments/paypal.py:1136
+msgid "license"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:10
+msgid "ANGUILLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:100
+msgid "SOUTH KOREA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:101
+msgid "KUWAIT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:102
+msgid "CAYMAN ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:103
+msgid "KAZAKHSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:104
+msgid "LAOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:105
+msgid "SAINT LUCIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:106
+msgid "LIECHTENSTEIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:107
+msgid "SRI LANKA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:108
+msgid "LESOTHO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:109
+msgid "LITHUANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:11
+msgid "ALBANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:110
+msgid "LUXEMBOURG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:111
+msgid "LATVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:112
+msgid "MOROCCO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:113
+msgid "MADAGASCAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:114
+msgid "MARSHALL ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:115
+msgid "MALI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:116
+msgid "MONGOLIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:117
+msgid "MARTINIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:118
+msgid "MAURITANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:119
+msgid "MONTSERRAT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:12
+msgid "ARMENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:120
+msgid "MALTA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:121
+msgid "MAURITIUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:122
+msgid "MALDIVES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:123
+msgid "MALAWI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:124
+msgid "MEXICO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:125
+msgid "MALAYSIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:126
+msgid "MOZAMBIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:127
+msgid "NAMIBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:128
+msgid "NEW CALEDONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:129
+msgid "NIGER"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:13
+msgid "NETHERLANDS ANTILLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:130
+msgid "NORFOLK ISLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:131
+msgid "NICARAGUA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:132
+msgid "NETHERLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:133
+msgid "NORWAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:134
+msgid "NEPAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:135
+msgid "NAURU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:136
+msgid "NIUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:137
+msgid "NEW ZEALAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:138
+msgid "OMAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:139
+msgid "PANAMA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:14
+msgid "ANGOLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:140
+msgid "PERU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:141
+msgid "FRENCH POLYNESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:142
+msgid "PAPUA NEW GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:143
+msgid "PHILIPPINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:144
+msgid "POLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:145
+msgid "SAINT PIERRE AND MIQUELON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:146
+msgid "PITCAIRN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:147
+msgid "PORTUGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:148
+msgid "PALAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:149
+msgid "QATAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:15
+msgid "ARGENTINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:150
+msgid "REUNION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:151
+msgid "ROMANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:152
+msgid "SERBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:153
+msgid "RUSSIAN FEDERATION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:154
+msgid "RWANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:155
+msgid "SAUDI ARABIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:156
+msgid "SOLOMON ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:157
+msgid "SEYCHELLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:158
+msgid "SWEDEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:159
+msgid "SINGAPORE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:16
+msgid "AUSTRIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:160
+msgid "SAINT HELENA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:161
+msgid "SLOVENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:162
+msgid "SVALBARD AND JAN MAYEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:163
+msgid "SLOVAKIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:164
+msgid "SIERRA LEONE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:165
+msgid "SAN MARINO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:166
+msgid "SENEGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:167
+msgid "SOMALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:168
+msgid "SURINAME"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:169
+msgid "SAO TOME AND PRINCIPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:17
+msgid "AUSTRALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:170
+msgid "EL SALVADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:171
+msgid "SWAZILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:172
+msgid "TURKS AND CAICOS ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:173
+msgid "CHAD"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:174
+msgid "TOGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:175
+msgid "THAILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:176
+msgid "TAJIKISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:177
+msgid "TURKMENISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:178
+msgid "TUNISIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:179
+msgid "TONGA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:18
+msgid "ARUBA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:180
+msgid "TURKEY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:181
+msgid "TRINIDAD AND TOBAGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:182
+msgid "TUVALU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:183
+msgid "TAIWAN, PROVINCE OF CHINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:184
+msgid "TANZANIA, UNITED REPUBLIC OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:185
+msgid "UKRAINE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:186
+msgid "UGANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:187
+msgid "UNITED STATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:188
+msgid "URUGUAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:189
+msgid "HOLY SEE (VATICAN CITY STATE)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:19
+msgid "AZERBAIJAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:190
+msgid "SAINT VINCENT AND THE GRENADINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:191
+msgid "VENEZUELA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:192
+msgid "VIRGIN ISLANDS, BRITISH"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:193
+msgid "VIETNAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:194
+msgid "VANUATU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:195
+msgid "WALLIS AND FUTUNA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:196
+msgid "SAMOA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:197
+msgid "YEMEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:198
+msgid "MAYOTTE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:199
+msgid "SOUTH AFRICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:20
+msgid "BOSNIA-HERZEGOVINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:200
+msgid "ZAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:21
+msgid "BARBADOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:22
+msgid "BELGIUM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:23
+msgid "BURKINA FASO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:24
+msgid "BULGARIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:25
+msgid "BAHRAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:26
+msgid "BURUNDI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:27
+msgid "BENIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:28
+msgid "BERMUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:29
+msgid "BRUNEI DARUSSALAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:30
+msgid "BOLIVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:31
+msgid "BRAZIL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:32
+msgid "BAHAMAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:33
+msgid "BHUTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:34
+msgid "BOTSWANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:35
+msgid "BELIZE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:36
+msgid "CANADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:37
+msgid "DEMOCRATIC REPUBLIC OF CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:38
+msgid "CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:39
+msgid "SWITZERLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:40
+msgid "COOK ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:41
+msgid "CHILE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:42
+msgid "CHINA (domestic Chinese bank transactions only)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:43
+msgid "CHINA (For CUP, bank card and cross-border transactions)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:44
+msgid "COLOMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:45
+msgid "COSTA RICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:46
+msgid "CAPE VERDE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:47
+msgid "CYPRUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:48
+msgid "CZECH REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:49
+msgid "GERMANY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:50
+msgid "DJIBOUTI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:51
+msgid "DENMARK"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:52
+msgid "DOMINICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:53
+msgid "DOMINICAN REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:54
+msgid "ALGERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:55
+msgid "ECUADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:56
+msgid "ESTONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:57
+msgid "EGYPT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:58
+msgid "ERITERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:59
+msgid "SPAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:60
+msgid "ETHIOPIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:61
+msgid "FINLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:62
+msgid "FIJI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:63
+msgid "FALKLAND ISLANDS (MALVINAS)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:64
+msgid "MICRONESIA, FEDERATED STATES OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:65
+msgid "FRANCE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:66
+msgid "GABON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:67
+msgid "UNITED KINGDOM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:68
+msgid "GRENADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:69
+msgid "GEORGIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:7
+msgid "ANDORRA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:70
+msgid "FRENCH GUIANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:71
+msgid "GIBRALTAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:72
+msgid "GREENLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:73
+msgid "GAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:74
+msgid "GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:75
+msgid "GUADELOUPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:76
+msgid "GREECE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:77
+msgid "GUATEMALA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:78
+msgid "GUAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:79
+msgid "GUINEA BISSAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:8
+msgid "UNITED ARAB EMIRATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:80
+msgid "GUYANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:81
+msgid "HONG KONG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:82
+msgid "HONDURAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:83
+msgid "CROATIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:84
+msgid "HUNGARY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:85
+msgid "INDONESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:86
+msgid "IRELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:87
+msgid "ISRAEL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:88
+msgid "INDIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:89
+msgid "ICELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:9
+msgid "ANTIGUA AND BARBUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:90
+msgid "ITALY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:91
+msgid "JAMAICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:92
+msgid "JORDAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:93
+msgid "JAPAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:94
+msgid "KENYA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:95
+msgid "KYRGYZSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:96
+msgid "CAMBODIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:97
+msgid "KIRIBATI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:98
+msgid "COMOROS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:99
+msgid "SAINT KITTS AND NEVIS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:34
+msgid "Australian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:35
+msgid "Brazilian real"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:36
+msgid "Canadian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:37
+msgid "Swiss franc"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:38
+msgid "Czech koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:39
+msgid "Danish krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:41
+msgid "Pound sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:42
+msgid "Hong Kong dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:43
+msgid "Hungarian forint"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:44
+msgid "Israeli new shekel"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:45
+msgid "Japanese yen"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:46
+msgid "Mexican peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:47
+msgid "Malaysian ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:48
+msgid "Norwegian krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:49
+msgid "New Zealand dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:50
+msgid "Philippine peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:51
+msgid "Polish zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:52
+msgid "Swedish krona"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:53
+msgid "Singapore dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:54
+msgid "Thai baht"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:55
+msgid "Turkish lira"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:56
+msgid "New Taiwan dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:57
+msgid "United States dollar"
+msgstr ""
+
+#: waeup/ikoba/products/batching.py:52
+msgid "Product Processor"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:126
+msgid "Create product"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/products/browser.py:137
+msgid "Product ${a} added."
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:195
+msgid "Manage product "
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:59 waeup/ikoba/products/browser.py:79
+#: waeup/ikoba/products/export.py:40 waeup/ikoba/products/viewlets.py:49
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:31
+msgid "Products"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:91
+msgid "Manage product section"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:96
+#: waeup/ikoba/products/browser.py:123
+msgid "Add product"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:22
+msgid "Setting parameters no yet configured."
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containerpage.pt:10
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:41
+msgid "Category"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:6
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:6
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: waeup/ikoba/products/browser_templates/productpage.pt:5
+msgid "Terms and Conditions:"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:48
+#: waeup/ikoba/products/browser_templates/containerpage.pt:8
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:37
+msgid "Product Id"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:55
+msgid "Product Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:61
+msgid "Contract Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:62
+msgid "Product title is used if empty."
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:67
+msgid "Contract Category"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:81
+msgid "Valid from"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:87
+msgid "Valid to"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:48
+msgid "Fee"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:52
+msgid "Currency"
+msgstr ""
+
+#: waeup/ikoba/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/ikoba/userscontainer.py:76
+msgid "Users"
+msgstr "Benutzer"
+
+#: waeup/ikoba/utils/countries.py:10
+msgid "Albania"
+msgstr "Albanien"
+
+#: waeup/ikoba/utils/countries.py:100
+msgid "Guinea"
+msgstr "Guinea"
+
+#: waeup/ikoba/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr "Guinea-Bissau"
+
+#: waeup/ikoba/utils/countries.py:102
+msgid "Guyana"
+msgstr "Guyana"
+
+#: waeup/ikoba/utils/countries.py:103
+msgid "Haiti"
+msgstr "Haiti"
+
+#: waeup/ikoba/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr "Heard Island and Mcdonald Islands"
+
+#: waeup/ikoba/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr "Vatikan"
+
+#: waeup/ikoba/utils/countries.py:106
+msgid "Honduras"
+msgstr "Honduras"
+
+#: waeup/ikoba/utils/countries.py:107
+msgid "Hong Kong"
+msgstr "Hong Kong"
+
+#: waeup/ikoba/utils/countries.py:108
+msgid "Hungary"
+msgstr "Ungarn"
+
+#: waeup/ikoba/utils/countries.py:109
+msgid "Iceland"
+msgstr "Island"
+
+#: waeup/ikoba/utils/countries.py:11
+msgid "Algeria"
+msgstr "Algerien"
+
+#: waeup/ikoba/utils/countries.py:110
+msgid "India"
+msgstr "Indien"
+
+#: waeup/ikoba/utils/countries.py:111
+msgid "Indonesia"
+msgstr "Indonesien"
+
+#: waeup/ikoba/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr "Iran"
+
+#: waeup/ikoba/utils/countries.py:113
+msgid "Iraq"
+msgstr "Irak"
+
+#: waeup/ikoba/utils/countries.py:114
+msgid "Ireland"
+msgstr "Irland"
+
+#: waeup/ikoba/utils/countries.py:115
+msgid "Isle of Man"
+msgstr "Isle of Man"
+
+#: waeup/ikoba/utils/countries.py:116
+msgid "Israel"
+msgstr "Israel"
+
+#: waeup/ikoba/utils/countries.py:117
+msgid "Italy"
+msgstr "Italien"
+
+#: waeup/ikoba/utils/countries.py:118
+msgid "Jamaica"
+msgstr "Jamaica"
+
+#: waeup/ikoba/utils/countries.py:119
+msgid "Japan"
+msgstr "Japan"
+
+#: waeup/ikoba/utils/countries.py:12
+msgid "American Samoa"
+msgstr "American Samoa"
+
+#: waeup/ikoba/utils/countries.py:120
+msgid "Jersey"
+msgstr "Jersey"
+
+#: waeup/ikoba/utils/countries.py:121
+msgid "Jordan"
+msgstr "Jordanien"
+
+#: waeup/ikoba/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr "Kazakhstan"
+
+#: waeup/ikoba/utils/countries.py:123
+msgid "Kenya"
+msgstr "Kenia"
+
+#: waeup/ikoba/utils/countries.py:124
+msgid "Kiribati"
+msgstr "Kiribati"
+
+#: waeup/ikoba/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr "Korea, Democratic People's Republic of"
+
+#: waeup/ikoba/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr "Korea, Republic of"
+
+#: waeup/ikoba/utils/countries.py:127
+msgid "Kuwait"
+msgstr "Kuwait"
+
+#: waeup/ikoba/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr "Kyrgyzstan"
+
+#: waeup/ikoba/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr "Lao People's Democratic Republic"
+
+#: waeup/ikoba/utils/countries.py:13
+msgid "Andorra"
+msgstr "Andorra"
+
+#: waeup/ikoba/utils/countries.py:130
+msgid "Latvia"
+msgstr "Lettland"
+
+#: waeup/ikoba/utils/countries.py:131
+msgid "Lebanon"
+msgstr "Libanon"
+
+#: waeup/ikoba/utils/countries.py:132
+msgid "Lesotho"
+msgstr "Lesotho"
+
+#: waeup/ikoba/utils/countries.py:133
+msgid "Liberia"
+msgstr "Liberia"
+
+#: waeup/ikoba/utils/countries.py:134
+msgid "Libya"
+msgstr "Libyen"
+
+#: waeup/ikoba/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr "Liechtenstein"
+
+#: waeup/ikoba/utils/countries.py:136
+msgid "Lithuania"
+msgstr "Littauen"
+
+#: waeup/ikoba/utils/countries.py:137
+msgid "Luxembourg"
+msgstr "Luxemburg"
+
+#: waeup/ikoba/utils/countries.py:138
+msgid "Macao"
+msgstr "Macao"
+
+#: waeup/ikoba/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr "Mazedonien"
+
+#: waeup/ikoba/utils/countries.py:14
+msgid "Angola"
+msgstr "Angola"
+
+#: waeup/ikoba/utils/countries.py:140
+msgid "Madagascar"
+msgstr "Madagascar"
+
+#: waeup/ikoba/utils/countries.py:141
+msgid "Malawi"
+msgstr "Malawi"
+
+#: waeup/ikoba/utils/countries.py:142
+msgid "Malaysia"
+msgstr "Malaysia"
+
+#: waeup/ikoba/utils/countries.py:143
+msgid "Maldives"
+msgstr "Malediven"
+
+#: waeup/ikoba/utils/countries.py:144
+msgid "Mali"
+msgstr "Mali"
+
+#: waeup/ikoba/utils/countries.py:145
+msgid "Malta"
+msgstr "Malta"
+
+#: waeup/ikoba/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr "Marshall-Inseln"
+
+#: waeup/ikoba/utils/countries.py:147
+msgid "Martinique"
+msgstr "Martinique"
+
+#: waeup/ikoba/utils/countries.py:148
+msgid "Mauritania"
+msgstr "Mauretanien"
+
+#: waeup/ikoba/utils/countries.py:149
+msgid "Mauritius"
+msgstr "Mauritius"
+
+#: waeup/ikoba/utils/countries.py:15
+msgid "Anguilla"
+msgstr "Anguilla"
+
+#: waeup/ikoba/utils/countries.py:150
+msgid "Mayotte"
+msgstr "Mayotte"
+
+#: waeup/ikoba/utils/countries.py:151
+msgid "Mexico"
+msgstr "Mexiko"
+
+#: waeup/ikoba/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr "Micronesia, Federated States of"
+
+#: waeup/ikoba/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr "Moldavien"
+
+#: waeup/ikoba/utils/countries.py:154
+msgid "Monaco"
+msgstr "Monaco"
+
+#: waeup/ikoba/utils/countries.py:155
+msgid "Mongolia"
+msgstr "Mongolei"
+
+#: waeup/ikoba/utils/countries.py:156
+msgid "Montenegro"
+msgstr "Montenegro"
+
+#: waeup/ikoba/utils/countries.py:157
+msgid "Montserrat"
+msgstr "Montserrat"
+
+#: waeup/ikoba/utils/countries.py:158
+msgid "Morocco"
+msgstr "Marokko"
+
+#: waeup/ikoba/utils/countries.py:159
+msgid "Mozambique"
+msgstr "Mosambik"
+
+#: waeup/ikoba/utils/countries.py:16
+msgid "Antarctica"
+msgstr "Antarktis"
+
+#: waeup/ikoba/utils/countries.py:160
+msgid "Myanmar"
+msgstr "Myanmar"
+
+#: waeup/ikoba/utils/countries.py:161
+msgid "Namibia"
+msgstr "Namibia"
+
+#: waeup/ikoba/utils/countries.py:162
+msgid "Nauru"
+msgstr "Nauru"
+
+#: waeup/ikoba/utils/countries.py:163
+msgid "Nepal"
+msgstr "Nepal"
+
+#: waeup/ikoba/utils/countries.py:164
+msgid "Netherlands"
+msgstr "Niederlande"
+
+#: waeup/ikoba/utils/countries.py:165
+msgid "New Caledonia"
+msgstr "New Caledonia"
+
+#: waeup/ikoba/utils/countries.py:166
+msgid "New Zealand"
+msgstr "Neuseeland"
+
+#: waeup/ikoba/utils/countries.py:167
+msgid "Nicaragua"
+msgstr "Nicaragua"
+
+#: waeup/ikoba/utils/countries.py:168
+msgid "Niger"
+msgstr "Niger"
+
+#: waeup/ikoba/utils/countries.py:169 waeup/ikoba/utils/utils.py:51
+msgid "Nigeria"
+msgstr "Nigeria"
+
+#: waeup/ikoba/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr "Antigua and Barbuda"
+
+#: waeup/ikoba/utils/countries.py:170
+msgid "Niue"
+msgstr "Niue"
+
+#: waeup/ikoba/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr "Norfolk Island"
+
+#: waeup/ikoba/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr "Northern Mariana Islands"
+
+#: waeup/ikoba/utils/countries.py:173
+msgid "Norway"
+msgstr "Norwegen"
+
+#: waeup/ikoba/utils/countries.py:174
+msgid "Oman"
+msgstr "Oman"
+
+#: waeup/ikoba/utils/countries.py:175
+msgid "Pakistan"
+msgstr "Pakistan"
+
+#: waeup/ikoba/utils/countries.py:176
+msgid "Palau"
+msgstr "Palau"
+
+#: waeup/ikoba/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr "Palestina"
+
+#: waeup/ikoba/utils/countries.py:178
+msgid "Panama"
+msgstr "Panama"
+
+#: waeup/ikoba/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr "Papua New Guinea"
+
+#: waeup/ikoba/utils/countries.py:18
+msgid "Argentina"
+msgstr "Argentinien"
+
+#: waeup/ikoba/utils/countries.py:180
+msgid "Paraguay"
+msgstr "Paraguay"
+
+#: waeup/ikoba/utils/countries.py:181
+msgid "Peru"
+msgstr "Peru"
+
+#: waeup/ikoba/utils/countries.py:182
+msgid "Philippines"
+msgstr "Philippinen"
+
+#: waeup/ikoba/utils/countries.py:183
+msgid "Pitcairn"
+msgstr "Pitcairn"
+
+#: waeup/ikoba/utils/countries.py:184
+msgid "Poland"
+msgstr "Polen"
+
+#: waeup/ikoba/utils/countries.py:185
+msgid "Portugal"
+msgstr "Portugal"
+
+#: waeup/ikoba/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr "Puerto Rico"
+
+#: waeup/ikoba/utils/countries.py:187
+msgid "Qatar"
+msgstr "Qatar"
+
+#: waeup/ikoba/utils/countries.py:188
+msgid "Réunion"
+msgstr "Réunion"
+
+#: waeup/ikoba/utils/countries.py:189
+msgid "Romania"
+msgstr "Rumänien"
+
+#: waeup/ikoba/utils/countries.py:19
+msgid "Armenia"
+msgstr "Armenien"
+
+#: waeup/ikoba/utils/countries.py:190
+msgid "Russian Federation"
+msgstr "Russland"
+
+#: waeup/ikoba/utils/countries.py:191
+msgid "Rwanda"
+msgstr "Ruanda"
+
+#: waeup/ikoba/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr "Saint Barthélemy"
+
+#: waeup/ikoba/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr "Saint Helena, Ascension and Tristan Da Cunha"
+
+#: waeup/ikoba/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr "Saint Kitts and Nevis"
+
+#: waeup/ikoba/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr "Saint Lucia"
+
+#: waeup/ikoba/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr "Saint Martin (French Part)"
+
+#: waeup/ikoba/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr "Saint Pierre and Miquelon"
+
+#: waeup/ikoba/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr "Saint Vincent and the Grenadines"
+
+#: waeup/ikoba/utils/countries.py:199
+msgid "Samoa"
+msgstr "Samoa"
+
+#: waeup/ikoba/utils/countries.py:20
+msgid "Aruba"
+msgstr "Aruba"
+
+#: waeup/ikoba/utils/countries.py:200
+msgid "San Marino"
+msgstr "San Marino"
+
+#: waeup/ikoba/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr "Sao Tome and Principe"
+
+#: waeup/ikoba/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr "Saudi-Arabien"
+
+#: waeup/ikoba/utils/countries.py:203
+msgid "Senegal"
+msgstr "Senegal"
+
+#: waeup/ikoba/utils/countries.py:204
+msgid "Serbia"
+msgstr "Serbien"
+
+#: waeup/ikoba/utils/countries.py:205
+msgid "Seychelles"
+msgstr "Seyschellen"
+
+#: waeup/ikoba/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr "Sierra Leone"
+
+#: waeup/ikoba/utils/countries.py:207
+msgid "Singapore"
+msgstr "Singapur"
+
+#: waeup/ikoba/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr "Sint Maarten (Niederlande)"
+
+#: waeup/ikoba/utils/countries.py:209
+msgid "Slovakia"
+msgstr "Slowakei"
+
+#: waeup/ikoba/utils/countries.py:21
+msgid "Australia"
+msgstr "Australien"
+
+#: waeup/ikoba/utils/countries.py:210
+msgid "Slovenia"
+msgstr "Slovenien"
+
+#: waeup/ikoba/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr "Solomon Islands"
+
+#: waeup/ikoba/utils/countries.py:212
+msgid "Somalia"
+msgstr "Somalia"
+
+#: waeup/ikoba/utils/countries.py:213
+msgid "South Africa"
+msgstr "Südafrika"
+
+#: waeup/ikoba/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr "South Georgia and the South Sandwich Islands"
+
+#: waeup/ikoba/utils/countries.py:215
+msgid "South Sudan"
+msgstr "Südsudan"
+
+#: waeup/ikoba/utils/countries.py:216
+msgid "Spain"
+msgstr "Spanien"
+
+#: waeup/ikoba/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr "Sri Lanka"
+
+#: waeup/ikoba/utils/countries.py:218
+msgid "Sudan"
+msgstr "Sudan"
+
+#: waeup/ikoba/utils/countries.py:219
+msgid "Suriname"
+msgstr "Suriname"
+
+#: waeup/ikoba/utils/countries.py:22
+msgid "Austria"
+msgstr "Österreich"
+
+#: waeup/ikoba/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr "Svalbard and Jan Mayen"
+
+#: waeup/ikoba/utils/countries.py:221
+msgid "Swaziland"
+msgstr "Swaziland"
+
+#: waeup/ikoba/utils/countries.py:222
+msgid "Sweden"
+msgstr "Schweden"
+
+#: waeup/ikoba/utils/countries.py:223
+msgid "Switzerland"
+msgstr "Schweiz"
+
+#: waeup/ikoba/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr "Syrien"
+
+#: waeup/ikoba/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr "Taiwan"
+
+#: waeup/ikoba/utils/countries.py:226
+msgid "Tajikistan"
+msgstr "Tajikistan"
+
+#: waeup/ikoba/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr "Tansania"
+
+#: waeup/ikoba/utils/countries.py:228
+msgid "Thailand"
+msgstr "Thailand"
+
+#: waeup/ikoba/utils/countries.py:229
+msgid "Timor-leste"
+msgstr "Ost-Timor"
+
+#: waeup/ikoba/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr "Azerbaijan"
+
+#: waeup/ikoba/utils/countries.py:230
+msgid "Togo"
+msgstr "Togo"
+
+#: waeup/ikoba/utils/countries.py:231
+msgid "Tokelau"
+msgstr "Tokelau"
+
+#: waeup/ikoba/utils/countries.py:232
+msgid "Tonga"
+msgstr "Tonga"
+
+#: waeup/ikoba/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr "Trinidad and Tobago"
+
+#: waeup/ikoba/utils/countries.py:234
+msgid "Tunisia"
+msgstr "Tunesien"
+
+#: waeup/ikoba/utils/countries.py:235
+msgid "Turkey"
+msgstr "Türkei"
+
+#: waeup/ikoba/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr "Turkmenistan"
+
+#: waeup/ikoba/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr "Turks and Caicos Islands"
+
+#: waeup/ikoba/utils/countries.py:238
+msgid "Tuvalu"
+msgstr "Tuvalu"
+
+#: waeup/ikoba/utils/countries.py:239
+msgid "Uganda"
+msgstr "Uganda"
+
+#: waeup/ikoba/utils/countries.py:24
+msgid "Bahamas"
+msgstr "Bahamas"
+
+#: waeup/ikoba/utils/countries.py:240
+msgid "Ukraine"
+msgstr "Ukraine"
+
+#: waeup/ikoba/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr "Vereinigte Arabische Emirate"
+
+#: waeup/ikoba/utils/countries.py:242
+msgid "United Kingdom"
+msgstr "Vereinigtes Königreich"
+
+#: waeup/ikoba/utils/countries.py:243
+msgid "United States"
+msgstr "Vereinigte Staaten von Amerika"
+
+#: waeup/ikoba/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr "United States Minor Outlying Islands"
+
+#: waeup/ikoba/utils/countries.py:245
+msgid "Uruguay"
+msgstr "Uruguay"
+
+#: waeup/ikoba/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr "Usbekistan"
+
+#: waeup/ikoba/utils/countries.py:247
+msgid "Vanuatu"
+msgstr "Vanuatu"
+
+#: waeup/ikoba/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr "Venezuela, Bolivarian Republic of"
+
+#: waeup/ikoba/utils/countries.py:249
+msgid "Viet Nam"
+msgstr "Vietnam"
+
+#: waeup/ikoba/utils/countries.py:25
+msgid "Bahrain"
+msgstr "Bahrain"
+
+#: waeup/ikoba/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr "Virgin Islands, British"
+
+#: waeup/ikoba/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr "Virgin Islands, U.S."
+
+#: waeup/ikoba/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr "Wallis and Futuna"
+
+#: waeup/ikoba/utils/countries.py:253
+msgid "Western Sahara"
+msgstr "Westsahara"
+
+#: waeup/ikoba/utils/countries.py:254
+msgid "Yemen"
+msgstr "Jemen"
+
+#: waeup/ikoba/utils/countries.py:255
+msgid "Zambia"
+msgstr "Sambia"
+
+#: waeup/ikoba/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr "Simbabwe"
+
+#: waeup/ikoba/utils/countries.py:26
+msgid "Bangladesh"
+msgstr "Bangladesh"
+
+#: waeup/ikoba/utils/countries.py:27
+msgid "Barbados"
+msgstr "Barbados"
+
+#: waeup/ikoba/utils/countries.py:28
+msgid "Belarus"
+msgstr "Weißrussland"
+
+#: waeup/ikoba/utils/countries.py:29
+msgid "Belgium"
+msgstr "Belgien"
+
+#: waeup/ikoba/utils/countries.py:30
+msgid "Belize"
+msgstr "Belize"
+
+#: waeup/ikoba/utils/countries.py:31
+msgid "Benin"
+msgstr "Benin"
+
+#: waeup/ikoba/utils/countries.py:32
+msgid "Bermuda"
+msgstr "Bermuda"
+
+#: waeup/ikoba/utils/countries.py:33
+msgid "Bhutan"
+msgstr "Bhutan"
+
+#: waeup/ikoba/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr "Bolivien"
+
+#: waeup/ikoba/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr "Bonaire, Sint Eustatius and Saba"
+
+#: waeup/ikoba/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr "Bosnia and Herzegovina"
+
+#: waeup/ikoba/utils/countries.py:37
+msgid "Botswana"
+msgstr "Botswana"
+
+#: waeup/ikoba/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr "Bouvet Island"
+
+#: waeup/ikoba/utils/countries.py:39
+msgid "Brazil"
+msgstr "Brasilien"
+
+#: waeup/ikoba/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr "British Indian Ocean Territory"
+
+#: waeup/ikoba/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr "Brunei Darussalam"
+
+#: waeup/ikoba/utils/countries.py:42
+msgid "Bulgaria"
+msgstr "Bulgarien"
+
+#: waeup/ikoba/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr "Burkina Faso"
+
+#: waeup/ikoba/utils/countries.py:44
+msgid "Burundi"
+msgstr "Burundi"
+
+#: waeup/ikoba/utils/countries.py:45
+msgid "Cambodia"
+msgstr "Cambodia"
+
+#: waeup/ikoba/utils/countries.py:46
+msgid "Cameroon"
+msgstr "Kamerun"
+
+#: waeup/ikoba/utils/countries.py:47
+msgid "Canada"
+msgstr "Kanada"
+
+#: waeup/ikoba/utils/countries.py:48
+msgid "Cape Verde"
+msgstr "Cape Verde"
+
+#: waeup/ikoba/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr "Cayman Islands"
+
+#: waeup/ikoba/utils/countries.py:50
+msgid "Central African Republic"
+msgstr "Zentralafrikanische Republik"
+
+#: waeup/ikoba/utils/countries.py:51
+msgid "Chad"
+msgstr "Tschad"
+
+#: waeup/ikoba/utils/countries.py:52
+msgid "Chile"
+msgstr "Chile"
+
+#: waeup/ikoba/utils/countries.py:53
+msgid "China"
+msgstr "China"
+
+#: waeup/ikoba/utils/countries.py:54
+msgid "Christmas Island"
+msgstr "Christmas Island"
+
+#: waeup/ikoba/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr "Cocos (Keeling) Islands"
+
+#: waeup/ikoba/utils/countries.py:56
+msgid "Colombia"
+msgstr "Kolumbien"
+
+#: waeup/ikoba/utils/countries.py:57
+msgid "Comoros"
+msgstr "Komoren"
+
+#: waeup/ikoba/utils/countries.py:58
+msgid "Congo"
+msgstr "Kongo"
+
+#: waeup/ikoba/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr "Kongo, Demokratische Republik"
+
+#: waeup/ikoba/utils/countries.py:60
+msgid "Cook Islands"
+msgstr "Cook Islands"
+
+#: waeup/ikoba/utils/countries.py:61
+msgid "Costa Rica"
+msgstr "Costa Rica"
+
+#: waeup/ikoba/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr "Côte d'Ivoire (Elfenbeinküste)"
+
+#: waeup/ikoba/utils/countries.py:63
+msgid "Croatia"
+msgstr "Kroatien"
+
+#: waeup/ikoba/utils/countries.py:64
+msgid "Cuba"
+msgstr "Kuba"
+
+#: waeup/ikoba/utils/countries.py:65
+msgid "Curaçao"
+msgstr "Curaçao"
+
+#: waeup/ikoba/utils/countries.py:66
+msgid "Cyprus"
+msgstr "Zypern"
+
+#: waeup/ikoba/utils/countries.py:67
+msgid "Czech Republic"
+msgstr "Tschechische Republik"
+
+#: waeup/ikoba/utils/countries.py:68
+msgid "Denmark"
+msgstr "Dänemark"
+
+#: waeup/ikoba/utils/countries.py:69
+msgid "Djibouti"
+msgstr "Djibouti"
+
+#: waeup/ikoba/utils/countries.py:70
+msgid "Dominica"
+msgstr "Dominica"
+
+#: waeup/ikoba/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr "Dominikanische Republik"
+
+#: waeup/ikoba/utils/countries.py:72
+msgid "Ecuador"
+msgstr "Ecuador"
+
+#: waeup/ikoba/utils/countries.py:73
+msgid "Egypt"
+msgstr "Ägypten"
+
+#: waeup/ikoba/utils/countries.py:74
+msgid "El Salvador"
+msgstr "El Salvador"
+
+#: waeup/ikoba/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr "Äquatorial-Guinea"
+
+#: waeup/ikoba/utils/countries.py:76
+msgid "Eritrea"
+msgstr "Eritrea"
+
+#: waeup/ikoba/utils/countries.py:77
+msgid "Estonia"
+msgstr "Estland"
+
+#: waeup/ikoba/utils/countries.py:78
+msgid "Ethiopia"
+msgstr "Äthiopien"
+
+#: waeup/ikoba/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr "alklandinseln"
+
+#: waeup/ikoba/utils/countries.py:8
+msgid "Afghanistan"
+msgstr "Afghanistan"
+
+#: waeup/ikoba/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr "Faroe-Inseln"
+
+#: waeup/ikoba/utils/countries.py:81
+msgid "Fiji"
+msgstr "Fiji"
+
+#: waeup/ikoba/utils/countries.py:82
+msgid "Finland"
+msgstr "Finnland"
+
+#: waeup/ikoba/utils/countries.py:83
+msgid "France"
+msgstr "Frankreich"
+
+#: waeup/ikoba/utils/countries.py:84
+msgid "French Guiana"
+msgstr "Französisch-Guiana"
+
+#: waeup/ikoba/utils/countries.py:85
+msgid "French Polynesia"
+msgstr "Französisch-Plynesien"
+
+#: waeup/ikoba/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr "French Southern Territories"
+
+#: waeup/ikoba/utils/countries.py:87
+msgid "Gabon"
+msgstr "Gabun"
+
+#: waeup/ikoba/utils/countries.py:88
+msgid "Gambia"
+msgstr "Gambia"
+
+#: waeup/ikoba/utils/countries.py:89
+msgid "Georgia"
+msgstr "Georgien"
+
+#: waeup/ikoba/utils/countries.py:9
+msgid "Åland Islands"
+msgstr "Åland Islands"
+
+#: waeup/ikoba/utils/countries.py:90 waeup/ikoba/utils/utils.py:50
+msgid "Germany"
+msgstr "Deutschland"
+
+#: waeup/ikoba/utils/countries.py:91
+msgid "Ghana"
+msgstr "Ghana"
+
+#: waeup/ikoba/utils/countries.py:92
+msgid "Gibraltar"
+msgstr "Gibraltar"
+
+#: waeup/ikoba/utils/countries.py:93
+msgid "Greece"
+msgstr "Griechenland"
+
+#: waeup/ikoba/utils/countries.py:94
+msgid "Greenland"
+msgstr "Grönland"
+
+#: waeup/ikoba/utils/countries.py:95
+msgid "Grenada"
+msgstr "Grenada"
+
+#: waeup/ikoba/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr "Guadeloupe"
+
+#: waeup/ikoba/utils/countries.py:97
+msgid "Guam"
+msgstr "Guam"
+
+#: waeup/ikoba/utils/countries.py:98
+msgid "Guatemala"
+msgstr "Guatemala"
+
+#: waeup/ikoba/utils/countries.py:99
+msgid "Guernsey"
+msgstr "Guernsey"
+
+#: waeup/ikoba/utils/utils.py:110
+msgid "no contract"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:155
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+"Vollständiger Name: ${a}\n"
+"Benutzer-Id: ${b}\n"
+"Benutzertyp: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+
+#: waeup/ikoba/utils/utils.py:207
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Registration and Application Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:52
+msgid "U.S."
+msgstr "USA"
+
+#: waeup/ikoba/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr "Ungültiges Datetime-Format"
+
+#: waeup/ikoba/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr "Leere Feld."
+
+#: waeup/ikoba/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr "Internationale Vorwahl erwartet: '+NNN'"
+
+#: waeup/ikoba/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr "Telefonnummern dürfen nur Nummern enthalten."
+
+#: waeup/ikoba/widgets/sequencewidget.pt:15
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+"Vergessen Sie nicht, das Formular zu sichern, wenn Sie Elemente hinzugefügt "
+"oder entfernt haben."
+
+#. Default: "Remove selected items"
+#: waeup/ikoba/widgets/sequencewidget.pt:23
+msgid "remove-selected-items"
+msgstr "Ausgewählte Elemente löschen"
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/de/LC_MESSAGES/zope.po
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/de/LC_MESSAGES/zope.po	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/de/LC_MESSAGES/zope.po	(revision 12677)
@@ -0,0 +1,58 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.IKOBA\n"
+"PO-Revision-Date: 2012-04-13 10:21+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Germany <henrik@waeup.org>\n"
+"Language: de-DE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"POT-Creation-Date: \n"
+
+#. Default: "Nothing"
+#: src/zope/app/form/browser/source.py:65
+#: src/zope/app/form/browser/source.py:212
+#: src/zope/app/form/browser/source.py:222
+msgid "SourceDisplayWidget-missing"
+msgstr "--"
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:231
+msgid "item-missing-single-value-for-display"
+msgstr "--"
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:250
+msgid "vocabulary-missing-multiple-value-for-display"
+msgstr "--"
+
+#. Default: "(no value)"
+#: src/zope/app/form/browser/itemswidgets.py:417
+#: src/zope/app/form/browser/itemswidgets.py:447
+msgid "vocabulary-missing-single-value-for-edit"
+msgstr "--"
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/itemswidgets.py:486
+msgid "vocabulary-missing-multiple-value-for-edit"
+msgstr "--"
+
+#. Default: "(no value available)"
+#: src/zope/app/form/browser/sequencewidget.py:256
+msgid "sequence-value-not-provided"
+msgstr "--"
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/sequencewidget.py:259
+msgid "sequence-value-is-empty"
+msgstr "--"
+
+#: src/zope/app/form/browser/boolwidgets.py:121
+msgid "True"
+msgstr "ja"
+
+#: src/zope/app/form/browser/boolwidgets.py:122
+msgid "False"
+msgstr "nein"
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/en/LC_MESSAGES/waeup.ikoba.po
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/en/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/en/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
@@ -0,0 +1,4905 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.Ikoba\n"
+"POT-Creation-Date: Mon Feb  2 08:48:41 2015\n"
+"PO-Revision-Date: 2014-11-13 14:47+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Germany <henrik@waeup.org>\n"
+"Language: en_US\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-SourceCharset: utf-8\n"
+"X-Generator: Poedit 1.6.5\n"
+
+#: waeup/ikoba/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:106
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:119 waeup/ikoba/browser/pages.py:470
+msgid "Administration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:126 waeup/ikoba/browser/viewlets.py:320
+#: waeup/ikoba/browser/viewlets.py:544
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:133 waeup/ikoba/browser/pages.py:517
+#: waeup/ikoba/browser/viewlets.py:329 waeup/ikoba/browser/viewlets.py:554
+msgid "Officers"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:140 waeup/ikoba/browser/pages.py:763
+#: waeup/ikoba/browser/pages.py:1363 waeup/ikoba/browser/viewlets.py:338
+#: waeup/ikoba/browser/viewlets.py:563
+msgid "Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:147 waeup/ikoba/browser/reports.py:37
+#: waeup/ikoba/browser/viewlets.py:347 waeup/ikoba/browser/viewlets.py:572
+msgid "Reports"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:92
+msgid "Home"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:108
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/ikoba/browser/templates/datacenterpage.pt:17
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:109
+msgid "Scan"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:129
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:130
+msgid "Delete"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:164
+msgid "No local file selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:44
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:59
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:71
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:76
+msgid "${a} file format expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:82
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:92
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:173
+#: waeup/ikoba/customers/interfaces.py:200 waeup/ikoba/interfaces.py:428
+#: waeup/ikoba/interfaces.py:504
+msgid "Email Address"
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:176
+msgid "Enter your email address stored in Ikoba."
+msgstr ""
+
+#: waeup/ikoba/browser/layout.py:102 waeup/ikoba/customers/browser.py:387
+#: waeup/ikoba/customers/browser.py:690 waeup/ikoba/customers/browser.py:973
+#: waeup/ikoba/customers/browser.py:1385 waeup/ikoba/customers/browser.py:1416
+#: waeup/ikoba/documents/browser.py:259
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1039 waeup/ikoba/browser/pages.py:1068
+#: waeup/ikoba/browser/pages.py:1173 waeup/ikoba/browser/pages.py:1285
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1071
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1072
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1117
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:112
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1125
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1138 waeup/ikoba/browser/pages.py:1261
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1150
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1176 waeup/ikoba/browser/pages.py:1404
+msgid "Reset"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1177
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1178
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1179
+msgid "Perform import"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1244
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:125 waeup/ikoba/browser/pages.py:772
+#: waeup/ikoba/customers/browser.py:207
+msgid "No item selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1287
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1337
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1339
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1347
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1350
+msgid "Show"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1365 waeup/ikoba/browser/viewlets.py:185
+msgid "Search"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1367
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1375
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1390
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1393
+msgid "No search results found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1401
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1421
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1424
+msgid "Given storage path cannot be used: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1428
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1440
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1442
+msgid "Create CSV file"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:146 waeup/ikoba/customers/browser.py:220
+#: waeup/ikoba/customers/browser.py:883 waeup/ikoba/customers/browser.py:1253
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1470
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1514
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1540 waeup/ikoba/customers/browser.py:543
+#: waeup/ikoba/customers/browser.py:625
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1550
+msgid "No record found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1564 waeup/ikoba/customers/browser.py:589
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1568
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:188 waeup/ikoba/customers/browser.py:84
+#: waeup/ikoba/customers/browser.py:422
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:259 waeup/ikoba/browser/viewlets.py:266
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:40
+#: waeup/ikoba/browser/templates/customersitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:270
+msgid "You logged in."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:307
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:315
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:318
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:333
+msgid "You have been logged out. Thanks for using WAeUP Ikoba!"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:375
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:381 waeup/ikoba/browser/pages.py:661
+msgid "Send message now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:400 waeup/ikoba/browser/pages.py:433
+#: waeup/ikoba/browser/pages.py:678 waeup/ikoba/customers/browser.py:372
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:420
+msgid "Send now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:435
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:460
+msgid "<h1>Welcome to WAeUP.Ikoba</h1>"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:518 waeup/ikoba/customers/viewlets.py:207
+#: waeup/ikoba/customers/viewlets.py:332 waeup/ikoba/customers/viewlets.py:404
+#: waeup/ikoba/documents/viewlets.py:60 waeup/ikoba/documents/viewlets.py:79
+#: waeup/ikoba/products/viewlets.py:60 waeup/ikoba/products/viewlets.py:79
+msgid "Manage"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:519 waeup/ikoba/browser/viewlets.py:175
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:526
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:563 waeup/ikoba/browser/pages.py:565
+#: waeup/ikoba/browser/viewlets.py:403
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:587
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:608
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:616 waeup/ikoba/browser/pages.py:737
+#: waeup/ikoba/browser/pages.py:1403 waeup/ikoba/customers/browser.py:400
+#: waeup/ikoba/customers/browser.py:732 waeup/ikoba/customers/browser.py:774
+#: waeup/ikoba/customers/browser.py:990 waeup/ikoba/customers/browser.py:1008
+#: waeup/ikoba/customers/browser.py:1402 waeup/ikoba/customers/browser.py:1426
+#: waeup/ikoba/customers/browser.py:1443 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:269 waeup/ikoba/documents/browser.py:293
+#: waeup/ikoba/documents/browser.py:308 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:106 waeup/ikoba/products/browser.py:184
+#: waeup/ikoba/products/browser.py:197
+msgid "Save"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:642
+msgid "User settings have been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:645 waeup/ikoba/browser/pages.py:1070
+#: waeup/ikoba/browser/pages.py:1175 waeup/ikoba/customers/browser.py:759
+#: waeup/ikoba/customers/browser.py:790 waeup/ikoba/customers/browser.py:946
+#: waeup/ikoba/customers/browser.py:1312 waeup/ikoba/documents/browser.py:106
+#: waeup/ikoba/documents/browser.py:152 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:273 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:110
+#: waeup/ikoba/products/browser.py:184 waeup/ikoba/products/browser.py:206
+msgid "Cancel"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:659 waeup/ikoba/customers/browser.py:353
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:688 waeup/ikoba/browser/viewlets.py:354
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:701 waeup/ikoba/browser/viewlets.py:370
+msgid "My Roles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:733
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:745
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:746
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:747
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:752
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:766 waeup/ikoba/customers/browser.py:176
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:101
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:783
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:787
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:798
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:800 waeup/ikoba/browser/pages.py:816
+#: waeup/ikoba/browser/pages.py:1041 waeup/ikoba/browser/pages.py:1349
+#: waeup/ikoba/browser/pages.py:1366 waeup/ikoba/browser/pages.py:1405
+#: waeup/ikoba/browser/pages.py:1443
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:812
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:815
+msgid "Upload"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:855
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:857
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:861
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:880
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:882 waeup/ikoba/browser/pages.py:1571
+#: waeup/ikoba/customers/browser.py:374
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:890
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:905
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:91
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:913
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:924
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:207 waeup/ikoba/browser/pdf.py:472
+#: waeup/ikoba/browser/pdf.py:482
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:16
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:754
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/ikoba/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data. Please pick the "
+"type of objects you want to export from the selection below. The file will "
+"be generated and then be made available for you to download in the table "
+"below. Files can only be downloaded within 24 hours after generation. The "
+"download link disappears afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:42
+msgid "Export Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:43
+msgid "Exporter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:44
+msgid "Filter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:45
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:6
+msgid "Creator"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:46
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:7
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:47
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:8
+msgid "Status"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:72
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:39
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/ikoba/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:14
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/ikoba/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:14
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:13
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/filedisplay.pt:15
+msgid "HTML Element:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:13
+msgid "Password"
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/ikoba/browser/templates/loginpage.pt:26
+msgid "login_trouble1"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer, or your customer record has been created for you? Acquire a Password Activation Code (PWD) and inititialize your customer account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:29
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer? Create your customer account <strong><a href=\"createaccount\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:34
+msgid "login_trouble4"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer and your customer record has been created for you? Inititialize your customer account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:38
+msgid "login_trouble5"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:4
+msgid "Identifier"
+msgstr ""
+
+#. Default: "Or simply forgot your id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:42
+msgid "login_trouble3"
+msgstr ""
+
+#. Default: "Customers can enter either their customer id, registration number or email address. Officers must enter their user name."
+#: waeup/ikoba/browser/templates/loginpage.pt:7
+msgid "identifier_hint"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:46
+#: waeup/ikoba/browser/templates/customersitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:52
+#: waeup/ikoba/browser/templates/customersitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:6
+#: waeup/ikoba/browser/templates/customersitelayout.pt:4
+msgid "WAeUP.Ikoba - Registration and Application System"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:15
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:16
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:30
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:9
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:27
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:10
+#: waeup/ikoba/customers/browser_templates/basepage.pt:14
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:24
+msgid "Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:165
+msgid "Add"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:305
+msgid "Any link text"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:387 waeup/ikoba/customers/viewlets.py:198
+msgid "Send email"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:396
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:412
+msgid "Show logs"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:421
+msgid "Upload data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:430
+msgid "Process data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:439
+msgid "Export data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:448
+msgid "View processed files"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:508
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:93 waeup/ikoba/browser/viewlets.py:155
+#: waeup/ikoba/customers/viewlets.py:284 waeup/ikoba/customers/viewlets.py:347
+#: waeup/ikoba/customers/viewlets.py:422
+msgid "Edit"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:142
+msgid "Customer record created"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/batching.py:197
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:461
+msgid "Customer Sample Document Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:562
+msgid "Customer Sample Contract Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:60
+msgid "Customer Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1013
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1017 waeup/ikoba/customers/browser.py:1452
+msgid "Form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1029 waeup/ikoba/documents/browser.py:324
+msgid "Trigger document transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:106 waeup/ikoba/customers/export.py:130
+#: waeup/ikoba/customers/viewlets.py:54
+msgid "Customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1077
+msgid "Documents of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1085
+msgid "Customer Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1091
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:8
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:9
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:4
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:8
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:8
+msgid "Id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1092 waeup/ikoba/customers/browser.py:1527
+#: waeup/ikoba/products/productoptions.py:44
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:9
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:9
+#: waeup/ikoba/products/browser_templates/containerpage.pt:9
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:39
+msgid "Title"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1093 waeup/ikoba/customers/browser.py:1528
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:10
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:10
+msgid "Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1094 waeup/ikoba/customers/browser.py:1529
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1095 waeup/ikoba/customers/browser.py:1530
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:12
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:12
+msgid "Last Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1181 waeup/ikoba/customers/viewlets.py:160
+msgid "Contracts"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:1226
+msgid "${a}: Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1229 waeup/ikoba/customers/browser.py:1270
+#: waeup/ikoba/customers/browser.py:1295
+msgid "Add contract"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1234
+msgid "Remove selected contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1240
+msgid "No contract selected."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:132
+msgid "Find customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1325
+msgid "Select product"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:133 waeup/ikoba/customers/browser.py:175
+msgid "Find customer(s)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1336
+msgid "Save and proceed"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1448
+msgid "Apply now (final submit)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1464
+msgid "Trigger contract transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:150 waeup/ikoba/customers/browser.py:192
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1512
+msgid "Contracts of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1520
+msgid "Customer Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:156 waeup/ikoba/customers/browser.py:198
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:162 waeup/ikoba/customers/browser.py:205
+#: waeup/ikoba/customers/browser.py:563 waeup/ikoba/customers/browser.py:575
+msgid "No customer found."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:174 waeup/ikoba/customers/viewlets.py:180
+msgid "Manage customer section"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:233 waeup/ikoba/customers/viewlets.py:188
+msgid "Add customer"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:236
+msgid "Create customer record"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:241
+msgid "Customer created."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:256
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:279
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:283
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:289
+msgid "You successfully logged in as customer."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:312
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:314
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:320
+msgid "set"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:321
+msgid "unset"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:385
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:435
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:447 waeup/ikoba/customers/browser.py:1044
+#: waeup/ikoba/customers/browser.py:1479 waeup/ikoba/documents/browser.py:339
+msgid "No transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:450 waeup/ikoba/customers/browser.py:1047
+#: waeup/ikoba/customers/browser.py:1482
+msgid "Apply"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:457 waeup/ikoba/customers/browser.py:1055
+#: waeup/ikoba/customers/browser.py:1490 waeup/ikoba/documents/browser.py:350
+msgid "Transition '%s' executed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:474
+msgid "Customer account has been activated."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:494
+msgid "Customer account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:513
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:523
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:557
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:566
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:610
+msgid "Create customer account"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:64
+msgid ""
+"You can not edit some data after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:648
+msgid "You have successfully created a customer account for the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:669
+msgid "Your request was successful."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:67
+msgid ""
+"You can not edit your document after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:688 waeup/ikoba/customers/viewlets.py:303
+msgid "Upload files"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:70
+msgid ""
+"You can not edit your contract after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:721
+msgid "Edit base data and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:722
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:741
+msgid "Passport picture is missing."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:746
+msgid "Save and request registration now"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:755
+msgid "Registration form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:771 waeup/ikoba/customers/viewlets.py:293
+msgid "Change password"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:785
+msgid "Password changed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:816 waeup/ikoba/customers/viewlets.py:166
+#: waeup/ikoba/documents/browser.py:62 waeup/ikoba/documents/browser.py:82
+#: waeup/ikoba/documents/viewlets.py:49
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:3
+msgid "Documents"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:856
+msgid "${a}: Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:859 waeup/ikoba/customers/browser.py:900
+#: waeup/ikoba/customers/browser.py:924 waeup/ikoba/documents/browser.py:96
+#: waeup/ikoba/documents/browser.py:119 waeup/ikoba/documents/browser.py:129
+msgid "Add document"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:864 waeup/ikoba/documents/browser.py:101
+msgid "Remove selected documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:870
+msgid "No document selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:935 waeup/ikoba/customers/browser.py:1306
+#: waeup/ikoba/documents/browser.py:144
+msgid "${a} added."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:94
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:56
+msgid "Select all"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:10
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:11
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:12
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:12
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:23
+msgid "Search Results"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:32
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:9
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractaddpage.pt:7
+msgid "Contract Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:20
+msgid "Contract Type:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:19
+msgid ""
+"A customer record will be created and your login credentials will be sent to "
+"the email address given above. To be able to proceed you must provide a "
+"valid email address!"
+msgstr ""
+
+#. Default: "Ikoba is creating a temporary customer password which will replace the original password chosen by the customer. The password will be valid for ${minutes} minutes. The customer can't login during this time period. After expiration the customer can login with the original password again."
+#: waeup/ikoba/customers/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/ikoba/customers/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:19
+msgid ""
+"Your customer record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:8
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:14
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:44
+msgid "Thanks for using Ikoba!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:3
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:147
+#: waeup/ikoba/customers/documents.py:162
+msgid "Customer has not yet been approved."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:153
+msgid "Attached documents must be verified first."
+msgstr ""
+
+#: waeup/ikoba/customers/documents.py:165
+msgid "No file uploaded."
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:194
+msgid "Customer Sample Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:246
+msgid "Customer Sample Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:142
+#: waeup/ikoba/customers/fileviewlets.py:143
+#: waeup/ikoba/documents/fileviewlets.py:39
+#: waeup/ikoba/documents/fileviewlets.py:40
+msgid "PDF File"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:167
+#: waeup/ikoba/customers/fileviewlets.py:168
+#: waeup/ikoba/documents/fileviewlets.py:57
+#: waeup/ikoba/documents/fileviewlets.py:58
+msgid "PDF Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:48
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:60
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:90
+#: waeup/ikoba/customers/fileviewlets.py:91
+#: waeup/ikoba/customers/fileviewlets.py:115
+#: waeup/ikoba/customers/fileviewlets.py:116
+msgid "Sample Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:101
+#: waeup/ikoba/customers/interfaces.py:195
+#: waeup/ikoba/customers/interfaces.py:213
+msgid "First Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:106
+#: waeup/ikoba/customers/interfaces.py:218
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:111
+#: waeup/ikoba/customers/interfaces.py:223
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:116
+msgid "Sex"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:122
+#: waeup/ikoba/customers/interfaces.py:161
+#: waeup/ikoba/customers/interfaces.py:190
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:129
+#: waeup/ikoba/customers/interfaces.py:228
+msgid "Email"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:136 waeup/ikoba/interfaces.py:433
+#: waeup/ikoba/interfaces.py:511
+msgid "Phone"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:252
+#: waeup/ikoba/documents/interfaces.py:52
+msgid "Document Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:303
+msgid "Contract Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:309
+#: waeup/ikoba/customers/interfaces.py:332
+msgid "Product"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:315
+#: waeup/ikoba/customers/interfaces.py:365
+#: waeup/ikoba/products/interfaces.py:73
+msgid "Options/Fees"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:344
+#: waeup/ikoba/customers/interfaces.py:379
+msgid "Document"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:355
+msgid "Comment"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:82
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:88
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:90
+msgid ""
+"This message will be shown if and only if deactivated customers try to login."
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:96
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:31
+msgid "Customer Id"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:161
+msgid "Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:180
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:240
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:25
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:30
+msgid "Connected Files"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:301 waeup/ikoba/customers/utils.py:309
+#: waeup/ikoba/customers/utils.py:317 waeup/ikoba/documents/utils.py:32
+msgid "created"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:302
+msgid "started"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:303
+msgid "requested"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:304 waeup/ikoba/customers/utils.py:311
+msgid "approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:305
+msgid "provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:310
+msgid "submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:312 waeup/ikoba/customers/utils.py:320
+msgid "rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:313 waeup/ikoba/customers/utils.py:321
+msgid "expired"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:318
+msgid "submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:319
+msgid "verified"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:325
+msgid "Sample Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:326 waeup/ikoba/documents/utils.py:37
+msgid "PDF Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:330
+msgid "Sample Contract"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:374
+msgid "Contractor"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:377
+msgid "Document Owner"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:386
+msgid "Contract Data"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:401
+msgid "Transaction History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:407
+msgid "Registration History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:425 waeup/ikoba/products/interfaces.py:95
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:32
+msgid "Terms and Conditions"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:114
+msgid "My Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:115
+msgid "My Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:116 waeup/ikoba/customers/viewlets.py:172
+msgid "History"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:137 waeup/ikoba/customers/viewlets.py:154
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:217 waeup/ikoba/customers/viewlets.py:363
+#: waeup/ikoba/customers/viewlets.py:438 waeup/ikoba/documents/viewlets.py:89
+msgid "Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:227
+msgid "Login as customer"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:236
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:248 waeup/ikoba/customers/viewlets.py:269
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:257
+msgid "Activate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:283
+msgid "Edit and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:323
+msgid "Download documents overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:372 waeup/ikoba/customers/viewlets.py:447
+#: waeup/ikoba/documents/viewlets.py:69 waeup/ikoba/products/viewlets.py:69
+msgid "View"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:383
+msgid "Download document slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:395
+msgid "Download contracts overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:413
+msgid "Manage official data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:462
+msgid "Download contract slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:98
+msgid "My Data"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:58
+msgid "male"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:60
+msgid "female"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:103 waeup/ikoba/customers/workflow.py:124
+msgid "Reset to requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:104 waeup/ikoba/customers/workflow.py:125
+msgid "Reset to 'requested'"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:111 waeup/ikoba/customers/workflow.py:209
+#: waeup/ikoba/customers/workflow.py:210 waeup/ikoba/customers/workflow.py:216
+#: waeup/ikoba/customers/workflow.py:217 waeup/ikoba/customers/workflow.py:223
+#: waeup/ikoba/customers/workflow.py:224 waeup/ikoba/customers/workflow.py:237
+#: waeup/ikoba/customers/workflow.py:238 waeup/ikoba/customers/workflow.py:324
+#: waeup/ikoba/customers/workflow.py:325 waeup/ikoba/customers/workflow.py:331
+#: waeup/ikoba/customers/workflow.py:332 waeup/ikoba/customers/workflow.py:338
+#: waeup/ikoba/customers/workflow.py:339 waeup/ikoba/customers/workflow.py:352
+#: waeup/ikoba/customers/workflow.py:353
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:180
+msgid "Create contract"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:183
+msgid "Contract created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:188
+msgid "Submit for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:189
+msgid "Submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:195
+msgid "Approve"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:196
+msgid "Approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:202 waeup/ikoba/customers/workflow.py:317
+msgid "Reject"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:203 waeup/ikoba/customers/workflow.py:318
+msgid "REJECTED"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:230 waeup/ikoba/customers/workflow.py:231
+#: waeup/ikoba/customers/workflow.py:345 waeup/ikoba/customers/workflow.py:346
+msgid "Set to expired"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:295 waeup/ikoba/documents/workflow.py:37
+msgid "Create document"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:298 waeup/ikoba/documents/workflow.py:40
+msgid "Document created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:303
+msgid "Submit for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:304
+msgid "Submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:310
+msgid "Verify"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:311
+msgid "Verified"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:45
+msgid "Create customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:48
+msgid "Customer created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:53
+msgid "Start registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:56
+msgid "Customer registration started"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:61
+msgid "Request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:62
+msgid "Customer registration requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:68
+msgid "Approve customer provisionally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:69
+msgid "Customer registration provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:75
+msgid "Approve finally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:76
+msgid "Customer registration finally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:82
+msgid "Approve customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:83
+msgid "Customer registration approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:89
+msgid "Reject customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:90
+msgid "Customer registration rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:96 waeup/ikoba/customers/workflow.py:110
+#: waeup/ikoba/customers/workflow.py:117
+msgid "Reset customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:97 waeup/ikoba/customers/workflow.py:118
+msgid "Reset to initial customer state"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:135
+msgid "Public PDF Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:147
+msgid "Public HTML Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:159
+msgid "Public REST Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:140 waeup/ikoba/products/browser.py:133
+msgid "The id chosen already exists."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:210
+#: waeup/ikoba/documents/fileviewlets.py:80
+msgid "The document requested has not yet been published."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:256 waeup/ikoba/documents/browser.py:282
+#: waeup/ikoba/products/browser.py:185 waeup/ikoba/products/browser.py:215
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:257 waeup/ikoba/documents/browser.py:278
+#: waeup/ikoba/products/browser.py:186 waeup/ikoba/products/browser.py:211
+msgid "Add local role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:267
+msgid "Manage document "
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:342
+msgid "Apply now"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:94
+msgid "Manage document section"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:1
+#: waeup/ikoba/products/browser_templates/containerpage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentaddform.pt:7
+#: waeup/ikoba/customers/browser_templates/documentaddpage.pt:7
+msgid "Document Type"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:12
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:12
+msgid "Files"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:16
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:16
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:21
+msgid "Document Type:"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:37
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:48
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:5
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:37
+msgid "Name"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:39
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:50
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:39
+msgid "Local Role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:6
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:6
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:7
+msgid "Metadata"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:9
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:12
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:7
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:9
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:25
+msgid "Content:"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:101
+msgid "Public HTML Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:110
+msgid "Public REST Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:92
+msgid "Public PDF Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:110
+msgid "Multilingual content in REST (reStructuredText) format"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:58
+msgid "Document Title"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:78
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:97
+#: waeup/ikoba/products/interfaces.py:97
+msgid "Multilingual content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:33
+msgid "published"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:38
+msgid "HTML Document"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:39
+msgid "REST Document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:45
+msgid "Publish document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:48
+msgid "Document published"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:53
+msgid "Retract documet"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:56
+msgid "Document retracted"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:361
+msgid "Processor name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:387
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:394
+msgid "Email to:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:401
+msgid "Subject:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:405
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:409
+msgid "Text:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:438 waeup/ikoba/interfaces.py:494
+msgid "Public Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:448 waeup/ikoba/interfaces.py:449
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:455
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:456
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:485
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:35
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:46
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:35
+msgid "User Id"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:490
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:500
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:517
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:564
+msgid "Name of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:565
+msgid "Sample Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:570
+msgid "Abbreviated Title of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:576
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:588
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:594
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:601
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:602
+msgid "Ikoba Contact"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:607
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:614
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:66
+msgid "new"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:67
+msgid "completed"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:68
+msgid "pending"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:69
+msgid "active"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:70
+msgid "assigned"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:71
+msgid "callbacks"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/ikoba/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:100
+msgid "Iranian Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:101
+msgid "Iceland Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:102
+msgid "Jamaican Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:103
+msgid "Jordanian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:104
+msgid "Yen"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:105
+msgid "Kenyan Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:106
+msgid "Som"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:107
+msgid "Riel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:108
+msgid "Comoro Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:110
+msgid "North Korean Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:113
+msgid "Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:114
+msgid "Kuwaiti Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:115
+msgid "Cayman Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:116
+msgid "Tenge"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:117
+msgid "Kip"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:118
+msgid "Lebanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:119
+msgid "Sri Lanka Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:120
+msgid "Liberian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:121
+msgid "Loti"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:122
+msgid "Lithuanian Litas"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:123
+msgid "Libyan Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:124
+msgid "Moroccan Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:125
+msgid "Moldovan Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:126
+msgid "Malagasy Ariary"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:128
+msgid "Denar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:131
+msgid "Kyat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:132
+msgid "Tugrik"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:133
+msgid "Pataca"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:134
+msgid "Ouguiya"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:135
+msgid "Mauritius Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:136
+msgid "Rufiyaa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:137
+msgid "Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:138
+msgid "Mexican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:139
+msgid "Mexican Unidad de Inversion (UDI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:140
+msgid "Malaysian Ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:141
+msgid "Mozambique Metical"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:142
+msgid "Namibia Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:143
+msgid "Naira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:144
+msgid "Cordoba Oro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:145
+msgid "Norwegian Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:146
+msgid "Nepalese Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:147
+msgid "New Zealand Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:148
+msgid "Rial Omani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:149
+msgid "Balboa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:150
+msgid "Nuevo Sol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:151
+msgid "Kina"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:152
+msgid "Philippine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:153
+msgid "Pakistan Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:154
+msgid "Zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:155
+msgid "Guarani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:156
+msgid "Qatari Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:157
+msgid "New Romanian Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:158
+msgid "Serbian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:159
+msgid "Russian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:160
+msgid "Rwanda Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:161
+msgid "Saudi Riyal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:162
+msgid "Solomon Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:163
+msgid "Seychelles Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:164
+msgid "Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:165
+msgid "Swedish Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:166
+msgid "Singapore Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:168
+msgid "Saint Helena Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:171
+msgid "Leone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:172
+msgid "Somali Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:173
+msgid "Surinam Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:174
+msgid "South Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:175
+msgid "Dobra"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:176
+msgid "El Salvador Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:177
+msgid "Syrian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:178
+msgid "Lilangeni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:179
+msgid "Baht"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:180
+msgid "Somoni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:181
+msgid "Turkmenistan New Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:182
+msgid "Tunisian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:183
+msgid "Pa'anga"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:184
+msgid "Turkish Lira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:185
+msgid "Trinidad and Tobago Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:186
+msgid "New Taiwan Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:187
+msgid "Tanzanian Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:188
+msgid "Hryvnia"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:189
+msgid "Uganda Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:190
+msgid "US Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:191
+msgid "US Dollar (Next day)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:193
+msgid "Uruguay Peso en Unidades Indexadas (URUIURUI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:196
+msgid "Peso Uruguayo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:197
+msgid "Uzbekistan Sum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:198
+msgid "Bolivar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:199
+msgid "Dong"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:200
+msgid "Vatu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:201
+msgid "Tala"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:202
+msgid "CFA Franc BEAC"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:204
+msgid "East Caribbean Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:207
+msgid "CFA Franc BCEAO"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:208
+msgid "CFP Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:209
+msgid "Yemeni Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:210
+msgid "Rand"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:211
+msgid "Zambian Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:212
+msgid "Zimbabwe Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:23
+msgid "UAE Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:24
+msgid "Afghani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:25
+msgid "Lek"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:26
+msgid "Armenian Dram"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:28
+msgid "Netherlands Antillean Guilder"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:31
+msgid "Kwanza"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:32
+msgid "Argentine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:33
+msgid "Australian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:34
+msgid "Aruban Florin"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:35
+msgid "Azerbaijanian Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:36
+msgid "Convertible Mark"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:37
+msgid "Barbados Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:38
+msgid "Taka"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:39
+msgid "Bulgarian Lev"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:40
+msgid "Bahraini Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:41
+msgid "Burundi Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:42
+msgid "Bermudian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:43
+msgid "Brunei Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:44
+msgid "Boliviano"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:45
+msgid "Mvdol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:46
+msgid "Brazilian Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:47
+msgid "Bahamian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:48
+msgid "Ngultrum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:49
+msgid "Pula"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:50
+msgid "Belarussian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:51
+msgid "Belize Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:52
+msgid "Canadian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:54
+msgid "Congolese Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:57
+msgid "WIR Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:58
+msgid "Swiss Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:59
+msgid "WIR Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:60
+msgid "Unidad de Fomento"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:61
+msgid "Chilean Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:62
+msgid "Yuan Renminbi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:63
+msgid "Colombian Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:64
+msgid "Unidad de Valor Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:65
+msgid "Costa Rican Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:66
+msgid "Peso Convertible"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:67
+msgid "Cuban Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:68
+msgid "Cabo Verde Escudo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:69
+msgid "Czech Koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:70
+msgid "Djibouti Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:71
+msgid "Danish Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:72
+msgid "Dominican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:73
+msgid "Algerian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:74
+msgid "Egyptian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:75
+msgid "-Nakfa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:76
+msgid "Ethiopian Birr"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:77
+#: waeup/ikoba/payments/paypal_currencies.py:40
+msgid "Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:78
+msgid "Fiji Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:80
+msgid "Falkland Islands Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:83
+msgid "Pound Sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:84
+msgid "Lari"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:85
+msgid "Ghana Cedi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:86
+msgid "Gibraltar Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:87
+msgid "Dalasi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:88
+msgid "Guinea Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:89
+msgid "Quetzal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:90
+msgid "Guyana Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:91
+msgid "Hong Kong Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:92
+msgid "Lempira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:93
+msgid "Croatian Kuna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:94
+msgid "Gourde"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:95
+msgid "Forint"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:96
+msgid "Rupiah"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:97
+msgid "New Israeli Sheqel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:98
+msgid "Indian Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:99
+msgid "Iraqi Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:168
+msgid "Payment State"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:175
+msgid "Creation Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:181
+msgid "Payment Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:187
+msgid "Amount"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:188
+msgid "The overall sum payed, including all taxes fees, etc."
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:217
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:224
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:231
+msgid "Response Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:238
+msgid "Response Description"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:34
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:35
+msgid "Paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:36
+msgid "Failed"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1147
+msgid "Credit Card (PayPal)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1223
+msgid "PayPal Payment"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:73
+msgid "pieces"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:74 waeup/ikoba/payments/paypal.py:1136
+msgid "license"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:10
+msgid "ANGUILLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:100
+msgid "SOUTH KOREA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:101
+msgid "KUWAIT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:102
+msgid "CAYMAN ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:103
+msgid "KAZAKHSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:104
+msgid "LAOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:105
+msgid "SAINT LUCIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:106
+msgid "LIECHTENSTEIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:107
+msgid "SRI LANKA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:108
+msgid "LESOTHO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:109
+msgid "LITHUANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:11
+msgid "ALBANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:110
+msgid "LUXEMBOURG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:111
+msgid "LATVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:112
+msgid "MOROCCO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:113
+msgid "MADAGASCAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:114
+msgid "MARSHALL ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:115
+msgid "MALI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:116
+msgid "MONGOLIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:117
+msgid "MARTINIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:118
+msgid "MAURITANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:119
+msgid "MONTSERRAT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:12
+msgid "ARMENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:120
+msgid "MALTA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:121
+msgid "MAURITIUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:122
+msgid "MALDIVES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:123
+msgid "MALAWI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:124
+msgid "MEXICO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:125
+msgid "MALAYSIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:126
+msgid "MOZAMBIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:127
+msgid "NAMIBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:128
+msgid "NEW CALEDONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:129
+msgid "NIGER"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:13
+msgid "NETHERLANDS ANTILLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:130
+msgid "NORFOLK ISLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:131
+msgid "NICARAGUA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:132
+msgid "NETHERLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:133
+msgid "NORWAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:134
+msgid "NEPAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:135
+msgid "NAURU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:136
+msgid "NIUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:137
+msgid "NEW ZEALAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:138
+msgid "OMAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:139
+msgid "PANAMA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:14
+msgid "ANGOLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:140
+msgid "PERU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:141
+msgid "FRENCH POLYNESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:142
+msgid "PAPUA NEW GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:143
+msgid "PHILIPPINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:144
+msgid "POLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:145
+msgid "SAINT PIERRE AND MIQUELON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:146
+msgid "PITCAIRN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:147
+msgid "PORTUGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:148
+msgid "PALAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:149
+msgid "QATAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:15
+msgid "ARGENTINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:150
+msgid "REUNION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:151
+msgid "ROMANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:152
+msgid "SERBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:153
+msgid "RUSSIAN FEDERATION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:154
+msgid "RWANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:155
+msgid "SAUDI ARABIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:156
+msgid "SOLOMON ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:157
+msgid "SEYCHELLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:158
+msgid "SWEDEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:159
+msgid "SINGAPORE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:16
+msgid "AUSTRIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:160
+msgid "SAINT HELENA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:161
+msgid "SLOVENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:162
+msgid "SVALBARD AND JAN MAYEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:163
+msgid "SLOVAKIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:164
+msgid "SIERRA LEONE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:165
+msgid "SAN MARINO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:166
+msgid "SENEGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:167
+msgid "SOMALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:168
+msgid "SURINAME"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:169
+msgid "SAO TOME AND PRINCIPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:17
+msgid "AUSTRALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:170
+msgid "EL SALVADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:171
+msgid "SWAZILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:172
+msgid "TURKS AND CAICOS ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:173
+msgid "CHAD"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:174
+msgid "TOGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:175
+msgid "THAILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:176
+msgid "TAJIKISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:177
+msgid "TURKMENISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:178
+msgid "TUNISIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:179
+msgid "TONGA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:18
+msgid "ARUBA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:180
+msgid "TURKEY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:181
+msgid "TRINIDAD AND TOBAGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:182
+msgid "TUVALU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:183
+msgid "TAIWAN, PROVINCE OF CHINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:184
+msgid "TANZANIA, UNITED REPUBLIC OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:185
+msgid "UKRAINE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:186
+msgid "UGANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:187
+msgid "UNITED STATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:188
+msgid "URUGUAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:189
+msgid "HOLY SEE (VATICAN CITY STATE)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:19
+msgid "AZERBAIJAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:190
+msgid "SAINT VINCENT AND THE GRENADINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:191
+msgid "VENEZUELA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:192
+msgid "VIRGIN ISLANDS, BRITISH"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:193
+msgid "VIETNAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:194
+msgid "VANUATU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:195
+msgid "WALLIS AND FUTUNA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:196
+msgid "SAMOA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:197
+msgid "YEMEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:198
+msgid "MAYOTTE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:199
+msgid "SOUTH AFRICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:20
+msgid "BOSNIA-HERZEGOVINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:200
+msgid "ZAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:21
+msgid "BARBADOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:22
+msgid "BELGIUM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:23
+msgid "BURKINA FASO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:24
+msgid "BULGARIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:25
+msgid "BAHRAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:26
+msgid "BURUNDI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:27
+msgid "BENIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:28
+msgid "BERMUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:29
+msgid "BRUNEI DARUSSALAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:30
+msgid "BOLIVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:31
+msgid "BRAZIL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:32
+msgid "BAHAMAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:33
+msgid "BHUTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:34
+msgid "BOTSWANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:35
+msgid "BELIZE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:36
+msgid "CANADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:37
+msgid "DEMOCRATIC REPUBLIC OF CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:38
+msgid "CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:39
+msgid "SWITZERLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:40
+msgid "COOK ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:41
+msgid "CHILE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:42
+msgid "CHINA (domestic Chinese bank transactions only)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:43
+msgid "CHINA (For CUP, bank card and cross-border transactions)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:44
+msgid "COLOMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:45
+msgid "COSTA RICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:46
+msgid "CAPE VERDE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:47
+msgid "CYPRUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:48
+msgid "CZECH REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:49
+msgid "GERMANY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:50
+msgid "DJIBOUTI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:51
+msgid "DENMARK"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:52
+msgid "DOMINICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:53
+msgid "DOMINICAN REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:54
+msgid "ALGERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:55
+msgid "ECUADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:56
+msgid "ESTONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:57
+msgid "EGYPT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:58
+msgid "ERITERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:59
+msgid "SPAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:60
+msgid "ETHIOPIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:61
+msgid "FINLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:62
+msgid "FIJI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:63
+msgid "FALKLAND ISLANDS (MALVINAS)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:64
+msgid "MICRONESIA, FEDERATED STATES OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:65
+msgid "FRANCE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:66
+msgid "GABON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:67
+msgid "UNITED KINGDOM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:68
+msgid "GRENADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:69
+msgid "GEORGIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:7
+msgid "ANDORRA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:70
+msgid "FRENCH GUIANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:71
+msgid "GIBRALTAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:72
+msgid "GREENLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:73
+msgid "GAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:74
+msgid "GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:75
+msgid "GUADELOUPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:76
+msgid "GREECE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:77
+msgid "GUATEMALA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:78
+msgid "GUAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:79
+msgid "GUINEA BISSAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:8
+msgid "UNITED ARAB EMIRATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:80
+msgid "GUYANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:81
+msgid "HONG KONG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:82
+msgid "HONDURAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:83
+msgid "CROATIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:84
+msgid "HUNGARY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:85
+msgid "INDONESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:86
+msgid "IRELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:87
+msgid "ISRAEL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:88
+msgid "INDIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:89
+msgid "ICELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:9
+msgid "ANTIGUA AND BARBUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:90
+msgid "ITALY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:91
+msgid "JAMAICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:92
+msgid "JORDAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:93
+msgid "JAPAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:94
+msgid "KENYA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:95
+msgid "KYRGYZSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:96
+msgid "CAMBODIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:97
+msgid "KIRIBATI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:98
+msgid "COMOROS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:99
+msgid "SAINT KITTS AND NEVIS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:34
+msgid "Australian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:35
+msgid "Brazilian real"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:36
+msgid "Canadian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:37
+msgid "Swiss franc"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:38
+msgid "Czech koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:39
+msgid "Danish krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:41
+msgid "Pound sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:42
+msgid "Hong Kong dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:43
+msgid "Hungarian forint"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:44
+msgid "Israeli new shekel"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:45
+msgid "Japanese yen"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:46
+msgid "Mexican peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:47
+msgid "Malaysian ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:48
+msgid "Norwegian krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:49
+msgid "New Zealand dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:50
+msgid "Philippine peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:51
+msgid "Polish zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:52
+msgid "Swedish krona"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:53
+msgid "Singapore dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:54
+msgid "Thai baht"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:55
+msgid "Turkish lira"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:56
+msgid "New Taiwan dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:57
+msgid "United States dollar"
+msgstr ""
+
+#: waeup/ikoba/products/batching.py:52
+msgid "Product Processor"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:126
+msgid "Create product"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/products/browser.py:137
+msgid "Product ${a} added."
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:195
+msgid "Manage product "
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:59 waeup/ikoba/products/browser.py:79
+#: waeup/ikoba/products/export.py:40 waeup/ikoba/products/viewlets.py:49
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:31
+msgid "Products"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:91
+msgid "Manage product section"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:96
+#: waeup/ikoba/products/browser.py:123
+msgid "Add product"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:22
+msgid "Setting parameters no yet configured."
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containerpage.pt:10
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:41
+msgid "Category"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:6
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:6
+msgid "Settings"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productpage.pt:5
+msgid "Terms and Conditions:"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:48
+#: waeup/ikoba/products/browser_templates/containerpage.pt:8
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:37
+msgid "Product Id"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:55
+msgid "Product Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:61
+msgid "Contract Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:62
+msgid "Product title is used if empty."
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:67
+msgid "Contract Category"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:81
+msgid "Valid from"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:87
+msgid "Valid to"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:48
+msgid "Fee"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:52
+msgid "Currency"
+msgstr ""
+
+#: waeup/ikoba/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/ikoba/userscontainer.py:76
+msgid "Users"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:169 waeup/ikoba/utils/utils.py:51
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:90 waeup/ikoba/utils/utils.py:50
+msgid "Germany"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:110
+msgid "no contract"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:155
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:207
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Registration and Application Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:52
+msgid "U.S."
+msgstr ""
+
+#: waeup/ikoba/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/ikoba/widgets/sequencewidget.pt:15
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/ikoba/widgets/sequencewidget.pt:23
+msgid "remove-selected-items"
+msgstr ""
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/en/LC_MESSAGES/zope.po
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/en/LC_MESSAGES/zope.po	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/en/LC_MESSAGES/zope.po	(revision 12677)
@@ -0,0 +1,58 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.IKOBA\n"
+"PO-Revision-Date: 2012-04-13 10:18+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Germany <henrik@waeup.org>\n"
+"Language: en-US\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"POT-Creation-Date: \n"
+
+#. Default: "Nothing"
+#: src/zope/app/form/browser/source.py:65
+#: src/zope/app/form/browser/source.py:212
+#: src/zope/app/form/browser/source.py:222
+msgid "SourceDisplayWidget-missing"
+msgstr "--"
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:231
+msgid "item-missing-single-value-for-display"
+msgstr "--"
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:250
+msgid "vocabulary-missing-multiple-value-for-display"
+msgstr "--"
+
+#. Default: "(no value)"
+#: src/zope/app/form/browser/itemswidgets.py:417
+#: src/zope/app/form/browser/itemswidgets.py:447
+msgid "vocabulary-missing-single-value-for-edit"
+msgstr "--"
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/itemswidgets.py:486
+msgid "vocabulary-missing-multiple-value-for-edit"
+msgstr "--"
+
+#. Default: "(no value available)"
+#: src/zope/app/form/browser/sequencewidget.py:256
+msgid "sequence-value-not-provided"
+msgstr "--"
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/sequencewidget.py:259
+msgid "sequence-value-is-empty"
+msgstr "--"
+
+#: src/zope/app/form/browser/boolwidgets.py:121
+msgid "True"
+msgstr "yes"
+
+#: src/zope/app/form/browser/boolwidgets.py:122
+msgid "False"
+msgstr "no"
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/fr/LC_MESSAGES/waeup.ikoba.po
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/fr/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/fr/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
@@ -0,0 +1,4947 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.Ikoba\n"
+"POT-Creation-Date: Mon Feb  2 08:48:41 2015\n"
+"PO-Revision-Date: 2015-02-02 08:33+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Niger <isouaba@waeup.org>\n"
+"Language: fr_FR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-SourceCharset: utf-8\n"
+"X-Generator: Poedit 1.6.5\n"
+
+#: waeup/ikoba/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:106
+msgid "Portal Settings"
+msgstr "Paramètres du portail"
+
+#: waeup/ikoba/browser/breadcrumbs.py:119 waeup/ikoba/browser/pages.py:470
+msgid "Administration"
+msgstr "Administration"
+
+#: waeup/ikoba/browser/breadcrumbs.py:126 waeup/ikoba/browser/viewlets.py:320
+#: waeup/ikoba/browser/viewlets.py:544
+msgid "Portal Configuration"
+msgstr "Configuration du portail"
+
+#: waeup/ikoba/browser/breadcrumbs.py:133 waeup/ikoba/browser/pages.py:517
+#: waeup/ikoba/browser/viewlets.py:329 waeup/ikoba/browser/viewlets.py:554
+msgid "Officers"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:140 waeup/ikoba/browser/pages.py:763
+#: waeup/ikoba/browser/pages.py:1363 waeup/ikoba/browser/viewlets.py:338
+#: waeup/ikoba/browser/viewlets.py:563
+msgid "Data Center"
+msgstr "Centre des données"
+
+#: waeup/ikoba/browser/breadcrumbs.py:147 waeup/ikoba/browser/reports.py:37
+#: waeup/ikoba/browser/viewlets.py:347 waeup/ikoba/browser/viewlets.py:572
+msgid "Reports"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:92
+msgid "Home"
+msgstr "Accueil"
+
+#: waeup/ikoba/browser/fileviewlets.py:108
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/ikoba/browser/templates/datacenterpage.pt:17
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr "Fichier"
+
+#: waeup/ikoba/browser/fileviewlets.py:109
+msgid "Scan"
+msgstr "Scan"
+
+#: waeup/ikoba/browser/fileviewlets.py:129
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:130
+msgid "Delete"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:164
+msgid "No local file selected."
+msgstr "Aucun fichier local sélectioné"
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:44
+msgid "${a} deleted."
+msgstr "${a} supprimé."
+
+#: waeup/ikoba/browser/fileviewlets.py:59
+msgid "Uploaded file is too big."
+msgstr "Le fichier envoyé est trop grand."
+
+#: waeup/ikoba/browser/fileviewlets.py:71
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:76
+msgid "${a} file format expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:82
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:92
+msgid "File ${a} uploaded."
+msgstr "Fichier ${a} chargé."
+
+#: waeup/ikoba/browser/interfaces.py:173
+#: waeup/ikoba/customers/interfaces.py:200 waeup/ikoba/interfaces.py:428
+#: waeup/ikoba/interfaces.py:504
+msgid "Email Address"
+msgstr "Adresse email"
+
+#: waeup/ikoba/browser/interfaces.py:176
+msgid "Enter your email address stored in Ikoba."
+msgstr ""
+
+#: waeup/ikoba/browser/layout.py:102 waeup/ikoba/customers/browser.py:387
+#: waeup/ikoba/customers/browser.py:690 waeup/ikoba/customers/browser.py:973
+#: waeup/ikoba/customers/browser.py:1385 waeup/ikoba/customers/browser.py:1416
+#: waeup/ikoba/documents/browser.py:259
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1039 waeup/ikoba/browser/pages.py:1068
+#: waeup/ikoba/browser/pages.py:1173 waeup/ikoba/browser/pages.py:1285
+msgid "Process CSV file"
+msgstr "Traitement fichier CSV"
+
+#: waeup/ikoba/browser/pages.py:1071
+msgid "Back to step 1"
+msgstr "Retour à l’étape 1"
+
+#: waeup/ikoba/browser/pages.py:1072
+msgid "Proceed to step 3"
+msgstr "Passer à l’étape 3"
+
+#: waeup/ikoba/browser/pages.py:1117
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+"Deux en-têtes: chaque nom de colonne doit apparaître qu’une seule fois."
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:112
+msgid "Local role successfully removed: ${a}"
+msgstr "Role local supprimé avec success: ${a}"
+
+#: waeup/ikoba/browser/pages.py:1125
+msgid "Replace imported file!"
+msgstr "Remplacer le fichier importé!"
+
+#: waeup/ikoba/browser/pages.py:1138 waeup/ikoba/browser/pages.py:1261
+msgid "Import aborted."
+msgstr "Importation échouée."
+
+#: waeup/ikoba/browser/pages.py:1150
+msgid "Update mode only!"
+msgstr "Mettre à jour mode seulement!"
+
+#: waeup/ikoba/browser/pages.py:1176 waeup/ikoba/browser/pages.py:1404
+msgid "Reset"
+msgstr "Réinitialiser"
+
+#: waeup/ikoba/browser/pages.py:1177
+msgid "Set headerfields"
+msgstr "Mettre l’en-tête des champ"
+
+#: waeup/ikoba/browser/pages.py:1178
+msgid "Back to step 2"
+msgstr "Retour à l’étape 2"
+
+#: waeup/ikoba/browser/pages.py:1179
+msgid "Perform import"
+msgstr "Effectuer l’importation"
+
+#: waeup/ikoba/browser/pages.py:1244
+msgid "Edit headers or replace imported file!"
+msgstr "Editer les en-têtes ou remplacer le fichier importé"
+
+#: waeup/ikoba/browser/pages.py:125 waeup/ikoba/browser/pages.py:772
+#: waeup/ikoba/customers/browser.py:207
+msgid "No item selected."
+msgstr "Aucun element sélectionné."
+
+#: waeup/ikoba/browser/pages.py:1287
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1337
+msgid "Processing of ${a} rows failed."
+msgstr "Traitement des ${a} lignes erronées."
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1339
+msgid "Successfully processed ${a} rows."
+msgstr "${a} lignes traitées avec succès."
+
+#: waeup/ikoba/browser/pages.py:1347
+msgid "Show logfiles"
+msgstr "Afficher les journaux "
+
+#: waeup/ikoba/browser/pages.py:1350
+msgid "Show"
+msgstr "Afficher"
+
+#: waeup/ikoba/browser/pages.py:1365 waeup/ikoba/browser/viewlets.py:185
+msgid "Search"
+msgstr "Recherche "
+
+#: waeup/ikoba/browser/pages.py:1367
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1375
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+"Les fichiers de log ne peuvent être cherches seulement sur les systèmes "
+"d’exploitation basés sur l’Unix."
+
+#: waeup/ikoba/browser/pages.py:1390
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1393
+msgid "No search results found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1401
+msgid "Edit data center settings"
+msgstr "Editer les paramètres du centre de données"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1421
+msgid "File already existed (not copied): ${a}"
+msgstr "Fichier déjà existant (ne pas copié): ${a}"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1424
+msgid "Given storage path cannot be used: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1428
+msgid "New storage path succefully set."
+msgstr "Le nouveau chemin de stockage a été défini avec succès."
+
+#: waeup/ikoba/browser/pages.py:1440
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1442
+msgid "Create CSV file"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:146 waeup/ikoba/customers/browser.py:220
+#: waeup/ikoba/customers/browser.py:883 waeup/ikoba/customers/browser.py:1253
+msgid "Successfully removed: ${a}"
+msgstr "Supprimer avec succès: ${a}"
+
+#: waeup/ikoba/browser/pages.py:1470
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1514
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1540 waeup/ikoba/customers/browser.py:543
+#: waeup/ikoba/customers/browser.py:625
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1550
+msgid "No record found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1564 waeup/ikoba/customers/browser.py:589
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1568
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+"Un email contenant votre nom d’utilisateur et mot de passé a été envoyé à "
+"${a}."
+
+#: waeup/ikoba/browser/pages.py:188 waeup/ikoba/customers/browser.py:84
+#: waeup/ikoba/customers/browser.py:422
+msgid "Form has been saved."
+msgstr "Formulaire a été Sauvegardé"
+
+#: waeup/ikoba/browser/pages.py:259 waeup/ikoba/browser/viewlets.py:266
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:40
+#: waeup/ikoba/browser/templates/customersitelayout.pt:39
+msgid "Login"
+msgstr "Connexion"
+
+#: waeup/ikoba/browser/pages.py:270
+msgid "You logged in."
+msgstr "Vous etes connecté"
+
+#: waeup/ikoba/browser/pages.py:307
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:315
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:318
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:333
+msgid "You have been logged out. Thanks for using WAeUP Ikoba!"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:375
+msgid "Contact ${a}"
+msgstr "Contacte ${a}"
+
+#: waeup/ikoba/browser/pages.py:381 waeup/ikoba/browser/pages.py:661
+msgid "Send message now"
+msgstr "Envoyer un message maintenant"
+
+#: waeup/ikoba/browser/pages.py:400 waeup/ikoba/browser/pages.py:433
+#: waeup/ikoba/browser/pages.py:678 waeup/ikoba/customers/browser.py:372
+msgid "Your message has been sent."
+msgstr "Votre message a été envoyé."
+
+#: waeup/ikoba/browser/pages.py:420
+msgid "Send now"
+msgstr "Envoyer maintenant"
+
+#: waeup/ikoba/browser/pages.py:435
+msgid "A smtp server error occurred."
+msgstr "Une erreur de serveur smtp s’est produite."
+
+#: waeup/ikoba/browser/pages.py:460
+msgid "<h1>Welcome to WAeUP.Ikoba</h1>"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:518 waeup/ikoba/customers/viewlets.py:207
+#: waeup/ikoba/customers/viewlets.py:332 waeup/ikoba/customers/viewlets.py:404
+#: waeup/ikoba/documents/viewlets.py:60 waeup/ikoba/documents/viewlets.py:79
+#: waeup/ikoba/products/viewlets.py:60 waeup/ikoba/products/viewlets.py:79
+msgid "Manage"
+msgstr "Gérer"
+
+#: waeup/ikoba/browser/pages.py:519 waeup/ikoba/browser/viewlets.py:175
+msgid "Remove"
+msgstr "Supprimer"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:526
+msgid "User account ${a} successfully deleted."
+msgstr "Le compte d’utilisateur ${a} supprimé avec succès."
+
+#: waeup/ikoba/browser/pages.py:563 waeup/ikoba/browser/pages.py:565
+#: waeup/ikoba/browser/viewlets.py:403
+msgid "Add user"
+msgstr "Ajouter utilisateur"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:587
+msgid "User account ${a} successfully added."
+msgstr "Compte utilisateur ${a} ajouté avec succès."
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:608
+msgid "Edit user ${a}"
+msgstr "Editer utilisateur ${a}"
+
+#: waeup/ikoba/browser/pages.py:616 waeup/ikoba/browser/pages.py:737
+#: waeup/ikoba/browser/pages.py:1403 waeup/ikoba/customers/browser.py:400
+#: waeup/ikoba/customers/browser.py:732 waeup/ikoba/customers/browser.py:774
+#: waeup/ikoba/customers/browser.py:990 waeup/ikoba/customers/browser.py:1008
+#: waeup/ikoba/customers/browser.py:1402 waeup/ikoba/customers/browser.py:1426
+#: waeup/ikoba/customers/browser.py:1443 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:269 waeup/ikoba/documents/browser.py:293
+#: waeup/ikoba/documents/browser.py:308 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:106 waeup/ikoba/products/browser.py:184
+#: waeup/ikoba/products/browser.py:197
+msgid "Save"
+msgstr "Sauvegarder"
+
+#: waeup/ikoba/browser/pages.py:642
+msgid "User settings have been saved."
+msgstr "Les paramètres de l’utilisateur ont été sauvegardés."
+
+#: waeup/ikoba/browser/pages.py:645 waeup/ikoba/browser/pages.py:1070
+#: waeup/ikoba/browser/pages.py:1175 waeup/ikoba/customers/browser.py:759
+#: waeup/ikoba/customers/browser.py:790 waeup/ikoba/customers/browser.py:946
+#: waeup/ikoba/customers/browser.py:1312 waeup/ikoba/documents/browser.py:106
+#: waeup/ikoba/documents/browser.py:152 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:273 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:110
+#: waeup/ikoba/products/browser.py:184 waeup/ikoba/products/browser.py:206
+msgid "Cancel"
+msgstr "Annuller"
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:659 waeup/ikoba/customers/browser.py:353
+msgid "Send message to ${a}"
+msgstr "Envoyer message à ${a}"
+
+#: waeup/ikoba/browser/pages.py:688 waeup/ikoba/browser/viewlets.py:354
+msgid "My Preferences"
+msgstr "Mes Préférences"
+
+#: waeup/ikoba/browser/pages.py:701 waeup/ikoba/browser/viewlets.py:370
+msgid "My Roles"
+msgstr "Mes Roles"
+
+#: waeup/ikoba/browser/pages.py:733
+msgid "Edit portal configuration"
+msgstr "Editer la configuration du portail"
+
+#: waeup/ikoba/browser/pages.py:745
+msgid "Update plugins"
+msgstr "Mise à jour modules"
+
+#: waeup/ikoba/browser/pages.py:746
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:747
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:752
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+"Les modules ont été mis à jour. Voir le fichier des journal pour plus de "
+"détails."
+
+#: waeup/ikoba/browser/pages.py:766 waeup/ikoba/customers/browser.py:176
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:101
+msgid "Remove selected"
+msgstr "Supprimer sélectionné"
+
+#: waeup/ikoba/browser/pages.py:783
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:787
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:798
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:800 waeup/ikoba/browser/pages.py:816
+#: waeup/ikoba/browser/pages.py:1041 waeup/ikoba/browser/pages.py:1349
+#: waeup/ikoba/browser/pages.py:1366 waeup/ikoba/browser/pages.py:1405
+#: waeup/ikoba/browser/pages.py:1443
+msgid "Back to Data Center"
+msgstr "Retour au centre des données"
+
+#: waeup/ikoba/browser/pages.py:812
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:815
+msgid "Upload"
+msgstr "Télécharger (vers le serveur)"
+
+#: waeup/ikoba/browser/pages.py:855
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:857
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:861
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:880
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:882 waeup/ikoba/browser/pages.py:1571
+#: waeup/ikoba/customers/browser.py:374
+msgid "An smtp server error occurred."
+msgstr "Une erreur du serveur smtp produite."
+
+#: waeup/ikoba/browser/pages.py:890
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:905
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:91
+msgid "No local role selected."
+msgstr "Aucun rôle local sélectionné."
+
+#: waeup/ikoba/browser/pages.py:913
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:924
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:207 waeup/ikoba/browser/pdf.py:472
+#: waeup/ikoba/browser/pdf.py:482
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:16
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr "Date"
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:754
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/ikoba/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr "resoudre_captcha"
+
+#: waeup/ikoba/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data. Please pick the "
+"type of objects you want to export from the selection below. The file will "
+"be generated and then be made available for you to download in the table "
+"below. Files can only be downloaded within 24 hours after generation. The "
+"download link disappears afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:42
+msgid "Export Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:43
+msgid "Exporter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:44
+msgid "Filter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:45
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:6
+msgid "Creator"
+msgstr "Auteur"
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:46
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:7
+msgid "Creation Date"
+msgstr "Date de Création"
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:47
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:8
+msgid "Status"
+msgstr "Statut"
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:72
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:39
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/ikoba/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr "Date du téléchargement"
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr "Etape 1"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr "Nom du fichier"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:14
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/ikoba/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr "Ensemble des données"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr "Taille"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+"En utilisant le traitement par lot vous pouvez créer en série, mettre à jour "
+"en série ou supprimer en série l’ensemble des données de la base de données "
+"en utilisant le fichier CSV."
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr "Sélectionner"
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+"S’il vous plait sélectionner un fichier pour le traitement à partir de la "
+"liste suivante."
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr "Etape 2"
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:14
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr "Mode:"
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+"S’il vous plait sélectionner un fichier de traitement et un mode de "
+"traitement à partir des sélections ci-dessous."
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr "Champs d’en-tête OK"
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr "Etape 3"
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+"Eventuellement modifier l’en-tête des champs du fichier d’importation ci-"
+"dessous."
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr "Etape 4"
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:13
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr "Mode de traitement:"
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr "Traitement par lots terminé."
+
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr "Actuellement aucun de fichier journal n’est valable"
+
+#: waeup/ikoba/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+"Le centre des données vous aide à gérer les données du portail. Vous pouvez "
+"télécharger ici le fichier CSV, qui sera disponible pour l’importation de la "
+"suite."
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr "Chemin de stockage:"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr "Fichier:"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr "Processeur:"
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/filedisplay.pt:15
+msgid "HTML Element:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:13
+msgid "Password"
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/ikoba/browser/templates/loginpage.pt:26
+msgid "login_trouble1"
+msgstr "Problème de connexion1"
+
+#. Default: "You don't have an account because you are a new customer, or your customer record has been created for you? Acquire a Password Activation Code (PWD) and inititialize your customer account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:29
+msgid "login_trouble2"
+msgstr "Problème de connexion2"
+
+#. Default: "You don't have an account because you are a new customer? Create your customer account <strong><a href=\"createaccount\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:34
+msgid "login_trouble4"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer and your customer record has been created for you? Inititialize your customer account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:38
+msgid "login_trouble5"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:4
+msgid "Identifier"
+msgstr ""
+
+#. Default: "Or simply forgot your id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:42
+msgid "login_trouble3"
+msgstr "Problème de connexion3"
+
+#. Default: "Customers can enter either their customer id, registration number or email address. Officers must enter their user name."
+#: waeup/ikoba/browser/templates/loginpage.pt:7
+msgid "identifier_hint"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr "Mes Rôles de Portail:"
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr "Mes Rôles locaux:"
+
+#: waeup/ikoba/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr "La page que vous tentez d’accéder n’est pas disponible."
+
+#: waeup/ikoba/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr "Vous essayez peut-être d’accéder à page non existante."
+
+#: waeup/ikoba/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr "S’il vous plait noter les points suivants:"
+
+#: waeup/ikoba/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr "Il se pourrait que vous ayez mal écrit l’URL."
+
+#: waeup/ikoba/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:46
+#: waeup/ikoba/browser/templates/customersitelayout.pt:45
+msgid "Contact"
+msgstr "Contact"
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:52
+#: waeup/ikoba/browser/templates/customersitelayout.pt:51
+msgid "Logout"
+msgstr "Déconnexion"
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:6
+#: waeup/ikoba/browser/templates/customersitelayout.pt:4
+msgid "WAeUP.Ikoba - Registration and Application System"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:15
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:16
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:30
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:9
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:27
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:10
+#: waeup/ikoba/customers/browser_templates/basepage.pt:14
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:24
+msgid "Password:"
+msgstr "Mot de passe"
+
+#: waeup/ikoba/browser/viewlets.py:165
+msgid "Add"
+msgstr "Ajouter"
+
+#: waeup/ikoba/browser/viewlets.py:305
+msgid "Any link text"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:387 waeup/ikoba/customers/viewlets.py:198
+msgid "Send email"
+msgstr "Envoyer un email"
+
+#: waeup/ikoba/browser/viewlets.py:396
+msgid "Edit settings"
+msgstr "Editer les Paramètres"
+
+#: waeup/ikoba/browser/viewlets.py:412
+msgid "Show logs"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:421
+msgid "Upload data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:430
+msgid "Process data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:439
+msgid "Export data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:448
+msgid "View processed files"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:508
+msgid "Enquiries"
+msgstr "Requêtes"
+
+#: waeup/ikoba/browser/viewlets.py:93 waeup/ikoba/browser/viewlets.py:155
+#: waeup/ikoba/customers/viewlets.py:284 waeup/ikoba/customers/viewlets.py:347
+#: waeup/ikoba/customers/viewlets.py:422
+msgid "Edit"
+msgstr "Editer"
+
+#: waeup/ikoba/customers/batching.py:142
+msgid "Customer record created"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/batching.py:197
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:461
+msgid "Customer Sample Document Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:562
+msgid "Customer Sample Contract Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:60
+msgid "Customer Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1013
+msgid "Final Submit"
+msgstr "Dernière Validation"
+
+#: waeup/ikoba/customers/browser.py:1017 waeup/ikoba/customers/browser.py:1452
+msgid "Form has been submitted."
+msgstr "Le formulaire a été validé."
+
+#: waeup/ikoba/customers/browser.py:1029 waeup/ikoba/documents/browser.py:324
+msgid "Trigger document transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:106 waeup/ikoba/customers/export.py:130
+#: waeup/ikoba/customers/viewlets.py:54
+msgid "Customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1077
+msgid "Documents of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1085
+msgid "Customer Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1091
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:8
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:9
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:4
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:8
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:8
+msgid "Id"
+msgstr "Id"
+
+#: waeup/ikoba/customers/browser.py:1092 waeup/ikoba/customers/browser.py:1527
+#: waeup/ikoba/products/productoptions.py:44
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:9
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:9
+#: waeup/ikoba/products/browser_templates/containerpage.pt:9
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:39
+msgid "Title"
+msgstr "Titre"
+
+#: waeup/ikoba/customers/browser.py:1093 waeup/ikoba/customers/browser.py:1528
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:10
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:10
+msgid "Type"
+msgstr "Type"
+
+#: waeup/ikoba/customers/browser.py:1094 waeup/ikoba/customers/browser.py:1529
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:33
+msgid "State"
+msgstr "Etat"
+
+#: waeup/ikoba/customers/browser.py:1095 waeup/ikoba/customers/browser.py:1530
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:12
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:12
+msgid "Last Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1181 waeup/ikoba/customers/viewlets.py:160
+msgid "Contracts"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:1226
+msgid "${a}: Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1229 waeup/ikoba/customers/browser.py:1270
+#: waeup/ikoba/customers/browser.py:1295
+msgid "Add contract"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1234
+msgid "Remove selected contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1240
+msgid "No contract selected."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:132
+msgid "Find customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1325
+msgid "Select product"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:133 waeup/ikoba/customers/browser.py:175
+msgid "Find customer(s)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1336
+msgid "Save and proceed"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1448
+msgid "Apply now (final submit)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1464
+msgid "Trigger contract transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:150 waeup/ikoba/customers/browser.py:192
+msgid "Empty search string"
+msgstr "Chaîne de recherche vide"
+
+#: waeup/ikoba/customers/browser.py:1512
+msgid "Contracts of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1520
+msgid "Customer Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:156 waeup/ikoba/customers/browser.py:198
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:162 waeup/ikoba/customers/browser.py:205
+#: waeup/ikoba/customers/browser.py:563 waeup/ikoba/customers/browser.py:575
+msgid "No customer found."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:174 waeup/ikoba/customers/viewlets.py:180
+msgid "Manage customer section"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:233 waeup/ikoba/customers/viewlets.py:188
+msgid "Add customer"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:236
+msgid "Create customer record"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:241
+msgid "Customer created."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:256
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:279
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:283
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:289
+msgid "You successfully logged in as customer."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:312
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:314
+msgid "${a}: Base Data"
+msgstr "${a}: Données de Base"
+
+#: waeup/ikoba/customers/browser.py:320
+msgid "set"
+msgstr "Prêt"
+
+#: waeup/ikoba/customers/browser.py:321
+msgid "unset"
+msgstr "Non prêt"
+
+#: waeup/ikoba/customers/browser.py:385
+msgid "Manage base data"
+msgstr "Gestion données de base"
+
+#: waeup/ikoba/customers/browser.py:435
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:447 waeup/ikoba/customers/browser.py:1044
+#: waeup/ikoba/customers/browser.py:1479 waeup/ikoba/documents/browser.py:339
+msgid "No transition"
+msgstr "Pas de transition"
+
+#: waeup/ikoba/customers/browser.py:450 waeup/ikoba/customers/browser.py:1047
+#: waeup/ikoba/customers/browser.py:1482
+msgid "Apply"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:457 waeup/ikoba/customers/browser.py:1055
+#: waeup/ikoba/customers/browser.py:1490 waeup/ikoba/documents/browser.py:350
+msgid "Transition '%s' executed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:474
+msgid "Customer account has been activated."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:494
+msgid "Customer account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:513
+msgid "${a}: History"
+msgstr "${a}: Histoire"
+
+#: waeup/ikoba/customers/browser.py:523
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:557
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:566
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:610
+msgid "Create customer account"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:64
+msgid ""
+"You can not edit some data after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:648
+msgid "You have successfully created a customer account for the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:669
+msgid "Your request was successful."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:67
+msgid ""
+"You can not edit your document after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:688 waeup/ikoba/customers/viewlets.py:303
+msgid "Upload files"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:70
+msgid ""
+"You can not edit your contract after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:721
+msgid "Edit base data and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:722
+msgid "Edit base data"
+msgstr "Editer données de base"
+
+#: waeup/ikoba/customers/browser.py:741
+msgid "Passport picture is missing."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:746
+msgid "Save and request registration now"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:755
+msgid "Registration form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:771 waeup/ikoba/customers/viewlets.py:293
+msgid "Change password"
+msgstr "Changer mot de passe"
+
+#: waeup/ikoba/customers/browser.py:785
+msgid "Password changed."
+msgstr "Mot de passé changé."
+
+#: waeup/ikoba/customers/browser.py:816 waeup/ikoba/customers/viewlets.py:166
+#: waeup/ikoba/documents/browser.py:62 waeup/ikoba/documents/browser.py:82
+#: waeup/ikoba/documents/viewlets.py:49
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:3
+msgid "Documents"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:856
+msgid "${a}: Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:859 waeup/ikoba/customers/browser.py:900
+#: waeup/ikoba/customers/browser.py:924 waeup/ikoba/documents/browser.py:96
+#: waeup/ikoba/documents/browser.py:119 waeup/ikoba/documents/browser.py:129
+msgid "Add document"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:864 waeup/ikoba/documents/browser.py:101
+msgid "Remove selected documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:870
+msgid "No document selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:935 waeup/ikoba/customers/browser.py:1306
+#: waeup/ikoba/documents/browser.py:144
+msgid "${a} added."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:94
+msgid "The requested form is locked (read-only)."
+msgstr "Le formulaire demandé est verrouillé (lecture seule)."
+
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:56
+msgid "Select all"
+msgstr "Tout sélectionné"
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:10
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:10
+msgid "with name"
+msgstr "avec nom"
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:11
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:11
+msgid "with registration number"
+msgstr "avec numéro d’inscription"
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:12
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:12
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:23
+msgid "Search Results"
+msgstr "Résultats de la Recherche"
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:32
+msgid "Reg. Number"
+msgstr "Numéro Reg."
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:9
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:9
+msgid "with id"
+msgstr "avec id"
+
+#: waeup/ikoba/customers/browser_templates/contractaddpage.pt:7
+msgid "Contract Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:20
+msgid "Contract Type:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:19
+msgid ""
+"A customer record will be created and your login credentials will be sent to "
+"the email address given above. To be able to proceed you must provide a "
+"valid email address!"
+msgstr ""
+
+#. Default: "Ikoba is creating a temporary customer password which will replace the original password chosen by the customer. The password will be valid for ${minutes} minutes. The customer can't login during this time period. After expiration the customer can login with the original password again."
+#: waeup/ikoba/customers/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/ikoba/customers/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:19
+msgid ""
+"Your customer record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:8
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+"Veuillez résoudre le captcha<br />pour empêcher l'usage abusif de ce "
+"service :"
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr "L'email ne peut pas être envoyé. Réessayez plus tard."
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:14
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:44
+msgid "Thanks for using Ikoba!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:3
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr "Un email avec votre nom d'utilisateur et mot de passe a été envoyé à"
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr "Veuillez vérifier votre compte email pour procéder."
+
+#: waeup/ikoba/customers/contracts.py:147
+#: waeup/ikoba/customers/documents.py:162
+msgid "Customer has not yet been approved."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:153
+msgid "Attached documents must be verified first."
+msgstr ""
+
+#: waeup/ikoba/customers/documents.py:165
+msgid "No file uploaded."
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:194
+msgid "Customer Sample Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:246
+msgid "Customer Sample Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:142
+#: waeup/ikoba/customers/fileviewlets.py:143
+#: waeup/ikoba/documents/fileviewlets.py:39
+#: waeup/ikoba/documents/fileviewlets.py:40
+msgid "PDF File"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:167
+#: waeup/ikoba/customers/fileviewlets.py:168
+#: waeup/ikoba/documents/fileviewlets.py:57
+#: waeup/ikoba/documents/fileviewlets.py:58
+msgid "PDF Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:48
+msgid "Passport Picture"
+msgstr "Photo d’Identité"
+
+#: waeup/ikoba/customers/fileviewlets.py:60
+msgid "Passport Picture (jpg only)"
+msgstr "Photo d’Identité (jpg seulement)"
+
+#: waeup/ikoba/customers/fileviewlets.py:90
+#: waeup/ikoba/customers/fileviewlets.py:91
+#: waeup/ikoba/customers/fileviewlets.py:115
+#: waeup/ikoba/customers/fileviewlets.py:116
+msgid "Sample Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:101
+#: waeup/ikoba/customers/interfaces.py:195
+#: waeup/ikoba/customers/interfaces.py:213
+msgid "First Name"
+msgstr "Prénom"
+
+#: waeup/ikoba/customers/interfaces.py:106
+#: waeup/ikoba/customers/interfaces.py:218
+msgid "Middle Name"
+msgstr "Deuxième prénom"
+
+#: waeup/ikoba/customers/interfaces.py:111
+#: waeup/ikoba/customers/interfaces.py:223
+msgid "Last Name (Surname)"
+msgstr "Nom de famille"
+
+#: waeup/ikoba/customers/interfaces.py:116
+msgid "Sex"
+msgstr "Sexe"
+
+#: waeup/ikoba/customers/interfaces.py:122
+#: waeup/ikoba/customers/interfaces.py:161
+#: waeup/ikoba/customers/interfaces.py:190
+msgid "Registration Number"
+msgstr "Numéro d’Inscription"
+
+#: waeup/ikoba/customers/interfaces.py:129
+#: waeup/ikoba/customers/interfaces.py:228
+msgid "Email"
+msgstr "Email"
+
+#: waeup/ikoba/customers/interfaces.py:136 waeup/ikoba/interfaces.py:433
+#: waeup/ikoba/interfaces.py:511
+msgid "Phone"
+msgstr "Téléphone"
+
+#: waeup/ikoba/customers/interfaces.py:252
+#: waeup/ikoba/documents/interfaces.py:52
+msgid "Document Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:303
+msgid "Contract Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:309
+#: waeup/ikoba/customers/interfaces.py:332
+msgid "Product"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:315
+#: waeup/ikoba/customers/interfaces.py:365
+#: waeup/ikoba/products/interfaces.py:73
+msgid "Options/Fees"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:344
+#: waeup/ikoba/customers/interfaces.py:379
+msgid "Document"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:355
+msgid "Comment"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:82
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:88
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:90
+msgid ""
+"This message will be shown if and only if deactivated customers try to login."
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:96
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:31
+msgid "Customer Id"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:161
+msgid "Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:180
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:240
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:25
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:30
+msgid "Connected Files"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:301 waeup/ikoba/customers/utils.py:309
+#: waeup/ikoba/customers/utils.py:317 waeup/ikoba/documents/utils.py:32
+msgid "created"
+msgstr "créé"
+
+#: waeup/ikoba/customers/utils.py:302
+msgid "started"
+msgstr "Démarrée"
+
+#: waeup/ikoba/customers/utils.py:303
+msgid "requested"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:304 waeup/ikoba/customers/utils.py:311
+msgid "approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:305
+msgid "provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:310
+msgid "submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:312 waeup/ikoba/customers/utils.py:320
+msgid "rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:313 waeup/ikoba/customers/utils.py:321
+msgid "expired"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:318
+msgid "submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:319
+msgid "verified"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:325
+msgid "Sample Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:326 waeup/ikoba/documents/utils.py:37
+msgid "PDF Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:330
+msgid "Sample Contract"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:374
+msgid "Contractor"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:377
+msgid "Document Owner"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:386
+msgid "Contract Data"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:401
+msgid "Transaction History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:407
+msgid "Registration History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:425 waeup/ikoba/products/interfaces.py:95
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:32
+msgid "Terms and Conditions"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:114
+msgid "My Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:115
+msgid "My Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:116 waeup/ikoba/customers/viewlets.py:172
+msgid "History"
+msgstr "Histoire"
+
+#: waeup/ikoba/customers/viewlets.py:137 waeup/ikoba/customers/viewlets.py:154
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr "Données de Base"
+
+#: waeup/ikoba/customers/viewlets.py:217 waeup/ikoba/customers/viewlets.py:363
+#: waeup/ikoba/customers/viewlets.py:438 waeup/ikoba/documents/viewlets.py:89
+msgid "Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:227
+msgid "Login as customer"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:236
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:248 waeup/ikoba/customers/viewlets.py:269
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:257
+msgid "Activate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:283
+msgid "Edit and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:323
+msgid "Download documents overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:372 waeup/ikoba/customers/viewlets.py:447
+#: waeup/ikoba/documents/viewlets.py:69 waeup/ikoba/products/viewlets.py:69
+msgid "View"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:383
+msgid "Download document slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:395
+msgid "Download contracts overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:413
+msgid "Manage official data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:462
+msgid "Download contract slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:98
+msgid "My Data"
+msgstr "Mes Données"
+
+#: waeup/ikoba/customers/vocabularies.py:58
+msgid "male"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:60
+msgid "female"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:103 waeup/ikoba/customers/workflow.py:124
+msgid "Reset to requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:104 waeup/ikoba/customers/workflow.py:125
+msgid "Reset to 'requested'"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:111 waeup/ikoba/customers/workflow.py:209
+#: waeup/ikoba/customers/workflow.py:210 waeup/ikoba/customers/workflow.py:216
+#: waeup/ikoba/customers/workflow.py:217 waeup/ikoba/customers/workflow.py:223
+#: waeup/ikoba/customers/workflow.py:224 waeup/ikoba/customers/workflow.py:237
+#: waeup/ikoba/customers/workflow.py:238 waeup/ikoba/customers/workflow.py:324
+#: waeup/ikoba/customers/workflow.py:325 waeup/ikoba/customers/workflow.py:331
+#: waeup/ikoba/customers/workflow.py:332 waeup/ikoba/customers/workflow.py:338
+#: waeup/ikoba/customers/workflow.py:339 waeup/ikoba/customers/workflow.py:352
+#: waeup/ikoba/customers/workflow.py:353
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:180
+msgid "Create contract"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:183
+msgid "Contract created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:188
+msgid "Submit for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:189
+msgid "Submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:195
+msgid "Approve"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:196
+msgid "Approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:202 waeup/ikoba/customers/workflow.py:317
+msgid "Reject"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:203 waeup/ikoba/customers/workflow.py:318
+msgid "REJECTED"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:230 waeup/ikoba/customers/workflow.py:231
+#: waeup/ikoba/customers/workflow.py:345 waeup/ikoba/customers/workflow.py:346
+msgid "Set to expired"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:295 waeup/ikoba/documents/workflow.py:37
+msgid "Create document"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:298 waeup/ikoba/documents/workflow.py:40
+msgid "Document created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:303
+msgid "Submit for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:304
+msgid "Submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:310
+msgid "Verify"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:311
+msgid "Verified"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:45
+msgid "Create customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:48
+msgid "Customer created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:53
+msgid "Start registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:56
+msgid "Customer registration started"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:61
+msgid "Request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:62
+msgid "Customer registration requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:68
+msgid "Approve customer provisionally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:69
+msgid "Customer registration provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:75
+msgid "Approve finally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:76
+msgid "Customer registration finally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:82
+msgid "Approve customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:83
+msgid "Customer registration approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:89
+msgid "Reject customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:90
+msgid "Customer registration rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:96 waeup/ikoba/customers/workflow.py:110
+#: waeup/ikoba/customers/workflow.py:117
+msgid "Reset customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:97 waeup/ikoba/customers/workflow.py:118
+msgid "Reset to initial customer state"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:135
+msgid "Public PDF Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:147
+msgid "Public HTML Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:159
+msgid "Public REST Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:140 waeup/ikoba/products/browser.py:133
+msgid "The id chosen already exists."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:210
+#: waeup/ikoba/documents/fileviewlets.py:80
+msgid "The document requested has not yet been published."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:256 waeup/ikoba/documents/browser.py:282
+#: waeup/ikoba/products/browser.py:185 waeup/ikoba/products/browser.py:215
+msgid "Remove selected local roles"
+msgstr "Supprimer les rôles locaux sélectionnés"
+
+#: waeup/ikoba/documents/browser.py:257 waeup/ikoba/documents/browser.py:278
+#: waeup/ikoba/products/browser.py:186 waeup/ikoba/products/browser.py:211
+msgid "Add local role"
+msgstr "Ajouter rôle local"
+
+#: waeup/ikoba/documents/browser.py:267
+msgid "Manage document "
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:342
+msgid "Apply now"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:94
+msgid "Manage document section"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:1
+#: waeup/ikoba/products/browser_templates/containerpage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentaddform.pt:7
+#: waeup/ikoba/customers/browser_templates/documentaddpage.pt:7
+msgid "Document Type"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:12
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:12
+msgid "Files"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:16
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:16
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:21
+msgid "Document Type:"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:37
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:48
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:5
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:37
+msgid "Name"
+msgstr "Nom"
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:39
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:50
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:39
+msgid "Local Role"
+msgstr "Rôle Local"
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:6
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:6
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:7
+msgid "Metadata"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:9
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:12
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:7
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:9
+msgid "Local Roles"
+msgstr "Rôles Locaux"
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:25
+msgid "Content:"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:101
+msgid "Public HTML Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:110
+msgid "Public REST Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:92
+msgid "Public PDF Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:110
+msgid "Multilingual content in REST (reStructuredText) format"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:58
+msgid "Document Title"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:78
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:97
+#: waeup/ikoba/products/interfaces.py:97
+msgid "Multilingual content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:33
+msgid "published"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:38
+msgid "HTML Document"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:39
+msgid "REST Document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:45
+msgid "Publish document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:48
+msgid "Document published"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:53
+msgid "Retract documet"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:56
+msgid "Document retracted"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:361
+msgid "Processor name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:387
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:394
+msgid "Email to:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:401
+msgid "Subject:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:405
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:409
+msgid "Text:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:438 waeup/ikoba/interfaces.py:494
+msgid "Public Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:448 waeup/ikoba/interfaces.py:449
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:455
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:456
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:485
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:35
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:46
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:35
+msgid "User Id"
+msgstr "Id de l’utilisateur"
+
+#: waeup/ikoba/interfaces.py:490
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:34
+msgid "Full Name"
+msgstr "Nom Complet"
+
+#: waeup/ikoba/interfaces.py:500
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:517
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr "Rôles Portail"
+
+#: waeup/ikoba/interfaces.py:564
+msgid "Name of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:565
+msgid "Sample Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:570
+msgid "Abbreviated Title of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:576
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:588
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:594
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:601
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:602
+msgid "Ikoba Contact"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:607
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:614
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:66
+msgid "new"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:67
+msgid "completed"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:68
+msgid "pending"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:69
+msgid "active"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:70
+msgid "assigned"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:71
+msgid "callbacks"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/ikoba/objecthistory.py:66
+msgid "by"
+msgstr "par"
+
+#: waeup/ikoba/payments/currencies.py:100
+msgid "Iranian Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:101
+msgid "Iceland Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:102
+msgid "Jamaican Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:103
+msgid "Jordanian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:104
+msgid "Yen"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:105
+msgid "Kenyan Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:106
+msgid "Som"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:107
+msgid "Riel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:108
+msgid "Comoro Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:110
+msgid "North Korean Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:113
+msgid "Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:114
+msgid "Kuwaiti Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:115
+msgid "Cayman Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:116
+msgid "Tenge"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:117
+msgid "Kip"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:118
+msgid "Lebanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:119
+msgid "Sri Lanka Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:120
+msgid "Liberian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:121
+msgid "Loti"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:122
+msgid "Lithuanian Litas"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:123
+msgid "Libyan Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:124
+msgid "Moroccan Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:125
+msgid "Moldovan Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:126
+msgid "Malagasy Ariary"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:128
+msgid "Denar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:131
+msgid "Kyat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:132
+msgid "Tugrik"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:133
+msgid "Pataca"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:134
+msgid "Ouguiya"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:135
+msgid "Mauritius Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:136
+msgid "Rufiyaa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:137
+msgid "Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:138
+msgid "Mexican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:139
+msgid "Mexican Unidad de Inversion (UDI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:140
+msgid "Malaysian Ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:141
+msgid "Mozambique Metical"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:142
+msgid "Namibia Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:143
+msgid "Naira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:144
+msgid "Cordoba Oro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:145
+msgid "Norwegian Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:146
+msgid "Nepalese Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:147
+msgid "New Zealand Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:148
+msgid "Rial Omani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:149
+msgid "Balboa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:150
+msgid "Nuevo Sol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:151
+msgid "Kina"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:152
+msgid "Philippine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:153
+msgid "Pakistan Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:154
+msgid "Zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:155
+msgid "Guarani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:156
+msgid "Qatari Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:157
+msgid "New Romanian Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:158
+msgid "Serbian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:159
+msgid "Russian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:160
+msgid "Rwanda Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:161
+msgid "Saudi Riyal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:162
+msgid "Solomon Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:163
+msgid "Seychelles Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:164
+msgid "Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:165
+msgid "Swedish Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:166
+msgid "Singapore Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:168
+msgid "Saint Helena Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:171
+msgid "Leone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:172
+msgid "Somali Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:173
+msgid "Surinam Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:174
+msgid "South Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:175
+msgid "Dobra"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:176
+msgid "El Salvador Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:177
+msgid "Syrian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:178
+msgid "Lilangeni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:179
+msgid "Baht"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:180
+msgid "Somoni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:181
+msgid "Turkmenistan New Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:182
+msgid "Tunisian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:183
+msgid "Pa'anga"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:184
+msgid "Turkish Lira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:185
+msgid "Trinidad and Tobago Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:186
+msgid "New Taiwan Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:187
+msgid "Tanzanian Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:188
+msgid "Hryvnia"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:189
+msgid "Uganda Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:190
+msgid "US Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:191
+msgid "US Dollar (Next day)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:193
+msgid "Uruguay Peso en Unidades Indexadas (URUIURUI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:196
+msgid "Peso Uruguayo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:197
+msgid "Uzbekistan Sum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:198
+msgid "Bolivar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:199
+msgid "Dong"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:200
+msgid "Vatu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:201
+msgid "Tala"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:202
+msgid "CFA Franc BEAC"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:204
+msgid "East Caribbean Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:207
+msgid "CFA Franc BCEAO"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:208
+msgid "CFP Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:209
+msgid "Yemeni Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:210
+msgid "Rand"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:211
+msgid "Zambian Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:212
+msgid "Zimbabwe Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:23
+msgid "UAE Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:24
+msgid "Afghani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:25
+msgid "Lek"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:26
+msgid "Armenian Dram"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:28
+msgid "Netherlands Antillean Guilder"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:31
+msgid "Kwanza"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:32
+msgid "Argentine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:33
+msgid "Australian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:34
+msgid "Aruban Florin"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:35
+msgid "Azerbaijanian Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:36
+msgid "Convertible Mark"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:37
+msgid "Barbados Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:38
+msgid "Taka"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:39
+msgid "Bulgarian Lev"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:40
+msgid "Bahraini Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:41
+msgid "Burundi Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:42
+msgid "Bermudian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:43
+msgid "Brunei Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:44
+msgid "Boliviano"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:45
+msgid "Mvdol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:46
+msgid "Brazilian Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:47
+msgid "Bahamian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:48
+msgid "Ngultrum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:49
+msgid "Pula"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:50
+msgid "Belarussian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:51
+msgid "Belize Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:52
+msgid "Canadian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:54
+msgid "Congolese Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:57
+msgid "WIR Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:58
+msgid "Swiss Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:59
+msgid "WIR Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:60
+msgid "Unidad de Fomento"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:61
+msgid "Chilean Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:62
+msgid "Yuan Renminbi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:63
+msgid "Colombian Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:64
+msgid "Unidad de Valor Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:65
+msgid "Costa Rican Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:66
+msgid "Peso Convertible"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:67
+msgid "Cuban Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:68
+msgid "Cabo Verde Escudo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:69
+msgid "Czech Koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:70
+msgid "Djibouti Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:71
+msgid "Danish Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:72
+msgid "Dominican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:73
+msgid "Algerian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:74
+msgid "Egyptian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:75
+msgid "-Nakfa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:76
+msgid "Ethiopian Birr"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:77
+#: waeup/ikoba/payments/paypal_currencies.py:40
+msgid "Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:78
+msgid "Fiji Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:80
+msgid "Falkland Islands Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:83
+msgid "Pound Sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:84
+msgid "Lari"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:85
+msgid "Ghana Cedi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:86
+msgid "Gibraltar Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:87
+msgid "Dalasi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:88
+msgid "Guinea Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:89
+msgid "Quetzal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:90
+msgid "Guyana Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:91
+msgid "Hong Kong Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:92
+msgid "Lempira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:93
+msgid "Croatian Kuna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:94
+msgid "Gourde"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:95
+msgid "Forint"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:96
+msgid "Rupiah"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:97
+msgid "New Israeli Sheqel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:98
+msgid "Indian Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:99
+msgid "Iraqi Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:168
+msgid "Payment State"
+msgstr "Etat de Paiement"
+
+#: waeup/ikoba/payments/interfaces.py:175
+msgid "Creation Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:181
+msgid "Payment Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:187
+msgid "Amount"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:188
+msgid "The overall sum payed, including all taxes fees, etc."
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:217
+msgid "Activation Code"
+msgstr "Code d’Activation"
+
+#: waeup/ikoba/payments/interfaces.py:224
+msgid "Response Amount Approved"
+msgstr "Réponse totale approuvée"
+
+#: waeup/ikoba/payments/interfaces.py:231
+msgid "Response Code"
+msgstr "Code Réponse"
+
+#: waeup/ikoba/payments/interfaces.py:238
+msgid "Response Description"
+msgstr "Description de Réponse"
+
+#: waeup/ikoba/payments/interfaces.py:34
+msgid "Not yet paid"
+msgstr "Non encore payé"
+
+#: waeup/ikoba/payments/interfaces.py:35
+msgid "Paid"
+msgstr "Payé"
+
+#: waeup/ikoba/payments/interfaces.py:36
+msgid "Failed"
+msgstr "Manqué"
+
+#: waeup/ikoba/payments/paypal.py:1147
+msgid "Credit Card (PayPal)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1223
+msgid "PayPal Payment"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:73
+msgid "pieces"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:74 waeup/ikoba/payments/paypal.py:1136
+msgid "license"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:10
+msgid "ANGUILLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:100
+msgid "SOUTH KOREA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:101
+msgid "KUWAIT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:102
+msgid "CAYMAN ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:103
+msgid "KAZAKHSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:104
+msgid "LAOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:105
+msgid "SAINT LUCIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:106
+msgid "LIECHTENSTEIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:107
+msgid "SRI LANKA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:108
+msgid "LESOTHO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:109
+msgid "LITHUANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:11
+msgid "ALBANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:110
+msgid "LUXEMBOURG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:111
+msgid "LATVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:112
+msgid "MOROCCO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:113
+msgid "MADAGASCAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:114
+msgid "MARSHALL ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:115
+msgid "MALI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:116
+msgid "MONGOLIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:117
+msgid "MARTINIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:118
+msgid "MAURITANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:119
+msgid "MONTSERRAT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:12
+msgid "ARMENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:120
+msgid "MALTA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:121
+msgid "MAURITIUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:122
+msgid "MALDIVES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:123
+msgid "MALAWI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:124
+msgid "MEXICO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:125
+msgid "MALAYSIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:126
+msgid "MOZAMBIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:127
+msgid "NAMIBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:128
+msgid "NEW CALEDONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:129
+msgid "NIGER"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:13
+msgid "NETHERLANDS ANTILLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:130
+msgid "NORFOLK ISLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:131
+msgid "NICARAGUA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:132
+msgid "NETHERLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:133
+msgid "NORWAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:134
+msgid "NEPAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:135
+msgid "NAURU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:136
+msgid "NIUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:137
+msgid "NEW ZEALAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:138
+msgid "OMAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:139
+msgid "PANAMA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:14
+msgid "ANGOLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:140
+msgid "PERU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:141
+msgid "FRENCH POLYNESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:142
+msgid "PAPUA NEW GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:143
+msgid "PHILIPPINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:144
+msgid "POLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:145
+msgid "SAINT PIERRE AND MIQUELON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:146
+msgid "PITCAIRN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:147
+msgid "PORTUGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:148
+msgid "PALAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:149
+msgid "QATAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:15
+msgid "ARGENTINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:150
+msgid "REUNION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:151
+msgid "ROMANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:152
+msgid "SERBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:153
+msgid "RUSSIAN FEDERATION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:154
+msgid "RWANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:155
+msgid "SAUDI ARABIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:156
+msgid "SOLOMON ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:157
+msgid "SEYCHELLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:158
+msgid "SWEDEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:159
+msgid "SINGAPORE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:16
+msgid "AUSTRIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:160
+msgid "SAINT HELENA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:161
+msgid "SLOVENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:162
+msgid "SVALBARD AND JAN MAYEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:163
+msgid "SLOVAKIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:164
+msgid "SIERRA LEONE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:165
+msgid "SAN MARINO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:166
+msgid "SENEGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:167
+msgid "SOMALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:168
+msgid "SURINAME"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:169
+msgid "SAO TOME AND PRINCIPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:17
+msgid "AUSTRALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:170
+msgid "EL SALVADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:171
+msgid "SWAZILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:172
+msgid "TURKS AND CAICOS ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:173
+msgid "CHAD"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:174
+msgid "TOGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:175
+msgid "THAILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:176
+msgid "TAJIKISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:177
+msgid "TURKMENISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:178
+msgid "TUNISIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:179
+msgid "TONGA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:18
+msgid "ARUBA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:180
+msgid "TURKEY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:181
+msgid "TRINIDAD AND TOBAGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:182
+msgid "TUVALU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:183
+msgid "TAIWAN, PROVINCE OF CHINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:184
+msgid "TANZANIA, UNITED REPUBLIC OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:185
+msgid "UKRAINE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:186
+msgid "UGANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:187
+msgid "UNITED STATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:188
+msgid "URUGUAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:189
+msgid "HOLY SEE (VATICAN CITY STATE)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:19
+msgid "AZERBAIJAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:190
+msgid "SAINT VINCENT AND THE GRENADINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:191
+msgid "VENEZUELA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:192
+msgid "VIRGIN ISLANDS, BRITISH"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:193
+msgid "VIETNAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:194
+msgid "VANUATU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:195
+msgid "WALLIS AND FUTUNA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:196
+msgid "SAMOA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:197
+msgid "YEMEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:198
+msgid "MAYOTTE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:199
+msgid "SOUTH AFRICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:20
+msgid "BOSNIA-HERZEGOVINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:200
+msgid "ZAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:21
+msgid "BARBADOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:22
+msgid "BELGIUM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:23
+msgid "BURKINA FASO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:24
+msgid "BULGARIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:25
+msgid "BAHRAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:26
+msgid "BURUNDI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:27
+msgid "BENIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:28
+msgid "BERMUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:29
+msgid "BRUNEI DARUSSALAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:30
+msgid "BOLIVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:31
+msgid "BRAZIL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:32
+msgid "BAHAMAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:33
+msgid "BHUTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:34
+msgid "BOTSWANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:35
+msgid "BELIZE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:36
+msgid "CANADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:37
+msgid "DEMOCRATIC REPUBLIC OF CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:38
+msgid "CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:39
+msgid "SWITZERLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:40
+msgid "COOK ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:41
+msgid "CHILE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:42
+msgid "CHINA (domestic Chinese bank transactions only)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:43
+msgid "CHINA (For CUP, bank card and cross-border transactions)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:44
+msgid "COLOMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:45
+msgid "COSTA RICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:46
+msgid "CAPE VERDE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:47
+msgid "CYPRUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:48
+msgid "CZECH REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:49
+msgid "GERMANY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:50
+msgid "DJIBOUTI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:51
+msgid "DENMARK"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:52
+msgid "DOMINICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:53
+msgid "DOMINICAN REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:54
+msgid "ALGERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:55
+msgid "ECUADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:56
+msgid "ESTONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:57
+msgid "EGYPT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:58
+msgid "ERITERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:59
+msgid "SPAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:60
+msgid "ETHIOPIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:61
+msgid "FINLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:62
+msgid "FIJI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:63
+msgid "FALKLAND ISLANDS (MALVINAS)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:64
+msgid "MICRONESIA, FEDERATED STATES OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:65
+msgid "FRANCE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:66
+msgid "GABON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:67
+msgid "UNITED KINGDOM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:68
+msgid "GRENADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:69
+msgid "GEORGIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:7
+msgid "ANDORRA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:70
+msgid "FRENCH GUIANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:71
+msgid "GIBRALTAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:72
+msgid "GREENLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:73
+msgid "GAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:74
+msgid "GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:75
+msgid "GUADELOUPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:76
+msgid "GREECE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:77
+msgid "GUATEMALA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:78
+msgid "GUAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:79
+msgid "GUINEA BISSAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:8
+msgid "UNITED ARAB EMIRATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:80
+msgid "GUYANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:81
+msgid "HONG KONG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:82
+msgid "HONDURAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:83
+msgid "CROATIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:84
+msgid "HUNGARY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:85
+msgid "INDONESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:86
+msgid "IRELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:87
+msgid "ISRAEL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:88
+msgid "INDIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:89
+msgid "ICELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:9
+msgid "ANTIGUA AND BARBUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:90
+msgid "ITALY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:91
+msgid "JAMAICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:92
+msgid "JORDAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:93
+msgid "JAPAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:94
+msgid "KENYA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:95
+msgid "KYRGYZSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:96
+msgid "CAMBODIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:97
+msgid "KIRIBATI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:98
+msgid "COMOROS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:99
+msgid "SAINT KITTS AND NEVIS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:34
+msgid "Australian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:35
+msgid "Brazilian real"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:36
+msgid "Canadian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:37
+msgid "Swiss franc"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:38
+msgid "Czech koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:39
+msgid "Danish krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:41
+msgid "Pound sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:42
+msgid "Hong Kong dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:43
+msgid "Hungarian forint"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:44
+msgid "Israeli new shekel"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:45
+msgid "Japanese yen"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:46
+msgid "Mexican peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:47
+msgid "Malaysian ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:48
+msgid "Norwegian krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:49
+msgid "New Zealand dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:50
+msgid "Philippine peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:51
+msgid "Polish zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:52
+msgid "Swedish krona"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:53
+msgid "Singapore dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:54
+msgid "Thai baht"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:55
+msgid "Turkish lira"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:56
+msgid "New Taiwan dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:57
+msgid "United States dollar"
+msgstr ""
+
+#: waeup/ikoba/products/batching.py:52
+msgid "Product Processor"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:126
+msgid "Create product"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/products/browser.py:137
+msgid "Product ${a} added."
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:195
+msgid "Manage product "
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:59 waeup/ikoba/products/browser.py:79
+#: waeup/ikoba/products/export.py:40 waeup/ikoba/products/viewlets.py:49
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:31
+msgid "Products"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:91
+msgid "Manage product section"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:96
+#: waeup/ikoba/products/browser.py:123
+msgid "Add product"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:22
+msgid "Setting parameters no yet configured."
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containerpage.pt:10
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:41
+msgid "Category"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:6
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:6
+msgid "Settings"
+msgstr "Paramètres"
+
+#: waeup/ikoba/products/browser_templates/productpage.pt:5
+msgid "Terms and Conditions:"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:48
+#: waeup/ikoba/products/browser_templates/containerpage.pt:8
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:37
+msgid "Product Id"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:55
+msgid "Product Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:61
+msgid "Contract Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:62
+msgid "Product title is used if empty."
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:67
+msgid "Contract Category"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:81
+msgid "Valid from"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:87
+msgid "Valid to"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:48
+msgid "Fee"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:52
+msgid "Currency"
+msgstr ""
+
+#: waeup/ikoba/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/ikoba/userscontainer.py:76
+msgid "Users"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:169 waeup/ikoba/utils/utils.py:51
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:90 waeup/ikoba/utils/utils.py:50
+msgid "Germany"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:110
+msgid "no contract"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:155
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+"Nom complet: ${a}\n"
+"Utilisateur Id: ${b}\n"
+"Type d’Itulisateur: ${c}\n"
+"Portail: ${d}\n"
+"\n"
+"${e}\n"
+
+#: waeup/ikoba/utils/utils.py:207
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Registration and Application Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:52
+msgid "U.S."
+msgstr ""
+
+#: waeup/ikoba/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/ikoba/widgets/sequencewidget.pt:15
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/ikoba/widgets/sequencewidget.pt:23
+msgid "remove-selected-items"
+msgstr ""
+
+#~ msgid "Portal Users"
+#~ msgstr "Les utilisateurs du portail"
+
+#~ msgid "Change portrait"
+#~ msgstr "Changer portrait"
+
+#~ msgid "Payment Item"
+#~ msgstr "Item de Paiement"
+
+#~ msgid "Ticket Creation Date"
+#~ msgstr "Date de Création du Ticket "
+
+#~ msgid "Amount Authorized"
+#~ msgstr "Montant Autorisé"
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/ha/LC_MESSAGES/waeup.ikoba.po
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/ha/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/ha/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
@@ -0,0 +1,4906 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.Ikoba\n"
+"POT-Creation-Date: Mon Feb  2 08:48:41 2015\n"
+"PO-Revision-Date: 2012-03-09 07:17+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Niger <isouaba@waeup.org>\n"
+"Language: ha-NG-Latn\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-Language: Hausa\n"
+"X-Poedit-Country: NIGERIA\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: waeup/ikoba/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:106
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:119 waeup/ikoba/browser/pages.py:470
+msgid "Administration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:126 waeup/ikoba/browser/viewlets.py:320
+#: waeup/ikoba/browser/viewlets.py:544
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:133 waeup/ikoba/browser/pages.py:517
+#: waeup/ikoba/browser/viewlets.py:329 waeup/ikoba/browser/viewlets.py:554
+msgid "Officers"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:140 waeup/ikoba/browser/pages.py:763
+#: waeup/ikoba/browser/pages.py:1363 waeup/ikoba/browser/viewlets.py:338
+#: waeup/ikoba/browser/viewlets.py:563
+msgid "Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:147 waeup/ikoba/browser/reports.py:37
+#: waeup/ikoba/browser/viewlets.py:347 waeup/ikoba/browser/viewlets.py:572
+msgid "Reports"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:92
+msgid "Home"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:108
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/ikoba/browser/templates/datacenterpage.pt:17
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:109
+msgid "Scan"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:129
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:130
+msgid "Delete"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:164
+msgid "No local file selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:44
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:59
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:71
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:76
+msgid "${a} file format expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:82
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:92
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:173
+#: waeup/ikoba/customers/interfaces.py:200 waeup/ikoba/interfaces.py:428
+#: waeup/ikoba/interfaces.py:504
+msgid "Email Address"
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:176
+msgid "Enter your email address stored in Ikoba."
+msgstr ""
+
+#: waeup/ikoba/browser/layout.py:102 waeup/ikoba/customers/browser.py:387
+#: waeup/ikoba/customers/browser.py:690 waeup/ikoba/customers/browser.py:973
+#: waeup/ikoba/customers/browser.py:1385 waeup/ikoba/customers/browser.py:1416
+#: waeup/ikoba/documents/browser.py:259
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1039 waeup/ikoba/browser/pages.py:1068
+#: waeup/ikoba/browser/pages.py:1173 waeup/ikoba/browser/pages.py:1285
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1071
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1072
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1117
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:112
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1125
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1138 waeup/ikoba/browser/pages.py:1261
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1150
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1176 waeup/ikoba/browser/pages.py:1404
+msgid "Reset"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1177
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1178
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1179
+msgid "Perform import"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1244
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:125 waeup/ikoba/browser/pages.py:772
+#: waeup/ikoba/customers/browser.py:207
+msgid "No item selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1287
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1337
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1339
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1347
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1350
+msgid "Show"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1365 waeup/ikoba/browser/viewlets.py:185
+msgid "Search"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1367
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1375
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1390
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1393
+msgid "No search results found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1401
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1421
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1424
+msgid "Given storage path cannot be used: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1428
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1440
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1442
+msgid "Create CSV file"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:146 waeup/ikoba/customers/browser.py:220
+#: waeup/ikoba/customers/browser.py:883 waeup/ikoba/customers/browser.py:1253
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1470
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1514
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1540 waeup/ikoba/customers/browser.py:543
+#: waeup/ikoba/customers/browser.py:625
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1550
+msgid "No record found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1564 waeup/ikoba/customers/browser.py:589
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1568
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:188 waeup/ikoba/customers/browser.py:84
+#: waeup/ikoba/customers/browser.py:422
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:259 waeup/ikoba/browser/viewlets.py:266
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:40
+#: waeup/ikoba/browser/templates/customersitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:270
+msgid "You logged in."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:307
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:315
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:318
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:333
+msgid "You have been logged out. Thanks for using WAeUP Ikoba!"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:375
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:381 waeup/ikoba/browser/pages.py:661
+msgid "Send message now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:400 waeup/ikoba/browser/pages.py:433
+#: waeup/ikoba/browser/pages.py:678 waeup/ikoba/customers/browser.py:372
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:420
+msgid "Send now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:435
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:460
+msgid "<h1>Welcome to WAeUP.Ikoba</h1>"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:518 waeup/ikoba/customers/viewlets.py:207
+#: waeup/ikoba/customers/viewlets.py:332 waeup/ikoba/customers/viewlets.py:404
+#: waeup/ikoba/documents/viewlets.py:60 waeup/ikoba/documents/viewlets.py:79
+#: waeup/ikoba/products/viewlets.py:60 waeup/ikoba/products/viewlets.py:79
+msgid "Manage"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:519 waeup/ikoba/browser/viewlets.py:175
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:526
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:563 waeup/ikoba/browser/pages.py:565
+#: waeup/ikoba/browser/viewlets.py:403
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:587
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:608
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:616 waeup/ikoba/browser/pages.py:737
+#: waeup/ikoba/browser/pages.py:1403 waeup/ikoba/customers/browser.py:400
+#: waeup/ikoba/customers/browser.py:732 waeup/ikoba/customers/browser.py:774
+#: waeup/ikoba/customers/browser.py:990 waeup/ikoba/customers/browser.py:1008
+#: waeup/ikoba/customers/browser.py:1402 waeup/ikoba/customers/browser.py:1426
+#: waeup/ikoba/customers/browser.py:1443 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:269 waeup/ikoba/documents/browser.py:293
+#: waeup/ikoba/documents/browser.py:308 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:106 waeup/ikoba/products/browser.py:184
+#: waeup/ikoba/products/browser.py:197
+msgid "Save"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:642
+msgid "User settings have been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:645 waeup/ikoba/browser/pages.py:1070
+#: waeup/ikoba/browser/pages.py:1175 waeup/ikoba/customers/browser.py:759
+#: waeup/ikoba/customers/browser.py:790 waeup/ikoba/customers/browser.py:946
+#: waeup/ikoba/customers/browser.py:1312 waeup/ikoba/documents/browser.py:106
+#: waeup/ikoba/documents/browser.py:152 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:273 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:110
+#: waeup/ikoba/products/browser.py:184 waeup/ikoba/products/browser.py:206
+msgid "Cancel"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:659 waeup/ikoba/customers/browser.py:353
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:688 waeup/ikoba/browser/viewlets.py:354
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:701 waeup/ikoba/browser/viewlets.py:370
+msgid "My Roles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:733
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:745
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:746
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:747
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:752
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:766 waeup/ikoba/customers/browser.py:176
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:101
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:783
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:787
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:798
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:800 waeup/ikoba/browser/pages.py:816
+#: waeup/ikoba/browser/pages.py:1041 waeup/ikoba/browser/pages.py:1349
+#: waeup/ikoba/browser/pages.py:1366 waeup/ikoba/browser/pages.py:1405
+#: waeup/ikoba/browser/pages.py:1443
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:812
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:815
+msgid "Upload"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:855
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:857
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:861
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:880
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:882 waeup/ikoba/browser/pages.py:1571
+#: waeup/ikoba/customers/browser.py:374
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:890
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:905
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:91
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:913
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:924
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:207 waeup/ikoba/browser/pdf.py:472
+#: waeup/ikoba/browser/pdf.py:482
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:16
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:754
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/ikoba/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data. Please pick the "
+"type of objects you want to export from the selection below. The file will "
+"be generated and then be made available for you to download in the table "
+"below. Files can only be downloaded within 24 hours after generation. The "
+"download link disappears afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:42
+msgid "Export Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:43
+msgid "Exporter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:44
+msgid "Filter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:45
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:6
+msgid "Creator"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:46
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:7
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:47
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:8
+msgid "Status"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:72
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:39
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/ikoba/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:14
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/ikoba/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:14
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:13
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/filedisplay.pt:15
+msgid "HTML Element:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:13
+msgid "Password"
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/ikoba/browser/templates/loginpage.pt:26
+msgid "login_trouble1"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer, or your customer record has been created for you? Acquire a Password Activation Code (PWD) and inititialize your customer account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:29
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer? Create your customer account <strong><a href=\"createaccount\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:34
+msgid "login_trouble4"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer and your customer record has been created for you? Inititialize your customer account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:38
+msgid "login_trouble5"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:4
+msgid "Identifier"
+msgstr ""
+
+#. Default: "Or simply forgot your id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:42
+msgid "login_trouble3"
+msgstr ""
+
+#. Default: "Customers can enter either their customer id, registration number or email address. Officers must enter their user name."
+#: waeup/ikoba/browser/templates/loginpage.pt:7
+msgid "identifier_hint"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:46
+#: waeup/ikoba/browser/templates/customersitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:52
+#: waeup/ikoba/browser/templates/customersitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:6
+#: waeup/ikoba/browser/templates/customersitelayout.pt:4
+msgid "WAeUP.Ikoba - Registration and Application System"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:15
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:16
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:30
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:9
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:27
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:10
+#: waeup/ikoba/customers/browser_templates/basepage.pt:14
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:24
+msgid "Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:165
+msgid "Add"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:305
+msgid "Any link text"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:387 waeup/ikoba/customers/viewlets.py:198
+msgid "Send email"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:396
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:412
+msgid "Show logs"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:421
+msgid "Upload data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:430
+msgid "Process data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:439
+msgid "Export data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:448
+msgid "View processed files"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:508
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:93 waeup/ikoba/browser/viewlets.py:155
+#: waeup/ikoba/customers/viewlets.py:284 waeup/ikoba/customers/viewlets.py:347
+#: waeup/ikoba/customers/viewlets.py:422
+msgid "Edit"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:142
+msgid "Customer record created"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/batching.py:197
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:461
+msgid "Customer Sample Document Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:562
+msgid "Customer Sample Contract Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:60
+msgid "Customer Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1013
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1017 waeup/ikoba/customers/browser.py:1452
+msgid "Form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1029 waeup/ikoba/documents/browser.py:324
+msgid "Trigger document transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:106 waeup/ikoba/customers/export.py:130
+#: waeup/ikoba/customers/viewlets.py:54
+msgid "Customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1077
+msgid "Documents of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1085
+msgid "Customer Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1091
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:8
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:9
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:4
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:8
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:8
+msgid "Id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1092 waeup/ikoba/customers/browser.py:1527
+#: waeup/ikoba/products/productoptions.py:44
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:9
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:9
+#: waeup/ikoba/products/browser_templates/containerpage.pt:9
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:39
+msgid "Title"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1093 waeup/ikoba/customers/browser.py:1528
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:10
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:10
+msgid "Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1094 waeup/ikoba/customers/browser.py:1529
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1095 waeup/ikoba/customers/browser.py:1530
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:12
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:12
+msgid "Last Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1181 waeup/ikoba/customers/viewlets.py:160
+msgid "Contracts"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:1226
+msgid "${a}: Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1229 waeup/ikoba/customers/browser.py:1270
+#: waeup/ikoba/customers/browser.py:1295
+msgid "Add contract"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1234
+msgid "Remove selected contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1240
+msgid "No contract selected."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:132
+msgid "Find customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1325
+msgid "Select product"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:133 waeup/ikoba/customers/browser.py:175
+msgid "Find customer(s)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1336
+msgid "Save and proceed"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1448
+msgid "Apply now (final submit)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1464
+msgid "Trigger contract transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:150 waeup/ikoba/customers/browser.py:192
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1512
+msgid "Contracts of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1520
+msgid "Customer Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:156 waeup/ikoba/customers/browser.py:198
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:162 waeup/ikoba/customers/browser.py:205
+#: waeup/ikoba/customers/browser.py:563 waeup/ikoba/customers/browser.py:575
+msgid "No customer found."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:174 waeup/ikoba/customers/viewlets.py:180
+msgid "Manage customer section"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:233 waeup/ikoba/customers/viewlets.py:188
+msgid "Add customer"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:236
+msgid "Create customer record"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:241
+msgid "Customer created."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:256
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:279
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:283
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:289
+msgid "You successfully logged in as customer."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:312
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:314
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:320
+msgid "set"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:321
+msgid "unset"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:385
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:435
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:447 waeup/ikoba/customers/browser.py:1044
+#: waeup/ikoba/customers/browser.py:1479 waeup/ikoba/documents/browser.py:339
+msgid "No transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:450 waeup/ikoba/customers/browser.py:1047
+#: waeup/ikoba/customers/browser.py:1482
+msgid "Apply"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:457 waeup/ikoba/customers/browser.py:1055
+#: waeup/ikoba/customers/browser.py:1490 waeup/ikoba/documents/browser.py:350
+msgid "Transition '%s' executed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:474
+msgid "Customer account has been activated."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:494
+msgid "Customer account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:513
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:523
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:557
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:566
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:610
+msgid "Create customer account"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:64
+msgid ""
+"You can not edit some data after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:648
+msgid "You have successfully created a customer account for the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:669
+msgid "Your request was successful."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:67
+msgid ""
+"You can not edit your document after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:688 waeup/ikoba/customers/viewlets.py:303
+msgid "Upload files"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:70
+msgid ""
+"You can not edit your contract after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:721
+msgid "Edit base data and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:722
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:741
+msgid "Passport picture is missing."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:746
+msgid "Save and request registration now"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:755
+msgid "Registration form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:771 waeup/ikoba/customers/viewlets.py:293
+msgid "Change password"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:785
+msgid "Password changed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:816 waeup/ikoba/customers/viewlets.py:166
+#: waeup/ikoba/documents/browser.py:62 waeup/ikoba/documents/browser.py:82
+#: waeup/ikoba/documents/viewlets.py:49
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:3
+msgid "Documents"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:856
+msgid "${a}: Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:859 waeup/ikoba/customers/browser.py:900
+#: waeup/ikoba/customers/browser.py:924 waeup/ikoba/documents/browser.py:96
+#: waeup/ikoba/documents/browser.py:119 waeup/ikoba/documents/browser.py:129
+msgid "Add document"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:864 waeup/ikoba/documents/browser.py:101
+msgid "Remove selected documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:870
+msgid "No document selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:935 waeup/ikoba/customers/browser.py:1306
+#: waeup/ikoba/documents/browser.py:144
+msgid "${a} added."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:94
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:56
+msgid "Select all"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:10
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:11
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:12
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:12
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:23
+msgid "Search Results"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:32
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:9
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractaddpage.pt:7
+msgid "Contract Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:20
+msgid "Contract Type:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:19
+msgid ""
+"A customer record will be created and your login credentials will be sent to "
+"the email address given above. To be able to proceed you must provide a "
+"valid email address!"
+msgstr ""
+
+#. Default: "Ikoba is creating a temporary customer password which will replace the original password chosen by the customer. The password will be valid for ${minutes} minutes. The customer can't login during this time period. After expiration the customer can login with the original password again."
+#: waeup/ikoba/customers/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/ikoba/customers/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:19
+msgid ""
+"Your customer record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:8
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:14
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:44
+msgid "Thanks for using Ikoba!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:3
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:147
+#: waeup/ikoba/customers/documents.py:162
+msgid "Customer has not yet been approved."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:153
+msgid "Attached documents must be verified first."
+msgstr ""
+
+#: waeup/ikoba/customers/documents.py:165
+msgid "No file uploaded."
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:194
+msgid "Customer Sample Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:246
+msgid "Customer Sample Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:142
+#: waeup/ikoba/customers/fileviewlets.py:143
+#: waeup/ikoba/documents/fileviewlets.py:39
+#: waeup/ikoba/documents/fileviewlets.py:40
+msgid "PDF File"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:167
+#: waeup/ikoba/customers/fileviewlets.py:168
+#: waeup/ikoba/documents/fileviewlets.py:57
+#: waeup/ikoba/documents/fileviewlets.py:58
+msgid "PDF Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:48
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:60
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:90
+#: waeup/ikoba/customers/fileviewlets.py:91
+#: waeup/ikoba/customers/fileviewlets.py:115
+#: waeup/ikoba/customers/fileviewlets.py:116
+msgid "Sample Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:101
+#: waeup/ikoba/customers/interfaces.py:195
+#: waeup/ikoba/customers/interfaces.py:213
+msgid "First Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:106
+#: waeup/ikoba/customers/interfaces.py:218
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:111
+#: waeup/ikoba/customers/interfaces.py:223
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:116
+msgid "Sex"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:122
+#: waeup/ikoba/customers/interfaces.py:161
+#: waeup/ikoba/customers/interfaces.py:190
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:129
+#: waeup/ikoba/customers/interfaces.py:228
+msgid "Email"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:136 waeup/ikoba/interfaces.py:433
+#: waeup/ikoba/interfaces.py:511
+msgid "Phone"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:252
+#: waeup/ikoba/documents/interfaces.py:52
+msgid "Document Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:303
+msgid "Contract Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:309
+#: waeup/ikoba/customers/interfaces.py:332
+msgid "Product"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:315
+#: waeup/ikoba/customers/interfaces.py:365
+#: waeup/ikoba/products/interfaces.py:73
+msgid "Options/Fees"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:344
+#: waeup/ikoba/customers/interfaces.py:379
+msgid "Document"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:355
+msgid "Comment"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:82
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:88
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:90
+msgid ""
+"This message will be shown if and only if deactivated customers try to login."
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:96
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:31
+msgid "Customer Id"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:161
+msgid "Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:180
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:240
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:25
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:30
+msgid "Connected Files"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:301 waeup/ikoba/customers/utils.py:309
+#: waeup/ikoba/customers/utils.py:317 waeup/ikoba/documents/utils.py:32
+msgid "created"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:302
+msgid "started"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:303
+msgid "requested"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:304 waeup/ikoba/customers/utils.py:311
+msgid "approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:305
+msgid "provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:310
+msgid "submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:312 waeup/ikoba/customers/utils.py:320
+msgid "rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:313 waeup/ikoba/customers/utils.py:321
+msgid "expired"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:318
+msgid "submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:319
+msgid "verified"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:325
+msgid "Sample Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:326 waeup/ikoba/documents/utils.py:37
+msgid "PDF Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:330
+msgid "Sample Contract"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:374
+msgid "Contractor"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:377
+msgid "Document Owner"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:386
+msgid "Contract Data"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:401
+msgid "Transaction History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:407
+msgid "Registration History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:425 waeup/ikoba/products/interfaces.py:95
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:32
+msgid "Terms and Conditions"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:114
+msgid "My Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:115
+msgid "My Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:116 waeup/ikoba/customers/viewlets.py:172
+msgid "History"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:137 waeup/ikoba/customers/viewlets.py:154
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:217 waeup/ikoba/customers/viewlets.py:363
+#: waeup/ikoba/customers/viewlets.py:438 waeup/ikoba/documents/viewlets.py:89
+msgid "Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:227
+msgid "Login as customer"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:236
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:248 waeup/ikoba/customers/viewlets.py:269
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:257
+msgid "Activate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:283
+msgid "Edit and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:323
+msgid "Download documents overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:372 waeup/ikoba/customers/viewlets.py:447
+#: waeup/ikoba/documents/viewlets.py:69 waeup/ikoba/products/viewlets.py:69
+msgid "View"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:383
+msgid "Download document slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:395
+msgid "Download contracts overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:413
+msgid "Manage official data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:462
+msgid "Download contract slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:98
+msgid "My Data"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:58
+msgid "male"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:60
+msgid "female"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:103 waeup/ikoba/customers/workflow.py:124
+msgid "Reset to requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:104 waeup/ikoba/customers/workflow.py:125
+msgid "Reset to 'requested'"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:111 waeup/ikoba/customers/workflow.py:209
+#: waeup/ikoba/customers/workflow.py:210 waeup/ikoba/customers/workflow.py:216
+#: waeup/ikoba/customers/workflow.py:217 waeup/ikoba/customers/workflow.py:223
+#: waeup/ikoba/customers/workflow.py:224 waeup/ikoba/customers/workflow.py:237
+#: waeup/ikoba/customers/workflow.py:238 waeup/ikoba/customers/workflow.py:324
+#: waeup/ikoba/customers/workflow.py:325 waeup/ikoba/customers/workflow.py:331
+#: waeup/ikoba/customers/workflow.py:332 waeup/ikoba/customers/workflow.py:338
+#: waeup/ikoba/customers/workflow.py:339 waeup/ikoba/customers/workflow.py:352
+#: waeup/ikoba/customers/workflow.py:353
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:180
+msgid "Create contract"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:183
+msgid "Contract created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:188
+msgid "Submit for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:189
+msgid "Submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:195
+msgid "Approve"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:196
+msgid "Approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:202 waeup/ikoba/customers/workflow.py:317
+msgid "Reject"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:203 waeup/ikoba/customers/workflow.py:318
+msgid "REJECTED"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:230 waeup/ikoba/customers/workflow.py:231
+#: waeup/ikoba/customers/workflow.py:345 waeup/ikoba/customers/workflow.py:346
+msgid "Set to expired"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:295 waeup/ikoba/documents/workflow.py:37
+msgid "Create document"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:298 waeup/ikoba/documents/workflow.py:40
+msgid "Document created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:303
+msgid "Submit for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:304
+msgid "Submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:310
+msgid "Verify"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:311
+msgid "Verified"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:45
+msgid "Create customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:48
+msgid "Customer created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:53
+msgid "Start registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:56
+msgid "Customer registration started"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:61
+msgid "Request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:62
+msgid "Customer registration requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:68
+msgid "Approve customer provisionally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:69
+msgid "Customer registration provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:75
+msgid "Approve finally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:76
+msgid "Customer registration finally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:82
+msgid "Approve customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:83
+msgid "Customer registration approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:89
+msgid "Reject customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:90
+msgid "Customer registration rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:96 waeup/ikoba/customers/workflow.py:110
+#: waeup/ikoba/customers/workflow.py:117
+msgid "Reset customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:97 waeup/ikoba/customers/workflow.py:118
+msgid "Reset to initial customer state"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:135
+msgid "Public PDF Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:147
+msgid "Public HTML Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:159
+msgid "Public REST Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:140 waeup/ikoba/products/browser.py:133
+msgid "The id chosen already exists."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:210
+#: waeup/ikoba/documents/fileviewlets.py:80
+msgid "The document requested has not yet been published."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:256 waeup/ikoba/documents/browser.py:282
+#: waeup/ikoba/products/browser.py:185 waeup/ikoba/products/browser.py:215
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:257 waeup/ikoba/documents/browser.py:278
+#: waeup/ikoba/products/browser.py:186 waeup/ikoba/products/browser.py:211
+msgid "Add local role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:267
+msgid "Manage document "
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:342
+msgid "Apply now"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:94
+msgid "Manage document section"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:1
+#: waeup/ikoba/products/browser_templates/containerpage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentaddform.pt:7
+#: waeup/ikoba/customers/browser_templates/documentaddpage.pt:7
+msgid "Document Type"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:12
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:12
+msgid "Files"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:16
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:16
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:21
+msgid "Document Type:"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:37
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:48
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:5
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:37
+msgid "Name"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:39
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:50
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:39
+msgid "Local Role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:6
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:6
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:7
+msgid "Metadata"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:9
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:12
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:7
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:9
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:25
+msgid "Content:"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:101
+msgid "Public HTML Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:110
+msgid "Public REST Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:92
+msgid "Public PDF Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:110
+msgid "Multilingual content in REST (reStructuredText) format"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:58
+msgid "Document Title"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:78
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:97
+#: waeup/ikoba/products/interfaces.py:97
+msgid "Multilingual content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:33
+msgid "published"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:38
+msgid "HTML Document"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:39
+msgid "REST Document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:45
+msgid "Publish document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:48
+msgid "Document published"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:53
+msgid "Retract documet"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:56
+msgid "Document retracted"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:361
+msgid "Processor name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:387
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:394
+msgid "Email to:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:401
+msgid "Subject:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:405
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:409
+msgid "Text:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:438 waeup/ikoba/interfaces.py:494
+msgid "Public Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:448 waeup/ikoba/interfaces.py:449
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:455
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:456
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:485
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:35
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:46
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:35
+msgid "User Id"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:490
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:500
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:517
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:564
+msgid "Name of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:565
+msgid "Sample Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:570
+msgid "Abbreviated Title of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:576
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:588
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:594
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:601
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:602
+msgid "Ikoba Contact"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:607
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:614
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:66
+msgid "new"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:67
+msgid "completed"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:68
+msgid "pending"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:69
+msgid "active"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:70
+msgid "assigned"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:71
+msgid "callbacks"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/ikoba/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:100
+msgid "Iranian Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:101
+msgid "Iceland Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:102
+msgid "Jamaican Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:103
+msgid "Jordanian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:104
+msgid "Yen"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:105
+msgid "Kenyan Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:106
+msgid "Som"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:107
+msgid "Riel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:108
+msgid "Comoro Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:110
+msgid "North Korean Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:113
+msgid "Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:114
+msgid "Kuwaiti Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:115
+msgid "Cayman Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:116
+msgid "Tenge"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:117
+msgid "Kip"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:118
+msgid "Lebanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:119
+msgid "Sri Lanka Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:120
+msgid "Liberian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:121
+msgid "Loti"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:122
+msgid "Lithuanian Litas"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:123
+msgid "Libyan Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:124
+msgid "Moroccan Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:125
+msgid "Moldovan Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:126
+msgid "Malagasy Ariary"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:128
+msgid "Denar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:131
+msgid "Kyat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:132
+msgid "Tugrik"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:133
+msgid "Pataca"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:134
+msgid "Ouguiya"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:135
+msgid "Mauritius Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:136
+msgid "Rufiyaa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:137
+msgid "Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:138
+msgid "Mexican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:139
+msgid "Mexican Unidad de Inversion (UDI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:140
+msgid "Malaysian Ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:141
+msgid "Mozambique Metical"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:142
+msgid "Namibia Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:143
+msgid "Naira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:144
+msgid "Cordoba Oro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:145
+msgid "Norwegian Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:146
+msgid "Nepalese Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:147
+msgid "New Zealand Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:148
+msgid "Rial Omani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:149
+msgid "Balboa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:150
+msgid "Nuevo Sol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:151
+msgid "Kina"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:152
+msgid "Philippine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:153
+msgid "Pakistan Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:154
+msgid "Zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:155
+msgid "Guarani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:156
+msgid "Qatari Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:157
+msgid "New Romanian Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:158
+msgid "Serbian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:159
+msgid "Russian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:160
+msgid "Rwanda Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:161
+msgid "Saudi Riyal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:162
+msgid "Solomon Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:163
+msgid "Seychelles Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:164
+msgid "Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:165
+msgid "Swedish Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:166
+msgid "Singapore Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:168
+msgid "Saint Helena Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:171
+msgid "Leone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:172
+msgid "Somali Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:173
+msgid "Surinam Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:174
+msgid "South Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:175
+msgid "Dobra"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:176
+msgid "El Salvador Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:177
+msgid "Syrian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:178
+msgid "Lilangeni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:179
+msgid "Baht"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:180
+msgid "Somoni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:181
+msgid "Turkmenistan New Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:182
+msgid "Tunisian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:183
+msgid "Pa'anga"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:184
+msgid "Turkish Lira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:185
+msgid "Trinidad and Tobago Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:186
+msgid "New Taiwan Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:187
+msgid "Tanzanian Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:188
+msgid "Hryvnia"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:189
+msgid "Uganda Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:190
+msgid "US Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:191
+msgid "US Dollar (Next day)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:193
+msgid "Uruguay Peso en Unidades Indexadas (URUIURUI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:196
+msgid "Peso Uruguayo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:197
+msgid "Uzbekistan Sum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:198
+msgid "Bolivar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:199
+msgid "Dong"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:200
+msgid "Vatu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:201
+msgid "Tala"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:202
+msgid "CFA Franc BEAC"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:204
+msgid "East Caribbean Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:207
+msgid "CFA Franc BCEAO"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:208
+msgid "CFP Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:209
+msgid "Yemeni Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:210
+msgid "Rand"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:211
+msgid "Zambian Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:212
+msgid "Zimbabwe Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:23
+msgid "UAE Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:24
+msgid "Afghani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:25
+msgid "Lek"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:26
+msgid "Armenian Dram"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:28
+msgid "Netherlands Antillean Guilder"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:31
+msgid "Kwanza"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:32
+msgid "Argentine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:33
+msgid "Australian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:34
+msgid "Aruban Florin"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:35
+msgid "Azerbaijanian Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:36
+msgid "Convertible Mark"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:37
+msgid "Barbados Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:38
+msgid "Taka"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:39
+msgid "Bulgarian Lev"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:40
+msgid "Bahraini Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:41
+msgid "Burundi Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:42
+msgid "Bermudian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:43
+msgid "Brunei Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:44
+msgid "Boliviano"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:45
+msgid "Mvdol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:46
+msgid "Brazilian Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:47
+msgid "Bahamian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:48
+msgid "Ngultrum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:49
+msgid "Pula"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:50
+msgid "Belarussian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:51
+msgid "Belize Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:52
+msgid "Canadian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:54
+msgid "Congolese Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:57
+msgid "WIR Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:58
+msgid "Swiss Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:59
+msgid "WIR Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:60
+msgid "Unidad de Fomento"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:61
+msgid "Chilean Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:62
+msgid "Yuan Renminbi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:63
+msgid "Colombian Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:64
+msgid "Unidad de Valor Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:65
+msgid "Costa Rican Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:66
+msgid "Peso Convertible"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:67
+msgid "Cuban Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:68
+msgid "Cabo Verde Escudo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:69
+msgid "Czech Koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:70
+msgid "Djibouti Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:71
+msgid "Danish Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:72
+msgid "Dominican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:73
+msgid "Algerian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:74
+msgid "Egyptian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:75
+msgid "-Nakfa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:76
+msgid "Ethiopian Birr"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:77
+#: waeup/ikoba/payments/paypal_currencies.py:40
+msgid "Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:78
+msgid "Fiji Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:80
+msgid "Falkland Islands Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:83
+msgid "Pound Sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:84
+msgid "Lari"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:85
+msgid "Ghana Cedi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:86
+msgid "Gibraltar Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:87
+msgid "Dalasi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:88
+msgid "Guinea Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:89
+msgid "Quetzal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:90
+msgid "Guyana Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:91
+msgid "Hong Kong Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:92
+msgid "Lempira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:93
+msgid "Croatian Kuna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:94
+msgid "Gourde"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:95
+msgid "Forint"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:96
+msgid "Rupiah"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:97
+msgid "New Israeli Sheqel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:98
+msgid "Indian Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:99
+msgid "Iraqi Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:168
+msgid "Payment State"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:175
+msgid "Creation Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:181
+msgid "Payment Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:187
+msgid "Amount"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:188
+msgid "The overall sum payed, including all taxes fees, etc."
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:217
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:224
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:231
+msgid "Response Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:238
+msgid "Response Description"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:34
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:35
+msgid "Paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:36
+msgid "Failed"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1147
+msgid "Credit Card (PayPal)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1223
+msgid "PayPal Payment"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:73
+msgid "pieces"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:74 waeup/ikoba/payments/paypal.py:1136
+msgid "license"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:10
+msgid "ANGUILLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:100
+msgid "SOUTH KOREA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:101
+msgid "KUWAIT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:102
+msgid "CAYMAN ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:103
+msgid "KAZAKHSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:104
+msgid "LAOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:105
+msgid "SAINT LUCIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:106
+msgid "LIECHTENSTEIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:107
+msgid "SRI LANKA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:108
+msgid "LESOTHO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:109
+msgid "LITHUANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:11
+msgid "ALBANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:110
+msgid "LUXEMBOURG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:111
+msgid "LATVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:112
+msgid "MOROCCO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:113
+msgid "MADAGASCAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:114
+msgid "MARSHALL ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:115
+msgid "MALI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:116
+msgid "MONGOLIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:117
+msgid "MARTINIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:118
+msgid "MAURITANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:119
+msgid "MONTSERRAT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:12
+msgid "ARMENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:120
+msgid "MALTA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:121
+msgid "MAURITIUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:122
+msgid "MALDIVES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:123
+msgid "MALAWI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:124
+msgid "MEXICO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:125
+msgid "MALAYSIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:126
+msgid "MOZAMBIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:127
+msgid "NAMIBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:128
+msgid "NEW CALEDONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:129
+msgid "NIGER"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:13
+msgid "NETHERLANDS ANTILLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:130
+msgid "NORFOLK ISLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:131
+msgid "NICARAGUA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:132
+msgid "NETHERLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:133
+msgid "NORWAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:134
+msgid "NEPAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:135
+msgid "NAURU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:136
+msgid "NIUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:137
+msgid "NEW ZEALAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:138
+msgid "OMAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:139
+msgid "PANAMA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:14
+msgid "ANGOLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:140
+msgid "PERU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:141
+msgid "FRENCH POLYNESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:142
+msgid "PAPUA NEW GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:143
+msgid "PHILIPPINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:144
+msgid "POLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:145
+msgid "SAINT PIERRE AND MIQUELON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:146
+msgid "PITCAIRN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:147
+msgid "PORTUGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:148
+msgid "PALAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:149
+msgid "QATAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:15
+msgid "ARGENTINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:150
+msgid "REUNION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:151
+msgid "ROMANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:152
+msgid "SERBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:153
+msgid "RUSSIAN FEDERATION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:154
+msgid "RWANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:155
+msgid "SAUDI ARABIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:156
+msgid "SOLOMON ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:157
+msgid "SEYCHELLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:158
+msgid "SWEDEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:159
+msgid "SINGAPORE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:16
+msgid "AUSTRIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:160
+msgid "SAINT HELENA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:161
+msgid "SLOVENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:162
+msgid "SVALBARD AND JAN MAYEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:163
+msgid "SLOVAKIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:164
+msgid "SIERRA LEONE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:165
+msgid "SAN MARINO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:166
+msgid "SENEGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:167
+msgid "SOMALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:168
+msgid "SURINAME"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:169
+msgid "SAO TOME AND PRINCIPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:17
+msgid "AUSTRALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:170
+msgid "EL SALVADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:171
+msgid "SWAZILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:172
+msgid "TURKS AND CAICOS ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:173
+msgid "CHAD"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:174
+msgid "TOGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:175
+msgid "THAILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:176
+msgid "TAJIKISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:177
+msgid "TURKMENISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:178
+msgid "TUNISIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:179
+msgid "TONGA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:18
+msgid "ARUBA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:180
+msgid "TURKEY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:181
+msgid "TRINIDAD AND TOBAGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:182
+msgid "TUVALU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:183
+msgid "TAIWAN, PROVINCE OF CHINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:184
+msgid "TANZANIA, UNITED REPUBLIC OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:185
+msgid "UKRAINE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:186
+msgid "UGANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:187
+msgid "UNITED STATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:188
+msgid "URUGUAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:189
+msgid "HOLY SEE (VATICAN CITY STATE)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:19
+msgid "AZERBAIJAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:190
+msgid "SAINT VINCENT AND THE GRENADINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:191
+msgid "VENEZUELA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:192
+msgid "VIRGIN ISLANDS, BRITISH"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:193
+msgid "VIETNAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:194
+msgid "VANUATU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:195
+msgid "WALLIS AND FUTUNA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:196
+msgid "SAMOA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:197
+msgid "YEMEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:198
+msgid "MAYOTTE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:199
+msgid "SOUTH AFRICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:20
+msgid "BOSNIA-HERZEGOVINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:200
+msgid "ZAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:21
+msgid "BARBADOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:22
+msgid "BELGIUM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:23
+msgid "BURKINA FASO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:24
+msgid "BULGARIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:25
+msgid "BAHRAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:26
+msgid "BURUNDI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:27
+msgid "BENIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:28
+msgid "BERMUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:29
+msgid "BRUNEI DARUSSALAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:30
+msgid "BOLIVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:31
+msgid "BRAZIL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:32
+msgid "BAHAMAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:33
+msgid "BHUTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:34
+msgid "BOTSWANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:35
+msgid "BELIZE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:36
+msgid "CANADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:37
+msgid "DEMOCRATIC REPUBLIC OF CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:38
+msgid "CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:39
+msgid "SWITZERLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:40
+msgid "COOK ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:41
+msgid "CHILE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:42
+msgid "CHINA (domestic Chinese bank transactions only)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:43
+msgid "CHINA (For CUP, bank card and cross-border transactions)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:44
+msgid "COLOMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:45
+msgid "COSTA RICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:46
+msgid "CAPE VERDE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:47
+msgid "CYPRUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:48
+msgid "CZECH REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:49
+msgid "GERMANY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:50
+msgid "DJIBOUTI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:51
+msgid "DENMARK"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:52
+msgid "DOMINICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:53
+msgid "DOMINICAN REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:54
+msgid "ALGERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:55
+msgid "ECUADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:56
+msgid "ESTONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:57
+msgid "EGYPT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:58
+msgid "ERITERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:59
+msgid "SPAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:60
+msgid "ETHIOPIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:61
+msgid "FINLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:62
+msgid "FIJI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:63
+msgid "FALKLAND ISLANDS (MALVINAS)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:64
+msgid "MICRONESIA, FEDERATED STATES OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:65
+msgid "FRANCE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:66
+msgid "GABON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:67
+msgid "UNITED KINGDOM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:68
+msgid "GRENADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:69
+msgid "GEORGIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:7
+msgid "ANDORRA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:70
+msgid "FRENCH GUIANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:71
+msgid "GIBRALTAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:72
+msgid "GREENLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:73
+msgid "GAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:74
+msgid "GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:75
+msgid "GUADELOUPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:76
+msgid "GREECE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:77
+msgid "GUATEMALA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:78
+msgid "GUAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:79
+msgid "GUINEA BISSAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:8
+msgid "UNITED ARAB EMIRATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:80
+msgid "GUYANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:81
+msgid "HONG KONG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:82
+msgid "HONDURAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:83
+msgid "CROATIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:84
+msgid "HUNGARY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:85
+msgid "INDONESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:86
+msgid "IRELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:87
+msgid "ISRAEL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:88
+msgid "INDIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:89
+msgid "ICELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:9
+msgid "ANTIGUA AND BARBUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:90
+msgid "ITALY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:91
+msgid "JAMAICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:92
+msgid "JORDAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:93
+msgid "JAPAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:94
+msgid "KENYA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:95
+msgid "KYRGYZSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:96
+msgid "CAMBODIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:97
+msgid "KIRIBATI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:98
+msgid "COMOROS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:99
+msgid "SAINT KITTS AND NEVIS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:34
+msgid "Australian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:35
+msgid "Brazilian real"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:36
+msgid "Canadian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:37
+msgid "Swiss franc"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:38
+msgid "Czech koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:39
+msgid "Danish krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:41
+msgid "Pound sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:42
+msgid "Hong Kong dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:43
+msgid "Hungarian forint"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:44
+msgid "Israeli new shekel"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:45
+msgid "Japanese yen"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:46
+msgid "Mexican peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:47
+msgid "Malaysian ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:48
+msgid "Norwegian krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:49
+msgid "New Zealand dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:50
+msgid "Philippine peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:51
+msgid "Polish zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:52
+msgid "Swedish krona"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:53
+msgid "Singapore dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:54
+msgid "Thai baht"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:55
+msgid "Turkish lira"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:56
+msgid "New Taiwan dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:57
+msgid "United States dollar"
+msgstr ""
+
+#: waeup/ikoba/products/batching.py:52
+msgid "Product Processor"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:126
+msgid "Create product"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/products/browser.py:137
+msgid "Product ${a} added."
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:195
+msgid "Manage product "
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:59 waeup/ikoba/products/browser.py:79
+#: waeup/ikoba/products/export.py:40 waeup/ikoba/products/viewlets.py:49
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:31
+msgid "Products"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:91
+msgid "Manage product section"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:96
+#: waeup/ikoba/products/browser.py:123
+msgid "Add product"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:22
+msgid "Setting parameters no yet configured."
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containerpage.pt:10
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:41
+msgid "Category"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:6
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:6
+msgid "Settings"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productpage.pt:5
+msgid "Terms and Conditions:"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:48
+#: waeup/ikoba/products/browser_templates/containerpage.pt:8
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:37
+msgid "Product Id"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:55
+msgid "Product Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:61
+msgid "Contract Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:62
+msgid "Product title is used if empty."
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:67
+msgid "Contract Category"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:81
+msgid "Valid from"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:87
+msgid "Valid to"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:48
+msgid "Fee"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:52
+msgid "Currency"
+msgstr ""
+
+#: waeup/ikoba/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/ikoba/userscontainer.py:76
+msgid "Users"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:169 waeup/ikoba/utils/utils.py:51
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:90 waeup/ikoba/utils/utils.py:50
+msgid "Germany"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:110
+msgid "no contract"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:155
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:207
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Registration and Application Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:52
+msgid "U.S."
+msgstr ""
+
+#: waeup/ikoba/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/ikoba/widgets/sequencewidget.pt:15
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/ikoba/widgets/sequencewidget.pt:23
+msgid "remove-selected-items"
+msgstr ""
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/ig/LC_MESSAGES/waeup.ikoba.po
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/ig/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/ig/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
@@ -0,0 +1,4906 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.Ikoba\n"
+"POT-Creation-Date: Mon Feb  2 08:48:41 2015\n"
+"PO-Revision-Date: 2012-03-09 07:17+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Nigeria <isouaba@waeup.org>\n"
+"Language: ig-NG-Latn\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-Language: Igbo\n"
+"X-Poedit-Country: NIGERIA\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: waeup/ikoba/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:106
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:119 waeup/ikoba/browser/pages.py:470
+msgid "Administration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:126 waeup/ikoba/browser/viewlets.py:320
+#: waeup/ikoba/browser/viewlets.py:544
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:133 waeup/ikoba/browser/pages.py:517
+#: waeup/ikoba/browser/viewlets.py:329 waeup/ikoba/browser/viewlets.py:554
+msgid "Officers"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:140 waeup/ikoba/browser/pages.py:763
+#: waeup/ikoba/browser/pages.py:1363 waeup/ikoba/browser/viewlets.py:338
+#: waeup/ikoba/browser/viewlets.py:563
+msgid "Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:147 waeup/ikoba/browser/reports.py:37
+#: waeup/ikoba/browser/viewlets.py:347 waeup/ikoba/browser/viewlets.py:572
+msgid "Reports"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:92
+msgid "Home"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:108
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/ikoba/browser/templates/datacenterpage.pt:17
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:109
+msgid "Scan"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:129
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:130
+msgid "Delete"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:164
+msgid "No local file selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:44
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:59
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:71
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:76
+msgid "${a} file format expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:82
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:92
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:173
+#: waeup/ikoba/customers/interfaces.py:200 waeup/ikoba/interfaces.py:428
+#: waeup/ikoba/interfaces.py:504
+msgid "Email Address"
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:176
+msgid "Enter your email address stored in Ikoba."
+msgstr ""
+
+#: waeup/ikoba/browser/layout.py:102 waeup/ikoba/customers/browser.py:387
+#: waeup/ikoba/customers/browser.py:690 waeup/ikoba/customers/browser.py:973
+#: waeup/ikoba/customers/browser.py:1385 waeup/ikoba/customers/browser.py:1416
+#: waeup/ikoba/documents/browser.py:259
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1039 waeup/ikoba/browser/pages.py:1068
+#: waeup/ikoba/browser/pages.py:1173 waeup/ikoba/browser/pages.py:1285
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1071
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1072
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1117
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:112
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1125
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1138 waeup/ikoba/browser/pages.py:1261
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1150
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1176 waeup/ikoba/browser/pages.py:1404
+msgid "Reset"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1177
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1178
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1179
+msgid "Perform import"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1244
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:125 waeup/ikoba/browser/pages.py:772
+#: waeup/ikoba/customers/browser.py:207
+msgid "No item selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1287
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1337
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1339
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1347
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1350
+msgid "Show"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1365 waeup/ikoba/browser/viewlets.py:185
+msgid "Search"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1367
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1375
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1390
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1393
+msgid "No search results found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1401
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1421
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1424
+msgid "Given storage path cannot be used: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1428
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1440
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1442
+msgid "Create CSV file"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:146 waeup/ikoba/customers/browser.py:220
+#: waeup/ikoba/customers/browser.py:883 waeup/ikoba/customers/browser.py:1253
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1470
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1514
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1540 waeup/ikoba/customers/browser.py:543
+#: waeup/ikoba/customers/browser.py:625
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1550
+msgid "No record found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1564 waeup/ikoba/customers/browser.py:589
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1568
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:188 waeup/ikoba/customers/browser.py:84
+#: waeup/ikoba/customers/browser.py:422
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:259 waeup/ikoba/browser/viewlets.py:266
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:40
+#: waeup/ikoba/browser/templates/customersitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:270
+msgid "You logged in."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:307
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:315
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:318
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:333
+msgid "You have been logged out. Thanks for using WAeUP Ikoba!"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:375
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:381 waeup/ikoba/browser/pages.py:661
+msgid "Send message now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:400 waeup/ikoba/browser/pages.py:433
+#: waeup/ikoba/browser/pages.py:678 waeup/ikoba/customers/browser.py:372
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:420
+msgid "Send now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:435
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:460
+msgid "<h1>Welcome to WAeUP.Ikoba</h1>"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:518 waeup/ikoba/customers/viewlets.py:207
+#: waeup/ikoba/customers/viewlets.py:332 waeup/ikoba/customers/viewlets.py:404
+#: waeup/ikoba/documents/viewlets.py:60 waeup/ikoba/documents/viewlets.py:79
+#: waeup/ikoba/products/viewlets.py:60 waeup/ikoba/products/viewlets.py:79
+msgid "Manage"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:519 waeup/ikoba/browser/viewlets.py:175
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:526
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:563 waeup/ikoba/browser/pages.py:565
+#: waeup/ikoba/browser/viewlets.py:403
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:587
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:608
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:616 waeup/ikoba/browser/pages.py:737
+#: waeup/ikoba/browser/pages.py:1403 waeup/ikoba/customers/browser.py:400
+#: waeup/ikoba/customers/browser.py:732 waeup/ikoba/customers/browser.py:774
+#: waeup/ikoba/customers/browser.py:990 waeup/ikoba/customers/browser.py:1008
+#: waeup/ikoba/customers/browser.py:1402 waeup/ikoba/customers/browser.py:1426
+#: waeup/ikoba/customers/browser.py:1443 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:269 waeup/ikoba/documents/browser.py:293
+#: waeup/ikoba/documents/browser.py:308 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:106 waeup/ikoba/products/browser.py:184
+#: waeup/ikoba/products/browser.py:197
+msgid "Save"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:642
+msgid "User settings have been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:645 waeup/ikoba/browser/pages.py:1070
+#: waeup/ikoba/browser/pages.py:1175 waeup/ikoba/customers/browser.py:759
+#: waeup/ikoba/customers/browser.py:790 waeup/ikoba/customers/browser.py:946
+#: waeup/ikoba/customers/browser.py:1312 waeup/ikoba/documents/browser.py:106
+#: waeup/ikoba/documents/browser.py:152 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:273 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:110
+#: waeup/ikoba/products/browser.py:184 waeup/ikoba/products/browser.py:206
+msgid "Cancel"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:659 waeup/ikoba/customers/browser.py:353
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:688 waeup/ikoba/browser/viewlets.py:354
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:701 waeup/ikoba/browser/viewlets.py:370
+msgid "My Roles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:733
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:745
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:746
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:747
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:752
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:766 waeup/ikoba/customers/browser.py:176
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:101
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:783
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:787
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:798
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:800 waeup/ikoba/browser/pages.py:816
+#: waeup/ikoba/browser/pages.py:1041 waeup/ikoba/browser/pages.py:1349
+#: waeup/ikoba/browser/pages.py:1366 waeup/ikoba/browser/pages.py:1405
+#: waeup/ikoba/browser/pages.py:1443
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:812
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:815
+msgid "Upload"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:855
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:857
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:861
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:880
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:882 waeup/ikoba/browser/pages.py:1571
+#: waeup/ikoba/customers/browser.py:374
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:890
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:905
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:91
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:913
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:924
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:207 waeup/ikoba/browser/pdf.py:472
+#: waeup/ikoba/browser/pdf.py:482
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:16
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:754
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/ikoba/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data. Please pick the "
+"type of objects you want to export from the selection below. The file will "
+"be generated and then be made available for you to download in the table "
+"below. Files can only be downloaded within 24 hours after generation. The "
+"download link disappears afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:42
+msgid "Export Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:43
+msgid "Exporter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:44
+msgid "Filter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:45
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:6
+msgid "Creator"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:46
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:7
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:47
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:8
+msgid "Status"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:72
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:39
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/ikoba/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:14
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/ikoba/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:14
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:13
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/filedisplay.pt:15
+msgid "HTML Element:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:13
+msgid "Password"
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/ikoba/browser/templates/loginpage.pt:26
+msgid "login_trouble1"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer, or your customer record has been created for you? Acquire a Password Activation Code (PWD) and inititialize your customer account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:29
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer? Create your customer account <strong><a href=\"createaccount\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:34
+msgid "login_trouble4"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer and your customer record has been created for you? Inititialize your customer account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:38
+msgid "login_trouble5"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:4
+msgid "Identifier"
+msgstr ""
+
+#. Default: "Or simply forgot your id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:42
+msgid "login_trouble3"
+msgstr ""
+
+#. Default: "Customers can enter either their customer id, registration number or email address. Officers must enter their user name."
+#: waeup/ikoba/browser/templates/loginpage.pt:7
+msgid "identifier_hint"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:46
+#: waeup/ikoba/browser/templates/customersitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:52
+#: waeup/ikoba/browser/templates/customersitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:6
+#: waeup/ikoba/browser/templates/customersitelayout.pt:4
+msgid "WAeUP.Ikoba - Registration and Application System"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:15
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:16
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:30
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:9
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:27
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:10
+#: waeup/ikoba/customers/browser_templates/basepage.pt:14
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:24
+msgid "Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:165
+msgid "Add"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:305
+msgid "Any link text"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:387 waeup/ikoba/customers/viewlets.py:198
+msgid "Send email"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:396
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:412
+msgid "Show logs"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:421
+msgid "Upload data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:430
+msgid "Process data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:439
+msgid "Export data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:448
+msgid "View processed files"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:508
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:93 waeup/ikoba/browser/viewlets.py:155
+#: waeup/ikoba/customers/viewlets.py:284 waeup/ikoba/customers/viewlets.py:347
+#: waeup/ikoba/customers/viewlets.py:422
+msgid "Edit"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:142
+msgid "Customer record created"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/batching.py:197
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:461
+msgid "Customer Sample Document Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:562
+msgid "Customer Sample Contract Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:60
+msgid "Customer Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1013
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1017 waeup/ikoba/customers/browser.py:1452
+msgid "Form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1029 waeup/ikoba/documents/browser.py:324
+msgid "Trigger document transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:106 waeup/ikoba/customers/export.py:130
+#: waeup/ikoba/customers/viewlets.py:54
+msgid "Customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1077
+msgid "Documents of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1085
+msgid "Customer Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1091
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:8
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:9
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:4
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:8
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:8
+msgid "Id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1092 waeup/ikoba/customers/browser.py:1527
+#: waeup/ikoba/products/productoptions.py:44
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:9
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:9
+#: waeup/ikoba/products/browser_templates/containerpage.pt:9
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:39
+msgid "Title"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1093 waeup/ikoba/customers/browser.py:1528
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:10
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:10
+msgid "Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1094 waeup/ikoba/customers/browser.py:1529
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1095 waeup/ikoba/customers/browser.py:1530
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:12
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:12
+msgid "Last Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1181 waeup/ikoba/customers/viewlets.py:160
+msgid "Contracts"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:1226
+msgid "${a}: Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1229 waeup/ikoba/customers/browser.py:1270
+#: waeup/ikoba/customers/browser.py:1295
+msgid "Add contract"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1234
+msgid "Remove selected contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1240
+msgid "No contract selected."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:132
+msgid "Find customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1325
+msgid "Select product"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:133 waeup/ikoba/customers/browser.py:175
+msgid "Find customer(s)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1336
+msgid "Save and proceed"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1448
+msgid "Apply now (final submit)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1464
+msgid "Trigger contract transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:150 waeup/ikoba/customers/browser.py:192
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1512
+msgid "Contracts of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1520
+msgid "Customer Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:156 waeup/ikoba/customers/browser.py:198
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:162 waeup/ikoba/customers/browser.py:205
+#: waeup/ikoba/customers/browser.py:563 waeup/ikoba/customers/browser.py:575
+msgid "No customer found."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:174 waeup/ikoba/customers/viewlets.py:180
+msgid "Manage customer section"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:233 waeup/ikoba/customers/viewlets.py:188
+msgid "Add customer"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:236
+msgid "Create customer record"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:241
+msgid "Customer created."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:256
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:279
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:283
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:289
+msgid "You successfully logged in as customer."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:312
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:314
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:320
+msgid "set"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:321
+msgid "unset"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:385
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:435
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:447 waeup/ikoba/customers/browser.py:1044
+#: waeup/ikoba/customers/browser.py:1479 waeup/ikoba/documents/browser.py:339
+msgid "No transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:450 waeup/ikoba/customers/browser.py:1047
+#: waeup/ikoba/customers/browser.py:1482
+msgid "Apply"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:457 waeup/ikoba/customers/browser.py:1055
+#: waeup/ikoba/customers/browser.py:1490 waeup/ikoba/documents/browser.py:350
+msgid "Transition '%s' executed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:474
+msgid "Customer account has been activated."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:494
+msgid "Customer account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:513
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:523
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:557
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:566
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:610
+msgid "Create customer account"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:64
+msgid ""
+"You can not edit some data after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:648
+msgid "You have successfully created a customer account for the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:669
+msgid "Your request was successful."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:67
+msgid ""
+"You can not edit your document after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:688 waeup/ikoba/customers/viewlets.py:303
+msgid "Upload files"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:70
+msgid ""
+"You can not edit your contract after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:721
+msgid "Edit base data and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:722
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:741
+msgid "Passport picture is missing."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:746
+msgid "Save and request registration now"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:755
+msgid "Registration form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:771 waeup/ikoba/customers/viewlets.py:293
+msgid "Change password"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:785
+msgid "Password changed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:816 waeup/ikoba/customers/viewlets.py:166
+#: waeup/ikoba/documents/browser.py:62 waeup/ikoba/documents/browser.py:82
+#: waeup/ikoba/documents/viewlets.py:49
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:3
+msgid "Documents"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:856
+msgid "${a}: Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:859 waeup/ikoba/customers/browser.py:900
+#: waeup/ikoba/customers/browser.py:924 waeup/ikoba/documents/browser.py:96
+#: waeup/ikoba/documents/browser.py:119 waeup/ikoba/documents/browser.py:129
+msgid "Add document"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:864 waeup/ikoba/documents/browser.py:101
+msgid "Remove selected documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:870
+msgid "No document selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:935 waeup/ikoba/customers/browser.py:1306
+#: waeup/ikoba/documents/browser.py:144
+msgid "${a} added."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:94
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:56
+msgid "Select all"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:10
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:11
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:12
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:12
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:23
+msgid "Search Results"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:32
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:9
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractaddpage.pt:7
+msgid "Contract Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:20
+msgid "Contract Type:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:19
+msgid ""
+"A customer record will be created and your login credentials will be sent to "
+"the email address given above. To be able to proceed you must provide a "
+"valid email address!"
+msgstr ""
+
+#. Default: "Ikoba is creating a temporary customer password which will replace the original password chosen by the customer. The password will be valid for ${minutes} minutes. The customer can't login during this time period. After expiration the customer can login with the original password again."
+#: waeup/ikoba/customers/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/ikoba/customers/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:19
+msgid ""
+"Your customer record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:8
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:14
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:44
+msgid "Thanks for using Ikoba!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:3
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:147
+#: waeup/ikoba/customers/documents.py:162
+msgid "Customer has not yet been approved."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:153
+msgid "Attached documents must be verified first."
+msgstr ""
+
+#: waeup/ikoba/customers/documents.py:165
+msgid "No file uploaded."
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:194
+msgid "Customer Sample Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:246
+msgid "Customer Sample Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:142
+#: waeup/ikoba/customers/fileviewlets.py:143
+#: waeup/ikoba/documents/fileviewlets.py:39
+#: waeup/ikoba/documents/fileviewlets.py:40
+msgid "PDF File"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:167
+#: waeup/ikoba/customers/fileviewlets.py:168
+#: waeup/ikoba/documents/fileviewlets.py:57
+#: waeup/ikoba/documents/fileviewlets.py:58
+msgid "PDF Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:48
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:60
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:90
+#: waeup/ikoba/customers/fileviewlets.py:91
+#: waeup/ikoba/customers/fileviewlets.py:115
+#: waeup/ikoba/customers/fileviewlets.py:116
+msgid "Sample Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:101
+#: waeup/ikoba/customers/interfaces.py:195
+#: waeup/ikoba/customers/interfaces.py:213
+msgid "First Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:106
+#: waeup/ikoba/customers/interfaces.py:218
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:111
+#: waeup/ikoba/customers/interfaces.py:223
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:116
+msgid "Sex"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:122
+#: waeup/ikoba/customers/interfaces.py:161
+#: waeup/ikoba/customers/interfaces.py:190
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:129
+#: waeup/ikoba/customers/interfaces.py:228
+msgid "Email"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:136 waeup/ikoba/interfaces.py:433
+#: waeup/ikoba/interfaces.py:511
+msgid "Phone"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:252
+#: waeup/ikoba/documents/interfaces.py:52
+msgid "Document Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:303
+msgid "Contract Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:309
+#: waeup/ikoba/customers/interfaces.py:332
+msgid "Product"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:315
+#: waeup/ikoba/customers/interfaces.py:365
+#: waeup/ikoba/products/interfaces.py:73
+msgid "Options/Fees"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:344
+#: waeup/ikoba/customers/interfaces.py:379
+msgid "Document"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:355
+msgid "Comment"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:82
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:88
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:90
+msgid ""
+"This message will be shown if and only if deactivated customers try to login."
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:96
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:31
+msgid "Customer Id"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:161
+msgid "Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:180
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:240
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:25
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:30
+msgid "Connected Files"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:301 waeup/ikoba/customers/utils.py:309
+#: waeup/ikoba/customers/utils.py:317 waeup/ikoba/documents/utils.py:32
+msgid "created"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:302
+msgid "started"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:303
+msgid "requested"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:304 waeup/ikoba/customers/utils.py:311
+msgid "approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:305
+msgid "provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:310
+msgid "submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:312 waeup/ikoba/customers/utils.py:320
+msgid "rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:313 waeup/ikoba/customers/utils.py:321
+msgid "expired"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:318
+msgid "submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:319
+msgid "verified"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:325
+msgid "Sample Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:326 waeup/ikoba/documents/utils.py:37
+msgid "PDF Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:330
+msgid "Sample Contract"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:374
+msgid "Contractor"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:377
+msgid "Document Owner"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:386
+msgid "Contract Data"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:401
+msgid "Transaction History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:407
+msgid "Registration History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:425 waeup/ikoba/products/interfaces.py:95
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:32
+msgid "Terms and Conditions"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:114
+msgid "My Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:115
+msgid "My Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:116 waeup/ikoba/customers/viewlets.py:172
+msgid "History"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:137 waeup/ikoba/customers/viewlets.py:154
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:217 waeup/ikoba/customers/viewlets.py:363
+#: waeup/ikoba/customers/viewlets.py:438 waeup/ikoba/documents/viewlets.py:89
+msgid "Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:227
+msgid "Login as customer"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:236
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:248 waeup/ikoba/customers/viewlets.py:269
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:257
+msgid "Activate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:283
+msgid "Edit and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:323
+msgid "Download documents overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:372 waeup/ikoba/customers/viewlets.py:447
+#: waeup/ikoba/documents/viewlets.py:69 waeup/ikoba/products/viewlets.py:69
+msgid "View"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:383
+msgid "Download document slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:395
+msgid "Download contracts overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:413
+msgid "Manage official data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:462
+msgid "Download contract slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:98
+msgid "My Data"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:58
+msgid "male"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:60
+msgid "female"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:103 waeup/ikoba/customers/workflow.py:124
+msgid "Reset to requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:104 waeup/ikoba/customers/workflow.py:125
+msgid "Reset to 'requested'"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:111 waeup/ikoba/customers/workflow.py:209
+#: waeup/ikoba/customers/workflow.py:210 waeup/ikoba/customers/workflow.py:216
+#: waeup/ikoba/customers/workflow.py:217 waeup/ikoba/customers/workflow.py:223
+#: waeup/ikoba/customers/workflow.py:224 waeup/ikoba/customers/workflow.py:237
+#: waeup/ikoba/customers/workflow.py:238 waeup/ikoba/customers/workflow.py:324
+#: waeup/ikoba/customers/workflow.py:325 waeup/ikoba/customers/workflow.py:331
+#: waeup/ikoba/customers/workflow.py:332 waeup/ikoba/customers/workflow.py:338
+#: waeup/ikoba/customers/workflow.py:339 waeup/ikoba/customers/workflow.py:352
+#: waeup/ikoba/customers/workflow.py:353
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:180
+msgid "Create contract"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:183
+msgid "Contract created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:188
+msgid "Submit for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:189
+msgid "Submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:195
+msgid "Approve"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:196
+msgid "Approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:202 waeup/ikoba/customers/workflow.py:317
+msgid "Reject"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:203 waeup/ikoba/customers/workflow.py:318
+msgid "REJECTED"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:230 waeup/ikoba/customers/workflow.py:231
+#: waeup/ikoba/customers/workflow.py:345 waeup/ikoba/customers/workflow.py:346
+msgid "Set to expired"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:295 waeup/ikoba/documents/workflow.py:37
+msgid "Create document"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:298 waeup/ikoba/documents/workflow.py:40
+msgid "Document created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:303
+msgid "Submit for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:304
+msgid "Submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:310
+msgid "Verify"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:311
+msgid "Verified"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:45
+msgid "Create customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:48
+msgid "Customer created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:53
+msgid "Start registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:56
+msgid "Customer registration started"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:61
+msgid "Request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:62
+msgid "Customer registration requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:68
+msgid "Approve customer provisionally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:69
+msgid "Customer registration provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:75
+msgid "Approve finally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:76
+msgid "Customer registration finally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:82
+msgid "Approve customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:83
+msgid "Customer registration approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:89
+msgid "Reject customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:90
+msgid "Customer registration rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:96 waeup/ikoba/customers/workflow.py:110
+#: waeup/ikoba/customers/workflow.py:117
+msgid "Reset customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:97 waeup/ikoba/customers/workflow.py:118
+msgid "Reset to initial customer state"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:135
+msgid "Public PDF Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:147
+msgid "Public HTML Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:159
+msgid "Public REST Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:140 waeup/ikoba/products/browser.py:133
+msgid "The id chosen already exists."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:210
+#: waeup/ikoba/documents/fileviewlets.py:80
+msgid "The document requested has not yet been published."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:256 waeup/ikoba/documents/browser.py:282
+#: waeup/ikoba/products/browser.py:185 waeup/ikoba/products/browser.py:215
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:257 waeup/ikoba/documents/browser.py:278
+#: waeup/ikoba/products/browser.py:186 waeup/ikoba/products/browser.py:211
+msgid "Add local role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:267
+msgid "Manage document "
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:342
+msgid "Apply now"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:94
+msgid "Manage document section"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:1
+#: waeup/ikoba/products/browser_templates/containerpage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentaddform.pt:7
+#: waeup/ikoba/customers/browser_templates/documentaddpage.pt:7
+msgid "Document Type"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:12
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:12
+msgid "Files"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:16
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:16
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:21
+msgid "Document Type:"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:37
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:48
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:5
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:37
+msgid "Name"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:39
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:50
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:39
+msgid "Local Role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:6
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:6
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:7
+msgid "Metadata"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:9
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:12
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:7
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:9
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:25
+msgid "Content:"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:101
+msgid "Public HTML Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:110
+msgid "Public REST Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:92
+msgid "Public PDF Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:110
+msgid "Multilingual content in REST (reStructuredText) format"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:58
+msgid "Document Title"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:78
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:97
+#: waeup/ikoba/products/interfaces.py:97
+msgid "Multilingual content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:33
+msgid "published"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:38
+msgid "HTML Document"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:39
+msgid "REST Document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:45
+msgid "Publish document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:48
+msgid "Document published"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:53
+msgid "Retract documet"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:56
+msgid "Document retracted"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:361
+msgid "Processor name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:387
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:394
+msgid "Email to:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:401
+msgid "Subject:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:405
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:409
+msgid "Text:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:438 waeup/ikoba/interfaces.py:494
+msgid "Public Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:448 waeup/ikoba/interfaces.py:449
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:455
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:456
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:485
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:35
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:46
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:35
+msgid "User Id"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:490
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:500
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:517
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:564
+msgid "Name of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:565
+msgid "Sample Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:570
+msgid "Abbreviated Title of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:576
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:588
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:594
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:601
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:602
+msgid "Ikoba Contact"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:607
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:614
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:66
+msgid "new"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:67
+msgid "completed"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:68
+msgid "pending"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:69
+msgid "active"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:70
+msgid "assigned"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:71
+msgid "callbacks"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/ikoba/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:100
+msgid "Iranian Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:101
+msgid "Iceland Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:102
+msgid "Jamaican Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:103
+msgid "Jordanian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:104
+msgid "Yen"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:105
+msgid "Kenyan Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:106
+msgid "Som"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:107
+msgid "Riel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:108
+msgid "Comoro Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:110
+msgid "North Korean Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:113
+msgid "Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:114
+msgid "Kuwaiti Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:115
+msgid "Cayman Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:116
+msgid "Tenge"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:117
+msgid "Kip"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:118
+msgid "Lebanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:119
+msgid "Sri Lanka Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:120
+msgid "Liberian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:121
+msgid "Loti"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:122
+msgid "Lithuanian Litas"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:123
+msgid "Libyan Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:124
+msgid "Moroccan Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:125
+msgid "Moldovan Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:126
+msgid "Malagasy Ariary"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:128
+msgid "Denar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:131
+msgid "Kyat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:132
+msgid "Tugrik"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:133
+msgid "Pataca"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:134
+msgid "Ouguiya"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:135
+msgid "Mauritius Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:136
+msgid "Rufiyaa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:137
+msgid "Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:138
+msgid "Mexican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:139
+msgid "Mexican Unidad de Inversion (UDI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:140
+msgid "Malaysian Ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:141
+msgid "Mozambique Metical"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:142
+msgid "Namibia Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:143
+msgid "Naira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:144
+msgid "Cordoba Oro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:145
+msgid "Norwegian Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:146
+msgid "Nepalese Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:147
+msgid "New Zealand Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:148
+msgid "Rial Omani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:149
+msgid "Balboa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:150
+msgid "Nuevo Sol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:151
+msgid "Kina"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:152
+msgid "Philippine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:153
+msgid "Pakistan Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:154
+msgid "Zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:155
+msgid "Guarani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:156
+msgid "Qatari Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:157
+msgid "New Romanian Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:158
+msgid "Serbian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:159
+msgid "Russian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:160
+msgid "Rwanda Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:161
+msgid "Saudi Riyal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:162
+msgid "Solomon Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:163
+msgid "Seychelles Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:164
+msgid "Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:165
+msgid "Swedish Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:166
+msgid "Singapore Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:168
+msgid "Saint Helena Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:171
+msgid "Leone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:172
+msgid "Somali Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:173
+msgid "Surinam Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:174
+msgid "South Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:175
+msgid "Dobra"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:176
+msgid "El Salvador Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:177
+msgid "Syrian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:178
+msgid "Lilangeni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:179
+msgid "Baht"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:180
+msgid "Somoni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:181
+msgid "Turkmenistan New Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:182
+msgid "Tunisian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:183
+msgid "Pa'anga"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:184
+msgid "Turkish Lira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:185
+msgid "Trinidad and Tobago Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:186
+msgid "New Taiwan Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:187
+msgid "Tanzanian Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:188
+msgid "Hryvnia"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:189
+msgid "Uganda Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:190
+msgid "US Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:191
+msgid "US Dollar (Next day)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:193
+msgid "Uruguay Peso en Unidades Indexadas (URUIURUI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:196
+msgid "Peso Uruguayo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:197
+msgid "Uzbekistan Sum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:198
+msgid "Bolivar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:199
+msgid "Dong"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:200
+msgid "Vatu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:201
+msgid "Tala"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:202
+msgid "CFA Franc BEAC"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:204
+msgid "East Caribbean Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:207
+msgid "CFA Franc BCEAO"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:208
+msgid "CFP Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:209
+msgid "Yemeni Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:210
+msgid "Rand"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:211
+msgid "Zambian Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:212
+msgid "Zimbabwe Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:23
+msgid "UAE Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:24
+msgid "Afghani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:25
+msgid "Lek"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:26
+msgid "Armenian Dram"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:28
+msgid "Netherlands Antillean Guilder"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:31
+msgid "Kwanza"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:32
+msgid "Argentine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:33
+msgid "Australian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:34
+msgid "Aruban Florin"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:35
+msgid "Azerbaijanian Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:36
+msgid "Convertible Mark"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:37
+msgid "Barbados Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:38
+msgid "Taka"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:39
+msgid "Bulgarian Lev"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:40
+msgid "Bahraini Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:41
+msgid "Burundi Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:42
+msgid "Bermudian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:43
+msgid "Brunei Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:44
+msgid "Boliviano"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:45
+msgid "Mvdol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:46
+msgid "Brazilian Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:47
+msgid "Bahamian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:48
+msgid "Ngultrum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:49
+msgid "Pula"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:50
+msgid "Belarussian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:51
+msgid "Belize Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:52
+msgid "Canadian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:54
+msgid "Congolese Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:57
+msgid "WIR Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:58
+msgid "Swiss Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:59
+msgid "WIR Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:60
+msgid "Unidad de Fomento"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:61
+msgid "Chilean Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:62
+msgid "Yuan Renminbi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:63
+msgid "Colombian Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:64
+msgid "Unidad de Valor Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:65
+msgid "Costa Rican Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:66
+msgid "Peso Convertible"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:67
+msgid "Cuban Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:68
+msgid "Cabo Verde Escudo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:69
+msgid "Czech Koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:70
+msgid "Djibouti Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:71
+msgid "Danish Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:72
+msgid "Dominican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:73
+msgid "Algerian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:74
+msgid "Egyptian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:75
+msgid "-Nakfa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:76
+msgid "Ethiopian Birr"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:77
+#: waeup/ikoba/payments/paypal_currencies.py:40
+msgid "Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:78
+msgid "Fiji Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:80
+msgid "Falkland Islands Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:83
+msgid "Pound Sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:84
+msgid "Lari"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:85
+msgid "Ghana Cedi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:86
+msgid "Gibraltar Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:87
+msgid "Dalasi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:88
+msgid "Guinea Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:89
+msgid "Quetzal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:90
+msgid "Guyana Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:91
+msgid "Hong Kong Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:92
+msgid "Lempira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:93
+msgid "Croatian Kuna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:94
+msgid "Gourde"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:95
+msgid "Forint"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:96
+msgid "Rupiah"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:97
+msgid "New Israeli Sheqel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:98
+msgid "Indian Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:99
+msgid "Iraqi Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:168
+msgid "Payment State"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:175
+msgid "Creation Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:181
+msgid "Payment Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:187
+msgid "Amount"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:188
+msgid "The overall sum payed, including all taxes fees, etc."
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:217
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:224
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:231
+msgid "Response Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:238
+msgid "Response Description"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:34
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:35
+msgid "Paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:36
+msgid "Failed"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1147
+msgid "Credit Card (PayPal)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1223
+msgid "PayPal Payment"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:73
+msgid "pieces"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:74 waeup/ikoba/payments/paypal.py:1136
+msgid "license"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:10
+msgid "ANGUILLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:100
+msgid "SOUTH KOREA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:101
+msgid "KUWAIT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:102
+msgid "CAYMAN ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:103
+msgid "KAZAKHSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:104
+msgid "LAOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:105
+msgid "SAINT LUCIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:106
+msgid "LIECHTENSTEIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:107
+msgid "SRI LANKA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:108
+msgid "LESOTHO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:109
+msgid "LITHUANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:11
+msgid "ALBANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:110
+msgid "LUXEMBOURG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:111
+msgid "LATVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:112
+msgid "MOROCCO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:113
+msgid "MADAGASCAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:114
+msgid "MARSHALL ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:115
+msgid "MALI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:116
+msgid "MONGOLIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:117
+msgid "MARTINIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:118
+msgid "MAURITANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:119
+msgid "MONTSERRAT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:12
+msgid "ARMENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:120
+msgid "MALTA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:121
+msgid "MAURITIUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:122
+msgid "MALDIVES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:123
+msgid "MALAWI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:124
+msgid "MEXICO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:125
+msgid "MALAYSIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:126
+msgid "MOZAMBIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:127
+msgid "NAMIBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:128
+msgid "NEW CALEDONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:129
+msgid "NIGER"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:13
+msgid "NETHERLANDS ANTILLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:130
+msgid "NORFOLK ISLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:131
+msgid "NICARAGUA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:132
+msgid "NETHERLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:133
+msgid "NORWAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:134
+msgid "NEPAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:135
+msgid "NAURU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:136
+msgid "NIUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:137
+msgid "NEW ZEALAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:138
+msgid "OMAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:139
+msgid "PANAMA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:14
+msgid "ANGOLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:140
+msgid "PERU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:141
+msgid "FRENCH POLYNESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:142
+msgid "PAPUA NEW GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:143
+msgid "PHILIPPINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:144
+msgid "POLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:145
+msgid "SAINT PIERRE AND MIQUELON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:146
+msgid "PITCAIRN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:147
+msgid "PORTUGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:148
+msgid "PALAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:149
+msgid "QATAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:15
+msgid "ARGENTINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:150
+msgid "REUNION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:151
+msgid "ROMANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:152
+msgid "SERBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:153
+msgid "RUSSIAN FEDERATION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:154
+msgid "RWANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:155
+msgid "SAUDI ARABIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:156
+msgid "SOLOMON ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:157
+msgid "SEYCHELLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:158
+msgid "SWEDEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:159
+msgid "SINGAPORE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:16
+msgid "AUSTRIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:160
+msgid "SAINT HELENA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:161
+msgid "SLOVENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:162
+msgid "SVALBARD AND JAN MAYEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:163
+msgid "SLOVAKIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:164
+msgid "SIERRA LEONE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:165
+msgid "SAN MARINO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:166
+msgid "SENEGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:167
+msgid "SOMALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:168
+msgid "SURINAME"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:169
+msgid "SAO TOME AND PRINCIPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:17
+msgid "AUSTRALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:170
+msgid "EL SALVADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:171
+msgid "SWAZILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:172
+msgid "TURKS AND CAICOS ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:173
+msgid "CHAD"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:174
+msgid "TOGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:175
+msgid "THAILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:176
+msgid "TAJIKISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:177
+msgid "TURKMENISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:178
+msgid "TUNISIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:179
+msgid "TONGA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:18
+msgid "ARUBA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:180
+msgid "TURKEY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:181
+msgid "TRINIDAD AND TOBAGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:182
+msgid "TUVALU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:183
+msgid "TAIWAN, PROVINCE OF CHINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:184
+msgid "TANZANIA, UNITED REPUBLIC OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:185
+msgid "UKRAINE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:186
+msgid "UGANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:187
+msgid "UNITED STATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:188
+msgid "URUGUAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:189
+msgid "HOLY SEE (VATICAN CITY STATE)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:19
+msgid "AZERBAIJAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:190
+msgid "SAINT VINCENT AND THE GRENADINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:191
+msgid "VENEZUELA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:192
+msgid "VIRGIN ISLANDS, BRITISH"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:193
+msgid "VIETNAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:194
+msgid "VANUATU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:195
+msgid "WALLIS AND FUTUNA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:196
+msgid "SAMOA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:197
+msgid "YEMEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:198
+msgid "MAYOTTE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:199
+msgid "SOUTH AFRICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:20
+msgid "BOSNIA-HERZEGOVINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:200
+msgid "ZAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:21
+msgid "BARBADOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:22
+msgid "BELGIUM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:23
+msgid "BURKINA FASO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:24
+msgid "BULGARIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:25
+msgid "BAHRAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:26
+msgid "BURUNDI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:27
+msgid "BENIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:28
+msgid "BERMUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:29
+msgid "BRUNEI DARUSSALAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:30
+msgid "BOLIVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:31
+msgid "BRAZIL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:32
+msgid "BAHAMAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:33
+msgid "BHUTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:34
+msgid "BOTSWANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:35
+msgid "BELIZE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:36
+msgid "CANADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:37
+msgid "DEMOCRATIC REPUBLIC OF CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:38
+msgid "CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:39
+msgid "SWITZERLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:40
+msgid "COOK ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:41
+msgid "CHILE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:42
+msgid "CHINA (domestic Chinese bank transactions only)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:43
+msgid "CHINA (For CUP, bank card and cross-border transactions)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:44
+msgid "COLOMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:45
+msgid "COSTA RICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:46
+msgid "CAPE VERDE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:47
+msgid "CYPRUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:48
+msgid "CZECH REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:49
+msgid "GERMANY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:50
+msgid "DJIBOUTI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:51
+msgid "DENMARK"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:52
+msgid "DOMINICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:53
+msgid "DOMINICAN REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:54
+msgid "ALGERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:55
+msgid "ECUADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:56
+msgid "ESTONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:57
+msgid "EGYPT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:58
+msgid "ERITERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:59
+msgid "SPAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:60
+msgid "ETHIOPIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:61
+msgid "FINLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:62
+msgid "FIJI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:63
+msgid "FALKLAND ISLANDS (MALVINAS)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:64
+msgid "MICRONESIA, FEDERATED STATES OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:65
+msgid "FRANCE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:66
+msgid "GABON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:67
+msgid "UNITED KINGDOM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:68
+msgid "GRENADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:69
+msgid "GEORGIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:7
+msgid "ANDORRA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:70
+msgid "FRENCH GUIANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:71
+msgid "GIBRALTAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:72
+msgid "GREENLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:73
+msgid "GAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:74
+msgid "GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:75
+msgid "GUADELOUPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:76
+msgid "GREECE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:77
+msgid "GUATEMALA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:78
+msgid "GUAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:79
+msgid "GUINEA BISSAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:8
+msgid "UNITED ARAB EMIRATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:80
+msgid "GUYANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:81
+msgid "HONG KONG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:82
+msgid "HONDURAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:83
+msgid "CROATIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:84
+msgid "HUNGARY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:85
+msgid "INDONESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:86
+msgid "IRELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:87
+msgid "ISRAEL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:88
+msgid "INDIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:89
+msgid "ICELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:9
+msgid "ANTIGUA AND BARBUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:90
+msgid "ITALY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:91
+msgid "JAMAICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:92
+msgid "JORDAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:93
+msgid "JAPAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:94
+msgid "KENYA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:95
+msgid "KYRGYZSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:96
+msgid "CAMBODIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:97
+msgid "KIRIBATI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:98
+msgid "COMOROS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:99
+msgid "SAINT KITTS AND NEVIS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:34
+msgid "Australian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:35
+msgid "Brazilian real"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:36
+msgid "Canadian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:37
+msgid "Swiss franc"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:38
+msgid "Czech koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:39
+msgid "Danish krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:41
+msgid "Pound sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:42
+msgid "Hong Kong dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:43
+msgid "Hungarian forint"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:44
+msgid "Israeli new shekel"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:45
+msgid "Japanese yen"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:46
+msgid "Mexican peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:47
+msgid "Malaysian ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:48
+msgid "Norwegian krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:49
+msgid "New Zealand dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:50
+msgid "Philippine peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:51
+msgid "Polish zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:52
+msgid "Swedish krona"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:53
+msgid "Singapore dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:54
+msgid "Thai baht"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:55
+msgid "Turkish lira"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:56
+msgid "New Taiwan dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:57
+msgid "United States dollar"
+msgstr ""
+
+#: waeup/ikoba/products/batching.py:52
+msgid "Product Processor"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:126
+msgid "Create product"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/products/browser.py:137
+msgid "Product ${a} added."
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:195
+msgid "Manage product "
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:59 waeup/ikoba/products/browser.py:79
+#: waeup/ikoba/products/export.py:40 waeup/ikoba/products/viewlets.py:49
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:31
+msgid "Products"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:91
+msgid "Manage product section"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:96
+#: waeup/ikoba/products/browser.py:123
+msgid "Add product"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:22
+msgid "Setting parameters no yet configured."
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containerpage.pt:10
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:41
+msgid "Category"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:6
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:6
+msgid "Settings"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productpage.pt:5
+msgid "Terms and Conditions:"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:48
+#: waeup/ikoba/products/browser_templates/containerpage.pt:8
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:37
+msgid "Product Id"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:55
+msgid "Product Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:61
+msgid "Contract Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:62
+msgid "Product title is used if empty."
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:67
+msgid "Contract Category"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:81
+msgid "Valid from"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:87
+msgid "Valid to"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:48
+msgid "Fee"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:52
+msgid "Currency"
+msgstr ""
+
+#: waeup/ikoba/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/ikoba/userscontainer.py:76
+msgid "Users"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:169 waeup/ikoba/utils/utils.py:51
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:90 waeup/ikoba/utils/utils.py:50
+msgid "Germany"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:110
+msgid "no contract"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:155
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:207
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Registration and Application Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:52
+msgid "U.S."
+msgstr ""
+
+#: waeup/ikoba/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/ikoba/widgets/sequencewidget.pt:15
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/ikoba/widgets/sequencewidget.pt:23
+msgid "remove-selected-items"
+msgstr ""
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/waeup.ikoba.pot
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/waeup.ikoba.pot	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/waeup.ikoba.pot	(revision 12677)
@@ -0,0 +1,5071 @@
+##############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: Development/Unknown\n"
+"POT-Creation-Date: Tue Mar  3 12:07:48 2015\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Zope 3 Developers <zope3-dev@zope.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+
+#: waeup/ikoba/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:106
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:119
+#: waeup/ikoba/browser/pages.py:470
+msgid "Administration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:126
+#: waeup/ikoba/browser/viewlets.py:320
+#: waeup/ikoba/browser/viewlets.py:544
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:133
+#: waeup/ikoba/browser/pages.py:517
+#: waeup/ikoba/browser/viewlets.py:329
+#: waeup/ikoba/browser/viewlets.py:554
+msgid "Officers"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:140
+#: waeup/ikoba/browser/pages.py:763
+#: waeup/ikoba/browser/pages.py:1363
+#: waeup/ikoba/browser/viewlets.py:338
+#: waeup/ikoba/browser/viewlets.py:563
+msgid "Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:147
+#: waeup/ikoba/browser/reports.py:37
+#: waeup/ikoba/browser/viewlets.py:347
+#: waeup/ikoba/browser/viewlets.py:572
+msgid "Reports"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:92
+msgid "Home"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:108
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/ikoba/browser/templates/datacenterpage.pt:17
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:109
+msgid "Scan"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:129
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:130
+msgid "Delete"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:164
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:44
+#. Default: ""
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:59
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:71
+msgid "Could not determine file type."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:76
+#. Default: ""
+msgid "${a} file format expected."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:82
+#. Default: ""
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:92
+#. Default: ""
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:173
+#: waeup/ikoba/customers/interfaces.py:202
+#: waeup/ikoba/interfaces.py:428
+#: waeup/ikoba/interfaces.py:504
+msgid "Email Address"
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:176
+msgid "Enter your email address stored in Ikoba."
+msgstr ""
+
+#: waeup/ikoba/browser/layout.py:102
+#: waeup/ikoba/customers/browser.py:388
+#: waeup/ikoba/customers/browser.py:691
+#: waeup/ikoba/customers/browser.py:974
+#: waeup/ikoba/customers/browser.py:1385
+#: waeup/ikoba/customers/browser.py:1425
+#: waeup/ikoba/documents/browser.py:259
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1039
+#: waeup/ikoba/browser/pages.py:1068
+#: waeup/ikoba/browser/pages.py:1173
+#: waeup/ikoba/browser/pages.py:1285
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1071
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1072
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1117
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:112
+#. Default: ""
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1125
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1138
+#: waeup/ikoba/browser/pages.py:1261
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1150
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1176
+#: waeup/ikoba/browser/pages.py:1404
+msgid "Reset"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1177
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1178
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1179
+msgid "Perform import"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1244
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:125
+#: waeup/ikoba/browser/pages.py:772
+#: waeup/ikoba/customers/browser.py:208
+msgid "No item selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1287
+msgid "Process next"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1337
+#. Default: ""
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1339
+#. Default: ""
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1347
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1350
+msgid "Show"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1365
+#: waeup/ikoba/browser/viewlets.py:185
+msgid "Search"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1367
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1375
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1390
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1393
+msgid "No search results found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1401
+msgid "Edit data center settings"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1421
+#. Default: ""
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1424
+#. Default: ""
+msgid "Given storage path cannot be used: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1428
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1440
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1442
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:146
+#: waeup/ikoba/customers/browser.py:221
+#: waeup/ikoba/customers/browser.py:884
+#: waeup/ikoba/customers/browser.py:1254
+#. Default: ""
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1470
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1514
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1540
+#: waeup/ikoba/customers/browser.py:544
+#: waeup/ikoba/customers/browser.py:626
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1550
+msgid "No record found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1564
+#: waeup/ikoba/customers/browser.py:590
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1568
+#. Default: ""
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:188
+#: waeup/ikoba/customers/browser.py:85
+#: waeup/ikoba/customers/browser.py:423
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:259
+#: waeup/ikoba/browser/viewlets.py:266
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:40
+#: waeup/ikoba/browser/templates/customersitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:270
+msgid "You logged in."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:307
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:315
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:318
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:333
+msgid "You have been logged out. Thanks for using WAeUP Ikoba!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:375
+#. Default: ""
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:381
+#: waeup/ikoba/browser/pages.py:661
+msgid "Send message now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:400
+#: waeup/ikoba/browser/pages.py:433
+#: waeup/ikoba/browser/pages.py:678
+#: waeup/ikoba/customers/browser.py:373
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:420
+msgid "Send now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:435
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:460
+msgid "<h1>Welcome to WAeUP.Ikoba</h1>"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:518
+#: waeup/ikoba/customers/viewlets.py:207
+#: waeup/ikoba/customers/viewlets.py:332
+#: waeup/ikoba/customers/viewlets.py:404
+#: waeup/ikoba/documents/viewlets.py:60
+#: waeup/ikoba/documents/viewlets.py:79
+#: waeup/ikoba/products/viewlets.py:60
+#: waeup/ikoba/products/viewlets.py:79
+msgid "Manage"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:519
+#: waeup/ikoba/browser/viewlets.py:175
+msgid "Remove"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:526
+#. Default: ""
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:563
+#: waeup/ikoba/browser/pages.py:565
+#: waeup/ikoba/browser/viewlets.py:403
+msgid "Add user"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:587
+#. Default: ""
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:608
+#. Default: ""
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:616
+#: waeup/ikoba/browser/pages.py:737
+#: waeup/ikoba/browser/pages.py:1403
+#: waeup/ikoba/customers/browser.py:401
+#: waeup/ikoba/customers/browser.py:733
+#: waeup/ikoba/customers/browser.py:775
+#: waeup/ikoba/customers/browser.py:991
+#: waeup/ikoba/customers/browser.py:1009
+#: waeup/ikoba/customers/browser.py:1411
+#: waeup/ikoba/customers/browser.py:1436
+#: waeup/ikoba/customers/browser.py:1453
+#: waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:269
+#: waeup/ikoba/documents/browser.py:293
+#: waeup/ikoba/documents/browser.py:308
+#: waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:106
+#: waeup/ikoba/products/browser.py:184
+#: waeup/ikoba/products/browser.py:197
+msgid "Save"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:642
+msgid "User settings have been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:645
+#: waeup/ikoba/browser/pages.py:1070
+#: waeup/ikoba/browser/pages.py:1175
+#: waeup/ikoba/customers/browser.py:760
+#: waeup/ikoba/customers/browser.py:791
+#: waeup/ikoba/customers/browser.py:947
+#: waeup/ikoba/customers/browser.py:1309
+#: waeup/ikoba/documents/browser.py:106
+#: waeup/ikoba/documents/browser.py:152
+#: waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:273
+#: waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:94
+#: waeup/ikoba/products/browser.py:110
+#: waeup/ikoba/products/browser.py:184
+#: waeup/ikoba/products/browser.py:206
+msgid "Cancel"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:659
+#: waeup/ikoba/customers/browser.py:354
+#. Default: ""
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:688
+#: waeup/ikoba/browser/viewlets.py:354
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:701
+#: waeup/ikoba/browser/viewlets.py:370
+msgid "My Roles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:733
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:745
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:746
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:747
+msgid "Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:752
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:766
+#: waeup/ikoba/customers/browser.py:177
+#: waeup/ikoba/products/browser.py:94
+#: waeup/ikoba/products/browser.py:101
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:783
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:787
+#. Default: ""
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:798
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:800
+#: waeup/ikoba/browser/pages.py:816
+#: waeup/ikoba/browser/pages.py:1041
+#: waeup/ikoba/browser/pages.py:1349
+#: waeup/ikoba/browser/pages.py:1366
+#: waeup/ikoba/browser/pages.py:1405
+#: waeup/ikoba/browser/pages.py:1443
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:812
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:815
+msgid "Upload"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:855
+msgid "Import Manager"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:857
+#. Default: ""
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:861
+#. Default: ""
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:880
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:882
+#: waeup/ikoba/browser/pages.py:1571
+#: waeup/ikoba/customers/browser.py:375
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:890
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:905
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:91
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:913
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:924
+msgid "Your file contains forbidden characters or has invalid CSV format. First problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:207
+#: waeup/ikoba/browser/pdf.py:472
+#: waeup/ikoba/browser/pdf.py:482
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:16
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:533
+#. Default: ""
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:754
+#. Default: ""
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/reports.py:115
+msgid "Create report"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/changepw.pt:8
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:1
+msgid "Here you can create CSV files from parts of portal data. Please pick the type of objects you want to export from the selection below. The file will be generated and then be made available for you to download in the table below. Files can only be downloaded within 24 hours after generation. The download link disappears afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:43
+msgid "Export Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:44
+msgid "Exporter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:45
+msgid "Filter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:46
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:6
+msgid "Creator"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:47
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:7
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:48
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:8
+msgid "Status"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:73
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:39
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/ikoba/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:3
+msgid "Find below the last ${count} imports. The files contain the imported datasets."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:14
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/ikoba/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:2
+msgid "Using batch processing you can mass-create, mass-update, or mass-remove datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:14
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:8
+msgid "Please select a file-processor and a processing-mode from the selections below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:13
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:1
+msgid "The data center helps you to manage portal data. You can upload CSV files here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:1
+msgid "Before uploading a file check that your file header corresponds with header format of the selected processor. All available headers are listed below. After file upload click the 'Process data' button and proceed up to import step 3. Verify that the data format meets all the import criteria and requirements of the processor."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:62
+msgid "Import managers will be automatically informed by email after file upload. There is no need for assigning tickets or sending emails anymore. The data will be imported according to the information given."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/filedisplay.pt:15
+msgid "HTML Element:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:13
+msgid "Password"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:26
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+msgid "login_trouble1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:29
+#. Default: "You don't have an account because you are a new customer, or your customer record has been created for you? Acquire a Password Activation Code (PWD) and inititialize your customer account <strong><a href=\"setpassword\"> here</a></strong>."
+msgid "login_trouble2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:34
+#. Default: "You don't have an account because you are a new customer? Create your customer account <strong><a href=\"createaccount\"> here</a></strong>."
+msgid "login_trouble4"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:38
+#. Default: "You don't have an account because you are a new customer and your customer record has been created for you? Inititialize your customer account <strong><a href=\"requestpw\"> here</a></strong>."
+msgid "login_trouble5"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:4
+msgid "Identifier"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:42
+#. Default: "Or simply forgot your id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:7
+#. Default: "Customers can enter either their customer id, registration number or email address. Officers must enter their user name."
+msgid "identifier_hint"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainercreate.pt:2
+msgid "Here you can create reports representing portal data. Please pick the type of report you want to create from the selection below. Afterwards you can configure the report (do the settings). The report data will be calculated and then made available for you to view/download."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:46
+#: waeup/ikoba/browser/templates/customersitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:52
+#: waeup/ikoba/browser/templates/customersitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:6
+#: waeup/ikoba/browser/templates/customersitelayout.pt:4
+msgid "WAeUP.Ikoba - Registration and Application System"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:15
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:16
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:30
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:9
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:27
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:10
+#: waeup/ikoba/customers/browser_templates/basepage.pt:14
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:24
+msgid "Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:165
+msgid "Add"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:305
+msgid "Any link text"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:387
+#: waeup/ikoba/customers/viewlets.py:198
+msgid "Send email"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:396
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:412
+msgid "Show logs"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:421
+msgid "Upload data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:430
+msgid "Process data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:439
+msgid "Export data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:448
+msgid "View processed files"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:508
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:93
+#: waeup/ikoba/browser/viewlets.py:155
+#: waeup/ikoba/customers/viewlets.py:284
+#: waeup/ikoba/customers/viewlets.py:347
+#: waeup/ikoba/customers/viewlets.py:422
+msgid "Edit"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:157
+msgid "Customer record created"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:212
+#. Default: ""
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:476
+msgid "Customer Sample Document Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:616
+msgid "Customer Sample Contract Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:75
+msgid "Customer Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1014
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1018
+#: waeup/ikoba/customers/browser.py:1468
+msgid "Form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1030
+#: waeup/ikoba/documents/browser.py:324
+msgid "Trigger document transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:107
+#: waeup/ikoba/customers/export.py:130
+#: waeup/ikoba/customers/viewlets.py:54
+msgid "Customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1078
+msgid "Documents of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1086
+msgid "Customer Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1092
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:8
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:9
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:4
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:8
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:8
+msgid "Id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1093
+#: waeup/ikoba/customers/browser.py:1543
+#: waeup/ikoba/products/productoptions.py:44
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:9
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:9
+#: waeup/ikoba/products/browser_templates/containerpage.pt:9
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:39
+msgid "Title"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1094
+#: waeup/ikoba/customers/browser.py:1544
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:10
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:10
+msgid "Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1095
+#: waeup/ikoba/customers/browser.py:1545
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1096
+#: waeup/ikoba/customers/browser.py:1546
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:12
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:12
+msgid "Last Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1182
+#: waeup/ikoba/customers/reports/contract_statistics.py:137
+#: waeup/ikoba/customers/viewlets.py:160
+msgid "Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1227
+#. Default: ""
+msgid "${a}: Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1230
+#: waeup/ikoba/customers/browser.py:1271
+#: waeup/ikoba/customers/browser.py:1293
+msgid "Add contract"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1235
+msgid "Remove selected contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1241
+msgid "No contract selected."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1322
+msgid "Select product"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:133
+msgid "Find customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1333
+msgid "Save and proceed"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:134
+#: waeup/ikoba/customers/browser.py:176
+msgid "Find customer(s)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1458
+msgid "Apply now (final submit)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1462
+msgid "Please read the terms and conditions and confirm your acceptance of these by ticking the confirmation box."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1480
+msgid "Trigger contract transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:151
+#: waeup/ikoba/customers/browser.py:193
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1528
+msgid "Contracts of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1536
+msgid "Customer Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:157
+#: waeup/ikoba/customers/browser.py:199
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1609
+msgid "Customer Signature"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1610
+msgid "Company Officer Signature"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1614
+msgid "Date, Customer Signature"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1615
+msgid "Date, Company Officer Signature"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:163
+#: waeup/ikoba/customers/browser.py:206
+#: waeup/ikoba/customers/browser.py:564
+#: waeup/ikoba/customers/browser.py:576
+msgid "No customer found."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:175
+#: waeup/ikoba/customers/viewlets.py:180
+msgid "Manage customer section"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:234
+#: waeup/ikoba/customers/viewlets.py:188
+msgid "Add customer"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:237
+msgid "Create customer record"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:242
+msgid "Customer created."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:257
+#. Default: ""
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:280
+msgid "Login now"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:284
+#. Default: ""
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:290
+msgid "You successfully logged in as customer."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:313
+#. Default: ""
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:315
+#. Default: ""
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:321
+msgid "set"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:322
+msgid "unset"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:386
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:436
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:448
+#: waeup/ikoba/customers/browser.py:1045
+#: waeup/ikoba/customers/browser.py:1495
+#: waeup/ikoba/documents/browser.py:339
+msgid "No transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:451
+#: waeup/ikoba/customers/browser.py:1048
+#: waeup/ikoba/customers/browser.py:1498
+msgid "Apply"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:458
+#: waeup/ikoba/customers/browser.py:1056
+#: waeup/ikoba/customers/browser.py:1506
+#: waeup/ikoba/documents/browser.py:350
+msgid "Transition '%s' executed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:475
+msgid "Customer account has been activated."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:495
+msgid "Customer account has been deactivated."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:514
+#. Default: ""
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:524
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:558
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:567
+msgid "Your password has already been set and used. Please proceed to the login page."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:611
+msgid "Create customer account"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:649
+msgid "You have successfully created a customer account for the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:65
+msgid "You can not edit some data after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:670
+msgid "Your request was successful."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:68
+msgid "You can not edit your document after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:689
+#: waeup/ikoba/customers/viewlets.py:303
+msgid "Upload files"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:71
+msgid "You can not edit your contract after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:722
+msgid "Edit base data and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:723
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:742
+msgid "Passport picture is missing."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:747
+msgid "Save and request registration now"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:756
+msgid "Registration form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:772
+#: waeup/ikoba/customers/viewlets.py:293
+msgid "Change password"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:786
+msgid "Password changed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:817
+#: waeup/ikoba/customers/viewlets.py:166
+#: waeup/ikoba/documents/browser.py:62
+#: waeup/ikoba/documents/browser.py:82
+#: waeup/ikoba/documents/viewlets.py:49
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:3
+msgid "Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:857
+#. Default: ""
+msgid "${a}: Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:860
+#: waeup/ikoba/customers/browser.py:901
+#: waeup/ikoba/customers/browser.py:925
+#: waeup/ikoba/documents/browser.py:96
+#: waeup/ikoba/documents/browser.py:119
+#: waeup/ikoba/documents/browser.py:129
+msgid "Add document"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:865
+#: waeup/ikoba/documents/browser.py:101
+msgid "Remove selected documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:871
+msgid "No document selected."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:936
+#: waeup/ikoba/customers/browser.py:1303
+#: waeup/ikoba/documents/browser.py:144
+#. Default: ""
+msgid "${a} added."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:95
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:56
+msgid "Select all"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:10
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:11
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:12
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:12
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:23
+msgid "Search Results"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:32
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:9
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractaddpage.pt:7
+msgid "Contract Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contracteditpage.pt:20
+msgid "I confirm that I have read, understood and accepted the Terms & Conditions."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:20
+msgid "Contract Type:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:29
+msgid "Validity Period:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:19
+msgid "A customer record will be created and your login credentials will be sent to the email address given above. To be able to proceed you must provide a valid email address!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/loginasstep1.pt:6
+#. Default: "Ikoba is creating a temporary customer password which will replace the original password chosen by the customer. The password will be valid for ${minutes} minutes. The customer can't login during this time period. After expiration the customer can login with the original password again."
+msgid "login_as_intro_1"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/loginasstep2.pt:5
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:19
+msgid "Your customer record will be looked up and your login credentials will be sent to the email address given above. To be able to proceed you must provide a valid email address!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:8
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:14
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:44
+msgid "Thanks for using Ikoba!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:3
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:34
+msgid "Please note that passwords are case-sensitive, <br />when entering your credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:146
+#: waeup/ikoba/customers/documents.py:162
+msgid "Customer has not yet been approved."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:152
+msgid "Attached documents must be verified first."
+msgstr ""
+
+#: waeup/ikoba/customers/documents.py:165
+msgid "No file uploaded."
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:194
+msgid "Customer Sample Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:246
+msgid "Customer Sample Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:142
+#: waeup/ikoba/customers/fileviewlets.py:143
+#: waeup/ikoba/documents/fileviewlets.py:39
+#: waeup/ikoba/documents/fileviewlets.py:40
+msgid "PDF File"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:167
+#: waeup/ikoba/customers/fileviewlets.py:168
+#: waeup/ikoba/documents/fileviewlets.py:57
+#: waeup/ikoba/documents/fileviewlets.py:58
+msgid "PDF Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:48
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:60
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:90
+#: waeup/ikoba/customers/fileviewlets.py:91
+#: waeup/ikoba/customers/fileviewlets.py:115
+#: waeup/ikoba/customers/fileviewlets.py:116
+msgid "Sample Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:103
+#: waeup/ikoba/customers/interfaces.py:197
+#: waeup/ikoba/customers/interfaces.py:215
+msgid "First Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:108
+#: waeup/ikoba/customers/interfaces.py:220
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:113
+#: waeup/ikoba/customers/interfaces.py:225
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:118
+msgid "Sex"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:124
+#: waeup/ikoba/customers/interfaces.py:163
+#: waeup/ikoba/customers/interfaces.py:192
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:131
+#: waeup/ikoba/customers/interfaces.py:230
+msgid "Email"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:138
+#: waeup/ikoba/interfaces.py:433
+#: waeup/ikoba/interfaces.py:511
+msgid "Phone"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:254
+#: waeup/ikoba/documents/interfaces.py:52
+msgid "Document Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:307
+msgid "Contract Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:313
+#: waeup/ikoba/customers/interfaces.py:336
+msgid "Product"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:319
+#: waeup/ikoba/products/interfaces.py:73
+msgid "Options/Fees"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:348
+#: waeup/ikoba/customers/interfaces.py:391
+msgid "Document"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:359
+msgid "Comment"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:84
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:90
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:92
+msgid "This message will be shown if and only if deactivated customers try to login."
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:98
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:31
+msgid "Customer Id"
+msgstr ""
+
+#: waeup/ikoba/customers/reports/contract_statistics.py:149
+#: waeup/ikoba/customers/reports/contract_statistics.py:226
+msgid "Contract Statistics"
+msgstr ""
+
+#: waeup/ikoba/customers/reports/contract_statistics.py:172
+msgid "Create contract statistics report"
+msgstr ""
+
+#: waeup/ikoba/customers/reports/contract_statistics.py:188
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:161
+msgid "Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:180
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:240
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:25
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:30
+msgid "Connected Files"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:301
+#: waeup/ikoba/customers/utils.py:309
+#: waeup/ikoba/customers/utils.py:317
+#: waeup/ikoba/documents/utils.py:32
+msgid "created"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:302
+msgid "started"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:303
+msgid "requested"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:304
+#: waeup/ikoba/customers/utils.py:311
+msgid "approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:305
+msgid "provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:310
+msgid "submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:312
+#: waeup/ikoba/customers/utils.py:320
+msgid "rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:313
+#: waeup/ikoba/customers/utils.py:321
+msgid "expired"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:318
+msgid "submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:319
+msgid "verified"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:325
+msgid "Sample Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:326
+#: waeup/ikoba/documents/utils.py:37
+msgid "PDF Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:330
+msgid "Sample Contract"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:374
+msgid "Contractor"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:377
+msgid "Document Owner"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:386
+msgid "Contract Data"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:401
+msgid "Transaction History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:407
+msgid "Registration History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:414
+msgid "Validity Period"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:432
+#: waeup/ikoba/products/interfaces.py:95
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:46
+#: waeup/ikoba/customers/browser_templates/contracteditpage.pt:15
+msgid "Terms and Conditions"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:114
+msgid "My Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:115
+msgid "My Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:116
+#: waeup/ikoba/customers/viewlets.py:172
+msgid "History"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:137
+#: waeup/ikoba/customers/viewlets.py:154
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:217
+#: waeup/ikoba/customers/viewlets.py:363
+#: waeup/ikoba/customers/viewlets.py:438
+#: waeup/ikoba/documents/viewlets.py:89
+msgid "Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:227
+msgid "Login as customer"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:236
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:248
+#: waeup/ikoba/customers/viewlets.py:269
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:257
+msgid "Activate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:283
+msgid "Edit and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:323
+msgid "Download documents overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:372
+#: waeup/ikoba/customers/viewlets.py:447
+#: waeup/ikoba/documents/viewlets.py:69
+#: waeup/ikoba/products/viewlets.py:69
+msgid "View"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:383
+msgid "Download document slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:395
+msgid "Download contracts overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:413
+msgid "Manage official data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:462
+msgid "Download contract slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:98
+msgid "My Data"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:58
+msgid "male"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:60
+msgid "female"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:104
+#: waeup/ikoba/customers/workflow.py:125
+msgid "Reset to requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:105
+#: waeup/ikoba/customers/workflow.py:126
+msgid "Reset to 'requested'"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:112
+#: waeup/ikoba/customers/workflow.py:215
+#: waeup/ikoba/customers/workflow.py:216
+#: waeup/ikoba/customers/workflow.py:222
+#: waeup/ikoba/customers/workflow.py:223
+#: waeup/ikoba/customers/workflow.py:229
+#: waeup/ikoba/customers/workflow.py:230
+#: waeup/ikoba/customers/workflow.py:243
+#: waeup/ikoba/customers/workflow.py:244
+#: waeup/ikoba/customers/workflow.py:334
+#: waeup/ikoba/customers/workflow.py:335
+#: waeup/ikoba/customers/workflow.py:341
+#: waeup/ikoba/customers/workflow.py:342
+#: waeup/ikoba/customers/workflow.py:348
+#: waeup/ikoba/customers/workflow.py:349
+#: waeup/ikoba/customers/workflow.py:362
+#: waeup/ikoba/customers/workflow.py:363
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:186
+msgid "Create contract"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:189
+msgid "Contract created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:194
+msgid "Submit for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:195
+msgid "Submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:201
+msgid "Approve"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:202
+msgid "Approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:208
+#: waeup/ikoba/customers/workflow.py:327
+msgid "Reject"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:209
+#: waeup/ikoba/customers/workflow.py:328
+msgid "REJECTED"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:236
+#: waeup/ikoba/customers/workflow.py:237
+#: waeup/ikoba/customers/workflow.py:355
+#: waeup/ikoba/customers/workflow.py:356
+msgid "Set to expired"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:305
+#: waeup/ikoba/documents/workflow.py:37
+msgid "Create document"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:308
+#: waeup/ikoba/documents/workflow.py:40
+msgid "Document created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:313
+msgid "Submit for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:314
+msgid "Submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:320
+msgid "Verify"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:321
+msgid "Verified"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:46
+msgid "Create customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:49
+msgid "Customer created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:54
+msgid "Start registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:57
+msgid "Customer registration started"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:62
+msgid "Request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:63
+msgid "Customer registration requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:69
+msgid "Approve customer provisionally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:70
+msgid "Customer registration provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:76
+msgid "Approve finally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:77
+msgid "Customer registration finally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:83
+msgid "Approve customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:84
+msgid "Customer registration approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:90
+msgid "Reject customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:91
+msgid "Customer registration rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:97
+#: waeup/ikoba/customers/workflow.py:111
+#: waeup/ikoba/customers/workflow.py:118
+msgid "Reset customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:98
+#: waeup/ikoba/customers/workflow.py:119
+msgid "Reset to initial customer state"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:135
+msgid "Public PDF Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:147
+msgid "Public HTML Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:159
+msgid "Public REST Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:140
+#: waeup/ikoba/products/browser.py:133
+msgid "The id chosen already exists."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:210
+#: waeup/ikoba/documents/fileviewlets.py:80
+msgid "The document requested has not yet been published."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:256
+#: waeup/ikoba/documents/browser.py:282
+#: waeup/ikoba/products/browser.py:185
+#: waeup/ikoba/products/browser.py:215
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:257
+#: waeup/ikoba/documents/browser.py:278
+#: waeup/ikoba/products/browser.py:186
+#: waeup/ikoba/products/browser.py:211
+msgid "Add local role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:267
+msgid "Manage document "
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:342
+msgid "Apply now"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:94
+msgid "Manage document section"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:1
+#: waeup/ikoba/products/browser_templates/containerpage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentaddform.pt:7
+#: waeup/ikoba/customers/browser_templates/documentaddpage.pt:7
+msgid "Document Type"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:12
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:12
+msgid "Files"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:16
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:16
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:21
+msgid "Document Type:"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:37
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:48
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:5
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:37
+msgid "Name"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:39
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:50
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:39
+msgid "Local Role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:6
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:6
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:7
+msgid "Metadata"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:9
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:12
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:7
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:9
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:25
+msgid "Content:"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:101
+msgid "Public HTML Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:110
+msgid "Public REST Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:92
+msgid "Public PDF Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:110
+msgid "Multilingual content in REST (reStructuredText) format"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:58
+msgid "Document Title"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:78
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:97
+#: waeup/ikoba/products/interfaces.py:97
+msgid "Multilingual content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:33
+msgid "published"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:38
+msgid "HTML Document"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:39
+msgid "REST Document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:45
+msgid "Publish document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:48
+msgid "Document published"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:53
+msgid "Retract documet"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:56
+msgid "Document retracted"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:361
+msgid "Processor name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:387
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:394
+msgid "Email to:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:401
+msgid "Subject:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:405
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:409
+msgid "Text:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:438
+#: waeup/ikoba/interfaces.py:494
+msgid "Public Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:448
+#: waeup/ikoba/interfaces.py:449
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:455
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:456
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:485
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:35
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:46
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:35
+msgid "User Id"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:490
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:500
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:517
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:564
+msgid "Name of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:565
+msgid "Sample Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:570
+msgid "Abbreviated Title of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:576
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:588
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:594
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:601
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:602
+msgid "Ikoba Contact"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:607
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:614
+msgid "Notify customers by email"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:619
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:66
+msgid "new"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:67
+msgid "completed"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:68
+msgid "pending"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:69
+msgid "active"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:70
+msgid "assigned"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:71
+msgid "callbacks"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/ikoba/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:100
+msgid "Iranian Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:101
+msgid "Iceland Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:102
+msgid "Jamaican Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:103
+msgid "Jordanian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:104
+msgid "Yen"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:105
+msgid "Kenyan Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:106
+msgid "Som"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:107
+msgid "Riel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:108
+msgid "Comoro Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:110
+msgid "North Korean Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:113
+msgid "Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:114
+msgid "Kuwaiti Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:115
+msgid "Cayman Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:116
+msgid "Tenge"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:117
+msgid "Kip"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:118
+msgid "Lebanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:119
+msgid "Sri Lanka Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:120
+msgid "Liberian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:121
+msgid "Loti"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:122
+msgid "Lithuanian Litas"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:123
+msgid "Libyan Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:124
+msgid "Moroccan Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:125
+msgid "Moldovan Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:126
+msgid "Malagasy Ariary"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:128
+msgid "Denar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:131
+msgid "Kyat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:132
+msgid "Tugrik"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:133
+msgid "Pataca"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:134
+msgid "Ouguiya"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:135
+msgid "Mauritius Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:136
+msgid "Rufiyaa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:137
+msgid "Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:138
+msgid "Mexican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:139
+msgid "Mexican Unidad de Inversion (UDI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:140
+msgid "Malaysian Ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:141
+msgid "Mozambique Metical"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:142
+msgid "Namibia Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:143
+msgid "Naira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:144
+msgid "Cordoba Oro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:145
+msgid "Norwegian Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:146
+msgid "Nepalese Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:147
+msgid "New Zealand Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:148
+msgid "Rial Omani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:149
+msgid "Balboa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:150
+msgid "Nuevo Sol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:151
+msgid "Kina"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:152
+msgid "Philippine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:153
+msgid "Pakistan Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:154
+msgid "Zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:155
+msgid "Guarani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:156
+msgid "Qatari Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:157
+msgid "New Romanian Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:158
+msgid "Serbian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:159
+msgid "Russian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:160
+msgid "Rwanda Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:161
+msgid "Saudi Riyal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:162
+msgid "Solomon Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:163
+msgid "Seychelles Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:164
+msgid "Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:165
+msgid "Swedish Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:166
+msgid "Singapore Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:168
+msgid "Saint Helena Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:171
+msgid "Leone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:172
+msgid "Somali Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:173
+msgid "Surinam Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:174
+msgid "South Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:175
+msgid "Dobra"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:176
+msgid "El Salvador Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:177
+msgid "Syrian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:178
+msgid "Lilangeni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:179
+msgid "Baht"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:180
+msgid "Somoni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:181
+msgid "Turkmenistan New Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:182
+msgid "Tunisian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:183
+msgid "Pa'anga"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:184
+msgid "Turkish Lira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:185
+msgid "Trinidad and Tobago Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:186
+msgid "New Taiwan Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:187
+msgid "Tanzanian Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:188
+msgid "Hryvnia"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:189
+msgid "Uganda Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:190
+msgid "US Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:191
+msgid "US Dollar (Next day)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:193
+msgid "Uruguay Peso en Unidades Indexadas (URUIURUI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:196
+msgid "Peso Uruguayo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:197
+msgid "Uzbekistan Sum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:198
+msgid "Bolivar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:199
+msgid "Dong"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:200
+msgid "Vatu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:201
+msgid "Tala"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:202
+msgid "CFA Franc BEAC"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:204
+msgid "East Caribbean Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:207
+msgid "CFA Franc BCEAO"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:208
+msgid "CFP Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:209
+msgid "Yemeni Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:210
+msgid "Rand"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:211
+msgid "Zambian Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:212
+msgid "Zimbabwe Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:23
+msgid "UAE Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:24
+msgid "Afghani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:25
+msgid "Lek"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:26
+msgid "Armenian Dram"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:28
+msgid "Netherlands Antillean Guilder"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:31
+msgid "Kwanza"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:32
+msgid "Argentine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:33
+msgid "Australian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:34
+msgid "Aruban Florin"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:35
+msgid "Azerbaijanian Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:36
+msgid "Convertible Mark"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:37
+msgid "Barbados Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:38
+msgid "Taka"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:39
+msgid "Bulgarian Lev"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:40
+msgid "Bahraini Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:41
+msgid "Burundi Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:42
+msgid "Bermudian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:43
+msgid "Brunei Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:44
+msgid "Boliviano"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:45
+msgid "Mvdol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:46
+msgid "Brazilian Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:47
+msgid "Bahamian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:48
+msgid "Ngultrum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:49
+msgid "Pula"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:50
+msgid "Belarussian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:51
+msgid "Belize Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:52
+msgid "Canadian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:54
+msgid "Congolese Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:57
+msgid "WIR Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:58
+msgid "Swiss Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:59
+msgid "WIR Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:60
+msgid "Unidad de Fomento"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:61
+msgid "Chilean Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:62
+msgid "Yuan Renminbi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:63
+msgid "Colombian Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:64
+msgid "Unidad de Valor Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:65
+msgid "Costa Rican Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:66
+msgid "Peso Convertible"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:67
+msgid "Cuban Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:68
+msgid "Cabo Verde Escudo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:69
+msgid "Czech Koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:70
+msgid "Djibouti Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:71
+msgid "Danish Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:72
+msgid "Dominican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:73
+msgid "Algerian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:74
+msgid "Egyptian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:75
+msgid "-Nakfa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:76
+msgid "Ethiopian Birr"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:77
+#: waeup/ikoba/payments/paypal_currencies.py:41
+msgid "Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:78
+msgid "Fiji Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:80
+msgid "Falkland Islands Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:83
+msgid "Pound Sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:84
+msgid "Lari"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:85
+msgid "Ghana Cedi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:86
+msgid "Gibraltar Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:87
+msgid "Dalasi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:88
+msgid "Guinea Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:89
+msgid "Quetzal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:90
+msgid "Guyana Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:91
+msgid "Hong Kong Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:92
+msgid "Lempira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:93
+msgid "Croatian Kuna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:94
+msgid "Gourde"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:95
+msgid "Forint"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:96
+msgid "Rupiah"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:97
+msgid "New Israeli Sheqel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:98
+msgid "Indian Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:99
+msgid "Iraqi Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:168
+msgid "Payment State"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:175
+msgid "Creation Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:181
+msgid "Payment Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:187
+msgid "Amount"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:188
+msgid "The overall sum payed, including all taxes fees, etc."
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:217
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:224
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:231
+msgid "Response Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:238
+msgid "Response Description"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:34
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:35
+msgid "Paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:36
+msgid "Failed"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1147
+msgid "Credit Card (PayPal)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1223
+msgid "PayPal Payment"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:73
+msgid "pieces"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:74
+#: waeup/ikoba/payments/paypal.py:1136
+msgid "license"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:10
+msgid "ANGUILLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:100
+msgid "SOUTH KOREA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:101
+msgid "KUWAIT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:102
+msgid "CAYMAN ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:103
+msgid "KAZAKHSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:104
+msgid "LAOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:105
+msgid "SAINT LUCIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:106
+msgid "LIECHTENSTEIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:107
+msgid "SRI LANKA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:108
+msgid "LESOTHO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:109
+msgid "LITHUANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:11
+msgid "ALBANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:110
+msgid "LUXEMBOURG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:111
+msgid "LATVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:112
+msgid "MOROCCO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:113
+msgid "MADAGASCAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:114
+msgid "MARSHALL ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:115
+msgid "MALI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:116
+msgid "MONGOLIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:117
+msgid "MARTINIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:118
+msgid "MAURITANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:119
+msgid "MONTSERRAT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:12
+msgid "ARMENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:120
+msgid "MALTA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:121
+msgid "MAURITIUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:122
+msgid "MALDIVES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:123
+msgid "MALAWI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:124
+msgid "MEXICO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:125
+msgid "MALAYSIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:126
+msgid "MOZAMBIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:127
+msgid "NAMIBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:128
+msgid "NEW CALEDONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:129
+msgid "NIGER"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:13
+msgid "NETHERLANDS ANTILLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:130
+msgid "NORFOLK ISLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:131
+msgid "NICARAGUA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:132
+msgid "NETHERLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:133
+msgid "NORWAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:134
+msgid "NEPAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:135
+msgid "NAURU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:136
+msgid "NIUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:137
+msgid "NEW ZEALAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:138
+msgid "OMAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:139
+msgid "PANAMA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:14
+msgid "ANGOLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:140
+msgid "PERU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:141
+msgid "FRENCH POLYNESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:142
+msgid "PAPUA NEW GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:143
+msgid "PHILIPPINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:144
+msgid "POLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:145
+msgid "SAINT PIERRE AND MIQUELON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:146
+msgid "PITCAIRN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:147
+msgid "PORTUGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:148
+msgid "PALAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:149
+msgid "QATAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:15
+msgid "ARGENTINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:150
+msgid "REUNION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:151
+msgid "ROMANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:152
+msgid "SERBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:153
+msgid "RUSSIAN FEDERATION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:154
+msgid "RWANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:155
+msgid "SAUDI ARABIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:156
+msgid "SOLOMON ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:157
+msgid "SEYCHELLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:158
+msgid "SWEDEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:159
+msgid "SINGAPORE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:16
+msgid "AUSTRIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:160
+msgid "SAINT HELENA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:161
+msgid "SLOVENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:162
+msgid "SVALBARD AND JAN MAYEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:163
+msgid "SLOVAKIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:164
+msgid "SIERRA LEONE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:165
+msgid "SAN MARINO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:166
+msgid "SENEGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:167
+msgid "SOMALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:168
+msgid "SURINAME"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:169
+msgid "SAO TOME AND PRINCIPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:17
+msgid "AUSTRALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:170
+msgid "EL SALVADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:171
+msgid "SWAZILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:172
+msgid "TURKS AND CAICOS ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:173
+msgid "CHAD"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:174
+msgid "TOGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:175
+msgid "THAILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:176
+msgid "TAJIKISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:177
+msgid "TURKMENISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:178
+msgid "TUNISIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:179
+msgid "TONGA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:18
+msgid "ARUBA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:180
+msgid "TURKEY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:181
+msgid "TRINIDAD AND TOBAGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:182
+msgid "TUVALU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:183
+msgid "TAIWAN, PROVINCE OF CHINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:184
+msgid "TANZANIA, UNITED REPUBLIC OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:185
+msgid "UKRAINE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:186
+msgid "UGANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:187
+msgid "UNITED STATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:188
+msgid "URUGUAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:189
+msgid "HOLY SEE (VATICAN CITY STATE)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:19
+msgid "AZERBAIJAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:190
+msgid "SAINT VINCENT AND THE GRENADINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:191
+msgid "VENEZUELA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:192
+msgid "VIRGIN ISLANDS, BRITISH"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:193
+msgid "VIETNAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:194
+msgid "VANUATU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:195
+msgid "WALLIS AND FUTUNA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:196
+msgid "SAMOA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:197
+msgid "YEMEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:198
+msgid "MAYOTTE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:199
+msgid "SOUTH AFRICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:20
+msgid "BOSNIA-HERZEGOVINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:200
+msgid "ZAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:21
+msgid "BARBADOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:22
+msgid "BELGIUM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:23
+msgid "BURKINA FASO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:24
+msgid "BULGARIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:25
+msgid "BAHRAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:26
+msgid "BURUNDI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:27
+msgid "BENIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:28
+msgid "BERMUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:29
+msgid "BRUNEI DARUSSALAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:30
+msgid "BOLIVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:31
+msgid "BRAZIL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:32
+msgid "BAHAMAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:33
+msgid "BHUTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:34
+msgid "BOTSWANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:35
+msgid "BELIZE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:36
+msgid "CANADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:37
+msgid "DEMOCRATIC REPUBLIC OF CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:38
+msgid "CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:39
+msgid "SWITZERLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:40
+msgid "COOK ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:41
+msgid "CHILE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:42
+msgid "CHINA (domestic Chinese bank transactions only)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:43
+msgid "CHINA (For CUP, bank card and cross-border transactions)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:44
+msgid "COLOMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:45
+msgid "COSTA RICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:46
+msgid "CAPE VERDE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:47
+msgid "CYPRUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:48
+msgid "CZECH REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:49
+msgid "GERMANY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:50
+msgid "DJIBOUTI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:51
+msgid "DENMARK"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:52
+msgid "DOMINICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:53
+msgid "DOMINICAN REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:54
+msgid "ALGERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:55
+msgid "ECUADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:56
+msgid "ESTONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:57
+msgid "EGYPT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:58
+msgid "ERITERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:59
+msgid "SPAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:60
+msgid "ETHIOPIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:61
+msgid "FINLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:62
+msgid "FIJI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:63
+msgid "FALKLAND ISLANDS (MALVINAS)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:64
+msgid "MICRONESIA, FEDERATED STATES OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:65
+msgid "FRANCE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:66
+msgid "GABON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:67
+msgid "UNITED KINGDOM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:68
+msgid "GRENADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:69
+msgid "GEORGIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:7
+msgid "ANDORRA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:70
+msgid "FRENCH GUIANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:71
+msgid "GIBRALTAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:72
+msgid "GREENLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:73
+msgid "GAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:74
+msgid "GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:75
+msgid "GUADELOUPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:76
+msgid "GREECE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:77
+msgid "GUATEMALA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:78
+msgid "GUAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:79
+msgid "GUINEA BISSAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:8
+msgid "UNITED ARAB EMIRATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:80
+msgid "GUYANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:81
+msgid "HONG KONG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:82
+msgid "HONDURAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:83
+msgid "CROATIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:84
+msgid "HUNGARY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:85
+msgid "INDONESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:86
+msgid "IRELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:87
+msgid "ISRAEL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:88
+msgid "INDIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:89
+msgid "ICELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:9
+msgid "ANTIGUA AND BARBUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:90
+msgid "ITALY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:91
+msgid "JAMAICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:92
+msgid "JORDAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:93
+msgid "JAPAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:94
+msgid "KENYA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:95
+msgid "KYRGYZSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:96
+msgid "CAMBODIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:97
+msgid "KIRIBATI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:98
+msgid "COMOROS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:99
+msgid "SAINT KITTS AND NEVIS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:35
+msgid "Australian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:36
+msgid "Brazilian real"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:37
+msgid "Canadian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:38
+msgid "Swiss franc"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:39
+msgid "Czech koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:40
+msgid "Danish krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:42
+msgid "Pound sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:43
+msgid "Hong Kong dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:44
+msgid "Hungarian forint"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:45
+msgid "Israeli new shekel"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:46
+msgid "Japanese yen"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:47
+msgid "Mexican peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:48
+msgid "Malaysian ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:49
+msgid "Norwegian krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:50
+msgid "New Zealand dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:51
+msgid "Philippine peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:52
+msgid "Polish z\305\202oty"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:53
+msgid "Swedish krona"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:54
+msgid "Singapore dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:55
+msgid "Thai baht"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:56
+msgid "Turkish lira"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:57
+msgid "New Taiwan dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:58
+msgid "United States dollar"
+msgstr ""
+
+#: waeup/ikoba/products/batching.py:52
+msgid "Product Processor"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:126
+msgid "Create product"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:137
+#. Default: ""
+msgid "Product ${a} added."
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:195
+msgid "Manage product "
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:59
+#: waeup/ikoba/products/browser.py:79
+#: waeup/ikoba/products/export.py:40
+#: waeup/ikoba/products/viewlets.py:49
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:31
+msgid "Products"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:91
+msgid "Manage product section"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:94
+#: waeup/ikoba/products/browser.py:96
+#: waeup/ikoba/products/browser.py:123
+msgid "Add product"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:22
+msgid "Setting parameters no yet configured."
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containerpage.pt:10
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:41
+msgid "Category"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:6
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:6
+msgid "Settings"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productpage.pt:5
+msgid "Terms and Conditions:"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:48
+#: waeup/ikoba/products/browser_templates/containerpage.pt:8
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:37
+msgid "Product Id"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:55
+msgid "Product Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:61
+msgid "Contract Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:62
+msgid "Product title is used if empty."
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:67
+msgid "Contract Category"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:81
+msgid "Valid from"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:87
+msgid "Valid to"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:48
+msgid "Fee"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:52
+msgid "Currency"
+msgstr ""
+
+#: waeup/ikoba/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/ikoba/userscontainer.py:76
+msgid "Users"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:169
+#: waeup/ikoba/utils/utils.py:52
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:188
+msgid "R\303\251union"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:192
+msgid "Saint Barth\303\251lemy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:62
+msgid "C\303\264te d'Ivoire"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:65
+msgid "Cura\303\247ao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:9
+msgid "\303\205land Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:90
+#: waeup/ikoba/utils/utils.py:51
+msgid "Germany"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/ikoba/utils/helpers.py:868
+msgid "indefinite"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:111
+msgid "no contract"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:156
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:208
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Registration and Application Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:256
+msgid ""
+"Dear ${a},\n"
+"\n"
+"The status of the following object has been changed:\n"
+"\n"
+"Object Id: ${c}\n"
+"Title: ${d}\n"
+"Transition: ${e}\n"
+"New state: ${f}\n"
+"\n"
+"Regards,\n"
+"\n"
+"${b}\n"
+"\n"
+"--\n"
+"${g}\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:53
+msgid "U.S."
+msgstr ""
+
+#: waeup/ikoba/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/ikoba/widgets/sequencewidget.pt:15
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#: waeup/ikoba/widgets/sequencewidget.pt:23
+#. Default: "Remove selected items"
+msgid "remove-selected-items"
+msgstr ""
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/yo/LC_MESSAGES/waeup.ikoba.po
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/yo/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/yo/LC_MESSAGES/waeup.ikoba.po	(revision 12677)
@@ -0,0 +1,4905 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.Ikoba\n"
+"POT-Creation-Date: Mon Feb  2 08:48:41 2015\n"
+"PO-Revision-Date: 2014-12-23 08:09+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Nigeria <isouaba@waeup.org>\n"
+"Language: yo_NG\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-SourceCharset: utf-8\n"
+"X-Generator: Poedit 1.6.5\n"
+
+#: waeup/ikoba/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:106
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:119 waeup/ikoba/browser/pages.py:470
+msgid "Administration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:126 waeup/ikoba/browser/viewlets.py:320
+#: waeup/ikoba/browser/viewlets.py:544
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:133 waeup/ikoba/browser/pages.py:517
+#: waeup/ikoba/browser/viewlets.py:329 waeup/ikoba/browser/viewlets.py:554
+msgid "Officers"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:140 waeup/ikoba/browser/pages.py:763
+#: waeup/ikoba/browser/pages.py:1363 waeup/ikoba/browser/viewlets.py:338
+#: waeup/ikoba/browser/viewlets.py:563
+msgid "Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:147 waeup/ikoba/browser/reports.py:37
+#: waeup/ikoba/browser/viewlets.py:347 waeup/ikoba/browser/viewlets.py:572
+msgid "Reports"
+msgstr ""
+
+#: waeup/ikoba/browser/breadcrumbs.py:92
+msgid "Home"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:108
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/ikoba/browser/templates/datacenterpage.pt:17
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:109
+msgid "Scan"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:129
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:130
+msgid "Delete"
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:164
+msgid "No local file selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:44
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:59
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/ikoba/browser/fileviewlets.py:71
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:76
+msgid "${a} file format expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:82
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/fileviewlets.py:92
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:173
+#: waeup/ikoba/customers/interfaces.py:200 waeup/ikoba/interfaces.py:428
+#: waeup/ikoba/interfaces.py:504
+msgid "Email Address"
+msgstr ""
+
+#: waeup/ikoba/browser/interfaces.py:176
+msgid "Enter your email address stored in Ikoba."
+msgstr ""
+
+#: waeup/ikoba/browser/layout.py:102 waeup/ikoba/customers/browser.py:387
+#: waeup/ikoba/customers/browser.py:690 waeup/ikoba/customers/browser.py:973
+#: waeup/ikoba/customers/browser.py:1385 waeup/ikoba/customers/browser.py:1416
+#: waeup/ikoba/documents/browser.py:259
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1039 waeup/ikoba/browser/pages.py:1068
+#: waeup/ikoba/browser/pages.py:1173 waeup/ikoba/browser/pages.py:1285
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1071
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1072
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1117
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:112
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1125
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1138 waeup/ikoba/browser/pages.py:1261
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1150
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1176 waeup/ikoba/browser/pages.py:1404
+msgid "Reset"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1177
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1178
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1179
+msgid "Perform import"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1244
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:125 waeup/ikoba/browser/pages.py:772
+#: waeup/ikoba/customers/browser.py:207
+msgid "No item selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1287
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1337
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1339
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1347
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1350
+msgid "Show"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1365 waeup/ikoba/browser/viewlets.py:185
+msgid "Search"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1367
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1375
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1390
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1393
+msgid "No search results found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1401
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1421
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1424
+msgid "Given storage path cannot be used: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1428
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1440
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1442
+msgid "Create CSV file"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:146 waeup/ikoba/customers/browser.py:220
+#: waeup/ikoba/customers/browser.py:883 waeup/ikoba/customers/browser.py:1253
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1470
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1514
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1540 waeup/ikoba/customers/browser.py:543
+#: waeup/ikoba/customers/browser.py:625
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1550
+msgid "No record found."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:1564 waeup/ikoba/customers/browser.py:589
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:1568
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:188 waeup/ikoba/customers/browser.py:84
+#: waeup/ikoba/customers/browser.py:422
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:259 waeup/ikoba/browser/viewlets.py:266
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:40
+#: waeup/ikoba/browser/templates/customersitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:270
+msgid "You logged in."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:307
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:315
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:318
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:333
+msgid "You have been logged out. Thanks for using WAeUP Ikoba!"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:375
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:381 waeup/ikoba/browser/pages.py:661
+msgid "Send message now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:400 waeup/ikoba/browser/pages.py:433
+#: waeup/ikoba/browser/pages.py:678 waeup/ikoba/customers/browser.py:372
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:420
+msgid "Send now"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:435
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:460
+msgid "<h1>Welcome to WAeUP.Ikoba</h1>"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:518 waeup/ikoba/customers/viewlets.py:207
+#: waeup/ikoba/customers/viewlets.py:332 waeup/ikoba/customers/viewlets.py:404
+#: waeup/ikoba/documents/viewlets.py:60 waeup/ikoba/documents/viewlets.py:79
+#: waeup/ikoba/products/viewlets.py:60 waeup/ikoba/products/viewlets.py:79
+msgid "Manage"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:519 waeup/ikoba/browser/viewlets.py:175
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:526
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:563 waeup/ikoba/browser/pages.py:565
+#: waeup/ikoba/browser/viewlets.py:403
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:587
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:608
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:616 waeup/ikoba/browser/pages.py:737
+#: waeup/ikoba/browser/pages.py:1403 waeup/ikoba/customers/browser.py:400
+#: waeup/ikoba/customers/browser.py:732 waeup/ikoba/customers/browser.py:774
+#: waeup/ikoba/customers/browser.py:990 waeup/ikoba/customers/browser.py:1008
+#: waeup/ikoba/customers/browser.py:1402 waeup/ikoba/customers/browser.py:1426
+#: waeup/ikoba/customers/browser.py:1443 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:269 waeup/ikoba/documents/browser.py:293
+#: waeup/ikoba/documents/browser.py:308 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:106 waeup/ikoba/products/browser.py:184
+#: waeup/ikoba/products/browser.py:197
+msgid "Save"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:642
+msgid "User settings have been saved."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:645 waeup/ikoba/browser/pages.py:1070
+#: waeup/ikoba/browser/pages.py:1175 waeup/ikoba/customers/browser.py:759
+#: waeup/ikoba/customers/browser.py:790 waeup/ikoba/customers/browser.py:946
+#: waeup/ikoba/customers/browser.py:1312 waeup/ikoba/documents/browser.py:106
+#: waeup/ikoba/documents/browser.py:152 waeup/ikoba/documents/browser.py:255
+#: waeup/ikoba/documents/browser.py:273 waeup/ikoba/products/browser.py:93
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:110
+#: waeup/ikoba/products/browser.py:184 waeup/ikoba/products/browser.py:206
+msgid "Cancel"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:659 waeup/ikoba/customers/browser.py:353
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:688 waeup/ikoba/browser/viewlets.py:354
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:701 waeup/ikoba/browser/viewlets.py:370
+msgid "My Roles"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:733
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:745
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:746
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:747
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:752
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:766 waeup/ikoba/customers/browser.py:176
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:101
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:783
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:787
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:798
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:800 waeup/ikoba/browser/pages.py:816
+#: waeup/ikoba/browser/pages.py:1041 waeup/ikoba/browser/pages.py:1349
+#: waeup/ikoba/browser/pages.py:1366 waeup/ikoba/browser/pages.py:1405
+#: waeup/ikoba/browser/pages.py:1443
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:812
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:815
+msgid "Upload"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:855
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:857
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pages.py:861
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:880
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:882 waeup/ikoba/browser/pages.py:1571
+#: waeup/ikoba/customers/browser.py:374
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:890
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:905
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:91
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:913
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/ikoba/browser/pages.py:924
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/ikoba/browser/pdf.py:207 waeup/ikoba/browser/pdf.py:472
+#: waeup/ikoba/browser/pdf.py:482
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:16
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/browser/pdf.py:754
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/ikoba/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/ikoba/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data. Please pick the "
+"type of objects you want to export from the selection below. The file will "
+"be generated and then be made available for you to download in the table "
+"below. Files can only be downloaded within 24 hours after generation. The "
+"download link disappears afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:42
+msgid "Export Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:43
+msgid "Exporter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:44
+msgid "Filter"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:45
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:6
+msgid "Creator"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:46
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:7
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:47
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:8
+msgid "Status"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterexportpage.pt:72
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:39
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/ikoba/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:14
+#: waeup/ikoba/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/ikoba/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:14
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:13
+#: waeup/ikoba/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/filedisplay.pt:15
+msgid "HTML Element:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:13
+msgid "Password"
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/ikoba/browser/templates/loginpage.pt:26
+msgid "login_trouble1"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer, or your customer record has been created for you? Acquire a Password Activation Code (PWD) and inititialize your customer account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:29
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer? Create your customer account <strong><a href=\"createaccount\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:34
+msgid "login_trouble4"
+msgstr ""
+
+#. Default: "You don't have an account because you are a new customer and your customer record has been created for you? Inititialize your customer account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:38
+msgid "login_trouble5"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/loginpage.pt:4
+msgid "Identifier"
+msgstr ""
+
+#. Default: "Or simply forgot your id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/ikoba/browser/templates/loginpage.pt:42
+msgid "login_trouble3"
+msgstr ""
+
+#. Default: "Customers can enter either their customer id, registration number or email address. Officers must enter their user name."
+#: waeup/ikoba/browser/templates/loginpage.pt:7
+msgid "identifier_hint"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:46
+#: waeup/ikoba/browser/templates/customersitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:52
+#: waeup/ikoba/browser/templates/customersitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/staffsitelayout.pt:6
+#: waeup/ikoba/browser/templates/customersitelayout.pt:4
+msgid "WAeUP.Ikoba - Registration and Application System"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:15
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:16
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:30
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/templates/usereditformpage.pt:9
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:27
+#: waeup/ikoba/customers/browser_templates/changepassword.pt:10
+#: waeup/ikoba/customers/browser_templates/basepage.pt:14
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:24
+msgid "Password:"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:165
+msgid "Add"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:305
+msgid "Any link text"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:387 waeup/ikoba/customers/viewlets.py:198
+msgid "Send email"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:396
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:412
+msgid "Show logs"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:421
+msgid "Upload data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:430
+msgid "Process data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:439
+msgid "Export data"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:448
+msgid "View processed files"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:508
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/ikoba/browser/viewlets.py:93 waeup/ikoba/browser/viewlets.py:155
+#: waeup/ikoba/customers/viewlets.py:284 waeup/ikoba/customers/viewlets.py:347
+#: waeup/ikoba/customers/viewlets.py:422
+msgid "Edit"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:142
+msgid "Customer record created"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/batching.py:197
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:461
+msgid "Customer Sample Document Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:562
+msgid "Customer Sample Contract Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/batching.py:60
+msgid "Customer Processor"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1013
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1017 waeup/ikoba/customers/browser.py:1452
+msgid "Form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1029 waeup/ikoba/documents/browser.py:324
+msgid "Trigger document transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:106 waeup/ikoba/customers/export.py:130
+#: waeup/ikoba/customers/viewlets.py:54
+msgid "Customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1077
+msgid "Documents of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1085
+msgid "Customer Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1091
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:8
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:9
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:4
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:8
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:8
+msgid "Id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1092 waeup/ikoba/customers/browser.py:1527
+#: waeup/ikoba/products/productoptions.py:44
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:9
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:9
+#: waeup/ikoba/products/browser_templates/containerpage.pt:9
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:39
+msgid "Title"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1093 waeup/ikoba/customers/browser.py:1528
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:10
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:10
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:10
+msgid "Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1094 waeup/ikoba/customers/browser.py:1529
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1095 waeup/ikoba/customers/browser.py:1530
+#: waeup/ikoba/customers/browser_templates/contractsmanagepage.pt:12
+#: waeup/ikoba/customers/browser_templates/documentsmanagepage.pt:12
+msgid "Last Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1181 waeup/ikoba/customers/viewlets.py:160
+msgid "Contracts"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:1226
+msgid "${a}: Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1229 waeup/ikoba/customers/browser.py:1270
+#: waeup/ikoba/customers/browser.py:1295
+msgid "Add contract"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1234
+msgid "Remove selected contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1240
+msgid "No contract selected."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:132
+msgid "Find customers"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1325
+msgid "Select product"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:133 waeup/ikoba/customers/browser.py:175
+msgid "Find customer(s)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1336
+msgid "Save and proceed"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1448
+msgid "Apply now (final submit)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1464
+msgid "Trigger contract transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:150 waeup/ikoba/customers/browser.py:192
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1512
+msgid "Contracts of"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:1520
+msgid "Customer Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:156 waeup/ikoba/customers/browser.py:198
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:162 waeup/ikoba/customers/browser.py:205
+#: waeup/ikoba/customers/browser.py:563 waeup/ikoba/customers/browser.py:575
+msgid "No customer found."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:174 waeup/ikoba/customers/viewlets.py:180
+msgid "Manage customer section"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:233 waeup/ikoba/customers/viewlets.py:188
+msgid "Add customer"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:236
+msgid "Create customer record"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:241
+msgid "Customer created."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:256
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:279
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:283
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:289
+msgid "You successfully logged in as customer."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:312
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:314
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:320
+msgid "set"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:321
+msgid "unset"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:385
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:435
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:447 waeup/ikoba/customers/browser.py:1044
+#: waeup/ikoba/customers/browser.py:1479 waeup/ikoba/documents/browser.py:339
+msgid "No transition"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:450 waeup/ikoba/customers/browser.py:1047
+#: waeup/ikoba/customers/browser.py:1482
+msgid "Apply"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:457 waeup/ikoba/customers/browser.py:1055
+#: waeup/ikoba/customers/browser.py:1490 waeup/ikoba/documents/browser.py:350
+msgid "Transition '%s' executed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:474
+msgid "Customer account has been activated."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:494
+msgid "Customer account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:513
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:523
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:557
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:566
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:610
+msgid "Create customer account"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:64
+msgid ""
+"You can not edit some data after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:648
+msgid "You have successfully created a customer account for the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:669
+msgid "Your request was successful."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:67
+msgid ""
+"You can not edit your document after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:688 waeup/ikoba/customers/viewlets.py:303
+msgid "Upload files"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:70
+msgid ""
+"You can not edit your contract after final submission. You really want to "
+"submit?"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:721
+msgid "Edit base data and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:722
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:741
+msgid "Passport picture is missing."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:746
+msgid "Save and request registration now"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:755
+msgid "Registration form has been submitted."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:771 waeup/ikoba/customers/viewlets.py:293
+msgid "Change password"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:785
+msgid "Password changed."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:816 waeup/ikoba/customers/viewlets.py:166
+#: waeup/ikoba/documents/browser.py:62 waeup/ikoba/documents/browser.py:82
+#: waeup/ikoba/documents/viewlets.py:49
+#: waeup/ikoba/documents/browser_templates/containermanagepage.pt:3
+msgid "Documents"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:856
+msgid "${a}: Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:859 waeup/ikoba/customers/browser.py:900
+#: waeup/ikoba/customers/browser.py:924 waeup/ikoba/documents/browser.py:96
+#: waeup/ikoba/documents/browser.py:119 waeup/ikoba/documents/browser.py:129
+msgid "Add document"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:864 waeup/ikoba/documents/browser.py:101
+msgid "Remove selected documents"
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:870
+msgid "No document selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/customers/browser.py:935 waeup/ikoba/customers/browser.py:1306
+#: waeup/ikoba/documents/browser.py:144
+msgid "${a} added."
+msgstr ""
+
+#: waeup/ikoba/customers/browser.py:94
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:56
+msgid "Select all"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:10
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:11
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:12
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:12
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:23
+msgid "Search Results"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:32
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:9
+#: waeup/ikoba/customers/browser_templates/containermanagepage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractaddpage.pt:7
+msgid "Contract Type"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:20
+msgid "Contract Type:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:19
+msgid ""
+"A customer record will be created and your login credentials will be sent to "
+"the email address given above. To be able to proceed you must provide a "
+"valid email address!"
+msgstr ""
+
+#. Default: "Ikoba is creating a temporary customer password which will replace the original password chosen by the customer. The password will be valid for ${minutes} minutes. The customer can't login during this time period. After expiration the customer can login with the original password again."
+#: waeup/ikoba/customers/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/ikoba/customers/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:19
+msgid ""
+"Your customer record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpw.pt:8
+#: waeup/ikoba/customers/browser_templates/createaccount.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:14
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:44
+msgid "Thanks for using Ikoba!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:3
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/ikoba/customers/browser_templates/requestpwmailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:147
+#: waeup/ikoba/customers/documents.py:162
+msgid "Customer has not yet been approved."
+msgstr ""
+
+#: waeup/ikoba/customers/contracts.py:153
+msgid "Attached documents must be verified first."
+msgstr ""
+
+#: waeup/ikoba/customers/documents.py:165
+msgid "No file uploaded."
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:194
+msgid "Customer Sample Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/export.py:246
+msgid "Customer Sample Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:142
+#: waeup/ikoba/customers/fileviewlets.py:143
+#: waeup/ikoba/documents/fileviewlets.py:39
+#: waeup/ikoba/documents/fileviewlets.py:40
+msgid "PDF File"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:167
+#: waeup/ikoba/customers/fileviewlets.py:168
+#: waeup/ikoba/documents/fileviewlets.py:57
+#: waeup/ikoba/documents/fileviewlets.py:58
+msgid "PDF Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:48
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:60
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/ikoba/customers/fileviewlets.py:90
+#: waeup/ikoba/customers/fileviewlets.py:91
+#: waeup/ikoba/customers/fileviewlets.py:115
+#: waeup/ikoba/customers/fileviewlets.py:116
+msgid "Sample Scan"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:101
+#: waeup/ikoba/customers/interfaces.py:195
+#: waeup/ikoba/customers/interfaces.py:213
+msgid "First Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:106
+#: waeup/ikoba/customers/interfaces.py:218
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:111
+#: waeup/ikoba/customers/interfaces.py:223
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:116
+msgid "Sex"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:122
+#: waeup/ikoba/customers/interfaces.py:161
+#: waeup/ikoba/customers/interfaces.py:190
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:129
+#: waeup/ikoba/customers/interfaces.py:228
+msgid "Email"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:136 waeup/ikoba/interfaces.py:433
+#: waeup/ikoba/interfaces.py:511
+msgid "Phone"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:252
+#: waeup/ikoba/documents/interfaces.py:52
+msgid "Document Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:303
+msgid "Contract Id"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:309
+#: waeup/ikoba/customers/interfaces.py:332
+msgid "Product"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:315
+#: waeup/ikoba/customers/interfaces.py:365
+#: waeup/ikoba/products/interfaces.py:73
+msgid "Options/Fees"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:344
+#: waeup/ikoba/customers/interfaces.py:379
+msgid "Document"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:355
+msgid "Comment"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:82
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:88
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:90
+msgid ""
+"This message will be shown if and only if deactivated customers try to login."
+msgstr ""
+
+#: waeup/ikoba/customers/interfaces.py:96
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:31
+msgid "Customer Id"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:161
+msgid "Name:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:180
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:240
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:25
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:30
+msgid "Connected Files"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:301 waeup/ikoba/customers/utils.py:309
+#: waeup/ikoba/customers/utils.py:317 waeup/ikoba/documents/utils.py:32
+msgid "created"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:302
+msgid "started"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:303
+msgid "requested"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:304 waeup/ikoba/customers/utils.py:311
+msgid "approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:305
+msgid "provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:310
+msgid "submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:312 waeup/ikoba/customers/utils.py:320
+msgid "rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:313 waeup/ikoba/customers/utils.py:321
+msgid "expired"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:318
+msgid "submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:319
+msgid "verified"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:325
+msgid "Sample Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:326 waeup/ikoba/documents/utils.py:37
+msgid "PDF Document"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:330
+msgid "Sample Contract"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:374
+msgid "Contractor"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:377
+msgid "Document Owner"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:386
+msgid "Contract Data"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:401
+msgid "Transaction History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:407
+msgid "Registration History"
+msgstr ""
+
+#: waeup/ikoba/customers/utils.py:425 waeup/ikoba/products/interfaces.py:95
+#: waeup/ikoba/customers/browser_templates/contractpage.pt:32
+msgid "Terms and Conditions"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:114
+msgid "My Contracts"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:115
+msgid "My Documents"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:116 waeup/ikoba/customers/viewlets.py:172
+msgid "History"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:137 waeup/ikoba/customers/viewlets.py:154
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:217 waeup/ikoba/customers/viewlets.py:363
+#: waeup/ikoba/customers/viewlets.py:438 waeup/ikoba/documents/viewlets.py:89
+msgid "Transition"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:227
+msgid "Login as customer"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:236
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:248 waeup/ikoba/customers/viewlets.py:269
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:257
+msgid "Activate account"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:283
+msgid "Edit and request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:323
+msgid "Download documents overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:372 waeup/ikoba/customers/viewlets.py:447
+#: waeup/ikoba/documents/viewlets.py:69 waeup/ikoba/products/viewlets.py:69
+msgid "View"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:383
+msgid "Download document slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:395
+msgid "Download contracts overview"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:413
+msgid "Manage official data"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:462
+msgid "Download contract slip"
+msgstr ""
+
+#: waeup/ikoba/customers/viewlets.py:98
+msgid "My Data"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:58
+msgid "male"
+msgstr ""
+
+#: waeup/ikoba/customers/vocabularies.py:60
+msgid "female"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:103 waeup/ikoba/customers/workflow.py:124
+msgid "Reset to requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:104 waeup/ikoba/customers/workflow.py:125
+msgid "Reset to 'requested'"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:111 waeup/ikoba/customers/workflow.py:209
+#: waeup/ikoba/customers/workflow.py:210 waeup/ikoba/customers/workflow.py:216
+#: waeup/ikoba/customers/workflow.py:217 waeup/ikoba/customers/workflow.py:223
+#: waeup/ikoba/customers/workflow.py:224 waeup/ikoba/customers/workflow.py:237
+#: waeup/ikoba/customers/workflow.py:238 waeup/ikoba/customers/workflow.py:324
+#: waeup/ikoba/customers/workflow.py:325 waeup/ikoba/customers/workflow.py:331
+#: waeup/ikoba/customers/workflow.py:332 waeup/ikoba/customers/workflow.py:338
+#: waeup/ikoba/customers/workflow.py:339 waeup/ikoba/customers/workflow.py:352
+#: waeup/ikoba/customers/workflow.py:353
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:180
+msgid "Create contract"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:183
+msgid "Contract created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:188
+msgid "Submit for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:189
+msgid "Submitted for approval"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:195
+msgid "Approve"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:196
+msgid "Approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:202 waeup/ikoba/customers/workflow.py:317
+msgid "Reject"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:203 waeup/ikoba/customers/workflow.py:318
+msgid "REJECTED"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:230 waeup/ikoba/customers/workflow.py:231
+#: waeup/ikoba/customers/workflow.py:345 waeup/ikoba/customers/workflow.py:346
+msgid "Set to expired"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:295 waeup/ikoba/documents/workflow.py:37
+msgid "Create document"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:298 waeup/ikoba/documents/workflow.py:40
+msgid "Document created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:303
+msgid "Submit for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:304
+msgid "Submitted for verification"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:310
+msgid "Verify"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:311
+msgid "Verified"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:45
+msgid "Create customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:48
+msgid "Customer created"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:53
+msgid "Start registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:56
+msgid "Customer registration started"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:61
+msgid "Request registration"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:62
+msgid "Customer registration requested"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:68
+msgid "Approve customer provisionally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:69
+msgid "Customer registration provisionally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:75
+msgid "Approve finally"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:76
+msgid "Customer registration finally approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:82
+msgid "Approve customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:83
+msgid "Customer registration approved"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:89
+msgid "Reject customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:90
+msgid "Customer registration rejected"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:96 waeup/ikoba/customers/workflow.py:110
+#: waeup/ikoba/customers/workflow.py:117
+msgid "Reset customer"
+msgstr ""
+
+#: waeup/ikoba/customers/workflow.py:97 waeup/ikoba/customers/workflow.py:118
+msgid "Reset to initial customer state"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:135
+msgid "Public PDF Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:147
+msgid "Public HTML Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/batching.py:159
+msgid "Public REST Document Processor"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:140 waeup/ikoba/products/browser.py:133
+msgid "The id chosen already exists."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:210
+#: waeup/ikoba/documents/fileviewlets.py:80
+msgid "The document requested has not yet been published."
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:256 waeup/ikoba/documents/browser.py:282
+#: waeup/ikoba/products/browser.py:185 waeup/ikoba/products/browser.py:215
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:257 waeup/ikoba/documents/browser.py:278
+#: waeup/ikoba/products/browser.py:186 waeup/ikoba/products/browser.py:211
+msgid "Add local role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:267
+msgid "Manage document "
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:342
+msgid "Apply now"
+msgstr ""
+
+#: waeup/ikoba/documents/browser.py:94
+msgid "Manage document section"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/containerpage.pt:1
+#: waeup/ikoba/products/browser_templates/containerpage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentaddform.pt:7
+#: waeup/ikoba/customers/browser_templates/documentaddpage.pt:7
+msgid "Document Type"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:9
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:12
+#: waeup/ikoba/customers/browser_templates/basemanagepage.pt:12
+msgid "Files"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/documentpage.pt:16
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:16
+#: waeup/ikoba/customers/browser_templates/documentpage.pt:21
+msgid "Document Type:"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:37
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:48
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:5
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:37
+msgid "Name"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:39
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:50
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:39
+msgid "Local Role"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:6
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:6
+#: waeup/ikoba/customers/browser_templates/documenteditpage.pt:7
+msgid "Metadata"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:9
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:12
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:7
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:9
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/ikoba/documents/browser_templates/htmldocumentpage.pt:25
+msgid "Content:"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:101
+msgid "Public HTML Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:110
+msgid "Public REST Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/export.py:92
+msgid "Public PDF Documents"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:110
+msgid "Multilingual content in REST (reStructuredText) format"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:58
+msgid "Document Title"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:78
+#: waeup/ikoba/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/ikoba/documents/interfaces.py:97
+#: waeup/ikoba/products/interfaces.py:97
+msgid "Multilingual content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:33
+msgid "published"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:38
+msgid "HTML Document"
+msgstr ""
+
+#: waeup/ikoba/documents/utils.py:39
+msgid "REST Document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:45
+msgid "Publish document"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:48
+msgid "Document published"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:53
+msgid "Retract documet"
+msgstr ""
+
+#: waeup/ikoba/documents/workflow.py:56
+msgid "Document retracted"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:361
+msgid "Processor name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:387
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:394
+msgid "Email to:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:401
+msgid "Subject:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:405
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:409
+msgid "Text:"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:438 waeup/ikoba/interfaces.py:494
+msgid "Public Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:448 waeup/ikoba/interfaces.py:449
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:455
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:456
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:485
+#: waeup/ikoba/documents/browser_templates/htmldocumentmanagepage.pt:35
+#: waeup/ikoba/documents/browser_templates/documentmanagepage.pt:46
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:35
+msgid "User Id"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:490
+#: waeup/ikoba/customers/browser_templates/containerpage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:500
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:517
+#: waeup/ikoba/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:564
+msgid "Name of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:565
+msgid "Sample Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:570
+msgid "Abbreviated Title of Company"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:576
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:588
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:594
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:601
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:602
+msgid "Ikoba Contact"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:607
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:614
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:66
+msgid "new"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:67
+msgid "completed"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:68
+msgid "pending"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:69
+msgid "active"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:70
+msgid "assigned"
+msgstr ""
+
+#: waeup/ikoba/interfaces.py:71
+msgid "callbacks"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/ikoba/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/ikoba/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/ikoba/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:100
+msgid "Iranian Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:101
+msgid "Iceland Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:102
+msgid "Jamaican Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:103
+msgid "Jordanian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:104
+msgid "Yen"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:105
+msgid "Kenyan Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:106
+msgid "Som"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:107
+msgid "Riel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:108
+msgid "Comoro Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:110
+msgid "North Korean Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:113
+msgid "Won"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:114
+msgid "Kuwaiti Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:115
+msgid "Cayman Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:116
+msgid "Tenge"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:117
+msgid "Kip"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:118
+msgid "Lebanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:119
+msgid "Sri Lanka Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:120
+msgid "Liberian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:121
+msgid "Loti"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:122
+msgid "Lithuanian Litas"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:123
+msgid "Libyan Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:124
+msgid "Moroccan Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:125
+msgid "Moldovan Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:126
+msgid "Malagasy Ariary"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:128
+msgid "Denar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:131
+msgid "Kyat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:132
+msgid "Tugrik"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:133
+msgid "Pataca"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:134
+msgid "Ouguiya"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:135
+msgid "Mauritius Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:136
+msgid "Rufiyaa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:137
+msgid "Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:138
+msgid "Mexican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:139
+msgid "Mexican Unidad de Inversion (UDI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:140
+msgid "Malaysian Ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:141
+msgid "Mozambique Metical"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:142
+msgid "Namibia Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:143
+msgid "Naira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:144
+msgid "Cordoba Oro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:145
+msgid "Norwegian Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:146
+msgid "Nepalese Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:147
+msgid "New Zealand Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:148
+msgid "Rial Omani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:149
+msgid "Balboa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:150
+msgid "Nuevo Sol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:151
+msgid "Kina"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:152
+msgid "Philippine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:153
+msgid "Pakistan Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:154
+msgid "Zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:155
+msgid "Guarani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:156
+msgid "Qatari Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:157
+msgid "New Romanian Leu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:158
+msgid "Serbian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:159
+msgid "Russian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:160
+msgid "Rwanda Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:161
+msgid "Saudi Riyal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:162
+msgid "Solomon Islands Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:163
+msgid "Seychelles Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:164
+msgid "Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:165
+msgid "Swedish Krona"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:166
+msgid "Singapore Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:168
+msgid "Saint Helena Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:171
+msgid "Leone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:172
+msgid "Somali Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:173
+msgid "Surinam Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:174
+msgid "South Sudanese Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:175
+msgid "Dobra"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:176
+msgid "El Salvador Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:177
+msgid "Syrian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:178
+msgid "Lilangeni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:179
+msgid "Baht"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:180
+msgid "Somoni"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:181
+msgid "Turkmenistan New Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:182
+msgid "Tunisian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:183
+msgid "Pa'anga"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:184
+msgid "Turkish Lira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:185
+msgid "Trinidad and Tobago Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:186
+msgid "New Taiwan Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:187
+msgid "Tanzanian Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:188
+msgid "Hryvnia"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:189
+msgid "Uganda Shilling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:190
+msgid "US Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:191
+msgid "US Dollar (Next day)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:193
+msgid "Uruguay Peso en Unidades Indexadas (URUIURUI)"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:196
+msgid "Peso Uruguayo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:197
+msgid "Uzbekistan Sum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:198
+msgid "Bolivar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:199
+msgid "Dong"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:200
+msgid "Vatu"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:201
+msgid "Tala"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:202
+msgid "CFA Franc BEAC"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:204
+msgid "East Caribbean Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:207
+msgid "CFA Franc BCEAO"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:208
+msgid "CFP Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:209
+msgid "Yemeni Rial"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:210
+msgid "Rand"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:211
+msgid "Zambian Kwacha"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:212
+msgid "Zimbabwe Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:23
+msgid "UAE Dirham"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:24
+msgid "Afghani"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:25
+msgid "Lek"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:26
+msgid "Armenian Dram"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:28
+msgid "Netherlands Antillean Guilder"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:31
+msgid "Kwanza"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:32
+msgid "Argentine Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:33
+msgid "Australian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:34
+msgid "Aruban Florin"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:35
+msgid "Azerbaijanian Manat"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:36
+msgid "Convertible Mark"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:37
+msgid "Barbados Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:38
+msgid "Taka"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:39
+msgid "Bulgarian Lev"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:40
+msgid "Bahraini Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:41
+msgid "Burundi Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:42
+msgid "Bermudian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:43
+msgid "Brunei Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:44
+msgid "Boliviano"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:45
+msgid "Mvdol"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:46
+msgid "Brazilian Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:47
+msgid "Bahamian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:48
+msgid "Ngultrum"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:49
+msgid "Pula"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:50
+msgid "Belarussian Ruble"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:51
+msgid "Belize Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:52
+msgid "Canadian Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:54
+msgid "Congolese Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:57
+msgid "WIR Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:58
+msgid "Swiss Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:59
+msgid "WIR Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:60
+msgid "Unidad de Fomento"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:61
+msgid "Chilean Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:62
+msgid "Yuan Renminbi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:63
+msgid "Colombian Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:64
+msgid "Unidad de Valor Real"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:65
+msgid "Costa Rican Colon"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:66
+msgid "Peso Convertible"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:67
+msgid "Cuban Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:68
+msgid "Cabo Verde Escudo"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:69
+msgid "Czech Koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:70
+msgid "Djibouti Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:71
+msgid "Danish Krone"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:72
+msgid "Dominican Peso"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:73
+msgid "Algerian Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:74
+msgid "Egyptian Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:75
+msgid "-Nakfa"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:76
+msgid "Ethiopian Birr"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:77
+#: waeup/ikoba/payments/paypal_currencies.py:40
+msgid "Euro"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:78
+msgid "Fiji Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:80
+msgid "Falkland Islands Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:83
+msgid "Pound Sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:84
+msgid "Lari"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:85
+msgid "Ghana Cedi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:86
+msgid "Gibraltar Pound"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:87
+msgid "Dalasi"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:88
+msgid "Guinea Franc"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:89
+msgid "Quetzal"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:90
+msgid "Guyana Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:91
+msgid "Hong Kong Dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:92
+msgid "Lempira"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:93
+msgid "Croatian Kuna"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:94
+msgid "Gourde"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:95
+msgid "Forint"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:96
+msgid "Rupiah"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:97
+msgid "New Israeli Sheqel"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:98
+msgid "Indian Rupee"
+msgstr ""
+
+#: waeup/ikoba/payments/currencies.py:99
+msgid "Iraqi Dinar"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:168
+msgid "Payment State"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:175
+msgid "Creation Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:181
+msgid "Payment Datetime"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:187
+msgid "Amount"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:188
+msgid "The overall sum payed, including all taxes fees, etc."
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:217
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:224
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:231
+msgid "Response Code"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:238
+msgid "Response Description"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:34
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:35
+msgid "Paid"
+msgstr ""
+
+#: waeup/ikoba/payments/interfaces.py:36
+msgid "Failed"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1147
+msgid "Credit Card (PayPal)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:1223
+msgid "PayPal Payment"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:73
+msgid "pieces"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal.py:74 waeup/ikoba/payments/paypal.py:1136
+msgid "license"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:10
+msgid "ANGUILLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:100
+msgid "SOUTH KOREA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:101
+msgid "KUWAIT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:102
+msgid "CAYMAN ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:103
+msgid "KAZAKHSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:104
+msgid "LAOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:105
+msgid "SAINT LUCIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:106
+msgid "LIECHTENSTEIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:107
+msgid "SRI LANKA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:108
+msgid "LESOTHO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:109
+msgid "LITHUANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:11
+msgid "ALBANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:110
+msgid "LUXEMBOURG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:111
+msgid "LATVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:112
+msgid "MOROCCO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:113
+msgid "MADAGASCAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:114
+msgid "MARSHALL ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:115
+msgid "MALI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:116
+msgid "MONGOLIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:117
+msgid "MARTINIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:118
+msgid "MAURITANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:119
+msgid "MONTSERRAT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:12
+msgid "ARMENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:120
+msgid "MALTA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:121
+msgid "MAURITIUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:122
+msgid "MALDIVES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:123
+msgid "MALAWI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:124
+msgid "MEXICO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:125
+msgid "MALAYSIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:126
+msgid "MOZAMBIQUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:127
+msgid "NAMIBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:128
+msgid "NEW CALEDONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:129
+msgid "NIGER"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:13
+msgid "NETHERLANDS ANTILLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:130
+msgid "NORFOLK ISLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:131
+msgid "NICARAGUA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:132
+msgid "NETHERLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:133
+msgid "NORWAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:134
+msgid "NEPAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:135
+msgid "NAURU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:136
+msgid "NIUE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:137
+msgid "NEW ZEALAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:138
+msgid "OMAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:139
+msgid "PANAMA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:14
+msgid "ANGOLA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:140
+msgid "PERU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:141
+msgid "FRENCH POLYNESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:142
+msgid "PAPUA NEW GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:143
+msgid "PHILIPPINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:144
+msgid "POLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:145
+msgid "SAINT PIERRE AND MIQUELON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:146
+msgid "PITCAIRN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:147
+msgid "PORTUGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:148
+msgid "PALAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:149
+msgid "QATAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:15
+msgid "ARGENTINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:150
+msgid "REUNION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:151
+msgid "ROMANIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:152
+msgid "SERBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:153
+msgid "RUSSIAN FEDERATION"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:154
+msgid "RWANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:155
+msgid "SAUDI ARABIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:156
+msgid "SOLOMON ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:157
+msgid "SEYCHELLES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:158
+msgid "SWEDEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:159
+msgid "SINGAPORE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:16
+msgid "AUSTRIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:160
+msgid "SAINT HELENA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:161
+msgid "SLOVENIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:162
+msgid "SVALBARD AND JAN MAYEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:163
+msgid "SLOVAKIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:164
+msgid "SIERRA LEONE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:165
+msgid "SAN MARINO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:166
+msgid "SENEGAL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:167
+msgid "SOMALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:168
+msgid "SURINAME"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:169
+msgid "SAO TOME AND PRINCIPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:17
+msgid "AUSTRALIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:170
+msgid "EL SALVADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:171
+msgid "SWAZILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:172
+msgid "TURKS AND CAICOS ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:173
+msgid "CHAD"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:174
+msgid "TOGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:175
+msgid "THAILAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:176
+msgid "TAJIKISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:177
+msgid "TURKMENISTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:178
+msgid "TUNISIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:179
+msgid "TONGA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:18
+msgid "ARUBA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:180
+msgid "TURKEY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:181
+msgid "TRINIDAD AND TOBAGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:182
+msgid "TUVALU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:183
+msgid "TAIWAN, PROVINCE OF CHINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:184
+msgid "TANZANIA, UNITED REPUBLIC OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:185
+msgid "UKRAINE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:186
+msgid "UGANDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:187
+msgid "UNITED STATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:188
+msgid "URUGUAY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:189
+msgid "HOLY SEE (VATICAN CITY STATE)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:19
+msgid "AZERBAIJAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:190
+msgid "SAINT VINCENT AND THE GRENADINES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:191
+msgid "VENEZUELA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:192
+msgid "VIRGIN ISLANDS, BRITISH"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:193
+msgid "VIETNAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:194
+msgid "VANUATU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:195
+msgid "WALLIS AND FUTUNA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:196
+msgid "SAMOA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:197
+msgid "YEMEN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:198
+msgid "MAYOTTE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:199
+msgid "SOUTH AFRICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:20
+msgid "BOSNIA-HERZEGOVINA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:200
+msgid "ZAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:21
+msgid "BARBADOS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:22
+msgid "BELGIUM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:23
+msgid "BURKINA FASO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:24
+msgid "BULGARIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:25
+msgid "BAHRAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:26
+msgid "BURUNDI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:27
+msgid "BENIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:28
+msgid "BERMUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:29
+msgid "BRUNEI DARUSSALAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:30
+msgid "BOLIVIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:31
+msgid "BRAZIL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:32
+msgid "BAHAMAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:33
+msgid "BHUTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:34
+msgid "BOTSWANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:35
+msgid "BELIZE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:36
+msgid "CANADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:37
+msgid "DEMOCRATIC REPUBLIC OF CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:38
+msgid "CONGO"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:39
+msgid "SWITZERLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:40
+msgid "COOK ISLANDS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:41
+msgid "CHILE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:42
+msgid "CHINA (domestic Chinese bank transactions only)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:43
+msgid "CHINA (For CUP, bank card and cross-border transactions)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:44
+msgid "COLOMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:45
+msgid "COSTA RICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:46
+msgid "CAPE VERDE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:47
+msgid "CYPRUS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:48
+msgid "CZECH REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:49
+msgid "GERMANY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:50
+msgid "DJIBOUTI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:51
+msgid "DENMARK"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:52
+msgid "DOMINICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:53
+msgid "DOMINICAN REPUBLIC"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:54
+msgid "ALGERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:55
+msgid "ECUADOR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:56
+msgid "ESTONIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:57
+msgid "EGYPT"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:58
+msgid "ERITERIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:59
+msgid "SPAIN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:60
+msgid "ETHIOPIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:61
+msgid "FINLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:62
+msgid "FIJI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:63
+msgid "FALKLAND ISLANDS (MALVINAS)"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:64
+msgid "MICRONESIA, FEDERATED STATES OF"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:65
+msgid "FRANCE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:66
+msgid "GABON"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:67
+msgid "UNITED KINGDOM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:68
+msgid "GRENADA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:69
+msgid "GEORGIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:7
+msgid "ANDORRA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:70
+msgid "FRENCH GUIANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:71
+msgid "GIBRALTAR"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:72
+msgid "GREENLAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:73
+msgid "GAMBIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:74
+msgid "GUINEA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:75
+msgid "GUADELOUPE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:76
+msgid "GREECE"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:77
+msgid "GUATEMALA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:78
+msgid "GUAM"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:79
+msgid "GUINEA BISSAU"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:8
+msgid "UNITED ARAB EMIRATES"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:80
+msgid "GUYANA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:81
+msgid "HONG KONG"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:82
+msgid "HONDURAS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:83
+msgid "CROATIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:84
+msgid "HUNGARY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:85
+msgid "INDONESIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:86
+msgid "IRELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:87
+msgid "ISRAEL"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:88
+msgid "INDIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:89
+msgid "ICELAND"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:9
+msgid "ANTIGUA AND BARBUDA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:90
+msgid "ITALY"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:91
+msgid "JAMAICA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:92
+msgid "JORDAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:93
+msgid "JAPAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:94
+msgid "KENYA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:95
+msgid "KYRGYZSTAN"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:96
+msgid "CAMBODIA"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:97
+msgid "KIRIBATI"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:98
+msgid "COMOROS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_countries.py:99
+msgid "SAINT KITTS AND NEVIS"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:34
+msgid "Australian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:35
+msgid "Brazilian real"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:36
+msgid "Canadian dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:37
+msgid "Swiss franc"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:38
+msgid "Czech koruna"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:39
+msgid "Danish krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:41
+msgid "Pound sterling"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:42
+msgid "Hong Kong dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:43
+msgid "Hungarian forint"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:44
+msgid "Israeli new shekel"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:45
+msgid "Japanese yen"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:46
+msgid "Mexican peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:47
+msgid "Malaysian ringgit"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:48
+msgid "Norwegian krone"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:49
+msgid "New Zealand dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:50
+msgid "Philippine peso"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:51
+msgid "Polish zloty"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:52
+msgid "Swedish krona"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:53
+msgid "Singapore dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:54
+msgid "Thai baht"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:55
+msgid "Turkish lira"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:56
+msgid "New Taiwan dollar"
+msgstr ""
+
+#: waeup/ikoba/payments/paypal_currencies.py:57
+msgid "United States dollar"
+msgstr ""
+
+#: waeup/ikoba/products/batching.py:52
+msgid "Product Processor"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:126
+msgid "Create product"
+msgstr ""
+
+#. Default: ""
+#: waeup/ikoba/products/browser.py:137
+msgid "Product ${a} added."
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:195
+msgid "Manage product "
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:59 waeup/ikoba/products/browser.py:79
+#: waeup/ikoba/products/export.py:40 waeup/ikoba/products/viewlets.py:49
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:11
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:31
+msgid "Products"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:91
+msgid "Manage product section"
+msgstr ""
+
+#: waeup/ikoba/products/browser.py:94 waeup/ikoba/products/browser.py:96
+#: waeup/ikoba/products/browser.py:123
+msgid "Add product"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:22
+msgid "Setting parameters no yet configured."
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/containerpage.pt:10
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:41
+msgid "Category"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productmanagepage.pt:6
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:6
+msgid "Settings"
+msgstr ""
+
+#: waeup/ikoba/products/browser_templates/productpage.pt:5
+msgid "Terms and Conditions:"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:48
+#: waeup/ikoba/products/browser_templates/containerpage.pt:8
+#: waeup/ikoba/products/browser_templates/containermanagepage.pt:37
+msgid "Product Id"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:55
+msgid "Product Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:61
+msgid "Contract Title"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:62
+msgid "Product title is used if empty."
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:67
+msgid "Contract Category"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:81
+msgid "Valid from"
+msgstr ""
+
+#: waeup/ikoba/products/interfaces.py:87
+msgid "Valid to"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:48
+msgid "Fee"
+msgstr ""
+
+#: waeup/ikoba/products/productoptions.py:52
+msgid "Currency"
+msgstr ""
+
+#: waeup/ikoba/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/ikoba/userscontainer.py:76
+msgid "Users"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:169 waeup/ikoba/utils/utils.py:51
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:90 waeup/ikoba/utils/utils.py:50
+msgid "Germany"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/ikoba/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:110
+msgid "no contract"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:155
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:207
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Registration and Application Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/ikoba/utils/utils.py:52
+msgid "U.S."
+msgstr ""
+
+#: waeup/ikoba/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/ikoba/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/ikoba/widgets/sequencewidget.pt:15
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/ikoba/widgets/sequencewidget.pt:23
+msgid "remove-selected-items"
+msgstr ""
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/zope.pot
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/zope.pot	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/locales/zope.pot	(revision 12677)
@@ -0,0 +1,56 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Development/Unknown\n"
+"POT-Creation-Date: Fri Mar 23 16:41:56 2012\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Zope 3 Developers <zope3-dev@zope.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Default: "Nothing"
+#: src/zope/app/form/browser/source.py:65
+#: src/zope/app/form/browser/source.py:212
+#: src/zope/app/form/browser/source.py:222
+msgid "SourceDisplayWidget-missing"
+msgstr ""
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:231
+msgid "item-missing-single-value-for-display"
+msgstr ""
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:250
+msgid "vocabulary-missing-multiple-value-for-display"
+msgstr ""
+
+#. Default: "(no value)"
+#: src/zope/app/form/browser/itemswidgets.py:417
+#: src/zope/app/form/browser/itemswidgets.py:447
+msgid "vocabulary-missing-single-value-for-edit"
+msgstr ""
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/itemswidgets.py:486
+msgid "vocabulary-missing-multiple-value-for-edit"
+msgstr ""
+
+#. Default: "(no value available)"
+#: src/zope/app/form/browser/sequencewidget.py:256
+msgid "sequence-value-not-provided"
+msgstr ""
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/sequencewidget.py:259
+msgid "sequence-value-is-empty"
+msgstr ""
+
+#: src/zope/app/form/browser/boolwidgets.py:121
+msgid "True"
+msgstr ""
+
+#: src/zope/app/form/browser/boolwidgets.py:122
+msgid "False"
+msgstr ""
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mail.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mail.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mail.zcml	(revision 12677)
@@ -0,0 +1,82 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:mail="http://namespaces.zope.org/mail">
+  <!--
+
+   SMTP Settings
+   *************
+
+   In here we define the mailers and mail deliveries we want to be
+   used by the portal.
+
+   They are set here (and not dynamically in the portal) to increase
+   security. Even portal users with all permissions cannot change mail
+   server credentials and related. You need filesystem access to
+   change/override the settings in here.
+
+   In the portal you can pick one of the deliveries for use with all
+   SMTP (aka mail) related functionality.
+
+   Each delivery (direct or queued) is bound to a certain mailer,
+   defined by a 'mail:smtpMailer' directive.
+
+   The smtpMailer directives define real mail hosts (local or remote)
+   where mail from the portal should be processed.
+
+   The 'mail:directDelivery' and 'mail:queuedDelivery' directives send
+   local mail to the mailer set in their 'mailer' attribute. While
+   directDelivery sends mail immediately when a request is finished
+   (see zope.sendmail for more info), queuedDelivery handles mail in a
+   background thread and sends mail independently from requests.
+
+   By default in Ikoba we have a fake mailer activated, that does not
+   send real mail to the outside world.
+  -->
+
+  <!-- A mail server on localhost -->
+  <!-- Mail server definitions require the following attributes:
+
+        - name: (string) name to be used by deliveries
+
+       Mail server definitions allow the following additional attribs:
+
+        - hostname   (string) hostname or IP(?) of mail server to use
+                              ('localhost' by default)
+        - port       (int number) port to connect to SMTP mailer
+                              (25 by default)
+        - username   (string) Username to log in to mail server
+        - password   (string) Password to log in to mail server
+
+  -->
+  <mail:smtpMailer
+      name="smtp.localhost"
+      hostname="localhost"
+      port="25"
+      />
+
+  <!-- The mail deliveries -->
+  <!-- Mail deliveries require the following attributes:
+
+        - name:       (string) this string will appear in portal select
+                               boxes
+        - permission: (string) zope default perms only
+        - mailer:     (string) name of some mailer registered above or
+                               'smtp'
+        - queuePath:  (string) must exist and contain cur/, new/, and
+                               tmp/ dirs. Relative paths are relative
+                               to the directory this files lives in.
+
+   -->
+  <mail:queuedDelivery
+      name="Queued SMTP on localhost"
+      permission="zope.Public"
+      mailer="smtp.localhost"
+      queuePath="../../../var/mailqueue"
+      />
+
+  <mail:directDelivery
+      name="Direct SMTP on localhost"
+      permission="zope.Public"
+      mailer="smtp.localhost"
+      />
+
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/maintenance.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/maintenance.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/maintenance.py	(revision 12677)
@@ -0,0 +1,155 @@
+"""Helpers for maintainers of ikoba sites.
+
+XXX: This stuff might go into a separate package, but right
+     now it is too less for a complete package.
+"""
+import sys
+from ZODB.FileStorage import FileStorage
+from ZODB.scripts.analyze import report, analyze, shorten
+from zope.component.hooks import setSite
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from zope.intid.interfaces import IIntIds
+
+
+def db_analyze(args=None):
+    """Run the analyze tool from ZODB package.
+    """
+    if args is None:
+        args = sys.argv[1:]
+
+    path = None
+    if len(args) > 0:
+        path = args[0]
+    else:
+        print
+        print "Analyze a ZODB file and print statistics"
+        print "about contained objects, sizes, etc."
+        print
+        print "Usage: %s <path-to-Data.fs>" % sys.argv[0]
+        print
+        sys.exit(0)
+
+    report(analyze(path))
+
+def update_catalog(site, cat_name, objects=[], func=None):
+    """Update a catalog.
+
+    Put `objects` or objects delivered by `func()` into the catalog
+    registered under `cat_name` in `site`.
+
+    Objects to be catalogued must be 'located', i.e. they must have a
+    __name__ and __parent__ (because they are adapted to
+    IKeyReference).
+
+    You can pass in objects as some iterable or as a function that is
+    called to deliver the set of objects to be catalogued.
+
+    A function takes precedence over object lists.
+    """
+    setSite(site)
+    cat = getUtility(ICatalog, name=cat_name)
+    intids = getUtility(IIntIds, context=cat)
+    if func is not None:
+        objects = func()
+    for ob in objects:
+        doc_id = intids.queryId(ob, None)
+        if doc_id is None:
+            doc_id = intids.register(ob)
+        cat.index_doc(doc_id, ob)
+    return cat
+
+def db_diff(args=None):
+    """Run the analyze tool from ZODB package to find diffs between
+       two ZODBs.
+    """
+    if args is None:
+        args = sys.argv[1:]
+
+    zodb_path1, zodb_path2 = (None, None)
+    if len(args) > 1:
+        zodb_path1, zodb_path2 = args[0:2]
+    else:
+        print
+        print "Analyze two ZODB files and print statistics"
+        print "about contained objects, sizes, etc."
+        print "Shows only differences."
+        print
+        print "Usage: %s <path-to-Data1.fs> <path-to-Data2.fs>" % sys.argv[0]
+        print
+        sys.exit(0)
+
+    a1 = analyze(zodb_path1)
+    a2 = analyze(zodb_path2)
+
+    diff_report(a1, a2)
+
+
+def diff_report(rep1, rep2):
+    print "Processed %d (%d, %d) records in %d (%d, %d) transactions" % (
+        rep1.OIDS + rep2.OIDS, rep1.OIDS, rep2.OIDS,
+        rep1.TIDS + rep2.TIDS, rep2.TIDS, rep2.TIDS)
+    print "Types used:"
+    fmt = "  %-44s %7s %9s %6s %7s"
+    fmtp = "%-44s %+7d %9d %5.1f%% %7.2f" # per-class format
+    fmtpplus = "+ %-44s %+7d %+9d %+5.1f%% %7.2f" # per-class format
+    fmtpminus = "- %-44s %+7d %+9d %+5.1f%% %7.2f" # per-class format
+    fmts = "  %44s %+7d %+8dk %+5.1f%% %+7.2f" # summary format
+    typemap1, typemap2 = rep1.TYPEMAP.keys(), rep2.TYPEMAP.keys()
+    typemap1.sort()
+    typemap2.sort()
+    typemap = list(set(typemap1 + typemap2))
+    typemap.sort()
+    print fmt % ("Class Name", "Count", "TBytes", "Pct", "AvgSize")
+    print fmt % ('-'*44, '-'*7, '-'*9, '-'*5, '-'*7)
+    for t in typemap:
+        if t in typemap1 and t in typemap2 and (
+            rep1.TYPESIZE[t] == rep2.TYPESIZE[t]) and (
+            rep1.TYPEMAP[t] == rep2.TYPEMAP[t]):
+            continue
+        if t not in typemap1:
+            cnt = rep2.TYPEMAP[t]
+            pct = rep2.TYPESIZE[t] * 100.0 / rep2.DBYTES
+            print fmtpplus % (shorten(t, 44), cnt, rep2.TYPESIZE[t],
+                      pct, rep2.TYPESIZE[t] * 1.0 / rep2.TYPEMAP[t])
+
+        elif t not in typemap2:
+            cnt = -rep1.TYPEMAP[t]
+            pct = rep1.TYPESIZE[t] * 100.0 / rep1.DBYTES
+            print fmtpminus % (shorten(t, 44), cnt, -rep1.TYPESIZE[t],
+                      pct, rep1.TYPESIZE[t] * 1.0 / rep1.TYPEMAP[t])
+        else:
+            cnt = rep2.TYPEMAP[t] - rep1.TYPEMAP[t]
+            pct1 = rep1.TYPESIZE[t] * 100.0 / rep1.DBYTES
+            pct2 = rep2.TYPESIZE[t] * 100.0 / rep2.DBYTES
+            pct = pct2 - pct1
+            size = rep2.TYPESIZE[t] - rep1.TYPESIZE[t]
+            if cnt > 0:
+                print fmtpplus % (
+                    shorten(t, 44), cnt, size,
+                    pct, rep2.TYPESIZE[t] * 1.0 / rep2.TYPEMAP[t])
+            else:
+                print fmtpminus % (
+                    shorten(t, 44), cnt, size,
+                    pct, rep1.TYPESIZE[t] * 1.0 / rep1.TYPEMAP[t])
+
+
+    print fmt % ('='*44, '='*7, '='*9, '='*5, '='*7)
+    print fmts % ('Current Objects', rep2.COIDS - rep1.COIDS,
+                  (rep2.CBYTES / 1024.0) - (rep1.CBYTES / 1024.0),
+                  (rep2.CBYTES * 100.0 / rep2.DBYTES) - (
+                      rep1.CBYTES * 100.0 / rep1.DBYTES),
+                  (rep2.CBYTES * 1.0 / rep2.COIDS) - (
+                      rep1.CBYTES * 1.0 / rep1.COIDS))
+    #if rep1.FOIDS and rep2.FOIDS:
+    fbytes = (rep2.FBYTES / 1024.0) - (rep1.FBYTES / 1024.0)
+    rep1_fpct = (rep1.DBYTES and
+                 (rep1.FBYTES * 100.0 / (rep1.DBYTES)) or 0.0)
+    rep2_fpct = (rep2.DBYTES and
+                 (rep2.FBYTES * 100.0 / (rep2.DBYTES)) or 0.0)
+    print fmts % ('Old Objects', rep2.FOIDS - rep1.FOIDS,
+                  fbytes, rep2_fpct - rep1_fpct,
+                  (rep2.FBYTES * 1.0 / (rep2.FOIDS or 1.0)) - (
+                      rep1.FBYTES * 1.0 / (rep1.FOIDS or 1.0)))
+    return
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/__init__.py	(revision 12677)
@@ -0,0 +1,10 @@
+"""This package contains everything regarding temporary jobs (mandates).
+"""
+# Make this a package.
+from waeup.ikoba.mandates.container import MandatesContainer
+from waeup.ikoba.mandates.mandate import Mandate
+
+__all__ = [
+    'MandatesContainer',
+    'Mandate',
+    ]
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/browser.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/browser.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/browser.py	(revision 12677)
@@ -0,0 +1,49 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Views for mandates.
+"""
+import grok
+from waeup.ikoba.browser.layout import UtilityView
+from waeup.ikoba.interfaces import ICompany
+from waeup.ikoba.interfaces import MessageFactory as _
+
+class MandateView(UtilityView, grok.View):
+    """View to execute mandate.
+    """
+    grok.context(ICompany)
+    grok.name('mandate')
+    grok.require('waeup.Public')
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        mandate_id = form.get('mandate_id', None)
+        if not mandate_id:
+            self.flash(_('Misuse'))
+            return
+        mandates = grok.getSite()['mandates']
+        mandate = mandates.get(mandate_id, None)
+        if mandate is None:
+            self.flash(_('No mandate.'))
+            return
+        msg = mandate.execute()
+        self.flash(msg)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, 'login'))
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/container.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/container.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/container.py	(revision 12677)
@@ -0,0 +1,66 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Containers which contain mandate objects.
+"""
+import grok
+from datetime import datetime
+from grok import index
+from waeup.ikoba.interfaces import IIkobaPluggable
+from waeup.ikoba.mandates.interfaces import IMandatesContainer, IMandate
+
+class MandatesContainer(grok.Container):
+    """This is a container for all kind of mandates.
+    """
+    grok.implements(IMandatesContainer)
+    grok.provides(IMandatesContainer)
+
+    def addMandate(self, mandate):
+        if not IMandate.providedBy(mandate):
+            raise TypeError(
+                'MandateContainers contain only IMandate instances')
+        self[mandate.mandate_id] = mandate
+        return
+
+    def removeExpired(self):
+        """Remove all expired mandates.
+        """
+        num_deleted = 0
+        for mandate in self.keys():
+            if self[mandate].expires < datetime.utcnow():
+                del self[mandate]
+                num_deleted += 1
+        return num_deleted
+
+class MandatesPlugin(grok.GlobalUtility):
+    """A plugin that creates container for mandates inside a company.
+    """
+    grok.implements(IIkobaPluggable)
+    grok.name('mandates')
+
+    def setup(self, site, name, logger):
+        if 'mandates' in site.keys():
+            logger.warn('Could not create container for mandates in Ikoba.')
+            return
+        site['mandates'] = MandatesContainer()
+        logger.info('Container for mandates created')
+        return
+
+    def update(self, site, name, logger):
+        if not 'mandates' in site.keys():
+            self.setup(site, name, logger)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/interfaces.py	(revision 12677)
@@ -0,0 +1,46 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+from zope.interface import Attribute
+from zope import schema
+from waeup.ikoba.interfaces import IIkobaObject
+
+class IMandatesContainer(IIkobaObject):
+    """A container for all kind of mandate objects.
+
+    """
+
+    def addMandate(mandate):
+        """Add mandate.
+        """
+
+    def removeExpired():
+        """Remove all expired mandates and return the number
+        of successfully removed mandates.
+        """
+
+class IMandate(IIkobaObject):
+    """A representation of all mandates.
+
+    """
+    mandate_id = Attribute('Mandate Identifier')
+    expires = Attribute('Expiration Datetime')
+    params = Attribute('Dictionary with mandate parameters')
+
+    def execute():
+        """Method which is performed by the mandate.
+        """
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/mandate.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/mandate.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/mandate.py	(revision 12677)
@@ -0,0 +1,78 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+These are the mandates.
+"""
+import grok
+import hashlib
+import os
+from datetime import datetime, timedelta
+from grok import index
+from waeup.ikoba.interfaces import IUserAccount
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.mandates.interfaces import IMandate
+
+class Mandate(grok.Model):
+    """This is a mandate.
+    """
+    grok.implements(IMandate)
+    grok.provides(IMandate)
+    grok.baseclass()
+
+    def __init__(self, days=1, mandate_id=None):
+        super(Mandate, self).__init__()
+        self.creation_date = datetime.utcnow() # offset-naive datetime
+        delta = timedelta(days=days)
+        self.expires = datetime.utcnow() + delta
+        if mandate_id is None:
+            mandate_id = os.urandom(20)
+            mandate_id = hashlib.md5(mandate_id).hexdigest()
+        self.mandate_id = mandate_id
+        self.params = {}
+        return
+
+    def execute(self):
+        return _('Nothing to do.')
+
+class PasswordMandate(Mandate):
+    """This is a mandate which can set a password.
+    """
+
+    def _setPassword(self):
+        user = self.params.get('user', None)
+        pwd = self.params.get('password', None)
+        if not None in (user, pwd):
+            try:
+                IUserAccount(user).setPassword(pwd)
+                return True
+            except:
+                return False
+        return False
+
+    def execute(self):
+        msg = _('Wrong mandate parameters.')
+        if self.expires < datetime.utcnow():
+            msg = _('Mandate expired.')
+        if self._setPassword():
+            msg = _('Password has been successfully set. '
+                    'Login with your new password.')
+            username = IUserAccount(self.params['user']).name
+            grok.getSite().logger.info(
+                'PasswordMandate used: %s ' % username)
+        del self.__parent__[self.mandate_id]
+        return msg
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/tests.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/tests.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/mandates/tests.py	(revision 12677)
@@ -0,0 +1,161 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for mandates.
+"""
+import tempfile
+import shutil
+import os
+from zope.testbrowser.testing import Browser
+from datetime import datetime, timedelta
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import createObject
+from zope.component.hooks import setSite, clearSite
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import IUserAccount
+from waeup.ikoba.mandates.interfaces import (
+    IMandatesContainer, IMandate)
+from waeup.ikoba.mandates.container import MandatesContainer
+from waeup.ikoba.mandates.mandate import PasswordMandate
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+class MandatesContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IMandatesContainer, MandatesContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IMandatesContainer, MandatesContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IMandate, PasswordMandate)
+            )
+        self.assertTrue(
+            verifyObject(
+                IMandate, PasswordMandate())
+            )
+        return
+
+    def setUp(self):
+        super(MandatesContainerTestCase, self).setUp()
+
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(MandatesContainerTestCase, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+    def test_set_customer_password(self):
+        customer = createObject('waeup.Customer')
+        # Add and execute a mandate with missing parameters.
+        mandate = PasswordMandate()
+        self.app['mandates'].addMandate(mandate)
+        msg = mandate.execute()
+        self.assertEqual(msg, u'Wrong mandate parameters.')
+        # Add and execute an expired mandate.
+        mandate = PasswordMandate(days=0)
+        self.app['mandates'].addMandate(mandate)
+        msg = mandate.execute()
+        self.assertEqual(msg, u'Mandate expired.')
+        # Add and execute a perfect mandate
+        mandate = PasswordMandate()
+        mandate.params['user'] = customer
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        msg = mandate.execute()
+        # Password has been set.
+        self.assertEqual(msg, 'Password has been successfully set. Login with your new password.')
+        self.assertTrue(IUserAccount(customer).checkPassword('mypwd1'))
+        # All mandates have been removed.
+        self.assertEqual(len(self.app['mandates'].keys()), 0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('system - PasswordMandate used: K1000000' in logcontent)
+
+    def test_set_officer_password(self):
+        self.app['users'].addUser('bob', 'bobssecret')
+        officer = self.app['users']['bob']
+        mandate = PasswordMandate()
+        mandate.params['user'] = officer
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        msg = mandate.execute()
+        # Password has been set.
+        self.assertEqual(msg, 'Password has been successfully set. Login with your new password.')
+        self.assertTrue(IUserAccount(officer).checkPassword('mypwd1'))
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('system - PasswordMandate used: bob' in logcontent)
+
+    def test_remove_expired(self):
+        # mandate1 is an old mandate which just expired.
+        mandate1 = PasswordMandate(days=0)
+        self.app['mandates'].addMandate(mandate1)
+        # mandate2 is a new mandate with default time delta.
+        mandate2 = PasswordMandate(mandate_id='23456')
+        self.app['mandates'].addMandate(mandate2)
+        self.assertEqual(len(self.app['mandates'].keys()), 2)
+        num_deleted = self.app['mandates'].removeExpired()
+        self.assertEqual(num_deleted, 1)
+        # Only the new mandate remains in the container.
+        self.assertEqual(len(self.app['mandates'].keys()), 1)
+        self.assertEqual([i for i in self.app['mandates'].keys()], [u'23456'])
+
+    def test_browser(self):
+        customer = createObject('waeup.Customer')
+        self.app['customers'].addCustomer(customer)
+        mandate = PasswordMandate()
+        mandate.params['user'] = customer
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        self.browser.open('http://localhost/app/mandate?mandate_id=%s'
+            % mandate.mandate_id)
+        # Password has been set.
+        self.assertTrue('Password has been successfully set. Login with your new password.'
+            in self.browser.contents)
+        self.assertTrue(IUserAccount(customer).checkPassword('mypwd1'))
+        # All mandates have been removed.
+        self.assertEqual(len(self.app['mandates'].keys()), 0)
+        # We redirect to login page not to the frontpage.
+        self.assertEqual(self.browser.url, 'http://localhost/app/login')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/meta.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/meta.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/meta.py	(revision 12677)
@@ -0,0 +1,107 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Grokkers for Ikoba 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.ikoba.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 = IkobaIndexesUpgradeSubscriber(
+            catalog_name, indexes, context, module_info)
+        subscribed = (site, IObjectUpgradeEvent)
+        config.action(
+            discriminator=None,
+            callable=component.provideHandler,
+            args=(subscriber, subscribed),
+            )
+        return True
+
+
+class IkobaIndexesUpgradeSubscriber(IndexesSetupSubscriber):
+    """Helper that sets up indexes when their Grok site is upgraded.
+
+    Each `grok.Indexes` class serves as an assertion that, whenever an
+    instance of its `grok.site()` is upgraded, the given list of
+    indexes should be generated if not already created as well.  But a
+    long period of time could elapse between when the application
+    starts (and its indexes are grokked), and the moment, maybe days
+    or weeks later, when a new instance of that `grok.Site` is
+    created.  Hence this `IndexesSetupSubscriber`: it can be
+    instantiated at grokking time with the index information, and then
+    registered with the Component Architecture as an event that should
+    be fired later, whenever the right kind of `grok.Site` is
+    instantiated.  At that point its `__call__` method is kicked off
+    and it makes sure the index catalogs get created properly.
+
+    """
+    def __call__(self, site, event):
+        #site.logger.info('Create catalog `%s` if not installed yet.'  % (
+        #        self.catalog_name,))
+        # make sure we have an intids
+        self._createIntIds(site)
+        # get the catalog
+        catalog = self._createCatalog(site)
+        # now install indexes
+        for name, index in self.indexes.items():
+            try:
+                #site.logger.info('Create index `%s` in catalog.' % name)
+                index.setup(catalog, name, self.context, self.module_info)
+                site.logger.info('%s: Index %s created.' % (self.catalog_name,name))
+            except KeyError: #, DuplicationError:
+                #site.logger.info('Index `%s` already in catalog.' % name)
+                pass
+        #site.logger.info('Catalog `%s` ready.' % self.catalog_name)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/meta.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/meta.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/meta.zcml	(revision 12677)
@@ -0,0 +1,26 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta"
+    xmlns:grok="http://namespaces.zope.org/grok">
+
+  <include package="grok" file="meta.zcml" />
+  <!-- include package="z3c.autoinclude" file="meta.zcml" / -->
+
+  <!-- Load the grokkers -->
+  <grok:grok package=".meta" />
+
+  <meta:directive
+      namespace="http://namespaces.waeup.org/ikoba"
+      name="datacenter"
+      schema=".zcml.IDataCenterConfig"
+      handler=".zcml.data_center_conf"
+      />
+
+  <meta:directive
+      namespace="http://namespaces.waeup.org/ikoba"
+      name="paypalconf"
+      schema=".zcml.IPayPalConfig"
+      handler=".zcml.paypal_conf"
+      />
+
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/objecthistory.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/objecthistory.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/objecthistory.py	(revision 12677)
@@ -0,0 +1,113 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+from datetime import datetime
+from persistent.list import PersistentList
+from zope.component import getUtility
+from zope.i18n import translate
+from zope.annotation.interfaces import IAnnotations
+from waeup.ikoba.interfaces import IObjectHistory, IIkobaObject, IIkobaUtils
+from waeup.ikoba.utils.helpers import get_current_principal, now
+
+from waeup.ikoba.interfaces import MessageFactory as _
+
+class ObjectHistory(grok.Adapter):
+    """A history for objects.
+
+    For any object implementing `IIkobaObject` which is annotatable,
+    we provide histories. A history for such an object can be obtained
+    by adapting it to `IObjectHistory`.
+    """
+    grok.context(IIkobaObject)
+    grok.implements(IObjectHistory)
+
+    history_key = 'waeup.history'
+
+    def __init__(self, context):
+        from zope.security.proxy import removeSecurityProxy
+        self.context = removeSecurityProxy(context)
+        self._annotations = IAnnotations(self.context)
+
+    def _getMessages(self):
+        return self._annotations.get(self.history_key, PersistentList())
+
+    @property
+    def messages(self):
+        """Get all messages as a persistent list of strings.
+        """
+        return self._getMessages()
+
+    def addMessage(self, msg, user=None):
+        """Add the message (history entry) in msg.
+
+        Any message will be stored with a timestamp and the current
+        user (principal) if user parameter is None.
+        """
+        msgs = self._getMessages()
+        tz = getUtility(IIkobaUtils).tzinfo
+        timestamp = now(tz).strftime("%Y-%m-%d %H:%M:%S %Z")
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        msg = translate(msg,'waeup.ikoba',target_language=portal_language)
+        by = translate(_('by'),'waeup.ikoba',target_language=portal_language)
+        if user == None:
+            user = get_current_principal()
+            if user is None:
+                usertitle = 'system'
+            elif user.id == 'zope.anybody':
+                usertitle = 'Anonymous'
+            else:
+                usertitle = getattr(user, 'public_name', None)
+                if not usertitle:
+                    usertitle = user.title
+            msg = u'%s - %s %s %s' % (timestamp, msg, by, usertitle)
+        elif user == 'undisclosed':
+            msg = u'%s - %s' % (timestamp, msg)
+        else:
+            msg = u'%s - %s %s %s' % (timestamp, msg, by, user)
+        msgs.append(msg)
+        self._annotations[self.history_key] = msgs
+        return
+
+    def modifyMessages(self, old, new):
+        """Replaces history messages.
+
+        Substring 'old' will be replaced by 'new' in all
+        messages.
+        """
+        old_msgs = self._getMessages()
+        new_msgs = PersistentList()
+        for msg in old_msgs:
+            new_msg = msg.replace(old, new)
+            new_msgs.append(new_msg)
+        self._annotations[self.history_key] = new_msgs
+        return
+
+    def removeMessage(self, number):
+        """Removes a single history message.
+
+        """
+        msgs = self._getMessages()
+        if not isinstance(number, int):
+            return False, 'Not a number'
+        try:
+            line = msgs[number]
+        except IndexError:
+            return False, 'Number out of range'
+        msgs.pop(number)
+        self._annotations[self.history_key] = msgs
+        return True, line
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/README-paypal.rst
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/README-paypal.rst	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/README-paypal.rst	(revision 12677)
@@ -0,0 +1,68 @@
+Support for PayPal(tm) Payments
+*******************************
+
+Ikoba generally supports paying things via PayPal. For this, we use
+the REST-API provided by PayPal.
+
+To make that work, you have to (1) obtain app credentials from PayPal
+and (2) make that credentials known to ikoba.
+
+
+Getting Credentials from PayPal
++++++++++++++++++++++++++++++++
+
+1) Create a general PayPal account
+
+   Browse https://www.paypal.com and create a new account. If you plan
+   to provide services or goods payed via PayPal. you should of course
+   create a business account. You can, however, start with a personal
+   account as it is upgradeable at a later point in time.
+
+   For only testing ikoba PayPal support, a personal account is
+   sufficient.
+
+2) Register as a developer at PayPal
+
+   Go to https://developer.paypal.com and log in with the credentials
+   obtained in step 1. This will create some fake users/merchants for
+   you, which are *not* suitable for use with the PayPal REST API.
+
+3) Create a REST-API compatible application
+
+   After logging in on https://developer.paypal.com/ go to the
+   dashboard and create a new app by clicking the respective button.
+
+Afterwards, in the developer dashboard under "My apps" you can find
+your new set of client IDs and secrets for both, sandbox and live use.
+
+
+Setting PayPal Client ID, secret, etc.
+++++++++++++++++++++++++++++++++++++++
+
+To make the credentials known to Ikoba, put them into configuration
+files like this::
+
+  # paypal.conf
+
+  [rest-client]
+  id = <YOUR-VERY-OWN-PAYPAL-REST-CLIENT-ID>
+  secret = <YOUR-VERY-OWN-PAYPAL-REST-CLIENT-SECRET>
+  # valid values: sandbox, live
+  mode = sandbox
+
+Out of the box, we expect these files to be in the following _two_
+locations:
+
+  path/to/ikoba/install/etc/paypal.conf
+  path/to/ikoba/install/etc/paypal-testing.conf
+
+Obviously, the second file is used for (automated) testing.
+
+.. note:: As these credentials are bound to
+          persons/companies/organisations, we do not share them via
+          version control.
+
+For developers: The credentials given via ZCML and config files are
+used on startup to initialize the PayPal SDK. Therefore, it should not
+be neccessary to care for the credentials when creating Payment
+objects after the Grok instance started.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/__init__.py	(revision 12677)
@@ -0,0 +1,10 @@
+"""This package contains everything regarding payments.
+"""
+# Make this a package.
+from waeup.ikoba.payments.container import PaymentsContainer
+from waeup.ikoba.payments.payment import Payment
+
+__all__ = [
+    'PaymentsContainer',
+    'Payment',
+    ]
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/catalog.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/catalog.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/catalog.py	(revision 12677)
@@ -0,0 +1,35 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components to help cataloging and searching payments.
+"""
+import grok
+from waeup.ikoba.interfaces import ICompany
+from waeup.ikoba.payments.interfaces import IPayment
+
+
+class PaymentIndexes(grok.Indexes):
+    """A catalog for all payments.
+    """
+    grok.site(ICompany)
+    grok.name('payments_catalog')
+    grok.context(IPayment)
+
+    payment_id = grok.index.Field(attribute='payment_id')
+    payer_id = grok.index.Field(attribute='payer_id')
+    state = grok.index.Field(attribute='state')
+    amount = grok.index.Field(attribute='amount')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/container.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/container.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/container.py	(revision 12677)
@@ -0,0 +1,42 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Containers which contain payment objects.
+"""
+import grok
+from waeup.ikoba.payments.interfaces import IPaymentsContainer
+from waeup.ikoba.utils.helpers import attrs_to_fields
+
+
+class PaymentsContainer(grok.Container):
+    """This is a container for all kind of payments.
+    """
+    grok.implements(IPaymentsContainer)
+    grok.provides(IPaymentsContainer)
+
+    def __init__(self):
+        super(PaymentsContainer, self).__init__()
+        return
+
+    def archive(self, id=None):
+        raise NotImplementedError()
+
+    def clear(self, id=None, archive=True):
+        raise NotImplementedError()
+
+PaymentsContainer = attrs_to_fields(PaymentsContainer)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/currencies.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/currencies.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/currencies.py	(revision 12677)
@@ -0,0 +1,226 @@
+"""Currencies
+"""
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import SimpleIkobaVocabulary
+
+#: Currencies according to ISO 4217
+#:
+#: Compiled from:
+#:   http://www.currency-iso.org/dam/downloads/table_a1.xml
+#: Entries without decimal units (such as funds or not officially
+#: traded currencies) were removed.
+#:
+#: Data published: 2014-08-15
+#:
+#: Keys are the currency codes (as given by ISO)
+#: Entries have four items:
+#:   0: Authority maintaining the currency (normally a country)
+#:   1: Name of currency
+#:   2: Number of decimals after decimal separator
+#:   3: Currency nuber in ISO listing
+#:
+ISO_4217_CURRENCIES = {
+    'AED': ('UNITED ARAB EMIRATES', _('UAE Dirham'), 2, '784'),
+    'AFN': ('AFGHANISTAN', _('Afghani'), 2, '971'),
+    'ALL': ('ALBANIA', _('Lek'), 2, '008'),
+    'AMD': ('ARMENIA', _('Armenian Dram'), 2, '051'),
+    'ANG': ('SINT MAARTEN (DUTCH PART)',
+            _('Netherlands Antillean Guilder'),
+            2,
+            '532'),
+    'AOA': ('ANGOLA', _('Kwanza'), 2, '973'),
+    'ARS': ('ARGENTINA', _('Argentine Peso'), 2, '032'),
+    'AUD': ('TUVALU', _('Australian Dollar'), 2, '036'),
+    'AWG': ('ARUBA', _('Aruban Florin'), 2, '533'),
+    'AZN': ('AZERBAIJAN', _('Azerbaijanian Manat'), 2, '944'),
+    'BAM': ('BOSNIA AND HERZEGOVINA', _('Convertible Mark'), 2, '977'),
+    'BBD': ('BARBADOS', _('Barbados Dollar'), 2, '052'),
+    'BDT': ('BANGLADESH', _('Taka'), 2, '050'),
+    'BGN': ('BULGARIA', _('Bulgarian Lev'), 2, '975'),
+    'BHD': ('BAHRAIN', _('Bahraini Dinar'), 3, '048'),
+    'BIF': ('BURUNDI', _('Burundi Franc'), 0, '108'),
+    'BMD': ('BERMUDA', _('Bermudian Dollar'), 2, '060'),
+    'BND': ('BRUNEI DARUSSALAM', _('Brunei Dollar'), 2, '096'),
+    'BOB': ('BOLIVIA, PLURINATIONAL STATE OF', _('Boliviano'), 2, '068'),
+    'BOV': ('BOLIVIA, PLURINATIONAL STATE OF', _('Mvdol'), 2, '984'),
+    'BRL': ('BRAZIL', _('Brazilian Real'), 2, '986'),
+    'BSD': ('BAHAMAS', _('Bahamian Dollar'), 2, '044'),
+    'BTN': ('BHUTAN', _('Ngultrum'), 2, '064'),
+    'BWP': ('BOTSWANA', _('Pula'), 2, '072'),
+    'BYR': ('BELARUS', _('Belarussian Ruble'), 0, '974'),
+    'BZD': ('BELIZE', _('Belize Dollar'), 2, '084'),
+    'CAD': ('CANADA', _('Canadian Dollar'), 2, '124'),
+    'CDF': ('CONGO, DEMOCRATIC REPUBLIC OF THE ',
+            _('Congolese Franc'),
+            2,
+            '976'),
+    'CHE': ('SWITZERLAND', _('WIR Euro'), 2, '947'),
+    'CHF': ('SWITZERLAND', _('Swiss Franc'), 2, '756'),
+    'CHW': ('SWITZERLAND', _('WIR Franc'), 2, '948'),
+    'CLF': ('CHILE', _('Unidad de Fomento'), 4, '990'),
+    'CLP': ('CHILE', _('Chilean Peso'), 0, '152'),
+    'CNY': ('CHINA', _('Yuan Renminbi'), 2, '156'),
+    'COP': ('COLOMBIA', _('Colombian Peso'), 2, '170'),
+    'COU': ('COLOMBIA', _('Unidad de Valor Real'), 2, '970'),
+    'CRC': ('COSTA RICA', _('Costa Rican Colon'), 2, '188'),
+    'CUC': ('CUBA', _('Peso Convertible'), 2, '931'),
+    'CUP': ('CUBA', _('Cuban Peso'), 2, '192'),
+    'CVE': ('CABO VERDE', _('Cabo Verde Escudo'), 2, '132'),
+    'CZK': ('CZECH REPUBLIC', _('Czech Koruna'), 2, '203'),
+    'DJF': ('DJIBOUTI', _('Djibouti Franc'), 0, '262'),
+    'DKK': ('GREENLAND', _('Danish Krone'), 2, '208'),
+    'DOP': ('DOMINICAN REPUBLIC', _('Dominican Peso'), 2, '214'),
+    'DZD': ('ALGERIA', _('Algerian Dinar'), 2, '012'),
+    'EGP': ('EGYPT', _('Egyptian Pound'), 2, '818'),
+    'ERN': ('ERITREA', _('-Nakfa'), 2, '232'),
+    'ETB': ('ETHIOPIA', _('Ethiopian Birr'), 2, '230'),
+    'EUR': ('SPAIN', _('Euro'), 2, '978'),
+    'FJD': ('FIJI', _('Fiji Dollar'), 2, '242'),
+    'FKP': ('FALKLAND ISLANDS (MALVINAS)',
+            _('Falkland Islands Pound'),
+            2,
+            '238'),
+    'GBP': ('UNITED KINGDOM', _('Pound Sterling'), 2, '826'),
+    'GEL': ('GEORGIA', _('Lari'), 2, '981'),
+    'GHS': ('GHANA', _('Ghana Cedi'), 2, '936'),
+    'GIP': ('GIBRALTAR', _('Gibraltar Pound'), 2, '292'),
+    'GMD': ('GAMBIA', _('Dalasi'), 2, '270'),
+    'GNF': ('GUINEA', _('Guinea Franc'), 0, '324'),
+    'GTQ': ('GUATEMALA', _('Quetzal'), 2, '320'),
+    'GYD': ('GUYANA', _('Guyana Dollar'), 2, '328'),
+    'HKD': ('HONG KONG', _('Hong Kong Dollar'), 2, '344'),
+    'HNL': ('HONDURAS', _('Lempira'), 2, '340'),
+    'HRK': ('CROATIA', _('Croatian Kuna'), 2, '191'),
+    'HTG': ('HAITI', _('Gourde'), 2, '332'),
+    'HUF': ('HUNGARY', _('Forint'), 2, '348'),
+    'IDR': ('INDONESIA', _('Rupiah'), 2, '360'),
+    'ILS': ('ISRAEL', _('New Israeli Sheqel'), 2, '376'),
+    'INR': ('INDIA', _('Indian Rupee'), 2, '356'),
+    'IQD': ('IRAQ', _('Iraqi Dinar'), 3, '368'),
+    'IRR': ('IRAN, ISLAMIC REPUBLIC OF', _('Iranian Rial'), 2, '364'),
+    'ISK': ('ICELAND', _('Iceland Krona'), 0, '352'),
+    'JMD': ('JAMAICA', _('Jamaican Dollar'), 2, '388'),
+    'JOD': ('JORDAN', _('Jordanian Dinar'), 3, '400'),
+    'JPY': ('JAPAN', _('Yen'), 0, '392'),
+    'KES': ('KENYA', _('Kenyan Shilling'), 2, '404'),
+    'KGS': ('KYRGYZSTAN', _('Som'), 2, '417'),
+    'KHR': ('CAMBODIA', _('Riel'), 2, '116'),
+    'KMF': ('COMOROS', _('Comoro Franc'), 0, '174'),
+    'KPW': (u"KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF",
+            _('North Korean Won'),
+            2,
+            '408'),
+    'KRW': ('KOREA, REPUBLIC OF', _('Won'), 0, '410'),
+    'KWD': ('KUWAIT', _('Kuwaiti Dinar'), 3, '414'),
+    'KYD': ('CAYMAN ISLANDS', _('Cayman Islands Dollar'), 2, '136'),
+    'KZT': ('KAZAKHSTAN', _('Tenge'), 2, '398'),
+    'LAK': (u"LAO PEOPLE'S DEMOCRATIC REPUBLIC", _('Kip'), 2, '418'),
+    'LBP': ('LEBANON', _('Lebanese Pound'), 2, '422'),
+    'LKR': ('SRI LANKA', _('Sri Lanka Rupee'), 2, '144'),
+    'LRD': ('LIBERIA', _('Liberian Dollar'), 2, '430'),
+    'LSL': ('LESOTHO', _('Loti'), 2, '426'),
+    'LTL': ('LITHUANIA', _('Lithuanian Litas'), 2, '440'),
+    'LYD': ('LIBYA', _('Libyan Dinar'), 3, '434'),
+    'MAD': ('WESTERN SAHARA', _('Moroccan Dirham'), 2, '504'),
+    'MDL': ('MOLDOVA, REPUBLIC OF', _('Moldovan Leu'), 2, '498'),
+    'MGA': ('MADAGASCAR', _('Malagasy Ariary'), 2, '969'),
+    'MKD': ('MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF',
+            _('Denar'),
+            2,
+            '807'),
+    'MMK': ('MYANMAR', _('Kyat'), 2, '104'),
+    'MNT': ('MONGOLIA', _('Tugrik'), 2, '496'),
+    'MOP': ('MACAO', _('Pataca'), 2, '446'),
+    'MRO': ('MAURITANIA', _('Ouguiya'), 2, '478'),
+    'MUR': ('MAURITIUS', _('Mauritius Rupee'), 2, '480'),
+    'MVR': ('MALDIVES', _('Rufiyaa'), 2, '462'),
+    'MWK': ('MALAWI', _('Kwacha'), 2, '454'),
+    'MXN': ('MEXICO', _('Mexican Peso'), 2, '484'),
+    'MXV': ('MEXICO', _('Mexican Unidad de Inversion (UDI)'), 2, '979'),
+    'MYR': ('MALAYSIA', _('Malaysian Ringgit'), 2, '458'),
+    'MZN': ('MOZAMBIQUE', _('Mozambique Metical'), 2, '943'),
+    'NAD': ('NAMIBIA', _('Namibia Dollar'), 2, '516'),
+    'NGN': ('NIGERIA', _('Naira'), 2, '566'),
+    'NIO': ('NICARAGUA', _('Cordoba Oro'), 2, '558'),
+    'NOK': ('SVALBARD AND JAN MAYEN', _('Norwegian Krone'), 2, '578'),
+    'NPR': ('NEPAL', _('Nepalese Rupee'), 2, '524'),
+    'NZD': ('TOKELAU', _('New Zealand Dollar'), 2, '554'),
+    'OMR': ('OMAN', _('Rial Omani'), 3, '512'),
+    'PAB': ('PANAMA', _('Balboa'), 2, '590'),
+    'PEN': ('PERU', _('Nuevo Sol'), 2, '604'),
+    'PGK': ('PAPUA NEW GUINEA', _('Kina'), 2, '598'),
+    'PHP': ('PHILIPPINES', _('Philippine Peso'), 2, '608'),
+    'PKR': ('PAKISTAN', _('Pakistan Rupee'), 2, '586'),
+    'PLN': ('POLAND', _('Zloty'), 2, '985'),
+    'PYG': ('PARAGUAY', _('Guarani'), 0, '600'),
+    'QAR': ('QATAR', _('Qatari Rial'), 2, '634'),
+    'RON': ('ROMANIA', _('New Romanian Leu'), 2, '946'),
+    'RSD': ('SERBIA', _('Serbian Dinar'), 2, '941'),
+    'RUB': ('RUSSIAN FEDERATION', _('Russian Ruble'), 2, '643'),
+    'RWF': ('RWANDA', _('Rwanda Franc'), 0, '646'),
+    'SAR': ('SAUDI ARABIA', _('Saudi Riyal'), 2, '682'),
+    'SBD': ('SOLOMON ISLANDS', _('Solomon Islands Dollar'), 2, '090'),
+    'SCR': ('SEYCHELLES', _('Seychelles Rupee'), 2, '690'),
+    'SDG': ('SUDAN', _('Sudanese Pound'), 2, '938'),
+    'SEK': ('SWEDEN', _('Swedish Krona'), 2, '752'),
+    'SGD': ('SINGAPORE', _('Singapore Dollar'), 2, '702'),
+    'SHP': ('SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA',
+            _('Saint Helena Pound'),
+            2,
+            '654'),
+    'SLL': ('SIERRA LEONE', _('Leone'), 2, '694'),
+    'SOS': ('SOMALIA', _('Somali Shilling'), 2, '706'),
+    'SRD': ('SURINAME', _('Surinam Dollar'), 2, '968'),
+    'SSP': ('SOUTH SUDAN', _('South Sudanese Pound'), 2, '728'),
+    'STD': ('SAO TOME AND PRINCIPE', _('Dobra'), 2, '678'),
+    'SVC': ('EL SALVADOR', _('El Salvador Colon'), 2, '222'),
+    'SYP': ('SYRIAN ARAB REPUBLIC', _('Syrian Pound'), 2, '760'),
+    'SZL': ('SWAZILAND', _('Lilangeni'), 2, '748'),
+    'THB': ('THAILAND', _('Baht'), 2, '764'),
+    'TJS': ('TAJIKISTAN', _('Somoni'), 2, '972'),
+    'TMT': ('TURKMENISTAN', _('Turkmenistan New Manat'), 2, '934'),
+    'TND': ('TUNISIA', _('Tunisian Dinar'), 3, '788'),
+    'TOP': ('TONGA', _(u"Pa'anga"), 2, '776'),
+    'TRY': ('TURKEY', _('Turkish Lira'), 2, '949'),
+    'TTD': ('TRINIDAD AND TOBAGO', _('Trinidad and Tobago Dollar'), 2, '780'),
+    'TWD': ('TAIWAN, PROVINCE OF CHINA', _('New Taiwan Dollar'), 2, '901'),
+    'TZS': ('TANZANIA, UNITED REPUBLIC OF', _('Tanzanian Shilling'), 2, '834'),
+    'UAH': ('UKRAINE', _('Hryvnia'), 2, '980'),
+    'UGX': ('UGANDA', _('Uganda Shilling'), 0, '800'),
+    'USD': ('VIRGIN ISLANDS (U.S.)', _('US Dollar'), 2, '840'),
+    'USN': ('UNITED STATES', _('US Dollar (Next day)'), 2, '997'),
+    'UYI': ('URUGUAY',
+            _('Uruguay Peso en Unidades Indexadas (URUIURUI)'),
+            0,
+            '940'),
+    'UYU': ('URUGUAY', _('Peso Uruguayo'), 2, '858'),
+    'UZS': ('UZBEKISTAN', _('Uzbekistan Sum'), 2, '860'),
+    'VEF': ('VENEZUELA, BOLIVARIAN REPUBLIC OF', _('Bolivar'), 2, '937'),
+    'VND': ('VIET NAM', _('Dong'), 0, '704'),
+    'VUV': ('VANUATU', _('Vatu'), 0, '548'),
+    'WST': ('SAMOA', _('Tala'), 2, '882'),
+    'XAF': ('GABON', _('CFA Franc BEAC'), 0, '950'),
+    'XCD': ('SAINT VINCENT AND THE GRENADINES',
+            _('East Caribbean Dollar'),
+            2,
+            '951'),
+    'XOF': ('TOGO', _('CFA Franc BCEAO'), 0, '952'),
+    'XPF': ('WALLIS AND FUTUNA', _('CFP Franc'), 0, '953'),
+    'YER': ('YEMEN', _('Yemeni Rial'), 2, '886'),
+    'ZAR': ('SOUTH AFRICA', _('Rand'), 2, '710'),
+    'ZMW': ('ZAMBIA', _('Zambian Kwacha'), 2, '967'),
+    'ZWL': ('ZIMBABWE', _('Zimbabwe Dollar'), 2, '932')
+    }
+
+
+def get_decimal_units(currency_code):
+    """Get the number of decimals after decimal separator for a currency.
+
+    Pass in a three-letter currency code. Returns a number.
+    """
+    return ISO_4217_CURRENCIES[currency_code][2]
+
+
+#: A vocabulary of ISO 4217 currencies
+ISO_4217_CURRENCIES_VOCAB = SimpleIkobaVocabulary(
+    *[(val[1], key) for key, val in ISO_4217_CURRENCIES.items()])
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/demo_provider.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/demo_provider.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/demo_provider.py	(revision 12677)
@@ -0,0 +1,25 @@
+import grok
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.payments.interfaces import IPaymentGatewayService
+from waeup.ikoba.payments.payment import Payment
+
+
+class DemoCreditcardPaymentService(grok.GlobalUtility):
+    """A demo payment gateway service.
+
+    This one supports credit card payments.
+    """
+    grok.implements(IPaymentGatewayService)
+    grok.name('demo_creditcard')
+
+    title = _(u'Demo Creditcard')
+
+    def create_payment(self, payer, payment_item_list=[],  payee=None):
+        """Create a payment.
+        """
+        payment = Payment()
+        payment.gateway_service = 'demo_creditcard'  # must be grok.name above
+        payment.payer_id = payer.payer_id
+        for item in payment_item_list:
+            payment.add_payment_item(item)
+        return payment
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py	(revision 12677)
@@ -0,0 +1,290 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import decimal
+from zc.sourcefactory.basic import BasicSourceFactory
+from zope import schema
+from zope.component import getUtilitiesFor
+from zope.container.interfaces import IContainer
+from zope.container.constraints import contains
+from zope.interface import Interface, Attribute
+from waeup.ikoba.interfaces import (
+    IIkobaObject, SimpleIkobaVocabulary, ContextualDictSourceFactoryBase)
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.payments.currencies import ISO_4217_CURRENCIES_VOCAB
+
+#: Possible states of payments
+STATE_UNPAID = 1
+STATE_PAID = 2
+STATE_FAILED = 4
+
+payment_states = SimpleIkobaVocabulary(
+    (_('Not yet paid'), STATE_UNPAID),
+    (_('Paid'), STATE_PAID),
+    (_('Failed'), STATE_FAILED),
+    )
+
+
+class PaymentGatewayServicesSource(BasicSourceFactory):
+    """A source that lists available payment services.
+
+    Suitable for forms etc. Token and value correspond to the name the
+    respective IPaymentGatewayService utility is registered with.
+    """
+
+    _services = None
+
+    @classmethod
+    def services(cls):
+        """Cache the services registered on startup.
+
+        We assume that services do not change after startup.
+        """
+        if cls._services is None:
+            cls._services = dict(getUtilitiesFor(IPaymentGatewayService))
+        return cls._services
+
+    def getValues(self):
+        """Get payment gateway registration names.
+        """
+        return sorted(PaymentGatewayServicesSource.services().keys())
+
+    def getTitle(self, value):
+        """Get title of the respective service, if it exists.
+        """
+        service = PaymentGatewayServicesSource.services().get(value, None)
+        if service is not None:
+            return service.title
+
+
+class IPaymentGatewayService(Interface):
+    """A financial gateway service.
+
+    Any gateway provider might provide several services. For instance
+    payments by credit card, scratch card, bank transfer, etc. An
+    `IPaymentGatewayService` represents one of those services.
+
+    Payment services are normally registered as a named global
+    utility.
+    """
+    title = schema.TextLine(
+        title=u'Title',
+        description=u'Human readable name of gateway service.',
+        required=True,
+        )
+
+    def create_payment(payer, payment_item_list,  payee):
+        """Create a payment.
+
+        For all parameters we expect an object, that implements
+        `IPayer`, `IPaymentItem`, or `IPayee` respectively. If not,
+        then the given objects must be at least adaptable to the
+        respective interface.
+
+        Therfore you can pass in some `Customer` as long as there is
+        some `IPayer` adapter for `Customer` objects defined.
+
+        Returns an `IPayment` object.
+        """
+
+
+class IPaymentGatewayServicesLister(Interface):
+    """A utility that lists all valid payment gateways.
+
+    This is a subset of the available payment methods, as some might
+    be disabled in some site.
+
+    Register your own lister in customized sites!
+    """
+
+
+class PaymentCategorySource(ContextualDictSourceFactoryBase):
+    """A payment category source delivers all categories of payments.
+
+    """
+    #: name of dict to deliver from ikoba utils.
+    DICT_NAME = 'PAYMENT_CATEGORIES'
+
+
+class IPaymentsContainer(IIkobaObject):
+    """A container for all kind of payment objects.
+
+    """
+
+
+class ICreditCard(Interface):
+    """A credit card.
+
+    A credit card is connected to a Payer.
+    """
+    credit_card_id = schema.TextLine(
+        title=u'Internal Credit Card ID',
+        required=True,
+        )
+
+
+class IPaymentItem(Interface):
+    """Something to sell.
+    """
+    item_id = schema.TextLine(
+        title=u'Payment Item ID',
+        required=True,
+        )
+
+    title = schema.TextLine(
+        title=u'Title',
+        description=u'A short title of the good sold.',
+        required=True,
+        default=u'Unnamed'
+        )
+
+    amount = schema.Decimal(
+        title=u'Amount',
+        description=u'Total amount, includung any taxes, fees, etc.',
+        required=True,
+        default=decimal.Decimal('0.00'),
+        )
+
+
+class IPayment(IContainer):
+    """A base representation of payments.
+
+    In a payment, a payer payes someone (the payee) for something, the
+    item to pay.
+
+    We currently support only the case where one payer pays one payee
+    for one item. The item might include taxes, handling,
+    shipping, etc.
+
+    As in RL any payment is actually performed by some financial
+    service provider (like paypal, interswitch, etc.), each of which
+    might provide several types of payments (credit card, scratch
+    card, you name it).
+
+    In Ikoba we call financial service providers 'gateway' and their
+    types of services are handled as gateway types. Therefore PayPal
+    handling a credit card payment is different from PayPal handling a
+    regular PayPal account transaction.
+
+    A payment can be approve()d, which means the act of paying was
+    really performed. It can also fail for any reason, in which case
+    we mark the payment 'failed'.
+    """
+    contains(IPaymentItem)
+
+    payment_id = schema.TextLine(
+        title=u'Payment Identifier',
+        default=None,
+        required=True,
+        )
+
+    payer_id = schema.TextLine(
+        title=u'Payer',
+        default=None,
+        required=True,
+        )
+
+    payee_id = schema.TextLine(
+        title=u'Payee',
+        default=None,
+        required=False,
+    )
+
+    gateway_service = schema.Choice(
+        title=u'Payment Gateway',
+        description=u'Payment gateway that handles this transaction.',
+        source=PaymentGatewayServicesSource(),
+        default=None,
+        required=True,
+        )
+
+    state = schema.Choice(
+        title=_(u'Payment State'),
+        default=STATE_UNPAID,
+        vocabulary=payment_states,
+        required=True,
+        )
+
+    creation_date = schema.Datetime(
+        title=_(u'Creation Datetime'),
+        readonly=False,
+        required=False,
+        )
+
+    payment_date = schema.Datetime(
+        title=_(u'Payment Datetime'),
+        required=False,
+        readonly=False,
+        )
+
+    currency = schema.Choice(
+        title=u'Currency',
+        source=ISO_4217_CURRENCIES_VOCAB,
+        required=True,
+        default='USD',
+        )
+
+    amount = Attribute("Sum of amounts of items contained")
+
+    def approve():
+        """Approve a payment.
+
+        The payment was approved and can now be considered payed. This
+        kind of approvement means the final one (in case there are
+        several instances to ask).
+        """
+
+    def mark_failed(reason=None):
+        """Mark the payment as failed.
+
+        A failed payment was canceled due to technical problems,
+        insufficient funds, etc.
+        """
+
+    def add_payment_item(item):
+        """Payments contain payment items.
+
+        Add one
+        """
+
+
+class IPayer(Interface):
+    """A payer.
+    """
+    payer_id = schema.TextLine(
+        title=u'Payer ID',
+        required=True,
+        )
+
+    first_name = schema.TextLine(
+        title=u'First Name',
+        required=True,
+        )
+
+    last_name = schema.TextLine(
+        title=u'Last Name',
+        required=True,
+        )
+
+
+class IPayee(Interface):
+    """A person or institution being paid.
+    """
+    payee_id = schema.TextLine(
+        title=u'Payee ID',
+        required=True
+        )
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py	(revision 12677)
@@ -0,0 +1,154 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+These are the payment tickets.
+"""
+import decimal
+import grok
+import uuid
+from datetime import datetime
+from zope.component import getUtilitiesFor
+from zope.event import notify
+from waeup.ikoba.utils.helpers import attrs_to_fields
+from waeup.ikoba.payments.interfaces import (
+    IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID,
+    IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
+    IPaymentGatewayServicesLister,
+    )
+from waeup.ikoba.utils.logger import Logger
+
+
+def get_payment_providers():
+    """Get all payment providers registered.
+    """
+    return dict(
+        getUtilitiesFor(IPaymentGatewayService)
+    )
+
+
+class PaymentGatewayServicesLister(grok.GlobalUtility):
+    grok.implements(IPaymentGatewayServicesLister)
+
+    def __call__(self):
+        """Get all services of payment gateways registered.
+        """
+        return get_payment_providers()
+
+
+class PaymentProviderServiceBase(grok.GlobalUtility):
+
+    grok.baseclass()
+    grok.implements(IPaymentGatewayService)
+
+    title = u'Sample Credit Card Service'
+
+
+@attrs_to_fields
+class Payment(grok.Container, Logger):
+    """This is a payment.
+    """
+    grok.implements(IPayment)
+    grok.provides(IPayment)
+
+    logger_name = 'waeup.ikoba.${sitename}.payments'
+    logger_filename = 'payments.log'
+    logger_format_str = '"%(asctime)s","%(user)s",%(message)s'
+
+    @property
+    def amount(self):
+        """The amount of a payment.
+
+        Equals the sum of items contained.
+        """
+        return sum(
+            [item.amount for item in self.values()],
+            decimal.Decimal("0.00")  # default value
+        )
+
+    def __init__(self):
+        super(Payment, self).__init__()
+        self.creation_date = datetime.utcnow()
+        self.payment_date = None
+        self.payment_id = u'PAY_' + unicode(uuid.uuid4().hex)
+        self.state = STATE_UNPAID
+        return
+
+    def approve(self, payment_date=None):
+        """A payment was approved.
+
+        Successful ending; the payment is marked as payed.
+
+        If `payment_date` is given, it must be a datetime object
+        giving a datetime in UTC timezone.
+
+        Raises ObjectModifiedEvent.
+        """
+        if payment_date is None:
+            payment_date = datetime.utcnow()
+        self.payment_date = payment_date
+        self.state = STATE_PAID
+        notify(grok.ObjectModifiedEvent(self))
+
+    def mark_failed(self, reason=None):
+        """Mark payment as failed.
+
+        Raises ObjectModifiedEvent.
+        """
+        self.state = STATE_FAILED
+        notify(grok.ObjectModifiedEvent(self))
+
+    def add_payment_item(self, item):
+        """Add `item`
+
+        Returns the key under which the `item` was stored. Please do
+        not make anby assumptions about the key. It will be a
+        string. That is all we can tell.
+
+        """
+        cnt = 0
+        while str(cnt) in self:
+            cnt += 1
+        self[str(cnt)] = item
+        return str(cnt)
+
+
+@attrs_to_fields
+class Payer(object):
+    """A Payment is for testing.
+
+    It cannot be stored in ZODB.
+    """
+    grok.implements(IPayer)
+
+
+@attrs_to_fields
+class PaymentItem(grok.Model):
+
+    grok.implements(IPaymentItem)
+
+    def __init__(self):
+        super(PaymentItem, self).__init__()
+
+
+@attrs_to_fields
+class Payee(object):
+    """Someone being paid.
+
+    This is for testing only and cannot be stored in ZODB.
+    """
+    grok.implements(IPayee)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal.py	(revision 12677)
@@ -0,0 +1,1223 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Support for PayPal payments.
+"""
+import decimal
+import grok
+import ConfigParser
+import inspect
+import paypalrestsdk
+import re
+import uuid
+from zope import schema
+from zope.component import queryUtility
+from zope.container.interfaces import IContainer
+from zope.interface import Interface, Attribute
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import IPayPalConfig, SimpleIkobaVocabulary
+from waeup.ikoba.utils.helpers import attrs_to_fields
+from waeup.ikoba.payments.interfaces import (
+    IPayment, IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
+    )
+from waeup.ikoba.payments.paypal_countries import COUNTRIES_VOCAB
+from waeup.ikoba.payments.paypal_currencies import CURRENCIES_VOCAB
+
+#: Intents allowed for paypal based payments
+PAYMENT_INTENTS = ('sale', 'authorize', 'order')
+
+#: Payment methods allowed by PayPal
+PAYMENT_METHODS = ('credit_card', 'paypal')
+
+#: Payer status set/accepted by PayPal
+PAYER_STATUS = ('VERIFIED', 'UNVERIFIED')
+
+#: Tax ID types accepted by PayPal (yes, this list is complete)
+TAX_ID_TYPES = ('BR_CPF', 'BR_CNPJ')
+
+#: Address types accepted by PayPal
+ADDRESS_TYPES = ('residential', 'business', 'mailbox')
+
+#: A vocabulary with address types
+ADDRESS_TYPES_VOCAB = SimpleIkobaVocabulary(
+    *[(_(x), x) for x in ADDRESS_TYPES])
+
+#: Credit card types accepted by PayPal
+CREDIT_CARD_TYPES = ('visa', 'mastercard', 'discover', 'amex')
+
+CREDIT_CARD_TYPES_VOCAB = SimpleIkobaVocabulary(
+    *[(_(x), x) for x in CREDIT_CARD_TYPES])
+
+#: Credit card status accepted by PayPal
+CREDIT_CARD_STATUS = ('expired', 'ok')
+
+CREDIT_CARD_STATUS_VOCAB = SimpleIkobaVocabulary(
+    *[(_(x), x) for x in CREDIT_CARD_STATUS])
+
+#: Stock keeping units we support.
+STOCK_KEEPING_UNITS = {
+    "pcs": _("pieces"),
+    "license": _("license"),
+    }
+
+STOCK_KEEPING_UNITS_VOCAB = SimpleIkobaVocabulary(
+    *[(value, key) for key, value in STOCK_KEEPING_UNITS.items()])
+
+#: Payment methods (flags) used by PayPal
+PAYMENT_OPTION_METHODS = ('INSTANT_FUNDING_SOURCE', )
+PAYMENT_OPTION_METHODS_VOCAB = SimpleIkobaVocabulary(
+    *[(_(x), x) for x in PAYMENT_OPTION_METHODS])
+
+
+def to_dict(obj, name_map={}):
+    """Turn `obj` into some dict representation.
+    """
+    result = dict()
+    for name in dir(obj):
+        if name.startswith('_'):
+            continue
+        value = getattr(obj, name)
+        if value is None:
+            continue
+        if inspect.ismethod(value):
+            continue
+        if hasattr(value, 'to_dict'):
+            value = value.to_dict()
+        elif isinstance(value, list):
+            value = [x.to_dict() for x in value]
+        elif isinstance(value, decimal.Decimal):
+            value = u"%.2f" % (value, )
+        else:
+            value = unicode(value)
+        name = name_map.get(name, name)
+        result[name] = value
+    return result
+
+
+def parse_paypal_config(path):
+    """Get paypal credentials from config file.
+
+    Returns a dict with following keys set:
+
+      ``client_id``, ``client_secret``, ``mode``.
+
+    Please note, that values might be `None`.
+    """
+    parser = ConfigParser.SafeConfigParser(
+        {'id': None, 'secret': None, 'mode': 'sandbox', }
+        )
+    parser.readfp(open(path))
+    return {
+        'client_id': parser.get('rest-client', 'id', None),
+        'client_secret': parser.get('rest-client', 'secret', None),
+        'mode': parser.get('rest-client', 'mode', 'sandbox')
+        }
+
+
+def get_paypal_config_file_path():
+    """Get the path of the configuration file (if any).
+
+    If no such file is registered, we get `None`.
+    """
+    util = queryUtility(IPayPalConfig)
+    if util is None:
+        return None
+    return util['path']
+
+
+def configure_sdk(path):
+    """Configure the PayPal SDK with values from config in `path`.
+
+    Parse paypal configuration from file in `path` and set
+    configuration in SDK accordingly.
+
+    Returns a dict with values from config file and path set.
+
+    This function will normally be called during start-up and only
+    this one time.
+
+    It is neccessary to authorize later calls to other part of the
+    PayPal API.
+    """
+    conf = parse_paypal_config(path)
+    paypalrestsdk.configure({
+        'mode': conf['mode'],
+        'client_id': conf['client_id'],
+        'client_secret': conf['client_secret'],
+        })
+    conf['path'] = path
+    return conf
+
+
+def get_access_token():
+    """Get an access token for further calls.
+    """
+    conf = parse_paypal_config(get_paypal_config_file_path())
+    api = paypalrestsdk.set_config(
+        mode=conf['mode'],  # sandbox or live
+        client_id=conf['client_id'],
+        client_secret=conf['client_secret'],
+        )
+    return api.get_access_token()
+
+
+class Payer(object):
+    """A payer as required in paypal payments.
+
+    According to Paypal docs:
+
+    `payment_method` must be one of ``'paypal'`` or ``'credit_card'``
+    as stored in `PAYMENT_METHODS`.
+
+    `funding_instruments` is a list of `FundingInstrument` objects. I
+    think the list must be empty for ``paypal`` payments and must
+    contain at least one entry for ``credit_card`` payments.
+
+    `payer_info` must be provided for ``paypal`` payments and might be
+    provided for ``credit_card`` payments. It's a `PayerInfo` object.
+
+    `status` reflects the payer's PayPal account status and is
+    currently supported for ``paypal`` payments. Allowed values are
+    ``'VERIFIED'`` and ``'UNVERIFIED'`` as stored in `PAYER_STATUS`.
+    """
+    def __init__(self, payment_method, funding_instruments=[],
+                 payer_info=None, status=None):
+        if payment_method not in PAYMENT_METHODS:
+            raise ValueError(
+                "Invalid payment method: use one of %s" %
+                (PAYMENT_METHODS, )
+                )
+        if status and status not in PAYER_STATUS:
+            raise ValueError(
+                "Invalid status: use one of %s" % (PAYER_STATUS, )
+                )
+        self.payment_method = payment_method
+        self.funding_instruments = funding_instruments
+        self.payer_info = payer_info
+        self.status = status
+
+
+class PayerInfo(object):
+    """Payer infos as required by Paypal payers.
+
+    Normally used with a `Payer` instance (which in turn is part of a
+    `payment`).
+
+    According to PayPal docs:
+
+    Pre-filled by PayPal when `payment_method` is ``'paypal'``.
+
+    `email`: 127 chars max. I don't think, this value is pre-filled.
+
+    `first_name`: First name of payer. Assigned by PayPal.
+
+    `last_naem`: Last name of payer. Assigned by PayPal.
+
+    `payer_id`: Payer ID as assigned by PayPal. Do not mix up with any
+    Ikoba Payer IDs.
+
+    `phone`: Phone number representing the payer. 20 chars max.
+
+    `shipping_address`: a shipping address object of payer. Assigned
+    by PayPal.
+
+    `tax_id_type`: Payer's tax ID type. Allowed values: ``'BR_CPF'``,
+    ``'BR_CNPJ'`` (I have not the slightest clue what that means).
+    Supported (PayPal-wise) with ``paypal`` payment method only (not
+    with ``credit_card``).
+
+    `tax_id`: Payer's tax ID. Here the same as for `tax_id_type`
+    applies (except that also other values are accepted).
+
+    By default all values are set to the empty string and shipping
+    address to `None`.
+
+    See also: :class:`Payer`
+    """
+    def __init__(self, email='', first_name='', last_name='',
+                 payer_id='', phone='', shipping_address=None,
+                 tax_id_type='', tax_id=''):
+        if tax_id_type and tax_id_type not in TAX_ID_TYPES:
+            raise ValueError(
+                "Invalid tax id type: use one of %s" %
+                (TAX_ID_TYPES, )
+                )
+        self.email = email
+        self.first_name = first_name
+        self.last_name = last_name
+        self.payer_id = payer_id
+        self.phone = phone
+        self.shipping_address = shipping_address
+        self.tax_id_type = tax_id_type
+        self.tax_id = tax_id
+
+
+class FundingInstrument(object):
+    """Representation of a payer's funding instrument as required by PayPal.
+
+    Represents always a credit card. Either by a complete set of
+    credit card infos or by a credit card token, which contains only a
+    limited set of credit card data and represents a full set stored
+    in PayPal vault.
+    """
+
+    def __init__(self, credit_card=None, credit_card_token=None):
+        if credit_card is None and credit_card_token is None:
+            raise ValueError(
+                "Must provide credit card data or a token")
+        if credit_card is not None and credit_card_token is not None:
+            raise ValueError(
+                "Must provide credit card data *or* a token, not both.")
+        self.credit_card = credit_card
+        self.credit_card_token = credit_card_token
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class ICreditCard(Interface):
+    """A credit card (full data set) as accepted by PayPal.
+    """
+
+    paypal_id = schema.TextLine(
+        title=u'PayPal ID',
+        description=u'ID of the credit card provided by PayPal.',
+        required=False,
+        )
+
+    external_customer_id = schema.TextLine(
+        title=u'Payer ID',
+        description=(u'A unique identifier for the credit card. This '
+                     u'identifier is provided by Ikoba and must be set '
+                     u'with all future transactions, once put into '
+                     u'PayPal vault.'),
+        required=True,
+        )
+
+    number = schema.TextLine(
+        title=u'Credit Card Number',
+        description=u'Numeric characters only w/o spaces or punctuation',
+        required=True,
+        )
+
+    credit_card_type = schema.Choice(
+        title=u'Credit Card Type',
+        description=u'Credit card types supported by PayPal.',
+        required=True,
+        source=CREDIT_CARD_TYPES_VOCAB,
+        )
+
+    expire_month = schema.Int(
+        title=u'Expiration Month',
+        description=u"Month, the credit card expires.",
+        required=True,
+        default=1,
+        min=1,
+        max=12,
+        )
+
+    expire_year = schema.Int(
+        title=u'Expiration Year',
+        description=u'4-digit expiration year.',
+        required=True,
+        default=2020,
+        min=1900,
+        max=9999,
+        )
+
+    cvv2 = schema.TextLine(
+        title=u'CVV2',
+        description=u'3-4 digit card validation code',
+        required=False,
+        )
+
+    first_name = schema.TextLine(
+        title=u'First Name',
+        description=u"Cardholder's first name.",
+        required=False,
+        )
+
+    last_name = schema.TextLine(
+        title=u'Last Name',
+        description=u"Cardholder's last name.",
+        required=False,
+        )
+
+    billing_address = Attribute(
+        "Billing address associated with card.")
+
+    state = schema.Choice(
+        title=u"Status",
+        description=(u"Status of credit card funding instrument. "
+                     u"Value assigned by PayPal."
+                     ),
+        required=False,
+        source=CREDIT_CARD_STATUS_VOCAB,
+        default=None,
+        )
+
+    valid_unti = schema.TextLine(
+        title=u'Valid until',
+        description=(u'Funding instrument expiratiopn date, '
+                     u'assigned by PayPal'),
+        required=False,
+        )
+
+
+@attrs_to_fields
+class CreditCard(object):
+    """A credit card (full info set) as used by PayPal.
+
+    Normally used with a `FundingInstrument` instance.
+
+    According to PayPal docs:
+
+    `paypal_id`: provided by PayPal when storing credit card
+          data. Required if using a stored credit card.
+
+    `external_customer_id`: Unique identifier. If none is given, we
+          assign a uuid. The uuid reads 'PAYER_<32 hex digits>'.
+
+    `number`: Credit card number. Numeric characters only with no
+          spaces or punctuation. The string must conform with modulo
+          and length required by each credit card type. Redacted in
+          responses. Required.
+
+    `credit_card_type`: One of ``'visa'``, ``'mastercard'``,
+          ``'discover'``, ``'amex'``. Required.
+
+    `expire_month`: Expiration month. A number from 1 through
+          12. Required.
+
+    `expire_year`: 4-digit expiration year. Required.
+
+    `cvv2`: 3-4 digit card validation code.
+
+    `first_name`: card holders first name.
+
+    `last_name`: card holders last name.
+
+    `billing_address`: Billing address associated with card. A
+      `Billing` instance.
+
+    `state`: state of the credit card funding instrument. Valid values
+      are ``'expired'`` and ``'ok'``. *Value assigned by PayPal.*
+
+    `paypal_valid_until`: Funding instrument expiration date.
+       *Value assigned by PayPal.* Not to confuse with the credit cards
+       expiration date, which is set via `expire_month` and
+       `expire_year`.
+
+    """
+    grok.implements(ICreditCard)
+
+    def __init__(self, paypal_id=None, external_customer_id=None,
+                 number=None, credit_card_type=None, expire_month=1,
+                 expire_year=2000, cvv2=None, first_name=None,
+                 last_name=None, billing_address=None, state=None,
+                 paypal_valid_until=None):
+        if not re.match('^[0-9]+$', number):
+            raise ValueError("Credit card number may "
+                             "not contain non-numbers.")
+        if external_customer_id is None:
+            external_customer_id = u'PAYER_' + unicode(uuid.uuid4().hex)
+        self.paypal_id = paypal_id
+        self.external_customer_id = external_customer_id
+        self.number = number
+        self.credit_card_type = credit_card_type
+        self.expire_month = expire_month
+        self.expire_year = expire_year
+        self.cvv2 = cvv2
+        self.first_name = first_name
+        self.last_name = last_name
+        self.billing_address = billing_address
+        self.state = state
+        self.paypal_valid_until = paypal_valid_until
+
+    def to_dict(self):
+        return to_dict(self, name_map={'credit_card_type': 'type'})
+
+
+class ICreditCardToken(Interface):
+    """A credit card token corresponding to a credit card stored with PayPal.
+    """
+    credit_card_id = schema.TextLine(
+        title=u"Credit Card ID",
+        description=u"ID if credit card previously stored with PayPal",
+        required=True,
+        )
+
+    external_customer_id = schema.TextLine(
+        title=u'Payer ID',
+        description=(u'A unique identifier for the credit card. This '
+                     u'identifier is provided by Ikoba and must be set '
+                     u'with all future transactions, once put into '
+                     u'PayPal vault.'),
+        required=True,
+        )
+
+    last4 = schema.TextLine(
+        title=u"Credit Card's last 4 numbers",
+        description=(
+            u"Last four digits of the stored credit card number. "
+            u"Value assigned by PayPal."),
+        required=False,
+        min_length=4,
+        max_length=4
+        )
+
+    credit_card_type = schema.Choice(
+        title=u'Credit Card Type',
+        description=(
+            u'Credit card type supported by PayPal. Value assigned '
+            u'by PayPal.'),
+        required=False,
+        source=CREDIT_CARD_TYPES_VOCAB,
+        )
+
+    expire_month = schema.Int(
+        title=u'Expiration Month',
+        description=u"Month, the credit card expires. Assigned by PayPal.",
+        required=True,
+        default=1,
+        min=1,
+        max=12,
+        )
+
+    expire_year = schema.Int(
+        title=u'Expiration Year',
+        description=u'4-digit expiration year. Assigned by PayPal.',
+        required=True,
+        default=2020,
+        min=1900,
+        max=9999,
+        )
+
+
+class CreditCardToken(object):
+    grok.implements(ICreditCardToken)
+
+    def __init__(self, credit_card_id, external_customer_id=None, last4=None,
+                 credit_card_type=None, expire_month=None, expire_year=None):
+        self.credit_card_id = credit_card_id
+        self.external_customer_id = external_customer_id
+        self.last4 = last4
+        self.credit_card_type = credit_card_type
+        self.expire_month = expire_month
+        self.expire_year = expire_year
+
+    def to_dict(self):
+        return to_dict(self, name_map={'credit_card_type': 'type'})
+
+
+class IShippingAddress(Interface):
+    """A shipping address as accepted by PayPal.
+    """
+    recipient_name = schema.TextLine(
+        title=u'Recipient Name',
+        required=True,
+        description=u'Name of the recipient at this address.',
+        max_length=50,
+        )
+
+    type = schema.Choice(
+        title=u'Address Type',
+        description=u'Address Type.',
+        required=False,
+        source=ADDRESS_TYPES_VOCAB,
+        )
+
+    line1 = schema.TextLine(
+        title=u'Address Line 1',
+        required=True,
+        description=u'Line 1 of the address (e.g., Number, street, etc.)',
+        max_length=100,
+        )
+
+    line2 = schema.TextLine(
+        title=u'Address Line 2',
+        required=False,
+        description=u'Line 2 of the address (e.g., Suite, apt #, etc.)',
+        max_length=100,
+        )
+
+    city = schema.TextLine(
+        title=u'City',
+        required=True,
+        description=u'City name',
+        max_length=50,
+        )
+
+    country_code = schema.Choice(
+        title=u'Country',
+        required=True,
+        description=u'2-letter country code',
+        source=COUNTRIES_VOCAB,
+        )
+
+    postal_code = schema.TextLine(
+        title=u'Postal code',
+        required=False,
+        description=(u'Zip code or equivalent is usually required '
+                     u'for countries that have them.'),
+        max_length=20,
+        )
+
+    state = schema.TextLine(
+        title=u'State',
+        required=False,
+        description=(u'2-letter code for US stated, and the '
+                     u'equivalent for other countries.'),
+        max_length=100,
+        )
+
+    phone = schema.TextLine(
+        title=u'Phone',
+        required=False,
+        description=u'Phone number in E.123 format.',
+        max_length=50,
+        )
+
+
+@attrs_to_fields
+class ShippingAddress(object):
+    """A shipping address as used in PayPal transactions.
+    """
+    grok.implements(IShippingAddress)
+
+    def __init__(self, recipient_name, type='residential', line1='',
+                 line2=None, city='', country_code=None,
+                 postal_code=None, state=None, phone=None):
+        self.recipient_name = recipient_name
+        self.type = type
+        self.line1 = line1
+        self.line2 = line2
+        self.city = city
+        self.country_code = country_code
+        self.postal_code = postal_code
+        self.state = state
+        self.phone = phone
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IAddress(Interface):
+    """An address as accepted by PayPal.
+    """
+    line1 = schema.TextLine(
+        title=u'Address Line 1',
+        required=True,
+        description=u'Line 1 of the address (e.g., Number, street, etc.)',
+        max_length=100,
+        )
+
+    line2 = schema.TextLine(
+        title=u'Address Line 2',
+        required=False,
+        description=u'Line 2 of the address (e.g., Suite, apt #, etc.)',
+        max_length=100,
+        )
+
+    city = schema.TextLine(
+        title=u'City',
+        required=True,
+        description=u'City name',
+        max_length=50,
+        )
+
+    country_code = schema.Choice(
+        title=u'Country',
+        required=True,
+        description=u'2-letter country code',
+        source=COUNTRIES_VOCAB,
+        )
+
+    postal_code = schema.TextLine(
+        title=u'Postal code',
+        required=False,
+        description=(u'Zip code or equivalent is usually required '
+                     u'for countries that have them.'),
+        max_length=20,
+        )
+
+    state = schema.TextLine(
+        title=u'State',
+        required=False,
+        description=(u'2-letter code for US stated, and the '
+                     u'equivalent for other countries.'),
+        max_length=100,
+        )
+
+    phone = schema.TextLine(
+        title=u'Phone',
+        required=False,
+        description=u'Phone number in E.123 format.',
+        max_length=50,
+        )
+
+
+@attrs_to_fields
+class Address(object):
+    """A postal address as used in PayPal transactions.
+    """
+    grok.implements(IAddress)
+
+    def __init__(self, line1='', line2=None, city='', country_code=None,
+                 postal_code=None, state=None, phone=None):
+        self.line1 = line1
+        self.line2 = line2
+        self.city = city
+        self.country_code = country_code
+        self.postal_code = postal_code
+        self.state = state
+        self.phone = phone
+
+    def to_dict(self):
+        """Turn Adress into a dict that can be fed to PayPal classes.
+        """
+        return to_dict(self)
+
+
+class AmountDetails(object):
+    """Amount details can be given with Amount objects.
+
+    All parameters are passed in as decimals (`decimal.Decimal`).
+
+    All numbers stored here, might have 10 characters max with
+    support for two decimal places.
+
+    No parameter is strictly required, except `subtotal`, which must
+    be set if any of the other values is set.
+
+    `shipping`: Amount charged for shipping.
+
+    `subtotal`: Amount for subtotal of the items. Automatically
+      computed. If no other item was set, subtotal is `None`.
+
+    `tax`: Amount charged for tax.
+
+    `fee`: Fee charged by PayPal. In case of a refund, this is the fee
+      amount refunded to the original recipient of the payment. Value
+      assigned by PayPal.
+
+    `handling_fee`: Amount being charged for the handling
+      fee. Currently supported with paypal payment_method only, but
+      available for credit_card payment_method at a later date.
+
+    `insurance`: Amount being charged for the insurance fee. Currently
+      supported with paypal payment_method only, but available for
+      credit_card payment_method at a later date.
+
+    `shipping_discount`: Amount being discounted for the shipping
+      fee. Currently supported with paypal payment_method only, but
+      available for credit_card payment_method at a later date.
+    """
+    def __init__(self, shipping=None, tax=None, fee=None,
+                 handling_fee=None, insurance=None, shipping_discount=None):
+        self.shipping = shipping
+        self.tax = tax
+        self.fee = fee
+        self.handling_fee = handling_fee
+        self.insurance = insurance
+        self.shipping_discount = shipping_discount
+
+    @property
+    def subtotal(self):
+        names = (
+            'shipping', 'tax', 'fee', 'handling_fee', 'insurance',
+            'shipping_discount'
+            )
+        result = None
+        for name in names:
+            val = getattr(self, name)
+            if name == 'shipping_discount' and val is not None:
+                val = -val
+            if val is not None:
+                if result is None:
+                    result = val
+                else:
+                    result += val
+        return result
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IAmount(Interface):
+    """An amount as used by PayPal in payments.
+    """
+    currency = schema.Choice(
+        title=u'Currency',
+        description=u'PayPal does not support all currencies. Required.',
+        required=True,
+        source=CURRENCIES_VOCAB,
+        default=u'USD',
+        )
+
+    total = schema.Decimal(
+        title=u'Total',
+        description=(
+            u'Total amount charged from the payer to the payee. '
+            u'In case of a refund, this is the refunded amount to '
+            u'the original payer from the payee.'),
+        required=True,
+        default=decimal.Decimal("0.00"),
+        max=decimal.Decimal("9999999.99")
+        )
+
+    details = Attribute(
+        """Additional details related to a payment amount.
+        """)
+
+
+@attrs_to_fields
+class Amount(object):
+    grok.implements(IAmount)
+
+    def __init__(self, currency="USD", total=decimal.Decimal("0.00"),
+                 details=None):
+        self.currency = currency
+        self.total = total
+        self.details = details
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IItem(Interface):
+    """PayPal Item.
+
+    Items in a PayPal context are goods sold to customers.
+    """
+    quantity = schema.Int(
+        title=u"Quantity",
+        description=u"Number of this particular items.",
+        required=True,
+        default=1,
+        max=9999999999,
+        )
+
+    name = schema.TextLine(
+        title=u"Name",
+        description=u"Item name",
+        required=True,
+        max_length=127,
+        )
+
+    price = schema.Decimal(
+        title=u"Price",
+        description=u"Price",
+        required=True,
+        max=decimal.Decimal("9999999.99"),
+        )
+
+    currency = schema.Choice(
+        title=u"Currency",
+        description=u"Currency",
+        source=CURRENCIES_VOCAB,
+        default=u'USD',
+        required=True,
+        )
+
+    sku = schema.Choice(
+        title=u"SKU",
+        description=u"Stock keeping unit corresponding to item.",
+        source=STOCK_KEEPING_UNITS_VOCAB,
+        required=False,
+        )
+
+    description = schema.TextLine(
+        title=u"Description",
+        description=(
+            u"Description of Item. Currently supported with paypal "
+            u"payments only."
+            ),
+        max_length=127,
+        required=False,
+        )
+
+    tax = schema.Decimal(
+        title=u"Tax",
+        description=(
+            u"Tax of the item. Currently supported with paypal "
+            u"payments only."
+            ),
+        required=False,
+        )
+
+
+@attrs_to_fields
+class Item(object):
+    """See IItem for docs.
+    """
+    grok.implements(IItem)
+
+    def __init__(self, name, quantity=1, price=decimal.Decimal("0.00"),
+                 currency="USD", sku=None, description=None, tax=None):
+        self.name = name
+        self.quantity = quantity
+        self.price = price
+        self.currency = currency
+        self.sku = sku
+        self.description = description
+        self.tax = tax
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IItemList(Interface):
+    """List of `Item` objects and a related `ShippingAddress`.
+
+    `items`: can be a simple list of `Item` objects.
+
+    `shipping_address`: a `ShippingAddress` object. Only needed if
+      different from Payer address.
+    """
+    items = schema.List(
+        title=u"Items",
+        description=u"PayPal Items are sold goods",
+        value_type=schema.Object(
+            title=u"Item",
+            description=u"Item in a list",
+            schema=IItem,
+            )
+        )
+
+    shipping_address = schema.Object(
+        title=u"Shipping Address",
+        description=u"Shipping address of receiver if different from payee.",
+        schema=IShippingAddress,
+        required=False,
+        )
+
+
+@attrs_to_fields
+class ItemList(object):
+    """List of `Item` objects and a related `ShippingAddress`.
+
+    `items`: can be a simple list of `Item` objects.
+
+    `shipping_address`: a `ShippingAddress` object. Only needed if
+      different from Payer address.
+    """
+    grok.implements(IItemList)
+
+    def __init__(self, items=[], shipping_address=None):
+        self.items = items
+        self.shipping_address = shipping_address
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IPaymentOptions(Interface):
+    """Payment options requested for a certain purchase unit.
+
+    `allowed_payment_method`: Optional payment method type. If
+      specified, the transaction will go through for only instant
+      payment. Allowed values: ``INSTANT_FUNDING_SOURCE``. Only for
+      use with the ``paypal`` payment_method, not relevant for the
+      ``credit_card`` payment_method.
+    """
+    allowed_payment_method = schema.Choice(
+        title=u"Allowed payment method",
+        description=(
+            u"Optional payment type. If specified, the transaction "
+            u"will go through for only instant payment. Only for use "
+            u"with paypal payment method, not relevant for credit cards."
+            ),
+        required=False,
+        source=PAYMENT_OPTION_METHODS_VOCAB,
+        )
+
+
+@attrs_to_fields
+class PaymentOptions(object):
+    """Payment options requested for a certain purchase unit.
+    """
+    grok.implements(IPaymentOptions)
+
+    def __init__(self, allowed_payment_method=None):
+        if allowed_payment_method not in (
+            None, 'INSTANT_FUNDING_SOURCE'):
+            raise ValueError(
+                "allowed_payment_method of PaymentOptions must be None or "
+                "'INSTANT_FUNDING_SOURCE'"
+                )
+        self.allowed_payment_method = allowed_payment_method
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class ITransaction(Interface):
+    """PayPal transactions provide payment transaction details.
+    """
+    amount = schema.Object(
+        title=u"Amount",
+        description=u"Amount being collected.",
+        schema=IAmount,
+        required=True,
+        )
+
+    description = schema.TextLine(
+        title=u"Description",
+        description=u"Description of transaction",
+        required=False,
+        max_length=127,
+        )
+
+    item_list = schema.Object(
+        title=u"Item List",
+        description=u"List of items",
+        required=False,
+        schema=IItemList,
+        )
+
+    # XXX: This should be defined more precisely: What kind of objects, etc.
+    #      PayPal docs say: "array of sale, authorization, capture, or refund,
+    #      objects"
+    related_resources = Attribute("Arbitrary objects")
+
+    invoice_number = schema.TextLine(
+        title=u"Invoice Number",
+        description=(
+            u"Invoice number used to track the payment. "
+            u"Currently supported with paypal payment_method only."
+            ),
+        required=False,
+        max_length=256,
+        )
+
+    custom = schema.TextLine(
+        title=u"Custom text",
+        description=(
+            u"Free-form field for the use of clients. Currently "
+            u"supported with paypal payment_method only."),
+        required=False,
+        max_length=256,
+        )
+
+    soft_descriptor = schema.TextLine(
+        title=u"Soft descriptor",
+        description=(
+            u"Soft descriptor used when charging this funding "
+            u"source. Currently supported with paypal payment_method only"
+            ),
+        required=False,
+        max_length=22,
+        )
+
+    payment_options = schema.Object(
+        title=u"Payment Options",
+        description=u"Payment options requested for this purchase unit.",
+        required=False,
+        schema=IPaymentOptions,
+        )
+
+
+@attrs_to_fields
+class Transaction(object):
+    # See ITransaction for description
+
+    grok.implements(ITransaction)
+
+    def __init__(self, amount, description=None, item_list=None,
+                 related_resources=[], invoice_number=None, custom=None,
+                 soft_descriptor=None, payment_options=None):
+        self.amount = amount
+        self.description = description
+        self.item_list = item_list
+        self.related_resources = related_resources
+        self.invoice_number = invoice_number
+        self.custom = custom
+        self.soft_descriptor = soft_descriptor
+        self.payment_options = payment_options
+
+    def to_dict(self):
+        """Give a `dict` representation of this `Transaction`.
+        """
+        return to_dict(self)
+
+
+def get_payment(intent='sale', payment_method='credit_card'):
+    """Construct a payment.
+
+    You have to `create()` the payment yourself.
+
+    Returns a paypalrestsdk Payment object, not an Ikoba payment.
+
+    As `intent` currently only the string ``'sale'`` is supported.
+
+    XXX: Just some sampledata yet.
+    """
+    if intent != "sale":
+        raise ValueError(
+            "Currently, only 'sale' is allowed as type of paypal"
+            "payment.")
+    payment = paypalrestsdk.Payment(
+        {
+            "intent": intent,
+            "payer": {
+                "payment_method": "credit_card",
+                "funding_instruments": [
+                    {
+                        "credit_card": {
+                            "type": "visa",
+                            "number": "4417119669820331",
+                            "expire_month": "11",
+                            "expire_year": "2018",
+                            "cvv2": "874",
+                            "first_name": "Joe",
+                            "last_name": "Shopper",
+                            "billing_address": {
+                                "line1": "52 N Main ST",
+                                "city": "Johnstown",
+                                "state": "OH",
+                                "postal_code": "43210",
+                                "country_code": "US"}
+                            }
+                        }
+                    ]},
+            "transactions": [{
+                "amount": {
+                    "total": "7.47",
+                    "currency": "USD",
+                    "details": {
+                        "subtotal": "7.41",
+                        "tax": "0.03",
+                        "shipping": "0.03"}},
+                "description": ("This is the payment "
+                                "transaction description.")
+                }]
+            }
+        )
+    return payment
+
+
+class IPayPalPayment(IPayment):
+    """A paypal payment.
+    """
+
+
+class PayPalPayment(grok.Model):
+    """A paypal payment.
+    """
+    pass
+
+
+class StandardItemAdapter(grok.Adapter):
+    grok.implements(ITransaction)
+    grok.adapts(IPaymentItem)
+
+
+def payment_item_to_transaction(item):
+    """Turn an IPaymentItem into an ITransaction
+    """
+    amount = Amount(currency=item.currency, total=item.amount)
+    description = item.title
+    item = Item(name=item.title, price=item.amount, currency=item.currency,
+                quantity=1, sku=_("license"))
+    item_list = ItemList(items=[item, ])
+    transaction = Transaction(
+        amount=amount, description=description, item_list=item_list)
+    return transaction
+
+
+class PayPalCreditCardService(grok.GlobalUtility):
+    grok.implements(IPaymentGatewayService)
+    grok.name('paypal_creditcard')
+
+    title = _(u'Credit Card (PayPal)')
+
+    def get_credit_card(self, customer_id):
+        """Get an ICreditCard for `customer_id`.
+
+        `None` if the payer has (yet) no credit card stored with ikoba.
+        """
+        site = grok.getSite()
+        if not IContainer.providedBy(site):
+            return None
+        if not 'creditcards' in site:
+            return None
+        return site['creditcards'].get(customer_id, None)
+
+    def store_credit_card(self, paypal_credit_card):
+        """Store `paypal_credit_card` in vault.
+
+        Returns a credit card token in case of success. Otherwise an
+        IOError is raised.
+
+        `paypal_credit_card` must provide `ICreditCard`. The credit
+        card created is stored at PayPal and gets a token stored
+        locally (in site['creditcards']).
+
+        If no `creditcards` folder is available in local site, we
+        create a new container.
+        """
+        site = grok.getSite()
+        if not 'creditcards' in site:
+            site['creditcards'] = grok.Container()
+        pp_credit_card = paypalrestsdk.CreditCard(
+            paypal_credit_card.to_dict())
+        result = pp_credit_card.create()
+        if not result:
+            # error
+            raise IOError(pp_credit_card.error)
+        result = CreditCardToken(
+            pp_credit_card.id, pp_credit_card.external_customer_id,
+            pp_credit_card.number, pp_credit_card.type,
+            pp_credit_card.expire_month, pp_credit_card.expire_year
+            )
+        site['creditcards'][pp_credit_card.external_customer_id] = result
+        return result
+
+    def create_payment(self, payer, payment_item,  payee=None):
+        """Create a creditcard payment.
+        """
+        if not IPayer.providedBy(payer):
+            payer = IPayer(payer)
+        if not IPaymentItem.providedBy(payment_item):
+            payment_item = IPaymentItem(payment_item)
+        if (payee is not None) and (not IPayee.providedBy(payee)):
+            payee = IPayee(payee)
+        credit_card = self.get_credit_card(payer.payer_id)
+        if credit_card is None:
+            raise ValueError("Payer %s has no credit card." % payer.payer_id)
+        transaction = payment_item_to_transaction(payment_item)
+        payment_dict = {
+            "intent": "sale",
+            "payer": {
+                "payment_method": "credit_card",
+                "funding_instruments": [{
+                    "credit_card_token": {
+                        "credit_card_id": credit_card.credit_card_id,
+                        }}]},
+            "transactions": [
+                transaction.to_dict()]
+            }
+        payment = paypalrestsdk.Payment(payment_dict)
+        return payment
+
+
+class PayPalRegularPaymentService(grok.GlobalUtility):
+    grok.implements(IPaymentGatewayService)
+    grok.name('paypal_regular')
+
+    title = _(u'PayPal Payment')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal_countries.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal_countries.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal_countries.py	(revision 12677)
@@ -0,0 +1,206 @@
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import SimpleIkobaVocabulary
+
+
+#: A list of country codes as accepted by PayPal
+COUNTRIES = {
+    'AD': _('ANDORRA'),
+    'AE': _('UNITED ARAB EMIRATES'),
+    'AG': _('ANTIGUA AND BARBUDA'),
+    'AI': _('ANGUILLA'),
+    'AL': _('ALBANIA'),
+    'AM': _('ARMENIA'),
+    'AN': _('NETHERLANDS ANTILLES'),
+    'AO': _('ANGOLA'),
+    'AR': _('ARGENTINA'),
+    'AT': _('AUSTRIA'),
+    'AU': _('AUSTRALIA'),
+    'AW': _('ARUBA'),
+    'AZ': _('AZERBAIJAN'),
+    'BA': _('BOSNIA-HERZEGOVINA'),
+    'BB': _('BARBADOS'),
+    'BE': _('BELGIUM'),
+    'BF': _('BURKINA FASO'),
+    'BG': _('BULGARIA'),
+    'BH': _('BAHRAIN'),
+    'BI': _('BURUNDI'),
+    'BJ': _('BENIN'),
+    'BM': _('BERMUDA'),
+    'BN': _('BRUNEI DARUSSALAM'),
+    'BO': _('BOLIVIA'),
+    'BR': _('BRAZIL'),
+    'BS': _('BAHAMAS'),
+    'BT': _('BHUTAN'),
+    'BW': _('BOTSWANA'),
+    'BZ': _('BELIZE'),
+    'CA': _('CANADA'),
+    'CD': _('DEMOCRATIC REPUBLIC OF CONGO'),
+    'CG': _('CONGO'),
+    'CH': _('SWITZERLAND'),
+    'CK': _('COOK ISLANDS'),
+    'CL': _('CHILE'),
+    'CN': _('CHINA (domestic Chinese bank transactions only)'),
+    'C2': _('CHINA (For CUP, bank card and cross-border transactions)'),
+    'CO': _('COLOMBIA'),
+    'CR': _('COSTA RICA'),
+    'CV': _('CAPE VERDE'),
+    'CY': _('CYPRUS'),
+    'CZ': _('CZECH REPUBLIC'),
+    'DE': _('GERMANY'),
+    'DJ': _('DJIBOUTI'),
+    'DK': _('DENMARK'),
+    'DM': _('DOMINICA'),
+    'DO': _('DOMINICAN REPUBLIC'),
+    'DZ': _('ALGERIA'),
+    'EC': _('ECUADOR'),
+    'EE': _('ESTONIA'),
+    'EG': _('EGYPT'),
+    'ER': _('ERITERIA'),
+    'ES': _('SPAIN'),
+    'ET': _('ETHIOPIA'),
+    'FI': _('FINLAND'),
+    'FJ': _('FIJI'),
+    'FK': _('FALKLAND ISLANDS (MALVINAS)'),
+    'FM': _('MICRONESIA, FEDERATED STATES OF'),
+    'FR': _('FRANCE'),
+    'GA': _('GABON'),
+    'GB': _('UNITED KINGDOM'),
+    'GD': _('GRENADA'),
+    'GE': _('GEORGIA'),
+    'GF': _('FRENCH GUIANA'),
+    'GI': _('GIBRALTAR'),
+    'GL': _('GREENLAND'),
+    'GM': _('GAMBIA'),
+    'GN': _('GUINEA'),
+    'GP': _('GUADELOUPE'),
+    'GR': _('GREECE'),
+    'GT': _('GUATEMALA'),
+    'GU': _('GUAM'),
+    'GW': _('GUINEA BISSAU'),
+    'GY': _('GUYANA'),
+    'HK': _('HONG KONG'),
+    'HN': _('HONDURAS'),
+    'HR': _('CROATIA'),
+    'HU': _('HUNGARY'),
+    'ID': _('INDONESIA'),
+    'IE': _('IRELAND'),
+    'IL': _('ISRAEL'),
+    'IN': _('INDIA'),
+    'IS': _('ICELAND'),
+    'IT': _('ITALY'),
+    'JM': _('JAMAICA'),
+    'JO': _('JORDAN'),
+    'JP': _('JAPAN'),
+    'KE': _('KENYA'),
+    'KG': _('KYRGYZSTAN'),
+    'KH': _('CAMBODIA'),
+    'KI': _('KIRIBATI'),
+    'KM': _('COMOROS'),
+    'KN': _('SAINT KITTS AND NEVIS'),
+    'KR': _('SOUTH KOREA'),
+    'KW': _('KUWAIT'),
+    'KY': _('CAYMAN ISLANDS'),
+    'KZ': _('KAZAKHSTAN'),
+    'LA': _('LAOS'),
+    'LC': _('SAINT LUCIA'),
+    'LI': _('LIECHTENSTEIN'),
+    'LK': _('SRI LANKA'),
+    'LS': _('LESOTHO'),
+    'LT': _('LITHUANIA'),
+    'LU': _('LUXEMBOURG'),
+    'LV': _('LATVIA'),
+    'MA': _('MOROCCO'),
+    'MG': _('MADAGASCAR'),
+    'MH': _('MARSHALL ISLANDS'),
+    'ML': _('MALI'),
+    'MN': _('MONGOLIA'),
+    'MQ': _('MARTINIQUE'),
+    'MR': _('MAURITANIA'),
+    'MS': _('MONTSERRAT'),
+    'MT': _('MALTA'),
+    'MU': _('MAURITIUS'),
+    'MV': _('MALDIVES'),
+    'MW': _('MALAWI'),
+    'MX': _('MEXICO'),
+    'MY': _('MALAYSIA'),
+    'MZ': _('MOZAMBIQUE'),
+    'NA': _('NAMIBIA'),
+    'NC': _('NEW CALEDONIA'),
+    'NE': _('NIGER'),
+    'NF': _('NORFOLK ISLAND'),
+    'NI': _('NICARAGUA'),
+    'NL': _('NETHERLANDS'),
+    'NO': _('NORWAY'),
+    'NP': _('NEPAL'),
+    'NR': _('NAURU'),
+    'NU': _('NIUE'),
+    'NZ': _('NEW ZEALAND'),
+    'OM': _('OMAN'),
+    'PA': _('PANAMA'),
+    'PE': _('PERU'),
+    'PF': _('FRENCH POLYNESIA'),
+    'PG': _('PAPUA NEW GUINEA'),
+    'PH': _('PHILIPPINES'),
+    'PL': _('POLAND'),
+    'PM': _('SAINT PIERRE AND MIQUELON'),
+    'PN': _('PITCAIRN'),
+    'PT': _('PORTUGAL'),
+    'PW': _('PALAU'),
+    'QA': _('QATAR'),
+    'RE': _('REUNION'),
+    'RO': _('ROMANIA'),
+    'RS': _('SERBIA'),
+    'RU': _('RUSSIAN FEDERATION'),
+    'RW': _('RWANDA'),
+    'SA': _('SAUDI ARABIA'),
+    'SB': _('SOLOMON ISLANDS'),
+    'SC': _('SEYCHELLES'),
+    'SE': _('SWEDEN'),
+    'SG': _('SINGAPORE'),
+    'SH': _('SAINT HELENA'),
+    'SI': _('SLOVENIA'),
+    'SJ': _('SVALBARD AND JAN MAYEN'),
+    'SK': _('SLOVAKIA'),
+    'SL': _('SIERRA LEONE'),
+    'SM': _('SAN MARINO'),
+    'SN': _('SENEGAL'),
+    'SO': _('SOMALIA'),
+    'SR': _('SURINAME'),
+    'ST': _('SAO TOME AND PRINCIPE'),
+    'SV': _('EL SALVADOR'),
+    'SZ': _('SWAZILAND'),
+    'TC': _('TURKS AND CAICOS ISLANDS'),
+    'TD': _('CHAD'),
+    'TG': _('TOGO'),
+    'TH': _('THAILAND'),
+    'TJ': _('TAJIKISTAN'),
+    'TM': _('TURKMENISTAN'),
+    'TN': _('TUNISIA'),
+    'TO': _('TONGA'),
+    'TR': _('TURKEY'),
+    'TT': _('TRINIDAD AND TOBAGO'),
+    'TV': _('TUVALU'),
+    'TW': _('TAIWAN, PROVINCE OF CHINA'),
+    'TZ': _('TANZANIA, UNITED REPUBLIC OF'),
+    'UA': _('UKRAINE'),
+    'UG': _('UGANDA'),
+    'US': _('UNITED STATES'),
+    'UY': _('URUGUAY'),
+    'VA': _('HOLY SEE (VATICAN CITY STATE)'),
+    'VC': _('SAINT VINCENT AND THE GRENADINES'),
+    'VE': _('VENEZUELA'),
+    'VG': _('VIRGIN ISLANDS, BRITISH'),
+    'VN': _('VIETNAM'),
+    'VU': _('VANUATU'),
+    'WF': _('WALLIS AND FUTUNA'),
+    'WS': _('SAMOA'),
+    'YE': _('YEMEN'),
+    'YT': _('MAYOTTE'),
+    'ZA': _('SOUTH AFRICA'),
+    'ZM': _('ZAMBIA'),
+    }
+
+
+#: A vocabulary of paypal supported countries
+COUNTRIES_VOCAB = SimpleIkobaVocabulary(
+    *[(val, key) for key, val in COUNTRIES.items()])
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal_currencies.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal_currencies.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal_currencies.py	(revision 12677)
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# $Id$
+#
+# Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import SimpleIkobaVocabulary
+
+#: The currencies accepted by PayPal.
+#: The entries represent:
+#:   ( title (i18nized), domestic_only, decimals_allowed)
+#:
+#: If `domestic_only` is True, then this currency is supported as a
+#: payment currency and a currency balance for in-country PayPal
+#: accounts only.
+#:
+#: If `decimals_allowed` is *not* True, then you cannot pass amounts
+#: with decimals.
+#:
+#: Status of 2014-12-11
+CURRENCIES = {
+    'AUD': (_(u'Australian dollar'), False, True),
+    'BRL': (_(u'Brazilian real'), True, True),
+    'CAD': (_(u'Canadian dollar'), False, True),
+    'CHF': (_(u'Swiss franc'), False, True),
+    'CZK': (_(u'Czech koruna'), False, True),
+    'DKK': (_(u'Danish krone'), False, True),
+    'EUR': (_(u'Euro'), False, True),
+    'GBP': (_(u'Pound sterling'), False, True),
+    'HKD': (_(u'Hong Kong dollar'), False, True),
+    'HUF': (_(u'Hungarian forint'), False, True),
+    'ILS': (_(u'Israeli new shekel'), False, True),
+    'JPY': (_(u'Japanese yen'), False, False),
+    'MXN': (_(u'Mexican peso'), False, True),
+    'MYR': (_(u'Malaysian ringgit'), True, True),
+    'NOK': (_(u'Norwegian krone'), False, True),
+    'NZD': (_(u'New Zealand dollar'), False, True),
+    'PHP': (_(u'Philippine peso'), False, True),
+    'PLN': (_(u'Polish złoty'), False, True),
+    'SEK': (_(u'Swedish krona'), False, True),
+    'SGD': (_(u'Singapore dollar'), False, True),
+    'THB': (_(u'Thai baht'), False, True),
+    'TRY': (_(u'Turkish lira'), True, True),
+    'TWD': (_(u'New Taiwan dollar'), False, False),
+    'USD': (_(u'United States dollar'), False, True)
+    }
+
+
+#: A vocabulary of paypal supported currencies
+CURRENCIES_VOCAB = SimpleIkobaVocabulary(
+    *[(val[0], key) for key, val in CURRENCIES.items()])
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/__init__.py	(revision 12677)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_container.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_container.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_container.py	(revision 12677)
@@ -0,0 +1,49 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the payments/container module.
+"""
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.payments.interfaces import IPaymentsContainer
+from waeup.ikoba.payments.container import PaymentsContainer
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+
+class PaymentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IPaymentsContainer, PaymentsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IPaymentsContainer, PaymentsContainer())
+            )
+        return
+
+    def test_base(self):
+        # We cannot call the fundamental methods of a base in that case
+        container = PaymentsContainer()
+        self.assertRaises(
+            NotImplementedError, container.archive)
+        self.assertRaises(
+            NotImplementedError, container.clear)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_currencies.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_currencies.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_currencies.py	(revision 12677)
@@ -0,0 +1,49 @@
+import unittest
+from zope.i18nmessageid.message import Message as i18nMessage
+from zope.interface.verify import verifyObject
+from zope.schema.interfaces import IVocabularyTokenized, ITerm
+from waeup.ikoba.payments.currencies import (
+    get_decimal_units, ISO_4217_CURRENCIES, ISO_4217_CURRENCIES_VOCAB,
+    )
+
+
+class CurrenciesTests(unittest.TestCase):
+
+    def test_get_decimal_units(self):
+        # we can get the number of decimals for a currency code
+        assert get_decimal_units('USD') == 2
+        assert get_decimal_units('JPY') == 0
+
+    def test_iso_currencies_no_linebreak(self):
+        # sanity check: make sure we didn't compile linebreaks
+        for key, value in ISO_4217_CURRENCIES.items():
+            assert '\n' not in key
+            assert '\n' not in value[0]
+            assert '\n' not in value[1]
+
+    def test_iso_currencies_types(self):
+        # sanity check: make sure we have the correct types in all tuples
+        for key, value in ISO_4217_CURRENCIES.items():
+            assert isinstance(value[1], i18nMessage)
+            assert isinstance(value[2], int)
+            assert isinstance(value[3], str)
+
+    def test_currencies_vocab_tokenized(self):
+        # we can get ISO currencies as a source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, ISO_4217_CURRENCIES_VOCAB)
+
+    def test_currencies_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = ISO_4217_CURRENCIES_VOCAB.getTerm('USD')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'US Dollar')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_currencies_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = ISO_4217_CURRENCIES_VOCAB.getTerm('USD')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'USD'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_demo_provider.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_demo_provider.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_demo_provider.py	(revision 12677)
@@ -0,0 +1,61 @@
+import decimal
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.testing import (
+    FunctionalLayer, FunctionalTestCase,
+    )
+from waeup.ikoba.payments.interfaces import (
+    IPaymentGatewayService, IPayment, STATE_UNPAID,
+    )
+from waeup.ikoba.payments.payment import Payer, PaymentItem, Payee
+from waeup.ikoba.payments.demo_provider import (
+    DemoCreditcardPaymentService,
+    )
+
+
+class DemoCreditcarPaymentServiceTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        service = DemoCreditcardPaymentService()
+        verifyClass(IPaymentGatewayService, DemoCreditcardPaymentService)
+        verifyObject(IPaymentGatewayService, service)
+
+    def test_create_payment(self):
+        # we can get payments from payment gateways
+        service = DemoCreditcardPaymentService()
+        payer, payment_item, payee = Payer(), PaymentItem(), Payee()
+        payer.payer_id = u'SOME_PAYER_ID'
+        result = service.create_payment(payer, [], payee)
+        assert IPayment.providedBy(result)
+        assert result.gateway_service == u'demo_creditcard'
+        assert result.state == STATE_UNPAID
+        assert len(result) == 0  # no items stored
+
+    def test_create_payment_honors_payment_item(self):
+        # we inspect payment items and take their values
+        service = DemoCreditcardPaymentService()
+        payer, payment_item, payee = Payer(), PaymentItem(), Payee()
+        payment_item.item_id = u'SOME_ITEM_ID'
+        payer.payer_id = u'SOME_PAYER_ID'
+        payment_item.amount = decimal.Decimal("300.99")
+        result = service.create_payment(payer, [payment_item], payee)
+        self.assertEqual(result.amount, payment_item.amount)
+        assert len(result) == 1
+
+    def test_create_payment_honors_payer(self):
+        # we inspect payers when creating their payments
+        service = DemoCreditcardPaymentService()
+        payer, payment_item, payee = Payer(), PaymentItem(), Payee()
+        payment_item.item_id = u'SOME_ITEM_ID'
+        payer.payer_id = u'SOME_PAYER_ID'
+        result = service.create_payment(payer, [payment_item], payee)
+        assert result.payer_id == payer.payer_id
+
+    def test_get_service_by_utility_name(self):
+        # we can get the demo credit card service by its utility name
+        from zope.component import queryUtility
+        service = queryUtility(
+            IPaymentGatewayService, name="demo_creditcard")
+        assert service is not None
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_interfaces.py	(revision 12677)
@@ -0,0 +1,42 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for payments interfaces.
+"""
+from waeup.ikoba.payments.interfaces import PaymentGatewayServicesSource
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+
+class PaymentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_payment_gateway_services_source(self):
+        # the payment gateway services source provides a list of registered
+        # payment gateways
+        source = PaymentGatewayServicesSource()
+        services = list(source)
+        assert len(services) > 0
+
+    def test_payment_gateway_services_source_title(self):
+        # we can get titles from gateway sources
+        source = PaymentGatewayServicesSource()
+        service1 = list(source)[0]
+        title = source.factory.getTitle(service1)
+        assert title != service1
+        assert isinstance(title, basestring)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_payment.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_payment.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_payment.py	(revision 12677)
@@ -0,0 +1,186 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import datetime
+import decimal
+import re
+import unittest
+from zope.component import getUtilitiesFor, getSiteManager, queryUtility
+from zope.interface import implements
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.payments.interfaces import (
+    IPayment, STATE_UNPAID, STATE_PAID, STATE_FAILED,
+    IPaymentGatewayService, IPaymentItem, IPaymentGatewayServicesLister,
+    )
+from waeup.ikoba.payments.payment import (
+    Payment, get_payment_providers, PaymentItem,
+    )
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+
+class HelperTests(unittest.TestCase):
+
+    def tearDown(self):
+        # unregister any IPaymentGatewayServices
+        sm = getSiteManager(None)
+        for name, util in getUtilitiesFor(IPaymentGatewayService):
+            sm.unregisterUtility(util, name=name)
+
+    def test_get_payment_providers_no_providers(self):
+        # we can get a dict of all payment providers
+        result = get_payment_providers()
+        assert isinstance(result, dict)
+        assert result == {}
+
+    def test_get_payment_providers(self):
+        # we get any payment providers registered
+        sm = getSiteManager(None)
+
+        class FakeUtil(object):
+            implements(IPaymentGatewayService)
+
+        fake_util = FakeUtil()
+        sm.registerUtility(fake_util, name=u'some_name')
+        result = get_payment_providers()
+        assert isinstance(result, dict)
+        assert result.keys() == ['some_name', ]
+        assert result['some_name'] is fake_util
+
+
+class FunctionalHelperTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_services_lister_is_registered(self):
+        # a lister of gateway services is registered on startup
+        util = queryUtility(IPaymentGatewayServicesLister)
+        assert util is not None
+
+    def test_services_are_really_listed(self):
+        # we can really get locally registered gateways when calling
+        util = queryUtility(IPaymentGatewayServicesLister)
+        assert len(util()) > 0
+
+
+class PaymentTests(unittest.TestCase):
+
+    def test_iface(self):
+        # Payments fullfill any interface contracts
+        obj = Payment()
+        verifyClass(IPayment, Payment)
+        verifyObject(IPayment, obj)
+
+    def test_payment_id_unique(self):
+        # we get unique payment ids
+        p1, p2 = Payment(), Payment()
+        id1, id2 = p1.payment_id, p2.payment_id
+        assert id1 != id2
+
+    def test_payment_id_format(self):
+        # payment ids have a special format: "PAY_<32 hex digits>"
+        id1 = Payment().payment_id
+        assert isinstance(id1, basestring)
+        assert re.match('PAY_[0-9a-f]{32}', id1)
+
+    def test_initial_state_is_unpaid(self):
+        # the initial state of payments is <unpaid>
+        p1 = Payment()
+        assert p1.state == STATE_UNPAID
+
+    def test_approve(self):
+        # we can approve payments
+        p1 = Payment()
+        p1.approve()
+        assert p1.state == STATE_PAID
+        assert p1.payment_date is not None
+        assert isinstance(p1.payment_date, datetime.datetime)
+
+    def test_approve_datetime_given(self):
+        # we can give a datetime
+        p1 = Payment()
+        some_datetime = datetime.datetime(2014, 1, 1, 0, 0, 0)
+        p1.approve(payment_date=some_datetime)
+        assert p1.payment_date == some_datetime
+
+    def test_approve_datetime_automatic(self):
+        # if we do not give a datetime, current one will be used
+        current = datetime.datetime.utcnow()
+        p1 = Payment()
+        p1.approve()
+        assert p1.payment_date >= current
+
+    def test_mark_failed(self):
+        # we can mark payments as failed
+        p1 = Payment()
+        p1.mark_failed()
+        assert p1.state == STATE_FAILED
+
+    def test_add_payment_item(self):
+        # we can add payment items
+        p1 = Payment()
+        item1 = PaymentItem()
+        result = p1.add_payment_item(item1)
+        assert len(p1) == 1  # do not make assumptions about result content
+        assert isinstance(result, basestring)
+
+    def test_add_payment_item_multiple(self):
+        # we can add several items
+        p1 = Payment()
+        item1 = PaymentItem()
+        item2 = PaymentItem()
+        result1 = p1.add_payment_item(item1)
+        result2 = p1.add_payment_item(item2)
+        assert len(p1) == 2  # do not make assumptions about result content
+        assert isinstance(result1, basestring)
+        assert isinstance(result2, basestring)
+
+    def test_amount(self):
+        # the amount of a payment is the sum of amounts of its items
+        p1 = Payment()
+        item1 = PaymentItem()
+        item2 = PaymentItem()
+        p1.add_payment_item(item1)
+        p1.add_payment_item(item2)
+        item1.amount = decimal.Decimal("12.25")
+        item2.amount = decimal.Decimal("0.5")
+        assert p1.amount == decimal.Decimal("12.75")
+
+    def test_amount_negative(self):
+        # we can sum up negative numbers
+        p1 = Payment()
+        item1 = PaymentItem()
+        item2 = PaymentItem()
+        p1.add_payment_item(item1)
+        p1.add_payment_item(item2)
+        item1.amount = decimal.Decimal("2.21")
+        item2.amount = decimal.Decimal("-3.23")
+        assert p1.amount == decimal.Decimal("-1.02")
+
+    def test_amount_empty(self):
+        # the amount of zero items is 0.00.
+        p1 = Payment()
+        assert p1.amount == decimal.Decimal("0.00")
+        assert isinstance(p1.amount, decimal.Decimal)
+
+
+class PaymentItemTests(unittest.TestCase):
+
+    def test_iface(self):
+        # PaymentItems fullfill any interface contracts
+        obj = PaymentItem()
+        verifyClass(IPaymentItem, PaymentItem)
+        verifyObject(IPaymentItem, obj)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal.py	(revision 12677)
@@ -0,0 +1,1244 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+import decimal
+import os
+import re
+import shutil
+import tempfile
+import unittest
+import paypalrestsdk
+from zope.component import (
+    getGlobalSiteManager, queryUtility,
+    )
+from zope.component.hooks import setSite, clearSite
+from zope.i18nmessageid.message import Message as i18nMessage
+from zope.interface import implements
+from zope.interface.verify import verifyObject, verifyClass
+from zope.schema.interfaces import IVocabularyTokenized, ITerm
+from waeup.ikoba.interfaces import IPayPalConfig
+from waeup.ikoba.payments.interfaces import (
+    IPaymentGatewayService, IPayer, IPayee, IPaymentItem,
+    )
+from waeup.ikoba.payments.paypal import (
+    get_paypal_config_file_path, parse_paypal_config, get_access_token,
+    configure_sdk, get_payment, Payer, PayerInfo, ShippingAddress,
+    ADDRESS_TYPES_VOCAB, IShippingAddress, Address, IAddress, to_dict,
+    CreditCard, CREDIT_CARD_TYPES_VOCAB, CREDIT_CARD_STATUS_VOCAB,
+    ICreditCard, ICreditCardToken, CreditCardToken, FundingInstrument,
+    AmountDetails, IAmount, Amount, IItem, Item, STOCK_KEEPING_UNITS_VOCAB,
+    IItemList, ItemList, IPaymentOptions, PaymentOptions, ITransaction,
+    Transaction, PAYMENT_OPTION_METHODS_VOCAB, PayPalCreditCardService,
+    PayPalRegularPaymentService,
+    )
+from waeup.ikoba.testing import (
+    FunctionalLayer, FunctionalTestCase,
+    )
+
+
+#
+# PayPal test config
+#
+
+EXTERNAL_PAYPAL_TESTS = False
+
+#
+# End of PayPal test config
+#
+
+
+def external_paypal_test(func):
+    """A decorator that can block test functions.
+    """
+    if not EXTERNAL_PAYPAL_TESTS:
+        myself = __file__
+        if myself.endswith('.pyc'):
+            myself = myself[:-2]
+        print "WARNING: external paypal tests are skipped!"
+        print "WARNING: edit %s to enable them." % myself
+        return
+    return func
+
+
+class HelperTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        # unregister any remaining utils registered during tests
+        util = queryUtility(IPayPalConfig)
+        if util is not None:
+            getGlobalSiteManager().unregisterUtility(util, IPayPalConfig)
+
+    def test_get_paypal_config_file_path(self):
+        # we can get a config file path (if registered)
+        path = os.path.join(self.workdir, 'sample.cfg')
+        getGlobalSiteManager().registerUtility(
+            {'path': path, }, IPayPalConfig, '')
+        result = get_paypal_config_file_path()
+        assert result == path
+
+    def test_get_paypal_config_file_path_no_file(self):
+        # without IPayPalConfig util set, we get `None`
+        result = get_paypal_config_file_path()
+        assert result is None
+
+    def test_parse_paypal_config(self):
+        # we can parse paypal configs
+        path = os.path.join(self.workdir, 'sample.cfg')
+        open(path, 'w').write(
+            "[rest-client]\n"
+            "id = myid\n"
+            "secret = mysecret\n"
+            "mode = live\n")
+        result = parse_paypal_config(path)
+        assert result['client_id'] == u'myid'
+        assert result['client_secret'] == u'mysecret'
+        assert result['mode'] == 'live'
+
+    def test_parse_paypal_config_defaults(self):
+        # we have fallback values (defaults) in place
+        path = os.path.join(self.workdir, 'sample.cfg')
+        open(path, 'w').write(
+            "[rest-client]\n"
+            )
+        result = parse_paypal_config(path)
+        assert result['client_id'] is None
+        assert result['client_secret'] is None
+        assert result['mode'] == "sandbox"
+
+    def test_configure_sdk(self):
+        # we can configure the paypal sdk
+        path = os.path.join(self.workdir, 'sample.cfg')
+        open(path, 'w').write(
+            "[rest-client]\n"
+            "id = my-special-id\n"
+            )
+        result = configure_sdk(path)
+        assert result['client_id'] == 'my-special-id'
+        assert result['client_secret'] is None
+        assert result['mode'] == "sandbox"
+        assert paypalrestsdk.api.__api__.mode == 'sandbox'
+        assert paypalrestsdk.api.__api__.client_id == 'my-special-id'
+
+    def test_get_payment_invalid_intent(self):
+        # only 'sale' is currently allowed
+        self.assertRaises(
+            ValueError, get_payment, intent='invalid')
+        self.assertRaises(
+            ValueError, get_payment, intent='order')
+
+    def test_to_dict(self):
+        # we can turn objects into dicts
+        class C1(object):
+            a = 1
+            b = "somestring"
+            c = u"unicodestring"
+            d = None
+        obj = C1()
+        self.assertEqual(
+            to_dict(obj),
+            {'a': u"1",
+             'b': u"somestring",
+             'c': u"unicodestring",
+             }
+            )
+
+    def test_to_dict_map(self):
+        # we can map attribute names
+        class C1(object):
+            a = 1
+            b = "somestring"
+        obj = C1()
+        self.assertEqual(
+            to_dict(obj, name_map={'a': 'replaced_a'}),
+            {'replaced_a': u"1",
+             'b': u"somestring",
+             })
+
+    def test_to_dict_lists(self):
+        # to_dict can handle lists
+        class C1(object):
+            a = 1
+
+            def to_dict(self):
+                return to_dict(self)
+
+        obj1 = C1()
+        obj2 = C1()
+        obj2.a = 2
+        obj3 = C1()
+        obj3.a = 3
+        obj1.foo = [obj2, obj3]
+        self.assertEqual(
+            to_dict(obj1),
+            {
+                'a': u'1',
+                'foo': [
+                    {'a': u'2'},
+                    {'a': u'3'},
+                    ]
+                }
+            )
+
+    def test_to_dict_decimals(self):
+        # decimals are converted to numbers with 2 decimals
+        class C1(object):
+            a = decimal.Decimal("0.1")
+        self.assertEqual(
+            to_dict(C1()),
+            {
+                'a': u"0.10",
+                }
+            )
+
+
+class PayerTests(unittest.TestCase):
+
+    def test_create(self):
+        # we can create payer objects
+        payer = Payer(payment_method='paypal')
+        assert payer.payment_method == 'paypal'
+        assert payer.funding_instruments == []
+        assert payer.payer_info is None
+        assert payer.status is None
+
+    def test_init_invalid_payment_meth(self):
+        # we must provide a valid payment method
+        payer = Payer(payment_method='paypal')
+        assert payer is not None
+        payer = Payer(payment_method='credit_card')
+        assert payer is not None
+        self.assertRaises(
+            ValueError, Payer, payment_method='invalid')
+
+    def test_init_invalid_payer_state(self):
+        # only certain values are allowed as payer states
+        payer = Payer(payment_method='paypal', status='VERIFIED')
+        assert payer is not None
+        payer = Payer(payment_method='paypal', status='UNVERIFIED')
+        assert payer is not None
+        self.assertRaises(
+            ValueError, Payer, payment_method='paypal', status='InVaLiD')
+
+
+class PayerInfoTests(unittest.TestCase):
+
+    def test_create(self):
+        # we can create payer infos
+        info = PayerInfo()
+        assert info.email == ''
+        assert info.first_name == ''
+        assert info.last_name == ''
+        assert info.payer_id == ''
+        assert info.phone == ''
+        assert info.shipping_address is None
+        assert info.tax_id_type == ''
+        assert info.tax_id == ''
+
+    def test_init_invalid_tax_id_type(self):
+        # onyl certain tax id types are allowed
+        info = PayerInfo(tax_id_type='BR_CPF')
+        assert info is not None
+        info = PayerInfo(tax_id_type='BR_CNPJ')
+        assert info is not None
+        self.assertRaises(
+            ValueError, PayerInfo, tax_id_type='INVALID_TYPE')
+
+
+class CreditCardTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+        verifyClass(ICreditCard, CreditCard)
+        verifyObject(ICreditCard, credit_card)
+
+    def test_create(self):
+        # we can create CreditCard objects
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+        assert credit_card.paypal_id is None
+        assert credit_card.external_customer_id is not None
+        assert credit_card.number == u"12345678"
+        assert credit_card.credit_card_type == "visa"
+        assert credit_card.expire_month == 4
+        assert credit_card.expire_year == 2012
+        assert credit_card.cvv2 is None
+        assert credit_card.first_name is None
+        assert credit_card.last_name is None
+        assert credit_card.billing_address is None
+        assert credit_card.state is None
+        assert credit_card.paypal_valid_until is None
+
+    def test_external_customer_id_given(self):
+        # we do not override given curstomer ids
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            external_customer_id=u'MySpecialPayerId',
+            )
+        assert credit_card.external_customer_id == u'MySpecialPayerId'
+
+    def test_external_customer_id_not_given(self):
+        # in case no customer id is given, we generate one
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+        # our customer ids contain a leading 'PAYER_' and 32 hex digits
+        assert re.match(
+            'PAYER_[0-9a-f]{32}$', credit_card.external_customer_id)
+
+    def test_number_is_checked(self):
+        # we do not accept invalid numbers
+        self.assertRaises(
+            ValueError, CreditCard,
+            number=u"not-a-number",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+
+    def test_to_str(self):
+        # we can turn CreditCard objects into dicts.
+        addr = Address(
+            line1=u"52 N Main ST",
+            city=u"Johnstown",
+            state=u"OH",
+            postal_code=u"43210",
+            country_code=u"US")
+        credit_card = CreditCard(
+            credit_card_type=u"visa",
+            external_customer_id=u"PAYER_0123456789012345678901",
+            number=u"4417119669820331",
+            expire_month=11,
+            expire_year=2018,
+            cvv2=u"874",
+            first_name=u"Joe",
+            last_name=u"Shopper",
+            billing_address=addr)
+        self.assertEqual(
+            credit_card.to_dict(),
+            {
+                "type": u"visa",
+                "number": u"4417119669820331",
+                "external_customer_id": u"PAYER_0123456789012345678901",
+                "expire_month": u"11",
+                "expire_year": u"2018",
+                "cvv2": u"874",
+                "first_name": u"Joe",
+                "last_name": u"Shopper",
+                "billing_address": {
+                    "line1": u"52 N Main ST",
+                    "city": u"Johnstown",
+                    "state": u"OH",
+                    "postal_code": u"43210",
+                    "country_code": u"US"}
+                }
+            )
+
+
+class CreditCardTokenTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            )
+        verifyClass(ICreditCardToken, CreditCardToken)
+        verifyObject(ICreditCardToken, token)
+
+    def test_create(self):
+        # we can create CreditCardToken objects
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            )
+        assert token.credit_card_id == u"12345678"
+        assert token.external_customer_id is None
+        assert token.credit_card_type is None
+        assert token.expire_month is None
+        assert token.expire_year is None
+        assert token.last4 is None
+
+    def test_customer_id_given(self):
+        # we do not override given customer ids
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            external_customer_id=u'MySpecialPayerId',
+            )
+        assert token.external_customer_id == u'MySpecialPayerId'
+
+    def test_to_str(self):
+        # we can turn CreditCardToken objects into dicts.
+        token = CreditCardToken(
+            credit_card_type=u"visa",
+            external_customer_id=u"PAYER_0123456789012345678901",
+            credit_card_id=u"12345678",
+            last4="8901",
+            expire_month=11,
+            expire_year=2018,
+            )
+        self.assertEqual(
+            token.to_dict(),
+            {
+                "credit_card_id": u"12345678",
+                "external_customer_id": u"PAYER_0123456789012345678901",
+                "last4": u"8901",
+                "type": u"visa",
+                "expire_month": u"11",
+                "expire_year": u"2018",
+                }
+            )
+
+
+class FundingInstrumentTests(unittest.TestCase):
+
+    def test_create(self):
+        # we can create FundingInstrument objects
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            )
+        instr = FundingInstrument(credit_card_token=token)
+        assert instr.credit_card_token is token
+
+    def test_require_credit_card_or_token(self):
+        # we require a credit card object or a token.
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            )
+        self.assertRaises(
+            ValueError, FundingInstrument,
+            credit_card=credit_card,
+            credit_card_token=token
+            )
+        self.assertRaises(
+            ValueError, FundingInstrument
+            )
+
+    def test_to_dict(self):
+        # we can turn Funding instruments into dicts
+        token = CreditCardToken(
+            credit_card_type=u"visa",
+            external_customer_id=u"PAYER_0123456789012345678901",
+            credit_card_id=u"12345678",
+            last4="8901",
+            expire_month=11,
+            expire_year=2018,
+            )
+        instr = FundingInstrument(credit_card_token=token)
+        result = instr.to_dict()
+        self.assertEqual(
+            result,
+            {
+                "credit_card_token": {
+                    "credit_card_id": u"12345678",
+                    "external_customer_id": u"PAYER_0123456789012345678901",
+                    "last4": u"8901",
+                    "type": u"visa",
+                    "expire_month": u"11",
+                    "expire_year": u"2018",
+                    }
+                }
+            )
+
+
+class AddressTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts
+        addr = Address(
+            line1=u'Address Line 1',
+            city=u'Somecity',
+            country_code=u'AT',
+            )
+        verifyClass(IAddress, Address)
+        verifyObject(IAddress, addr)
+
+    def test_create(self):
+        # we can create addresses
+        addr = Address(
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        assert addr.line1 == u'Honey Street 1'
+        assert addr.line2 is None
+        assert addr.city == u'Beartown'
+        assert addr.country_code == u'GB'
+        assert addr.postal_code is None
+        assert addr.state is None
+        assert addr.phone is None
+
+    def test_to_dict(self):
+        # we can turn addresses into dicts
+        addr = Address(
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        self.assertEqual(
+            addr.to_dict(),
+            {
+                'line1': u'Honey Street 1',
+                'city': u'Beartown',
+                'country_code': u'GB',
+                }
+            )
+        addr.line2 = u"Behind little tree"
+        self.assertEqual(
+            addr.to_dict(),
+            {
+                'line1': u'Honey Street 1',
+                'line2': u'Behind little tree',
+                'city': u'Beartown',
+                'country_code': u'GB',
+                }
+            )
+
+
+class ShippingAddressTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts
+        addr = ShippingAddress(
+            recipient_name=u'Foo Bar',
+            line1=u'Address Line 1',
+            city=u'Somecity',
+            country_code=u'AT',
+            )
+        verifyClass(IShippingAddress, ShippingAddress)
+        verifyObject(IShippingAddress, addr)
+
+    def test_create(self):
+        # we can create shipping addresses
+        addr = ShippingAddress(
+            recipient_name=u'Rob Receiver',
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        assert addr.recipient_name == u'Rob Receiver'
+        assert addr.type == u'residential'
+        assert addr.line1 == u'Honey Street 1'
+        assert addr.line2 is None
+        assert addr.city == u'Beartown'
+        assert addr.country_code == u'GB'
+        assert addr.postal_code is None
+        assert addr.state is None
+        assert addr.phone is None
+
+    def test_to_dict(self):
+        # we can turn shipping addresses into dicts
+        addr = ShippingAddress(
+            recipient_name=u'Rob Receiver',
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        self.assertEqual(
+            addr.to_dict(),
+            {
+                'recipient_name': u'Rob Receiver',
+                'type': u'residential',
+                'line1': u'Honey Street 1',
+                'city': u'Beartown',
+                'country_code': u'GB',
+                }
+            )
+        addr.line2 = u"Behind little tree"
+        self.assertEqual(
+            addr.to_dict(),
+            {
+                'recipient_name': u'Rob Receiver',
+                'type': u'residential',
+                'line1': u'Honey Street 1',
+                'line2': u'Behind little tree',
+                'city': u'Beartown',
+                'country_code': u'GB',
+                }
+            )
+
+
+class AmountDetailsTests(unittest.TestCase):
+
+    def test_create(self):
+        # we can create AmountDetail objects
+        details = AmountDetails()
+        assert details.shipping is None
+        assert details.subtotal is None
+        assert details.tax is None
+        assert details.fee is None
+        assert details.handling_fee is None
+        assert details.insurance is None
+        assert details.shipping_discount is None
+
+    def test_to_dict(self):
+        # we can turn AmountDetails into a dict
+        details = AmountDetails(
+            shipping=decimal.Decimal("0.10"),
+            tax=decimal.Decimal("0.30"),
+            fee=decimal.Decimal("0.40"),
+            handling_fee=decimal.Decimal("0.50"),
+            insurance=decimal.Decimal("0.60"),
+            shipping_discount=decimal.Decimal("0.70")
+            )
+        self.assertEqual(
+            details.to_dict(),
+            {
+                'shipping': u"0.10",
+                'subtotal': u"1.20",
+                'tax': u"0.30",
+                'fee': u"0.40",
+                'handling_fee': u"0.50",
+                'insurance': u"0.60",
+                'shipping_discount': u"0.70"
+                }
+            )
+
+    def test_subtotal_all_none(self):
+        # if all items are none, also subtotal is none
+        details = AmountDetails(
+            shipping=None, tax=None, fee=None, handling_fee=None,
+            insurance=None, shipping_discount=None,
+            )
+        assert details.subtotal is None
+        details.shipping_discount = decimal.Decimal("1.00")
+        assert details.subtotal == decimal.Decimal("-1.00")
+
+    def test_subtotal_sum(self):
+        # subtotal sums up correctly
+        details = AmountDetails(
+            shipping=decimal.Decimal("0.05"),
+            tax=decimal.Decimal("0.40"),
+            fee=decimal.Decimal("3.00"),
+            handling_fee=decimal.Decimal("20.00"),
+            insurance=decimal.Decimal("100.00"),
+            shipping_discount=None
+            )
+        self.assertEqual(details.subtotal, decimal.Decimal("123.45"))
+        details.shipping_discount = decimal.Decimal("0.00")
+        self.assertEqual(details.subtotal, decimal.Decimal("123.45"))
+        details.shipping_discount = decimal.Decimal("23.45")
+        self.assertEqual(details.subtotal, decimal.Decimal("100.00"))
+
+
+class AmountTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts.
+        amount = Amount()
+        verifyClass(IAmount, Amount)
+        verifyObject(IAmount, amount)
+
+    def test_create(self):
+        # we can create amount objects
+        details = AmountDetails(
+            shipping=decimal.Decimal("0.05"),
+            tax=decimal.Decimal("0.40"),
+            fee=decimal.Decimal("3.00"),
+            handling_fee=decimal.Decimal("20.00"),
+            insurance=decimal.Decimal("100.00"),
+            shipping_discount=None
+            )
+        amount = Amount(
+            total=decimal.Decimal("12.12"),
+            currency="USD",
+            details=details
+            )
+        assert amount.total == decimal.Decimal("12.12")
+        assert amount.currency == "USD"
+        assert amount.details is details
+
+    def test_to_dict(self):
+        # we can turn Amount objects into dicts
+        self.maxDiff = None
+        details = AmountDetails(
+            shipping=decimal.Decimal("0.05"),
+            tax=decimal.Decimal("0.40"),
+            fee=decimal.Decimal("3.00"),
+            handling_fee=decimal.Decimal("20.00"),
+            insurance=decimal.Decimal("100.00"),
+            shipping_discount=None
+            )
+        amount = Amount(
+            total=decimal.Decimal("12.12"),
+            currency="USD",
+            details=details
+            )
+        self.assertEqual(
+            amount.to_dict(),
+            {
+                'total': u'12.12',
+                'currency': u'USD',
+                'details': {
+                    'shipping': u'0.05',
+                    'subtotal': u'123.45',
+                    'tax': u'0.40',
+                    'fee': u'3.00',
+                    'handling_fee': u'20.00',
+                    'insurance': u'100.00',
+                    }
+                }
+            )
+
+
+class ItemTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        item = Item(name=u"Splendid Item")
+        verifyClass(IItem, Item)
+        verifyObject(IItem, item)
+
+    def test_create(self):
+        # we can create Item objects
+        item = Item(
+            quantity=3,
+            name=u"Splendid Thing",
+            price=decimal.Decimal("1.1"),
+            currency="USD",
+            sku="pcs",
+            description=u"Soo splendid!",
+            tax=decimal.Decimal("0.1"),
+            )
+        assert item.quantity == 3
+        assert item.name == u"Splendid Thing"
+        assert item.price == decimal.Decimal("1.1")
+        assert item.currency == "USD"
+        assert item.sku == "pcs"
+        assert item.description == u"Soo splendid!"
+        assert item.tax == decimal.Decimal("0.1")
+
+    def test_to_dict(self):
+        # we can turn Item objects into dicts
+        item = Item(
+            quantity=3,
+            name=u"Splendid Thing",
+            price=decimal.Decimal("1.1"),
+            currency="USD",
+            sku="pcs",
+            description=u"Soo splendid!",
+            tax=decimal.Decimal("0.1"),
+            )
+        self.assertEqual(
+            item.to_dict(),
+            {
+                "quantity": u"3",
+                "name": u"Splendid Thing",
+                "price": u"1.10",
+                "currency": u"USD",
+                "sku": u"pcs",
+                "description": u"Soo splendid!",
+                "tax": u"0.10"
+                }
+            )
+
+
+class ItemListTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        item_list = ItemList()
+        verifyClass(IItemList, ItemList)
+        verifyObject(IItemList, item_list)
+
+    def test_create_minimal(self):
+        # we can create ItemLists with a minimum of params
+        item_list = ItemList()
+        assert item_list.shipping_address is None
+        assert item_list.items == []
+
+    def test_create(self):
+        # we can create ItemLists
+        item1 = Item(
+            name=u"Splendid Thing",
+            )
+        item2 = Item(
+            name=u"Other Splendid Thing",
+            )
+        addr = ShippingAddress(
+            recipient_name=u'Rob Receiver',
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        item_list = ItemList(
+            shipping_address=addr,
+            items=[item1, item2])
+        assert item_list.shipping_address is addr
+        assert item_list.items == [item1, item2]
+
+    def test_to_dict(self):
+        # we can turn ITemLists into dicts
+        item = Item(
+            quantity=3,
+            name=u"Splendid Thing",
+            price=decimal.Decimal("1.1"),
+            currency="USD",
+            sku="pcs",
+            description=u"Soo splendid!",
+            tax=decimal.Decimal("0.1"),
+            )
+        addr = ShippingAddress(
+            recipient_name=u'Rob Receiver',
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        item_list = ItemList(items=[item, ], shipping_address=addr)
+        self.assertEqual(
+            item_list.to_dict(),
+            {
+                "items": [
+                    {
+                        "quantity": u"3",
+                        "name": u"Splendid Thing",
+                        "price": u"1.10",
+                        "currency": u"USD",
+                        "sku": u"pcs",
+                        "description": u"Soo splendid!",
+                        "tax": u"0.10"
+                        }
+                    ],
+                "shipping_address":
+                {
+                    'recipient_name': u'Rob Receiver',
+                    'type': u'residential',
+                    'line1': u'Honey Street 1',
+                    'city': u'Beartown',
+                    'country_code': u'GB',
+                    }
+                }
+            )
+
+
+class PaymentOptionsTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        opts = PaymentOptions()
+        verifyClass(IPaymentOptions, PaymentOptions)
+        verifyObject(IPaymentOptions, opts)
+
+    def test_create(self):
+        # we can create PaymentOptions objects
+        opts = PaymentOptions()
+        assert opts.allowed_payment_method is None
+
+    def test_allowed_payment_method_checked_in_init(self):
+        # any value apart from None, INSTANT... is rejected in __init__
+        self.assertRaises(
+            ValueError,
+            PaymentOptions, allowed_payment_method='NoTvAlID')
+
+    def test_to_dict(self):
+        # we can turn PaymentOptions into dicts
+        opts = PaymentOptions(
+            allowed_payment_method="INSTANT_FUNDING_SOURCE")
+        self.assertEqual(
+            opts.to_dict(),
+            {
+                'allowed_payment_method': "INSTANT_FUNDING_SOURCE",
+                }
+            )
+
+
+class TransactionTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        amount = Amount()
+        transaction = Transaction(amount=amount)
+        verifyClass(ITransaction, Transaction)
+        verifyObject(ITransaction, transaction)
+
+    def test_create(self):
+        # we can create transacions
+        amount = Amount()
+        transaction = Transaction(amount=amount)
+        assert transaction.amount is amount
+        assert transaction.description is None
+        assert transaction.item_list is None
+        assert transaction.related_resources == []
+        assert transaction.invoice_number is None
+        assert transaction.custom is None
+        assert transaction.soft_descriptor is None
+        assert transaction.payment_options is None
+
+    def test_to_dict(self):
+        # we can turn Transaction objects into dicts
+        transaction = Transaction(
+            amount=Amount(),
+            description=u"My description",
+            item_list=ItemList(),
+            related_resources=[],
+            invoice_number=u"12345",
+            custom=u"Some custom remark",
+            soft_descriptor=u"softdescriptor?",
+            payment_options=PaymentOptions(
+                allowed_payment_method="INSTANT_FUNDING_SOURCE"),
+            )
+        self.assertEqual(
+            transaction.to_dict(), {
+                'amount': {'currency': u'USD', 'total': u'0.00'},
+                'custom': u'Some custom remark',
+                'description': u'My description',
+                'invoice_number': u'12345',
+                'item_list': {
+                    'items': []
+                    },
+                'payment_options': {
+                    'allowed_payment_method': u'INSTANT_FUNDING_SOURCE'
+                    },
+                'related_resources': [],
+                'soft_descriptor': u'softdescriptor?'
+                }
+            )
+
+
+class AddressTypesVocabTests(unittest.TestCase):
+
+    def test_address_types_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, ADDRESS_TYPES_VOCAB)
+
+    def test_address_types_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = ADDRESS_TYPES_VOCAB.getTerm('residential')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'residential')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_address_types_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = ADDRESS_TYPES_VOCAB.getTerm('residential')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'residential'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class CreditCardTypesVocabTests(unittest.TestCase):
+
+    def test_credit_card_types_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, CREDIT_CARD_TYPES_VOCAB)
+
+    def test_credit_cards_types_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'visa')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_credit_cards_types_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'visa'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class CreditcardstatusVocabTests(unittest.TestCase):
+
+    def test_credit_card_status_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, CREDIT_CARD_STATUS_VOCAB)
+
+    def test_credit_cards_status_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = CREDIT_CARD_STATUS_VOCAB.getTerm('ok')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'ok')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_credit_cards_status_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = CREDIT_CARD_STATUS_VOCAB.getTerm('expired')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'expired'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class StockKeepingUnitsVocabTests(unittest.TestCase):
+
+    def test_sku_vocab_tokenized(self):
+        # we can get a tokenzed vocab for stock keeping units
+        verifyObject(IVocabularyTokenized, STOCK_KEEPING_UNITS_VOCAB)
+
+    def test_sku_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'pieces')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_sku_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'pcs'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class PaymentOptionMethodsVocabTests(unittest.TestCase):
+
+    def test_payment_option_methods_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, PAYMENT_OPTION_METHODS_VOCAB)
+
+    def test_payment_option_methods_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'INSTANT_FUNDING_SOURCE')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_payment_option_methods_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'INSTANT_FUNDING_SOURCE'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class FakePayer(object):
+
+    implements(IPayer)
+
+    payer_id = 'PAYER-123'
+    first_name = u'Joe'
+    last_name = u'Shopper'
+
+
+class FakePayee(object):
+
+    implements(IPayee)
+
+    payee_id = 'PAYEE-456'
+
+
+class FakePaymentItem(object):
+
+    implements(IPaymentItem)
+
+    title = u'Fake Item'
+    item_id = 'BILL-123456'
+    amount = decimal.Decimal("12.10")
+    currency = 'EUR'
+
+
+# Make sure all fake classes (and objects made of them) are up-to-date
+verifyClass(IPayer, FakePayer)
+verifyObject(IPayer, FakePayer())
+verifyClass(IPayee, FakePayee)
+verifyObject(IPayee, FakePayee())
+verifyClass(IPaymentItem, FakePaymentItem)
+verifyObject(IPaymentItem, FakePaymentItem())
+
+
+class PayPalCreditCardServiceTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def tearDown(self):
+        super(PayPalCreditCardServiceTests, self).tearDown()
+        clearSite()
+
+    def create_site(self):
+        # create a simple site in root.
+        class MyApp(grok.Application, grok.Container):
+            pass
+        site = MyApp()
+        self.getRootFolder()['app'] = site
+        setSite(site)
+        return site
+
+    def get_credit_card(self):
+        # get a valid credit card
+        credit_card = CreditCard(
+            credit_card_type=u"visa",
+            external_customer_id=u"PAYER_0123456789012345678901",
+            number=u"4417119669820331",
+            expire_month=11,
+            expire_year=2018,
+            cvv2=u"874",
+            first_name=u"Joe",
+            last_name=u"Shopper",
+            )
+        return credit_card
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        service = PayPalCreditCardService()
+        verifyClass(IPaymentGatewayService, PayPalCreditCardService)
+        verifyObject(IPaymentGatewayService, service)
+
+    def test_get_credit_card_no_site(self):
+        # we get simply None if no site is registered
+        service = PayPalCreditCardService()
+        assert service.get_credit_card('not-a-valid-payer-id') is None
+
+    def test_get_credit_card_site_not_a_container(self):
+        # we get simply None if site is not a container
+        site = grok.Application()  # does not provide IContainer
+        self.getRootFolder()['app'] = site
+        setSite(site)
+        service = PayPalCreditCardService()
+        assert service.get_credit_card('not-a-valid-payer-id') is None
+
+    def test_get_credit_card_no_container(self):
+        # we get simply None if no 'creditcards' container is in site
+        self.create_site()
+        service = PayPalCreditCardService()
+        assert service.get_credit_card('not-a-valid-payer-id') is None
+
+    def test_get_credit_card(self):
+        # we can get a credit card, if one was stored
+        class MyCard(object):
+            myid = 'CARD1'
+
+            def __eq__(self, obj):
+                return self.myid == obj.myid
+
+        site = self.create_site()
+        site['creditcards'] = grok.Container()
+        card = MyCard()
+        card.myid = 'CHANGED CARD ID'
+        site['creditcards'][u'CARD1'] = card
+        service = PayPalCreditCardService()
+        assert service.get_credit_card(u'CARD1') == card
+        assert service.get_credit_card(u'CARD2') is None
+
+    @external_paypal_test
+    def test_store_credit_card(self):
+        # we can (and must) store credit card data online.
+        site = self.create_site()
+        assert 'creditcards' not in site
+        service = PayPalCreditCardService()
+        credit_card = self.get_credit_card()
+        result = service.store_credit_card(credit_card)
+        # a missing creditcards container is created on-the-fly
+        assert 'creditcards' in site
+        assert ICreditCardToken.providedBy(result)
+        assert result.external_customer_id in site['creditcards']
+        assert site['creditcards'][result.external_customer_id] == result
+
+    @external_paypal_test
+    def test_store_credit_card_invalid(self):
+        # an exception is raised with invalid credit cards.
+        site = self.create_site()
+        service = PayPalCreditCardService()
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+        self.assertRaises(
+            IOError, service.store_credit_card, credit_card)
+
+    def test_create_payment_no_credictard(self):
+        # trying to create a payment without credit card raises an exception
+        service = PayPalCreditCardService()
+        exception = None
+        try:
+            service.create_payment(
+                payer=FakePayer(),
+                payment_item=FakePaymentItem(),
+                payee=FakePayee()
+                )
+        except (ValueError, ) as err:
+            exception = err
+        assert exception.message == 'Payer PAYER-123 has no credit card.'
+
+    @external_paypal_test
+    def test_create_payment(self):
+        # we can actually create payments
+        service = PayPalCreditCardService()
+        site = self.create_site()
+        credit_card = self.get_credit_card()
+        result = service.store_credit_card(credit_card)
+        payer = FakePayer()
+        payer.payer_id = result.external_customer_id
+        payment_item = FakePaymentItem()
+        payment = service.create_payment(
+            payer=payer, payment_item=payment_item)
+        result = payment.create()
+        assert result is True
+
+
+class FunctionalPaypalTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    @external_paypal_test
+    def test_get_access_token(self):
+        # we can get an access token
+        result = get_access_token()
+        assert isinstance(result, unicode)
+
+    @external_paypal_test
+    def test_get_payment(self):
+        # we can construct (paypal-)payment objects
+        payment = get_payment()
+        assert isinstance(payment, paypalrestsdk.Payment)
+        result = payment.create()
+        assert result is True
+
+    def test_paypal_services_registered(self):
+        # the PayPal gateway services are all registered
+        creditcard_service = queryUtility(
+            IPaymentGatewayService, name=u'paypal_creditcard')
+        assert creditcard_service is not None
+        assert isinstance(creditcard_service, PayPalCreditCardService)
+        paypal_regular_service = queryUtility(
+            IPaymentGatewayService, name=u'paypal_regular')
+        assert paypal_regular_service is not None
+        assert isinstance(paypal_regular_service, PayPalRegularPaymentService)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal_countries.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal_countries.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal_countries.py	(revision 12677)
@@ -0,0 +1,38 @@
+import unittest
+from zope.i18nmessageid.message import Message as i18nMessage
+from zope.interface.verify import verifyObject
+from zope.schema.interfaces import IVocabularyTokenized, ITerm
+from waeup.ikoba.payments.paypal_countries import COUNTRIES, COUNTRIES_VOCAB
+
+
+class CountryTests(unittest.TestCase):
+
+    def test_countries_is_dict(self):
+        # COUNTRIES is available
+        assert isinstance(COUNTRIES, dict)
+
+    def test_countries_contains_i18n_messages(self):
+        # the COUNTRIES values are i18n messages
+        assert 'DE' in COUNTRIES.keys()
+        assert isinstance(COUNTRIES['DE'], i18nMessage)
+
+
+class CountriesVocabTests(unittest.TestCase):
+
+    def test_countries_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, COUNTRIES_VOCAB)
+
+    def test_countries_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = COUNTRIES_VOCAB.getTerm('DE')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'GERMANY')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_countries_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = COUNTRIES_VOCAB.getTerm('DE')
+        assert ITerm.providedBy(result)
+        assert result.token == 'DE'
+        assert result.value == 'DE'
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal_currencies.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal_currencies.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal_currencies.py	(revision 12677)
@@ -0,0 +1,63 @@
+# $Id$
+#
+# Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import unittest
+from zope.i18nmessageid.message import Message as i18nMessage
+from zope.interface.verify import verifyObject
+from zope.schema.interfaces import IVocabularyTokenized, ITerm
+from waeup.ikoba.payments.paypal_currencies import (
+    CURRENCIES, CURRENCIES_VOCAB,
+    )
+
+
+class CurrencyTests(unittest.TestCase):
+
+    def test_currencies_is_dict(self):
+        # CURRENCIES is available
+        assert isinstance(CURRENCIES, dict)
+
+    def test_currencies_contains_i18n_messages(self):
+        # the CURRENCIES values are i18n messages
+        assert 'USD' in CURRENCIES.keys()
+        assert isinstance(CURRENCIES['USD'][0], i18nMessage)
+
+    def test_currencies_contain_domestic_infos(self):
+        # there is info about currencies valid domestic only avail.
+        CURRENCIES['USD'][1] is False
+        # there is info about whether the currency support decimals
+        CURRENCIES['USD'][2] is True
+
+
+class CurrenciesVocabTests(unittest.TestCase):
+
+    def test_currencies_vocab_tokenized(self):
+        # we can get a currencies source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, CURRENCIES_VOCAB)
+
+    def test_currencies_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = CURRENCIES_VOCAB.getTerm('USD')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'United States dollar')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_currencies_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = CURRENCIES_VOCAB.getTerm('USD')
+        assert ITerm.providedBy(result)
+        assert result.token == 'USD'
+        assert result.value == 'USD'
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/permissions.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/permissions.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/permissions.py	(revision 12677)
@@ -0,0 +1,302 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+from zope.component import getUtilitiesFor
+from zope.interface import Interface
+from zope.securitypolicy.interfaces import IRole, IPrincipalRoleMap
+from waeup.ikoba.interfaces import ILocalRolesAssignable
+
+class Public(grok.Permission):
+    """Everyone-can-do-this-permission.
+
+    This permission is meant to be applied to objects/views/pages
+    etc., that should be usable/readable by everyone.
+
+    We need this to be able to tune default permissions more
+    restrictive and open up some dedicated objects like the front
+    page.
+    """
+    grok.name('waeup.Public')
+
+class Anonymous(grok.Permission):
+    """Only-anonymous-can-do-this-permission.
+    """
+    grok.name('waeup.Anonymous')
+
+class Authenticated(grok.Permission):
+    """Only-logged-in-users-can-do-this-permission.
+    """
+    grok.name('waeup.Authenticated')
+
+class ViewProducts(grok.Permission):
+    grok.name('waeup.viewProducts')
+
+class ManageProducts(grok.Permission):
+    grok.name('waeup.manageProducts')
+
+class ManagePortal(grok.Permission):
+    grok.name('waeup.managePortal')
+
+class ManageUsers(grok.Permission):
+    grok.name('waeup.manageUsers')
+
+class EditUser(grok.Permission):
+    grok.name('waeup.editUser')
+
+class ManageDataCenter(grok.Permission):
+    grok.name('waeup.manageDataCenter')
+
+class ImportData(grok.Permission):
+    grok.name('waeup.importData')
+
+class ExportData(grok.Permission):
+    grok.name('waeup.exportData')
+
+class ManagePortalConfiguration(grok.Permission):
+    grok.name('waeup.managePortalConfiguration')
+
+
+# Local Roles
+
+class Owner(grok.Role):
+    grok.name('waeup.local.Owner')
+    grok.title(u'Owner')
+    grok.permissions('waeup.editUser')
+
+class ProductManager(grok.Role):
+    grok.name('waeup.local.ProductManager')
+    grok.title(u'Product Manager')
+    grok.permissions('waeup.manageProducts',
+                     'waeup.viewProducts',
+                     'waeup.exportData')
+
+# Site Roles
+class ProductsOfficer(grok.Role):
+    grok.name('waeup.ProductsOfficer')
+    grok.title(u'Products Officer (view only)')
+    grok.permissions('waeup.viewProducts')
+
+class ProductsManager(grok.Role):
+    grok.name('waeup.ProductsManager')
+    grok.title(u'Products Manager')
+    grok.permissions('waeup.viewProducts',
+                     'waeup.manageProducts')
+
+class DataCenterManager(grok.Role):
+    grok.name('waeup.DataCenterManager')
+    grok.title(u'Datacenter Manager')
+    grok.permissions('waeup.manageDataCenter')
+
+class ImportManager(grok.Role):
+    grok.name('waeup.ImportManager')
+    grok.title(u'Import Manager')
+    grok.permissions('waeup.manageDataCenter',
+                     'waeup.importData')
+
+class ExportManager(grok.Role):
+    grok.name('waeup.ExportManager')
+    grok.title(u'Export Manager')
+    grok.permissions('waeup.manageDataCenter',
+                     'waeup.exportData')
+
+class UsersManager(grok.Role):
+    grok.name('waeup.UsersManager')
+    grok.title(u'Users Manager')
+    grok.permissions('waeup.manageUsers',
+                     'waeup.editUser')
+
+class WorkflowManager(grok.Role):
+    grok.name('waeup.WorkflowManager')
+    grok.title(u'Workflow Manager')
+    grok.permissions('waeup.triggerTransition')
+
+class PortalManager(grok.Role):
+    grok.name('waeup.PortalManager')
+    grok.title(u'Portal Manager')
+    grok.permissions('waeup.managePortal',
+                     'waeup.manageUsers',
+                     'waeup.viewProducts', 'waeup.manageProducts',
+                     'waeup.viewDocuments', 'waeup.manageDocuments',
+                     'waeup.manageDataCenter',
+                     'waeup.importData',
+                     'waeup.exportData',
+                     'waeup.managePortalConfiguration',
+                     'waeup.editUser',
+                     'waeup.manageReports',
+                     'waeup.manageJobs',
+                     'waeup.viewCustomer', 'waeup.viewCustomers',
+                     'waeup.manageCustomer', 'waeup.viewCustomersContainer',
+                     'waeup.editCustomerDocuments',
+                     'waeup.triggerTransition',
+                     'waeup.viewCustomersTab',
+                     'waeup.editContracts'
+                     )
+
+class CCOfficer(grok.Role):
+    """This is basically a copy of the the PortalManager class. We exclude some
+    'dangerous' permissions by commenting them out.
+    """
+    grok.baseclass()
+    grok.name('waeup.CCOfficer')
+    grok.title(u'Computer Center Officer')
+    grok.permissions(#'waeup.managePortal',
+                     #'waeup.manageUsers',
+                     'waeup.viewProducts', 'waeup.manageProducts',
+                     'waeup.viewDocuments', 'waeup.manageDocuments',
+                     #'waeup.manageACBatches',
+                     'waeup.manageDataCenter',
+                     #'waeup.importData',
+                     'waeup.exportData',
+                     'waeup.managePortalConfiguration',
+                     #'waeup.editUser',
+                     'waeup.manageReports',
+                     #'waeup.manageJobs',
+                     'waeup.viewCustomer', 'waeup.viewCustomers',
+                     'waeup.manageCustomer', 'waeup.viewCustomersContainer',
+                     'waeup.editCustomerDocuments',
+                     'waeup.triggerTransition',
+                     'waeup.viewCustomersTab',
+                     'waeup.editContracts'
+                     )
+
+def get_all_roles():
+    """Return a list of tuples ``<ROLE-NAME>, <ROLE>``.
+    """
+    return getUtilitiesFor(IRole)
+
+def get_waeup_roles(also_local=False):
+    """Get all Ikoba roles.
+
+    Ikoba 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 Ikoba roles are such whose id starts
+    with ``waeup.local.`` prefix (this is also a convention).
+
+    Returns a generator of the found roles.
+    """
+    for name, item in get_all_roles():
+        if not name.startswith('waeup.'):
+            # Ignore non-Ikoba roles...
+            continue
+        if not also_local and name.startswith('waeup.local.'):
+            # Ignore local roles...
+            continue
+        yield item
+
+def get_waeup_role_names():
+    """Get the ids of all Ikoba roles.
+
+    See :func:`get_waeup_roles` for what a 'IkobaRole' is.
+
+    This function returns a sorted list of Ikoba role names.
+    """
+    return sorted([x.id for x in get_waeup_roles()])
+
+class LocalRolesAssignable(grok.Adapter):
+    """Default implementation for `ILocalRolesAssignable`.
+
+    This adapter returns a list for dictionaries for objects for which
+    we want to know the roles assignable to them locally.
+
+    The returned dicts contain a ``name`` and a ``title`` entry which
+    give a role (``name``) and a description, for which kind of users
+    the permission is meant to be used (``title``).
+
+    Having this adapter registered we make sure, that for each normal
+    object we get a valid `ILocalRolesAssignable` adapter.
+
+    Objects that want to offer certain local roles, can do so by
+    setting a (preferably class-) attribute to a list of role ids.
+
+    You can also define different adapters for different contexts to
+    have different role lookup mechanisms become available. But in
+    normal cases it should be sufficient to use this basic adapter.
+    """
+    grok.context(Interface)
+    grok.provides(ILocalRolesAssignable)
+
+    _roles = []
+
+    def __init__(self, context):
+        self.context = context
+        role_ids = getattr(context, 'local_roles', self._roles)
+        self._roles = [(name, role) for name, role in get_all_roles()
+                       if name in role_ids]
+        return
+
+    def __call__(self):
+        """Get a list of dictionaries containing ``names`` (the roles to
+        assign) and ``titles`` (some description of the type of user
+        to assign each role to).
+        """
+        list_of_dict = [dict(
+                name=name,
+                title=role.title,
+                description=role.description)
+                for name, role in self._roles]
+        return sorted(list_of_dict, key=lambda x: x['name'])
+
+def get_all_users():
+    """Get a list of dictionaries.
+    """
+    users = sorted(grok.getSite()['users'].items(), key=lambda x: x[1].title)
+    for key, val in users:
+        yield(dict(name=key, val=val))
+
+def get_users_with_local_roles(context):
+    """Get a list of dicts representing the local roles set for `context`.
+
+    Each dict returns `user_name`, `user_title`, `local_role`,
+    `local_role_title`, and `setting` for each entry in the local
+    roles map of the `context` object.
+    """
+    try:
+        role_map = IPrincipalRoleMap(context)
+    except TypeError:
+        # no map no roles.
+        raise StopIteration
+    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
+        user = grok.getSite()['users'].get(user_name,None)
+        user_title = getattr(user, 'title', user_name)
+        local_role_title = getattr(
+            dict(get_all_roles()).get(local_role, None), 'title', None)
+        yield dict(user_name = user_name,
+                   user_title = user_title,
+                   local_role = local_role,
+                   local_role_title = local_role_title,
+                   setting = setting)
+
+def get_users_with_role(role, context):
+    """Get a list of dicts representing the usres who have been granted
+    a role for `context`.
+    """
+    try:
+        role_map = IPrincipalRoleMap(context)
+    except TypeError:
+        # no map no roles.
+        raise StopIteration
+    for user_name, setting in role_map.getPrincipalsForRole(role):
+        user = grok.getSite()['users'].get(user_name,None)
+        user_title = getattr(user, 'title', user_name)
+        user_email = getattr(user, 'email', None)
+        yield dict(user_name = user_name,
+                   user_title = user_title,
+                   user_email = user_email,
+                   setting = setting)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/permissions.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/permissions.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/permissions.txt	(revision 12677)
@@ -0,0 +1,106 @@
+Ikoba permissions and roles
+**************************
+
+Permissions and roles used in a Ikoba portal.
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+Convenience functions
+=====================
+
+:mod:`waeup.ikoba` offers some convenience functions to handle security
+roles.
+
+:func:`get_all_roles`
+---------------------
+
+Gives us all roles defined in Ikoba. 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.ikoba.permissions import get_all_roles
+    >>> get_all_roles()
+    <generator object...at 0x...>
+
+    >>> sorted(list(get_all_roles()))
+    [(u'waeup.Customer', <waeup.ikoba.customers.permissions.CustomerRole object at 0x...]
+
+:func:`get_waeup_roles`
+-----------------------
+
+Gives us all roles, except the Ikoba specific roles. We can get a list
+with or without local roles:
+
+    >>> from waeup.ikoba.permissions import get_waeup_roles
+    >>> len(list(get_waeup_roles()))
+    15
+
+    >>> len(list(get_waeup_roles(also_local=True)))
+    19
+
+
+:func:`get_waeup_role_names`
+----------------------------
+
+We can get all role names defined in Ikoba (except 'local'
+roles that are meant not to be assigned globally):
+
+    >>> from waeup.ikoba.permissions import get_waeup_role_names
+    >>> list(get_waeup_role_names())
+    [u'waeup.Customer',
+    u'waeup.CustomerImpersonator',
+    u'waeup.CustomersManager',
+    u'waeup.CustomersOfficer',
+    u'waeup.DataCenterManager',
+    u'waeup.DocumentsManager',
+    u'waeup.DocumentsOfficer',
+    u'waeup.ExportManager',
+    u'waeup.ImportManager',
+    u'waeup.PortalManager',
+    u'waeup.ProductsManager',
+    u'waeup.ProductsOfficer',
+    u'waeup.UsersManager',
+    u'waeup.WorkflowManager',
+    u'waeup.xmlrpcusers1']
+
+:func:`get_users_with_local_roles`
+----------------------------------
+
+We can get all users and their roles for a certain context
+object. This even works for objects that cannot have local roles as
+they are not stored in the ZODB:
+
+    >>> from waeup.ikoba.permissions import get_users_with_local_roles
+    >>> mycontext = object()
+    >>> people_and_roles = get_users_with_local_roles(mycontext)
+    >>> people_and_roles
+    <generator object...at 0x...>
+
+In this case, the result is empty:
+
+    >>> people_and_roles = list(people_and_roles)
+    >>> people_and_roles
+    []
+
+:func:`get_users_with_role`
+---------------------------
+
+We can get all users with a specific role for a certain context
+object:
+
+    >>> from waeup.ikoba.permissions import get_users_with_role
+    >>> mycontext = object()
+    >>> people = get_users_with_role('waeup.portalManager', mycontext)
+    >>> people
+    <generator object...at 0x...>
+
+In this case, the result is empty:
+
+    >>> people = list(people)
+    >>> people
+    []
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/__init__.py	(revision 12677)
@@ -0,0 +1,10 @@
+"""This package contains everything regarding products.
+"""
+# Make this a package.
+from waeup.ikoba.products.container import ProductsContainer
+from waeup.ikoba.products.product import Product
+
+__all__ = [
+    'ProductsContainer',
+    'Product',
+    ]
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/batching.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/batching.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/batching.py	(revision 12677)
@@ -0,0 +1,99 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Batch processing components for product objects.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+"""
+import grok
+import unicodecsv as csv  # XXX: csv ops should move to dedicated module.
+from time import time
+from datetime import datetime
+from zope.i18n import translate
+from zope.interface import Interface
+from zope.schema import getFields
+from zope.component import queryUtility, getUtility, createObject
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from waeup.ikoba.interfaces import (
+    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
+    IObjectHistory, IGNORE_MARKER)
+from waeup.ikoba.interfaces import IIkobaUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.products.interfaces import IProduct
+from waeup.ikoba.utils.batching import BatchProcessor
+
+
+class ProductProcessor(BatchProcessor):
+    """A batch processor for IProduct objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'productprocessor'
+    grok.name(util_name)
+
+    name = _('Product Processor')
+    iface = IProduct
+
+    location_fields = ['product_id',]
+    factory_name = 'waeup.Product'
+
+    mode = None
+
+    def parentsExist(self, row, site):
+        return 'products' in site.keys()
+
+    def entryExists(self, row, site):
+        return row['product_id'] in site['products'].keys()
+
+    def getParent(self, row, site):
+        return site['products']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['product_id'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addProduct(obj)
+        return
+
+    def delEntry(self, row, site):
+        product = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if product is not None:
+            grok.getSite().logger.info(
+                '%s - Product removed' % product.product_id)
+            del parent[product.product_id]
+        return
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = super(ProductProcessor, self).updateEntry(
+            obj, row, site, filename)
+        # Log actions...
+        location_field = self.location_fields[0]
+        grok.getSite().logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, row[location_field], items_changed))
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser.py	(revision 12677)
@@ -0,0 +1,228 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""UI components for products offered by the company.
+"""
+
+import sys
+import grok
+import pytz
+from urllib import urlencode
+from datetime import datetime
+from zope.event import notify
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
+from zope.formlib.textwidgets import BytesDisplayWidget
+from zope.security import checkPermission
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import (
+    IContactForm, IObjectHistory, IIkobaObject, IIkobaUtils)
+from waeup.ikoba.browser.layout import (
+    IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage,
+    IkobaForm, NullValidator, jsaction, action, UtilityView)
+from waeup.ikoba.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget,
+    FriendlyDatetimeDisplayWidget)
+from waeup.ikoba.browser.breadcrumbs import Breadcrumb
+from waeup.ikoba.utils.helpers import html2dict
+from waeup.ikoba.browser.pages import (
+    delSubobjects, add_local_role, del_local_roles, msave,
+    LocalRoleAssignmentUtilityView)
+
+from waeup.ikoba.products.interfaces import (
+    IProductsContainer, IProduct)
+
+grok.context(IIkobaObject) # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+
+class ProductsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customers container.
+    """
+    grok.context(IProductsContainer)
+    title = _('Products')
+
+
+class ProductBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customer container.
+    """
+    grok.context(IProduct)
+
+    def title(self):
+        return self.context.title
+
+
+class ProductsContainerPage(IkobaDisplayFormPage):
+    """The standard view for product containers.
+    """
+    grok.context(IProductsContainer)
+    grok.name('index')
+    grok.require('waeup.Public')
+    grok.template('containerpage')
+    pnav = 1
+    label = _('Products')
+
+
+class ProductsContainerManageFormPage(IkobaEditFormPage,
+                                      LocalRoleAssignmentUtilityView):
+    """The manage page for customer containers.
+    """
+    grok.context(IProductsContainer)
+    grok.name('manage')
+    grok.require('waeup.manageProducts')
+    grok.template('containermanagepage')
+    pnav = 1
+    label = _('Manage product section')
+
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Add product'), _('Remove selected'),_('Cancel')]
+
+    @action(_('Add product'), style='primary', validator=NullValidator)
+    def addSubunit(self, **data):
+        self.redirect(self.url(self.context, 'addproduct'))
+        return
+
+    @jsaction(_('Remove selected'))
+    def delProducts(self, **data):
+        delSubobjects(self, redirect='manage', tab='2')
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        return msave(self, **data)
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+class ProductAddFormPage(IkobaAddFormPage):
+    """Add-form to add a customer.
+    """
+    grok.context(IProductsContainer)
+    grok.require('waeup.manageProducts')
+    grok.name('addproduct')
+    form_fields = grok.AutoFields(IProduct)
+    label = _('Add product')
+    pnav = 1
+
+    @action(_('Create product'), style='primary')
+    def addProduct(self, **data):
+        product = createObject(u'waeup.Product')
+        self.applyData(product, **data)
+        try:
+            self.context.addProduct(product)
+        except KeyError:
+            self.flash(_('The id chosen already exists.'),
+                       type='danger')
+            return
+        self.flash(
+            _("Product ${a} added.", mapping = {'a':data['product_id']}))
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        self.context.__parent__.logger.info(
+            '%s - added: %s' % (ob_class, data['product_id']))
+        self.redirect(self.url(self.context, u'manage')+'#tab2')
+        return
+
+
+class ProductDisplayFormPage(IkobaDisplayFormPage):
+    """ Page to display product data
+    """
+    grok.context(IProduct)
+    grok.name('index')
+    grok.require('waeup.Public')
+    grok.template('productpage')
+    #grok.template('basepage')
+    pnav = 1
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface).omit(
+            'terms_and_conditions', 'description')
+
+    @property
+    def label(self):
+        return self.context.title
+
+    @property
+    def description(self):
+        lang = self.request.cookies.get('ikoba.language')
+        html = self.context.description_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+            html = self.context.description_dict.get(portal_language,'')
+        return html
+
+    @property
+    def terms_and_conditions(self):
+        lang = self.request.cookies.get('ikoba.language')
+        html = self.context.tc_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+            html = self.context.tc_dict.get(portal_language,'')
+        return html
+
+
+class ProductManageFormPage(IkobaEditFormPage,
+                            LocalRoleAssignmentUtilityView):
+    """ View to manage product data
+    """
+    grok.context(IProduct)
+    grok.name('manage')
+    grok.require('waeup.manageProducts')
+    grok.template('productmanagepage')
+    pnav = 1
+
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions1 = [_('Remove selected local roles')]
+    tabtwoactions2 = [_('Add local role')]
+
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(
+            self.context.form_fields_interface).omit('product_id')
+
+    def label(self):
+        return _('Manage product ') + self.context.product_id
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        # Save multilingual dicts
+        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
+        desc = getattr(self.context, 'description', None)
+        self.context.description_dict = html2dict(desc, portal_language)
+        tc = getattr(self.context, 'terms_and_conditions', None)
+        self.context.tc_dict = html2dict(tc, portal_language)
+        return 
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,2,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,2,**data)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/containermanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/containermanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/containermanagepage.pt	(revision 12677)
@@ -0,0 +1,72 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.ikoba" method="POST" enctype="multipart/form-data">
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+    <a href="#tab1" data-toggle="tab">
+      <span i18n:translate="">Settings
+      </span></a>
+    </li>
+    <li>
+    <a href="#tab2" data-toggle="tab">
+      <span i18n:translate="">Products
+      </span> </a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <br /><p i18n:translate="">Setting parameters no yet configured.</p>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <h3 i18n:translate="">Products</h3>
+      <table class="ikoba-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Product Id
+            </th>
+            <th i18n:translate="">Title
+            </th>
+            <th i18n:translate="">Category
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="prod context/values">
+            <td>
+              <input type="checkbox" name="val_id"
+                     tal:attributes="value prod/product_id" />
+            </td>
+            <td>
+              <a tal:attributes="href python: view.url(prod)"
+                 tal:content="prod/__name__"> ID</a>
+            </td>
+            <td>
+              <span tal:content="prod/title">TITLE</span>
+            </td>
+            <td>
+              <span tal:content="prod/contract_category_title">CATEGORY</span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/containerpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/containerpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/containerpage.pt	(revision 12677)
@@ -0,0 +1,23 @@
+<div i18n:domain="waeup.ikoba" i18n:translate="" tal:condition="python: not len(context.keys())">
+There are no subobjects registered yet.
+</div>
+
+<table class="ikoba-data-table dataTable" i18n:domain="waeup.ikoba">
+  <thead>
+    <tr>
+      <th i18n:translate="">Product Id</th>
+      <th i18n:translate="">Title</th>
+      <th i18n:translate="">Category</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tal:repeat repeat="value context/values">
+      <tr tal:condition="python:layout.isAuthenticated() or value.active">
+        <td> <a tal:attributes="href python: view.url(value)">
+            <span tal:content="value/product_id">ID</span></a></td>
+        <td tal:content="value/title">TITLE</td>
+        <td tal:content="value/contract_category_title">CATEGORY</td>
+      </tr>
+    </tal:repeat>
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/productmanagepage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/productmanagepage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/productmanagepage.pt	(revision 12677)
@@ -0,0 +1,90 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.ikoba">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">Settings</a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="tab-pane active">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="ikoba-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+            <input type="checkbox" name="role_id"
+                   tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <div class="form-inline">
+        <br />
+        <div class="form-group">
+          <select id="user" name="user" class="form-control">
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+            <span tal:replace="user/val/title">USERTITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                  <input tal:condition="python:action.label in view.tabtwoactions2"
+                         tal:replace="structure action/render"/>
+                </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/productpage.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/productpage.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/browser_templates/productpage.pt	(revision 12677)
@@ -0,0 +1,18 @@
+<span tal:condition="view/description" tal:omit-tag="">
+  <p tal:content="structure view/description">Description</p>
+  <br />
+</span>
+
+<table class="form-table" i18n:domain="waeup.ikoba">
+  <tbody>
+    <tal:widgets content="structure provider:widgets" />
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Terms and Conditions:
+      </td>
+      <td>
+        <span tal:content="structure view/terms_and_conditions">TC</span>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/catalog.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/catalog.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/catalog.py	(revision 12677)
@@ -0,0 +1,31 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components to help cataloging and searching products.
+"""
+import grok
+from waeup.ikoba.interfaces import ICompany
+from waeup.ikoba.products.interfaces import IProduct
+
+class ProductIndexes(grok.Indexes):
+    """A catalog for all products.
+    """
+    grok.site(ICompany)
+    grok.name('products_catalog')
+    grok.context(IProduct)
+
+    product_id = grok.index.Field(attribute='product_id')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/container.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/container.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/container.py	(revision 12677)
@@ -0,0 +1,59 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Containers which contain product objects.
+"""
+import grok
+from grok import index
+from waeup.ikoba.interfaces import IIkobaPluggable
+from waeup.ikoba.products.interfaces import IProductsContainer, IProduct
+from waeup.ikoba.utils.helpers import attrs_to_fields
+from waeup.ikoba.utils.logger import Logger
+
+class ProductsContainer(grok.Container, Logger):
+    """This is a container for all kind of products.
+    """
+    grok.implements(IProductsContainer)
+    grok.provides(IProductsContainer)
+
+
+    def addProduct(self, product):
+        if not IProduct.providedBy(product):
+            raise TypeError(
+                'ProductsContainers contain only IProduct instances')
+        self[product.product_id] = product
+        return
+
+ProductsContainer = attrs_to_fields(ProductsContainer)
+
+
+class ContainerPlugin(grok.GlobalUtility):
+    """A plugin that creates container for Container inside a company.
+    """
+    grok.implements(IIkobaPluggable)
+    grok.name('productscontainer')
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        container = site['products']
+        for value in container.values():
+            if not getattr(value, 'description_dict', None):
+                value.description_dict = {}
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/export.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/export.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/export.py	(revision 12677)
@@ -0,0 +1,71 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Exporters for products.
+"""
+import grok
+from zope.securitypolicy.interfaces import IPrincipalRoleMap
+from waeup.ikoba.interfaces import ICSVExporter
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.utils.batching import ExporterBase
+from waeup.ikoba.utils.helpers import iface_names
+from waeup.ikoba.products.interfaces import IProduct
+
+
+class ProductExporter(grok.GlobalUtility, ExporterBase):
+    """Exporter for products.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('products')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(IProduct))) + (
+        'users_with_local_roles',)
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Products')
+
+    def mangle_value(self, value, name, context=None):
+        """Hook for mangling values in derived classes
+        """
+        if name == 'users_with_local_roles':
+            value = []
+            role_map = IPrincipalRoleMap(context)
+            for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
+                value.append({'user_name':user_name,'local_role':local_role})
+        if name == 'options' and value is not None:
+            value = [eval(entry.to_string()) for entry in value]
+        return super(ProductExporter, self).mangle_value(
+            value, name, context)
+
+    def export(self, products, filepath=None):
+        """Export `products`, an iterable, as CSV file.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for product in products:
+            self.write_item(product, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export products in productscontainer into filepath as CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        products = site.get('products', {})
+        return self.export(products.values(), filepath)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/interfaces.py	(revision 12677)
@@ -0,0 +1,107 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+from zope.interface import Attribute, Interface
+from zope import schema
+from waeup.ikoba.interfaces import (
+    IIkobaObject, validate_id,
+    ContextualDictSourceFactoryBase)
+from waeup.ikoba.schema import FormattedDate
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.products.productoptions import ProductOptionField
+
+class ConCatSource(ContextualDictSourceFactoryBase):
+    """A contract category source delivers all contract categories
+    provided in the portal.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'CON_CATS_DICT'
+
+class IProductsContainer(IIkobaObject):
+    """A container for all kind of product objects.
+
+    """
+
+    def addProduct(product):
+        """Add a product.
+        """
+
+class IProduct(IIkobaObject):
+    """A base representation of products.
+
+    """
+    product_id = schema.TextLine(
+        title = _(u'Product Id'),
+        default = u'NA',
+        required = True,
+        constraint=validate_id,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Product Title'),
+        required = True,
+        default = u'Unnamed',
+        )
+
+    contract_title = schema.TextLine(
+        title = _(u'Contract Title'),
+        description = _('Product title is used if empty.'),
+        required = False,
+        )
+
+    contract_category = schema.Choice(
+        title = _(u'Contract Category'),
+        source = ConCatSource(),
+        required = True,
+        )
+
+    options = schema.List(
+        title = _(u'Options/Fees'),
+        value_type = ProductOptionField(),
+        required = False,
+        readonly = False,
+        default = [],
+        )
+
+    valid_from = FormattedDate(
+        title = _(u'Valid from'),
+        required = False,
+        show_year = True,
+        )
+
+    valid_to = FormattedDate(
+        title = _(u'Valid to'),
+        required = False,
+        show_year = True,
+        )
+
+    description_dict = Attribute(
+        'Description as language dictionary with values in html format')
+
+    description = schema.Text(
+        title = _(u'Description'),
+        required = False,
+        description = _(u'Multilingual content in HTML format'),
+        )
+
+    tc_dict = Attribute('Terms and Conditions as language dictionary with values in html format')
+
+    terms_and_conditions = schema.Text(
+        title = _(u'Terms and Conditions'),
+        required = False,
+        description = _(u'Multilingual content in HTML format'),
+        )
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/product.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/product.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/product.py	(revision 12677)
@@ -0,0 +1,114 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+These are the products (e.g. licenses).
+"""
+import grok
+from time import time
+from grok import index
+from datetime import date
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from zope.component.interfaces import IFactory, ComponentLookupError
+from zope.interface import implementedBy
+from zope.i18n import translate
+from waeup.ikoba.interfaces import IIkobaUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.utils.helpers import attrs_to_fields, get_current_principal
+from waeup.ikoba.products.interfaces import (
+    IProduct, IProduct,)
+
+class Product(grok.Container):
+    """This is a product.
+    """
+    grok.implements(IProduct)
+    grok.provides(IProduct)
+    #grok.baseclass()
+
+    form_fields_interface = IProduct
+
+    local_roles = [
+        'waeup.local.ProductManager',
+        ]
+
+    def __init__(self, *args, **kw):
+        super(Product, self).__init__(*args, **kw)
+        self.tc_dict = {}
+        self.description_dict = {}
+
+    @property
+    def contract_category_title(self):
+        cc_dict = getUtility(IIkobaUtils).CON_CATS_DICT
+        return cc_dict[self.contract_category]
+
+    @property
+    def contract_autotitle(self):
+        if self.contract_title:
+            return self.contract_title
+        return self.title
+
+    @property
+    def active(self):
+        now = date.today()
+        return (not self.valid_from or self.valid_from <= now) and \
+               (not self.valid_to or self.valid_to >= now)
+
+Product = attrs_to_fields(Product)
+
+
+class ProductFactory(grok.GlobalUtility):
+    """A factory for products.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Product')
+    title = u"Create a new product.",
+    description = u"This factory instantiates new products."
+
+    def __call__(self, *args, **kw):
+        return Product(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(Product)
+
+@grok.subscribe(IProduct, grok.IObjectRemovedEvent)
+def handle_product_removed(product, event):
+    """If a product is deleted, we make sure that also referrers to
+    customer contract objects are removed.
+    """
+    prodid = product.product_id
+
+    # Find all customer contracts that refer to given product...
+    try:
+        cat = getUtility(ICatalog, name='contracts_catalog')
+    except ComponentLookupError:
+        # catalog not available. This might happen during tests.
+        return
+    results = cat.searchResults(last_product_id=(prodid, prodid))
+    for contract in results:
+        # Remove that referrer...
+        contract.product_object = None
+        notify(grok.ObjectModifiedEvent(contract))
+        contract.customer.__parent__.logger.info(
+            'ObjectRemovedEvent - %s - %s - removed: %s' % (
+                contract.customer.customer_id,
+                contract.contract_id,
+                prodid))
+    return
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/productoptions.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/productoptions.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/productoptions.py	(revision 12677)
@@ -0,0 +1,159 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components with interfaces representing and aggregating product options.
+"""
+import grok
+from decimal import Decimal
+from zope import schema
+from zope.schema.interfaces import IObject
+from zope.interface import Attribute, Interface
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema import Object
+from waeup.ikoba.interfaces import CurrencySource
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.widgets.objectwidget import (
+    IkobaObjectWidget, IkobaObjectDisplayWidget
+    )
+
+#: A unique default value.
+DEFAULT_VALUE = object()
+
+
+class IProductOption(Interface):
+    """A product option entry.
+
+    """
+    title = schema.TextLine(
+        title = _(u'Title'),
+        required = True,
+        )
+    fee = schema.Decimal(
+        title = _(u'Fee'),
+        required = False,
+        )
+    currency = schema.Choice(
+        title = _(u'Currency'),
+        source = CurrencySource(),
+        required = False,
+        )
+
+class IProductOptionField(IObject):
+    """A zope.schema-like field for usage in interfaces.
+
+    Marker interface to distuingish product options from ordinary
+    object fields. Needed for registration of widgets.
+    """
+
+class ProductOption(grok.Model):
+    """A product option entry contains a title, fee and currency.
+    """
+    grok.implements(IProductOption)
+    title = FieldProperty(IProductOption['title'])
+    fee = FieldProperty(IProductOption['fee'])
+    currency = FieldProperty(IProductOption['currency'])
+
+    def __init__(self, title=None, fee=None, currency=None):
+        super(ProductOption, self).__init__()
+        if title is not None:
+            self.title = title
+        if fee is not None:
+            self.fee = fee
+        if currency is not None:
+            self.currency = currency
+        return
+
+    def __eq__(self, obj):
+        """Two ProductOption objects are equal if their `title`, `fee` and
+           `grade` are equal.
+        """
+        for name in ('title', 'fee', 'currency',):
+            if getattr(self, name) != getattr(obj, name, DEFAULT_VALUE):
+                return False
+        return True
+
+    def __ne__(self, other):
+        """Two ProductOptions are not equal, if their equality test fails.
+
+        a != b <-> not(a == b). Python doc tell, that __ne__ should
+        also be provided, whenever __eq__ is implemented.
+        """
+        return not self.__eq__(other)
+
+
+    def to_string(self):
+        """A string representation that can be used in exports.
+
+        Returned is a unicode string of format ``(u'<TITLE>',u'<FEE>',u'<CURR>')``.
+        """
+        string = u"(u'%s', u'%s', u'%s')" % (self.title, self.fee, self.currency)
+        string = string.replace("u'None'", "None")
+        return string
+
+    @classmethod
+    def from_string(cls, string):
+        """Create new ProductOption instance based on `string`.
+
+        The string is expected to be in format as delivered by
+        meth:`to_string`.
+
+        This is a classmethod. This means, you normally will call::
+
+          ProductOption.from_string(mystring)
+
+        i.e. use the `ProductOption` class, not an instance thereof.
+        """
+        string = string.replace("u''", "None")
+        title, fee, currency = eval(string)
+        try:
+            fee = Decimal(fee)
+            title = unicode(title)
+            currency = unicode(currency)
+        except TypeError:
+            pass
+        return cls(title, fee, currency)
+
+class ProductOptionField(Object):
+    """A zope.schema-like field for usage in interfaces.
+
+    If you want to define an interface containing product options, you
+    can do so like this::
+
+      class IMyInterface(Interface):
+          my_option_entry = ProductOptionField()
+
+    Default widgets are registered to render product option fields.
+    """
+    grok.implements(IProductOptionField)
+
+    def __init__(self, **kw):
+        super(ProductOptionField, self).__init__(IProductOption, **kw)
+        return
+
+# register IkobaObjectWidgets as default widgets for IProductOptionFields
+@grok.adapter(IProductOptionField, IBrowserRequest)
+@grok.implementer(IInputWidget)
+def option_input_widget(obj, req):
+    return IkobaObjectWidget(obj, req, ProductOption)
+
+# register a display widget for IProductOptionFields
+@grok.adapter(IProductOptionField, IBrowserRequest)
+@grok.implementer(IDisplayWidget)
+def option_display_widget(obj, req):
+    return IkobaObjectDisplayWidget(obj, req, ProductOption)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/__init__.py	(revision 12677)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/sample_product_data.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/sample_product_data.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/sample_product_data.csv	(revision 12677)
@@ -0,0 +1,5 @@
+product_id,title,contract_category
+ABC,ABC title,license
+CDE,CDE title,license
+EFG,EFG title,license
+ABC,Duplicate product,license
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/sample_product_data_update.csv
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/sample_product_data_update.csv	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/sample_product_data_update.csv	(revision 12677)
@@ -0,0 +1,3 @@
+product_id,title
+ABC,ABC new title
+XYZ,Non-existing product
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_batching.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_batching.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_batching.py	(revision 12677)
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Unit tests for product data processors.
+"""
+import os
+import shutil
+import tempfile
+import unittest
+import datetime
+import grok
+from decimal import Decimal
+from time import time
+from zope.event import notify
+from zope.component import createObject, queryUtility
+from zope.component.hooks import setSite, clearSite
+from zope.catalog.interfaces import ICatalog
+from zope.interface.verify import verifyClass, verifyObject
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import IBatchProcessor, FatalCSVError, IUserAccount
+from waeup.ikoba.products.batching import ProductProcessor
+from waeup.ikoba.products.product import Product
+from waeup.ikoba.products.productoptions import ProductOption
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+PRODUCT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_product_data.csv'),
+    'rb').read()
+
+PRODUCT_HEADER_FIELDS = PRODUCT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+PRODUCT_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_product_data_update.csv'),
+    'rb').read()
+
+PRODUCT_HEADER_FIELDS_UPDATE = PRODUCT_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+class ProductImportExportSetup(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ProductImportExportSetup, self).setUp()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
+        app = Company()
+        app['datacenter'].setStoragePath(self.dc_root)
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(app)
+
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        return
+
+    def tearDown(self):
+        super(ProductImportExportSetup, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def setup_for_export(self):
+        product = Product()
+        product.product_id = u'LIC'
+        product.contract_category = u'license'
+        options = ProductOption()
+        options.title = u'Base Fee'
+        options.fee = Decimal('800.6')
+        options.currency = 'USD'
+        product.options = [options,]
+        product.valid_from = datetime.date(2015, 12, 4)
+        self.app['products'][product.product_id] = self.product = product
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        role_manager = IPrincipalRoleManager(product)
+        role_manager.assignRoleToPrincipal(u'johnsrole', u'john')
+        return
+
+
+class ProductProcessorTest(ProductImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ProductProcessorTest, self).setUp()
+
+        # Add product with subobjects
+        product = Product()
+        self.app['products'].addProduct(product)
+        product.title = u'Our Product'
+        notify(grok.ObjectModifiedEvent(product))
+        self.product = self.app['products'][product.product_id]
+
+        self.processor = ProductProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_product_data.csv')
+        self.csv_file_update = os.path.join(
+            self.workdir, 'sample_product_data_update.csv')
+        open(self.csv_file, 'wb').write(PRODUCT_SAMPLE_DATA)
+        open(self.csv_file_update, 'wb').write(PRODUCT_SAMPLE_DATA_UPDATE)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, ProductProcessor) is True
+
+    def test_parentsExist(self):
+        self.assertFalse(self.processor.parentsExist(None, dict()))
+        self.assertTrue(self.processor.parentsExist(None, self.app))
+
+    def test_entryExists(self):
+        assert self.processor.entryExists(
+            dict(product_id='ID_NONE'), self.app) is False
+        assert self.processor.entryExists(
+            dict(product_id='NA'), self.app) is True
+
+    def test_getParent(self):
+        parent = self.processor.getParent(None, self.app)
+        assert parent is self.app['products']
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(product_id='ID_NONE'), self.app) is None
+        assert self.processor.getEntry(
+            dict(product_id=self.product.product_id), self.app) is self.product
+
+    def test_addEntry(self):
+        new_product = Product()
+        new_product.product_id = u'NEW'
+        self.processor.addEntry(
+            new_product, dict(), self.app)
+        assert len(self.app['products'].keys()) == 2
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(product_id='ABC'))
+        self.assertEqual(len(errs),0)
+        return
+
+    def test_delEntry(self):
+        assert self.product.product_id in self.app['products'].keys()
+        self.processor.delEntry(
+            dict(product_id=self.product.product_id), self.app)
+        assert self.product.product_id not in self.app['products'].keys()
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PRODUCT_HEADER_FIELDS)
+        self.assertEqual(num_warns,1)
+        self.assertEqual(len(self.app['products']), 4)
+        self.assertEqual(self.app['products']['ABC'].title,'ABC title')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Product Processor - sample_product_data - '
+            'EFG - updated: product_id=EFG, title=EFG title, '
+            'contract_category=license\n'
+            in logcontent)
+        failcontent = open(fail_file).read()
+        self.assertTrue(
+            'ABC,Duplicate product,This object already exists.'
+            in failcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PRODUCT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update, PRODUCT_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,1)
+        # title has changed
+        self.assertEqual(self.app['products']['ABC'].title,'ABC new title')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Product Processor - sample_product_data_update - '
+            'ABC - updated: product_id=ABC, title=ABC new title\n'
+            in logcontent)
+        failcontent = open(fail_file).read()
+        self.assertTrue(
+            'XYZ,Non-existing product,Cannot update: no such entry'
+            in failcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_browser.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_browser.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_browser.py	(revision 12677)
@@ -0,0 +1,260 @@
+## $Id$
+## 
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the products-related UI components.
+"""
+import shutil
+import tempfile
+import pytz
+import base64
+from decimal import Decimal
+from datetime import datetime, timedelta, date
+from StringIO import StringIO
+import os
+import grok
+from zc.async.testing import wait_for_result
+from zope.event import notify
+from zope.component import createObject, queryUtility, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.catalog.interfaces import ICatalog
+from zope.security.interfaces import Unauthorized
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.testbrowser.testing import Browser
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import (
+    IUserAccount, IJobManager, APPROVED, NotIdValue)
+from waeup.ikoba.authentication import LocalRoleSetEvent
+from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
+from waeup.ikoba.browser.tests.test_pdf import samples_dir
+from waeup.ikoba.products.productoptions import ProductOption
+
+def lookup_submit_value(name, value, browser):
+    """Find a button with a certain value."""
+    for num in range(0, 100):
+        try:
+            button = browser.getControl(name=name, index=num)
+            if button.value.endswith(value):
+                return button
+        except IndexError:
+            break
+    return None
+
+class ProductsFullSetup(FunctionalTestCase):
+    # A test case that only contains a setup and teardown
+    #
+    # Complete setup for products handlings is rather complex and
+    # requires lots of things created before we can start. This is a
+    # setup that does all this, creates a company,
+    # etc.  so that we do not have to bother with that in different
+    # test cases.
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ProductsFullSetup, self).setUp()
+
+        # Setup a sample site for each test
+        app = Company()
+        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.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+
+        # Add user
+        self.app['users'].addUser(name='john', password='johnpwd',
+                                  title='John Tester',
+                                  email='john@tester.de')
+
+        # Add some product
+        self.product = createObject('waeup.Product')
+        self.product.product_id = u'LIC'
+        self.assertRaises(
+            NotIdValue, setattr, self.product, 'product_id', u'id with spaces')
+        self.product.title = u'Our License'
+        self.product.contract_category = u'license'
+        self.product.valid_from = date(2015, 12, 4)
+        self.app['products'].addProduct(self.product)
+
+        # Add session configuration
+        self.app['configuration'].carry_over = True
+        self.login_path = 'http://localhost/app/login'
+        self.container_path = 'http://localhost/app/products'
+        self.manage_container_path = self.container_path + '/manage'
+        self.add_product_path = self.container_path + '/addproduct'
+        self.product_path = self.container_path + '/LIC'
+        self.manage_product_path = self.container_path + '/LIC/manage'
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(ProductsFullSetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+class ProductsContainerUITests(ProductsFullSetup):
+    # Tests for ProductsContainer class views and pages
+
+    layer = FunctionalLayer
+
+    def test_anonymous_access(self):
+        # Anonymous users can view products containers ...
+        self.browser.open(self.container_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # ... but not manage
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.manage_container_path)
+        return
+
+    def test_manage_access(self):
+        # Managers can access the view page of products
+        # 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").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_container_path)
+        return
+
+    def test_add_delete_products(self):
+        # Managers can add search and remove products
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl("Add product").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.add_product_path)
+        self.browser.getControl(name="form.product_id").value = 'bob'
+        self.browser.getControl(name="form.title").value = 'My product'
+        self.browser.getControl(name="form.contract_category").value = ['license']
+        self.browser.getControl("Create product").click()
+        self.assertTrue('Product bob added' in self.browser.contents)
+        self.assertEqual(len(self.app['products'].keys()), 2)
+        self.assertEqual(self.app['products']['bob'].title, 'My product')
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='bob').selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+        self.assertEqual(len(self.app['products'].keys()), 1)
+
+
+class ProductsUITests(ProductsFullSetup):
+    # Tests for Products class views and pages
+
+    layer = FunctionalLayer
+
+    def test_anonymous_access(self):
+        # Anonymous users can access products ...
+        self.browser.open(self.product_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # ... but not manage products
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.manage_product_path)
+        return
+
+    def test_manage_access(self):
+        # Managers can access the view pages of products
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.product_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # There is a Manage button
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_product_path)
+        self.browser.getControl(name="form.title").value = 'New license'
+        self.browser.getControl(name="form.terms_and_conditions").value = """
+<h1>Hello World</h1>
+>>de<<
+<h1>Hallo Welt</h1>
+"""
+        self.browser.getControl("Save").click()
+        self.assertEqual(self.app['products']['LIC'].title, 'New license')
+        # tc_dict has been saved
+        self.assertEqual(self.app['products']['LIC'].tc_dict,
+            {'de': u'<div id="html">\n<h1>Hallo Welt</h1>\n</div id="html">',
+             'en': u'<div id="html">\n<h1>Hello World</h1>\n</div id="html">'})
+        # Managers can asign local roles
+        self.browser.getControl(name="user").value = ['john']
+        self.browser.getControl(
+            name="local_role").value = ['waeup.local.ProductManager']
+        self.browser.getControl("Add local role").click()
+        self.assertTrue('john|waeup.local.ProductManager' in self.browser.contents)
+        self.assertEqual(
+            self.app['users']['john'].getLocalRoles().get(
+                'waeup.local.ProductManager')[0],
+            self.product)
+        # There is a View button wich directs to the index page
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.product_path + '/index')
+        # If products is deleted also the local role disappears
+        self.browser.open(self.manage_container_path)
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='LIC').selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertEqual(
+            self.app['users']['john'].getLocalRoles().get('waeup.local.ProductManager'),
+            None)
+        return
+
+    def test_product_options(self):
+        prodoptions = ProductOption()
+        prodoptions.title = u'My option'
+        prodoptions.fee = Decimal('99.9')
+        prodoptions.currency = 'USD'
+        self.product.options = [prodoptions,]
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.product_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.product.options[0], prodoptions)
+        self.browser.open(self.manage_product_path)
+        self.browser.getControl("Save").click()
+        # applyData must not exchange the object
+        self.assertEqual(self.product.options[0], prodoptions)
+        # and thus not add a logging message (the log file
+        # do not even exist so far)
+        self.assertRaises(IOError, open, self.logfile, 'read')
+        # Now we change the content after opening the manage page
+        self.browser.open(self.manage_product_path)
+        prodoptions.title = u'My new option'
+        self.assertEqual(self.product.options[0].title, 'My new option')
+        # but save the opened form with the old data.
+        self.browser.getControl("Save").click()
+        # The object has been replaced,
+        self.assertTrue(self.product.options[0] != prodoptions)
+        # the old title has been restored
+        self.assertEqual(self.product.options[0].title, 'My option')
+        # and a logging message has been added.
+        logcontent = open(self.logfile).read()
+        self.assertTrue('saved: options' in logcontent)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_export.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_export.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_export.py	(revision 12677)
@@ -0,0 +1,86 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the product exporter.
+"""
+import os
+import grok
+import datetime
+from cStringIO import StringIO
+from zope.component import queryUtility, getUtility
+from zope.event import notify
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.ikoba.interfaces import (
+    ICSVExporter, IExtFileStore, IFileStoreNameChooser)
+from waeup.ikoba.products.export import ProductExporter
+from waeup.ikoba.products.product import Product
+from waeup.ikoba.products.tests.test_batching import ProductImportExportSetup
+from waeup.ikoba.testing import FunctionalLayer
+
+
+class ProductExporterTest(ProductImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ProductExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = ProductExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter,ProductExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an product exporter as utility
+        result = queryUtility(ICSVExporter, name="products")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export a product
+        exporter = ProductExporter()
+        exporter.export([self.product], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'contract_category,contract_title,description,options,'
+            'product_id,terms_and_conditions,title,valid_from,valid_to,users_with_local_roles\r\n'
+            'license,,,"[(u\'Base Fee\', u\'800.6\', u\'USD\')]",LIC,,Unnamed'
+            ',2015-12-04#,,"[{\'user_name\': u\'john\', \'local_role\': '
+            'u\'johnsrole\'}]"\r\n'
+            in result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all products
+        exporter = ProductExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'contract_category,contract_title,description,options,'
+            'product_id,terms_and_conditions,title,valid_from,valid_to,users_with_local_roles\r\n'
+            'license,,,"[(u\'Base Fee\', u\'800.6\', u\'USD\')]",LIC,,Unnamed'
+            ',2015-12-04#,,"[{\'user_name\': u\'john\', \'local_role\': '
+            'u\'johnsrole\'}]"\r\n'
+            in result
+            )
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_product.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_product.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_product.py	(revision 12677)
@@ -0,0 +1,63 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for products.
+"""
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import createObject
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.ikoba.products.interfaces import (
+    IProductsContainer, IProduct)
+from waeup.ikoba.interfaces import IObjectHistory
+from waeup.ikoba.products.container import ProductsContainer
+from waeup.ikoba.products.product import Product
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+class ProductsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IProductsContainer, ProductsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IProductsContainer, ProductsContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IProduct, Product)
+            )
+        self.assertTrue(
+            verifyObject(
+                IProduct, Product())
+            )
+        return
+
+    def test_addProduct(self):
+        container = ProductsContainer()
+        product = createObject(u'waeup.Product')
+        container.addProduct(product)
+        self.assertRaises(TypeError, container.addProduct, object())
+        self.assertEqual(product.product_id, 'NA')
+        self.assertEqual(container['NA'], product)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_productoptions.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_productoptions.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/tests/test_productoptions.py	(revision 12677)
@@ -0,0 +1,120 @@
+# Tests for schoolgrades module.
+import unittest
+from decimal import Decimal
+from zope.component import getGlobalSiteManager
+from zope.interface.verify import verifyObject, verifyClass
+from zope.schema.interfaces import ConstraintNotSatisfied, WrongType
+from waeup.ikoba.interfaces import  IIkobaUtils
+from waeup.ikoba.products.productoptions import (
+    IProductOption, IProductOptionField, ProductOption, ProductOptionField)
+from waeup.ikoba.utils.utils import IkobaUtils
+
+class ProductOptionTests(unittest.TestCase):
+
+    def setUp(self):
+        self.utils = IkobaUtils()
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(self.utils, IIkobaUtils)
+        self.valid_title = u'Option 1'
+        self.valid_fee = Decimal(55.5)
+        self.valid_currency = 'USD'
+        return
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.utils)
+        return
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces.
+        obj = ProductOption()
+        verifyObject(IProductOption, obj)
+        verifyClass(IProductOption, ProductOption)
+        return
+
+    def test_init(self):
+        # we can pass initial values
+        item1 = ProductOption()
+        item2 = ProductOption(
+            self.valid_title, self.valid_fee, self.valid_currency)
+        self.assertTrue(item1.title is None)
+        self.assertTrue(item1.fee is None)
+        self.assertTrue(item1.currency is None)
+        self.assertEqual(item2.title, self.valid_title)
+        self.assertEqual(item2.fee, self.valid_fee)
+        self.assertEqual(item2.currency, self.valid_currency)
+        return
+
+    def test_illegal_value(self):
+        # we do not accept values which do not meet the interface
+        item = ProductOption()
+        self.assertRaises(
+            WrongType, ProductOption, 6, 500, 'invalid')
+        return
+
+    def test_to_string(self):
+        # the string representation is handy for export
+        item1 = ProductOption()
+        item2 = ProductOption(
+            self.valid_title, self.valid_fee, self.valid_currency)
+        self.assertEqual(item1.to_string(), u"(None, None, None)")
+        self.assertEqual(item2.to_string(), u"(u'%s', u'%s', u'%s')" % (
+            self.valid_title, self.valid_fee, self.valid_currency))
+        return
+
+    def test_from_string(self):
+        # we can create new result entries based on strings
+        myinput = u"(u'%s',u'%s',u'%s')" % (
+            self.valid_title, self.valid_fee, self.valid_currency)
+        myinput2 = u"('%s','%s','%s')" % (
+            self.valid_title, self.valid_fee, self.valid_currency)
+        item1 = ProductOption.from_string(myinput)
+        item2 = ProductOption.from_string(u"(u'', u'', u'')")
+        item3 = ProductOption.from_string(u"(None, None, None)")
+        item4 = ProductOption.from_string(myinput2)
+        self.assertEqual(item1.title, self.valid_title)
+        self.assertEqual(item1.fee, self.valid_fee)
+        self.assertEqual(item1.currency, self.valid_currency)
+        self.assertTrue(item2.title is None)
+        self.assertTrue(item2.fee is None)
+        self.assertEqual(item2.currency, None)
+        self.assertTrue(item3.title is None)
+        self.assertTrue(item3.fee is None)
+        self.assertEqual(item3.currency, None)
+        self.assertEqual(item4.title, self.valid_title)
+        self.assertEqual(item4.fee, self.valid_fee)
+        self.assertEqual(item4.currency, self.valid_currency)
+        return
+
+    def test_eq(self):
+        # we can compare equality of ProductOption objects
+        item1 = ProductOption(self.valid_title, self.valid_fee, self.valid_currency)
+        item2 = ProductOption(self.valid_title, self.valid_fee, self.valid_currency)
+        item3 = ProductOption()
+        item4 = ProductOption()
+        assert item1 is not item2
+        assert item1 == item1
+        assert item1 == item2
+        assert item3 is not item4
+        assert item3 == item4
+        assert item1.__eq__(item2) is True
+        assert item1.__eq__(item3) is False
+
+    def test_ne(self):
+        # we can also tell, which ResultEntries are _not_ equal
+        item1 = ProductOption(self.valid_title, self.valid_fee, self.valid_currency)
+        item2 = ProductOption()
+        assert item1 != item2
+        assert item1.__ne__(item2) is True
+        assert item1.__ne__(item1) is False
+
+
+
+class ProductOptionFieldTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces.
+        obj = ProductOptionField()
+        verifyObject(IProductOptionField, obj)
+        verifyClass(IProductOptionField, ProductOptionField)
+        return
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/viewlets.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/viewlets.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/products/viewlets.py	(revision 12677)
@@ -0,0 +1,80 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Viewlet components for products offered by the company.
+"""
+
+import grok
+from waeup.ikoba.browser.viewlets import (
+    PrimaryNavTab, ManageActionButton, AddActionButton)
+from waeup.ikoba.interfaces import IIkobaObject
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.browser.viewlets import PrimaryNavTab
+
+from waeup.ikoba.products.interfaces import (
+    IProductsContainer, IProduct)
+from waeup.ikoba.products.browser import (
+    ProductsContainerManageFormPage, ProductsContainerPage,
+    ProductManageFormPage, ProductDisplayFormPage)
+
+
+grok.context(IIkobaObject)  # Make IIkobaObject the default context
+grok.templatedir('browser_templates')
+
+
+class ProductsTab(PrimaryNavTab):
+    """Products tab in primary navigation.
+    """
+
+    grok.context(IIkobaObject)
+    grok.order(1)
+    grok.require('waeup.Public')
+    grok.name('productstab')
+
+    pnav = 1
+    tab_title = _(u'Products')
+
+    @property
+    def link_target(self):
+        return self.view.application_url('products')
+
+class ProductsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IProductsContainer)
+    grok.view(ProductsContainerPage)
+    grok.require('waeup.manageProducts')
+    text = _('Manage')
+    target = 'manage'
+
+
+class ProductViewActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IProduct)
+    grok.view(ProductManageFormPage)
+    grok.require('waeup.manageProducts')
+    text = _('View')
+    target = 'index'
+    icon = 'actionicon_view.png'
+
+
+class ProductManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IProduct)
+    grok.view(ProductDisplayFormPage)
+    grok.require('waeup.manageProducts')
+    text = _('Manage')
+    target = 'manage'
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/reports.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/reports.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/reports.py	(revision 12677)
@@ -0,0 +1,385 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components for report generation.
+"""
+import grok
+import zc.async.interfaces
+from persistent.list import PersistentList
+from zope import schema
+from zope.component import getUtility, getUtilitiesFor
+from zope.component.hooks import setSite
+from zope.interface import implementer
+from zope.interface import Interface, Attribute
+from waeup.ikoba.async import AsyncJob
+from waeup.ikoba.interfaces import (
+    IJobManager, JOB_STATUS_MAP, IIkobaPluggable, IIkobaObject)
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.utils.helpers import now
+
+class IReport(Interface):
+    """A report.
+    """
+    args = Attribute("""The args passed to constructor""")
+
+    kwargs = Attribute("""The keywords passed to constructor""")
+
+    creation_dt = Attribute(
+        """Datetime when a report was created. The datetime should """
+        """reflect the point of time when the data was fetched, """
+        """not when any output was created.""")
+
+    def create_pdf():
+        """Generate a PDF copy.
+        """
+
+class IReportGenerator(IIkobaObject):
+    """A report generator.
+    """
+    title = Attribute("""Human readable description of report type.""")
+
+    def generate(site, args=[], kw={}):
+        """Generate a report.
+
+        `args` and `kw` are the parameters needed to create a specific
+        report (if any).
+        """
+
+class IReportJob(zc.async.interfaces.IJob):
+
+    finished = schema.Bool(
+        title = u'`True` if the job finished.`',
+        default = False,
+        )
+
+    failed = schema.Bool(
+        title = u"`True` iff the job finished and didn't provide a report.",
+        default = None,
+        )
+
+    def __init__(site, generator_name):
+        """Create a report job via generator."""
+
+class IReportJobContainer(Interface):
+    """A component that contains (maybe virtually) report jobs.
+    """
+    def start_report_job(report_generator_name, user_id, args=[], kw={}):
+        """Start asynchronous report job.
+
+        `report_generator_name`
+            is the name of a report generator utility to be used.
+
+        `user_id`
+            is the ID of the user that triggers the report generation.
+
+        `args` and `kw`
+            args and keywords passed to the generators `generate()`
+            method.
+
+        The job_id is stored along with exporter name and user id in a
+        persistent list.
+
+        Returns the job ID of the job started.
+        """
+
+    def get_running_report_jobs(user_id=None):
+        """Get report jobs for user with `user_id` as list of tuples.
+
+        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
+        that order. The ``<generator_name>`` is the utility name of the
+        used report generator.
+
+        If `user_id` is ``None``, all running report jobs are returned.
+        """
+
+    def get_report_jobs_status(user_id=None):
+        """Get running/completed report jobs for `user_id` as list of tuples.
+
+        Each tuple holds ``<raw status>, <status translated>,
+        <generator title>`` in that order, where ``<status
+        translated>`` and ``<generator title>`` are translated strings
+        representing the status of the job and the human readable
+        title of the report generator used.
+        """
+
+    def delete_report_entry(entry):
+        """Delete the report job denoted by `entry`.
+
+        Removes `entry` from the local `running_report_jobs` list and
+        also removes the regarding job via the local job manager.
+
+        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
+        as created by :meth:`start_report_job` or returned by
+        :meth:`get_running_report_jobs`.
+        """
+
+    def report_entry_from_job_id(job_id):
+        """Get entry tuple for `job_id`.
+
+        Returns ``None`` if no such entry can be found.
+        """
+
+class IReportsContainer(grok.interfaces.IContainer, IReportJobContainer,
+                        IIkobaObject):
+    """A grok container that holds report jobs.
+    """
+
+class manageReportsPermission(grok.Permission):
+    """A permission to manage reports.
+    """
+    grok.name('waeup.manageReports')
+
+def get_generators():
+    """Get available report generators.
+
+    Returns an iterator of tuples ``<NAME, GENERATOR>`` with ``NAME``
+    being the name under which the respective generator was
+    registered.
+    """
+    for name, util in getUtilitiesFor(IReportGenerator):
+        yield name, util
+    pass
+
+@implementer(IReport)
+class Report(object):
+    """A base for reports.
+    """
+    creation_dt = None
+
+    def __init__(self, args=[], kwargs={}):
+        self.args = args
+        self.kwargs = kwargs
+        self.creation_dt = now()
+
+    def create_pdf(self):
+        raise NotImplementedError()
+
+@implementer(IReportGenerator)
+class ReportGenerator(object):
+    """A base for report generators.
+    """
+    title = _("Unnamed Report")
+
+    def generate(self, site, args=[], kw={}):
+        result = Report()
+        return result
+
+def report_job(site, generator_name, args=[], kw={}):
+    """Get a generator and perform report creation.
+
+    `site`
+        is the site for which the report should be created.
+    `generator_name`
+        the global utility name under which the desired generator is
+        registered.
+    `args` and `kw`
+        Arguments and keywords to be passed to the `generate()` method of
+        the desired generator. While `args` should be a list, `kw` should
+        be a dictionary.
+    """
+    setSite(site)
+    generator = getUtility(IReportGenerator, name=generator_name)
+    report = generator.generate(site, *args, **kw)
+    return report
+
+@implementer(IReportJob)
+class AsyncReportJob(AsyncJob):
+    """An IJob that creates reports.
+
+    `AsyncReportJob` instances are regular `AsyncJob` instances with a
+    different constructor API. Instead of a callable to execute, you
+    must pass a `site`, some `generator_name`, and additional args and
+    keywords to create a report.
+
+    The real work is done when an instance of this class is put into a
+    queue. See :mod:`waeup.ikoba.async` to learn more about
+    asynchronous jobs.
+
+    The `generator_name` must be the name under which an IReportGenerator
+    utility was registered with the ZCA.
+
+    The `site` must be a valid site  or ``None``.
+
+    The result of an `AsyncReportJob` is an IReport object.
+    """
+    def __init__(self, site, generator_name, args=[], kw={}):
+        self._generator_name = generator_name
+        super(AsyncReportJob, self).__init__(
+            report_job, site, generator_name, args=args, kw=kw)
+
+    @property
+    def finished(self):
+        """A job is marked `finished` if it is completed.
+
+        Please note: a finished report job does not neccessarily
+        provide an IReport result. See meth:`failed`.
+        """
+        return self.status == zc.async.interfaces.COMPLETED
+
+    @property
+    def failed(self):
+        """A report job is marked failed iff it is finished and the
+        result does not provide IReport.
+
+        While a job is unfinished, the `failed` status is ``None``.
+
+        Failed jobs normally provide a `traceback` to examine reasons.
+        """
+        if not self.finished:
+            return None
+        if not IReport.providedBy(getattr(self, 'result', None)):
+            return True
+        return False
+
+@implementer(IReportJobContainer)
+class ReportJobContainer(object):
+    """A mix-in that provides functionality for asynchronous report jobs.
+    """
+    running_report_jobs = PersistentList()
+
+    def start_report_job(self, generator_name, user_id, args=[], kw={}):
+        """Start asynchronous export job.
+
+        `generator_name`
+            is the name of a report generator utility to be used.
+
+        `user_id`
+            is the ID of the user that triggers the report generation.
+
+        `args` and `kw`
+            args and keywords passed to the generators `generate()`
+            method.
+
+        The job_id is stored along with exporter name and user id in a
+        persistent list.
+
+        Returns the job ID of the job started.
+        """
+        site = grok.getSite()
+        manager = getUtility(IJobManager)
+        job = AsyncReportJob(site, generator_name, args=args, kw=kw)
+        job_id = manager.put(job)
+        # Make sure that the persisted list is stored in ZODB
+        self.running_report_jobs = PersistentList(self.running_report_jobs)
+        self.running_report_jobs.append((job_id, generator_name, user_id),)
+        return job_id
+
+    def get_running_report_jobs(self, user_id=None):
+        """Get report jobs for user with `user_id` as list of tuples.
+
+        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
+        that order. The ``<generator_name>`` is the utility name of the
+        used report generator.
+
+        If `user_id` is ``None``, all running report jobs are returned.
+        """
+        entries = []
+        to_delete = []
+        manager = getUtility(IJobManager)
+        for entry in self.running_report_jobs:
+            if user_id is not None and entry[2] != user_id:
+                continue
+            if manager.get(entry[0]) is None:
+                to_delete.append(entry)
+                continue
+            entries.append(entry)
+        if to_delete:
+            self.running_report_jobs = PersistentList(
+                [x for x in self.running_report_jobs if x not in to_delete])
+        return entries
+
+    def get_report_jobs_status(self, user_id=None):
+        """Get running/completed report jobs for `user_id` as list of tuples.
+
+        Each tuple holds ``<raw status>, <status translated>,
+        <generator title>`` in that order, where ``<status
+        translated>`` and ``<generator title>`` are translated strings
+        representing the status of the job and the human readable
+        title of the report generator used.
+        """
+        entries = self.get_running_report_jobs(user_id)
+        result = []
+        manager = getUtility(IJobManager)
+        for entry in entries:
+            job = manager.get(entry[0])
+            status, status_translated = JOB_STATUS_MAP[job.status]
+            generator = getUtility(IReportGenerator, name=entry[1])
+            generator_name = getattr(generator, 'title', 'unnamed')
+            result.append((status, status_translated, generator_name))
+        return result
+
+    def delete_report_entry(self, entry):
+        """Delete the report job denoted by `entry`.
+
+        Removes `entry` from the local `running_report_jobs` list and
+        also removes the regarding job via the local job manager.
+
+        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
+        as created by :meth:`start_report_job` or returned by
+        :meth:`get_running_report_jobs`.
+        """
+        manager = getUtility(IJobManager)
+        manager.remove(entry[0], self)
+        new_entries = [x for x in self.running_report_jobs
+                       if x != entry]
+        self.running_report_jobs = PersistentList(new_entries)
+        return
+
+    def report_entry_from_job_id(self, job_id):
+        """Get entry tuple for `job_id`.
+
+        Returns ``None`` if no such entry can be found.
+        """
+        for entry in self.running_report_jobs:
+            if entry[0] == job_id:
+                return entry
+        return None
+
+@implementer(IReportsContainer)
+class ReportsContainer(grok.Container, ReportJobContainer):
+    """A container for reports.
+    """
+
+@implementer(IIkobaPluggable)
+class ReportsContainerPlugin(grok.GlobalUtility):
+    """A plugin that updates sites to contain a reports container.
+    """
+
+    grok.name('reports')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        """Add a reports container for `site`.
+
+        If there is such an object already, we install a fresh one.
+        """
+        if site.get('reports', None) is not None:
+            del site['reports']
+            logger.info('Removed reports container for site "%s"' % name)
+        return self.update(site, name, logger)
+
+    def update(self, site, name, logger):
+        """Install a reports container in `site`.
+
+        If one exists already, do nothing.
+        """
+        if site.get('reports', None) is not None:
+            return
+        site['reports'] = ReportsContainer()
+        logger.info('Added reports container for site "%s"' % name)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/__init__.py	(revision 12677)
@@ -0,0 +1,8 @@
+from waeup.ikoba.schema.field import *
+
+#__all__ = tuple(name for name in globals() if not name.startswith('_'))
+
+__all__ = ('CustomizableErrorMsg',
+           'TextLineChoice',
+           'FormattedDate',
+           'PhoneNumber')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/field.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/field.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/field.py	(revision 12677)
@@ -0,0 +1,159 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Special fields.
+"""
+from zope.interface import implements
+from zope.schema import TextLine, Date
+from zope.schema.interfaces import (
+    ITextLine, IBaseVocabulary, ISource, IContextSourceBinder, InvalidValue,)
+from zope.schema.vocabulary import (
+    SimpleVocabulary, getVocabularyRegistry, VocabularyRegistryError)
+from waeup.ikoba.schema.interfaces import IFormattedDate, IPhoneNumber
+
+class CustomizableErrorMsg(object):
+    # Still work in progress
+
+    def __init__(self, not_in_vocab=InvalidValue, **kw):
+        self.not_in_vocab = not_in_vocab
+        return
+
+class TextLineChoice(TextLine, CustomizableErrorMsg):
+    """A TextLine field that also accepts sources, vocabs and values.
+
+    You can use this field like a regular zope.schema.TextLine field.
+
+    You can additionally give `source`, `values` or `vocabulary`
+    parameters which will work mainly as with regular
+    zope.schema.Choice fields (hence the name). Different to
+    `Choice` field, these parameters are not mandatory. Leave them
+    out and you have a regular `TextLine` field.
+
+    If you pass a simple source parameter, only those values are
+    allowed which also appear in the source. The source should of
+    course provide string values as suitable for TextLine fields only!
+
+    If, for example you create a TextLineChoice like this:
+
+      name = TextLineChoice(
+        title = u'Some name',
+        values = [u'foo', u'bar']  # unicode allowed only!
+        )
+
+    any formlib form rendering this field will only accept input u'foo'
+    or u'bar'.
+
+    The main advantage of this modified TextLine field is to support
+    contextual sources. That means you can define some
+    IContextSourceBinder component that looks up catalogs or something
+    external else and decide then, whether the entered value of a form
+    is allowed or not.
+
+    The code herein is mainly copied over from
+    zope.schema._field.Choice with slight modifications.
+    """
+    implements(ITextLine)
+
+    def __init__(self, values=None, vocabulary=None, source=None, **kw):
+        if vocabulary is not None:
+            assert (isinstance(vocabulary, basestring)
+                    or IBaseVocabulary.providedBy(vocabulary))
+            assert source is None, (
+                "You cannot specify both source and vocabulary.")
+        elif source is not None:
+            vocabulary = source
+
+        assert values is None or vocabulary is None, (
+               "You cannot specify both values and vocabulary.")
+
+        self.vocabulary = None
+        self.vocabularyName = None
+        if values is not None:
+            self.vocabulary = SimpleVocabulary.fromValues(values)
+        elif isinstance(vocabulary, (unicode, str)):
+            self.vocabularyName = vocabulary
+        elif (ISource.providedBy(vocabulary) or
+              IContextSourceBinder.providedBy(vocabulary)):
+            self.vocabulary = vocabulary
+        super(TextLineChoice, self).__init__(**kw)
+        return
+
+    source = property(lambda self: self.vocabulary)
+
+    def bind(self, object):
+        """See zope.schema._bootstrapinterfaces.IField."""
+        clone = super(TextLineChoice, self).bind(object)
+        # get registered vocabulary if needed:
+        if IContextSourceBinder.providedBy(self.vocabulary):
+            clone.vocabulary = self.vocabulary(object)
+            assert ISource.providedBy(clone.vocabulary)
+        elif clone.vocabulary is None and self.vocabularyName is not None:
+            vr = getVocabularyRegistry()
+            clone.vocabulary = vr.get(object, self.vocabularyName)
+            assert ISource.providedBy(clone.vocabulary)
+        return clone
+
+    def _validate(self, value):
+        """First validate against the regular TextLine rules. Then check any
+        vocabularies/sources.
+        """
+        # Do TextLine validation
+        super(TextLineChoice, self)._validate(value)
+
+        # Check allowed value range (the vocabulary part)
+        vocabulary = self.vocabulary
+        if vocabulary is None and self.vocabularyName is not None:
+            vr = getVocabularyRegistry()
+            try:
+                vocabulary = vr.get(None, self.vocabularyName)
+            except VocabularyRegistryError:
+                raise ValueError("Can't validate value without vocabulary")
+        if vocabulary and value not in vocabulary:
+            raise InvalidValue(value)
+        return
+
+class FormattedDate(Date):
+    """A date field that supports additional formatting attributes.
+
+    Stores extra attributes (see below). To make use of these
+    attributes in forms, you have to provide widgets that read them
+    and use them in their operations, for instance the
+    `waeup.ikoba.widgets.datewidget.FormattedDateWidget`.
+
+    Extra attributes are as follows:
+
+    `date_format`
+      additional attribute to describe desired date format. Must be a
+      string that can be fed to strftime/strptime functions. By
+      default `None`.
+
+    `show_year`
+      boolean indicating whether some kind of year selection should
+      be used with this instance. `False` by default.
+    """
+    implements(IFormattedDate)
+    date_format = None
+    show_year = False
+    def __init__(self, date_format=None, show_year=False, *args, **kw):
+        self.date_format = date_format
+        self.show_year = show_year
+        return super(FormattedDate, self).__init__(*args, **kw)
+
+class PhoneNumber(TextLine):
+    """A schema field for phone numbers.
+    """
+    implements(IPhoneNumber)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/interfaces.py	(revision 12677)
@@ -0,0 +1,42 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+from zope import schema
+from zope.schema.interfaces import IDate, ITextLine
+
+class IFormattedDate(IDate):
+    """A formatted date.
+
+    Basically a zope.schema.IDate, but with optional additional
+    attributes. These attributes _can_ be used by widgets to change
+    the way of editing or displaying a date.
+
+    The waeup.ikoba.widgets.datewidget.FormattedDateWidget is a widget
+    that supports these additional attributes.
+    """
+    show_year = schema.Bool(
+        title = u'Show year selector when editing this date?',
+        default = False,
+        )
+    date_format = schema.ASCII(
+        title = u'A date format string suitable for use with strftime.',
+        default = '%Y-%m-%d',
+        )
+
+class IPhoneNumber(ITextLine):
+    """A phone number.
+    """
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/tests/test_fields.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/tests/test_fields.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/tests/test_fields.py	(revision 12677)
@@ -0,0 +1,42 @@
+"""Tests for waeup.ikoba.schema fields.
+"""
+import unittest
+from zope.interface.verify import verifyClass, verifyObject
+from zope.schema.interfaces import IDate
+from waeup.ikoba.schema import FormattedDate, PhoneNumber
+from waeup.ikoba.schema.interfaces import IFormattedDate, IPhoneNumber
+
+class FormattedDateTests(unittest.TestCase):
+    # Tests for FormattedDate field.
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = FormattedDate()
+        verifyClass(IDate, FormattedDate)
+        verifyClass(IFormattedDate, FormattedDate)
+        verifyObject(IDate, obj)
+        verifyObject(IFormattedDate, obj)
+        return
+
+    def test_defaults(self):
+        # we get expected default values for dates.
+        obj = FormattedDate()
+        self.assertEqual(obj.show_year, False)
+        self.assertEqual(obj.date_format, None)
+        return
+
+    def test_attribs(self):
+        # we can set the promised attributes.
+        obj = FormattedDate(show_year=True, date_format='%d.%m.%Y')
+        self.assertEqual(obj.show_year, True)
+        self.assertEqual(obj.date_format, '%d.%m.%Y')
+        return
+
+class PhoneNumberTests(unittest.TestCase):
+    # Tests for PhoneNumber field
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = PhoneNumber()
+        verifyClass(IPhoneNumber, PhoneNumber)
+        verifyObject(IPhoneNumber, obj)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/tests/test_textlinechoice.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/tests/test_textlinechoice.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/schema/tests/test_textlinechoice.py	(revision 12677)
@@ -0,0 +1,200 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Test TextLineChoice
+"""
+import unittest
+from zope.interface import implements, directlyProvides
+from zope.schema import vocabulary
+from zope.schema.interfaces import (
+    InvalidValue, ValidationError,
+    IVocabulary, ISource, IContextSourceBinder,)
+from zope.schema.tests.test_strfield import TextLineTest
+from waeup.ikoba.schema import TextLineChoice
+
+# Define some helper functions, classes, vars for tests
+class DummyRegistry(vocabulary.VocabularyRegistry):
+    def get(self, object, name):
+        v = SampleVocabulary()
+        v.object = object
+        v.name = name
+        return v
+
+class SampleTerm(object):
+    pass
+
+SAMPLE_VALUES = [u'a', u'b', u'c', u'd']
+class SampleVocabulary(object):
+    implements(IVocabulary)
+
+    def __iter__(self):
+        return iter([self.getTerm(x) for x in SAMPLE_VALUES])
+
+    def __contains__(self, value):
+        return value in SAMPLE_VALUES
+        return 0 <= value < 10
+
+    def __len__(self):
+        return len(SAMPLE_VALUES)
+
+    def getTerm(self, value):
+        if value in self:
+            t = SampleTerm()
+            t.value = value
+            t.upper = value.upper() #2 * value
+            return t
+        raise LookupError("no such value: %r" % value)
+
+class LetterASource(object):
+    # A source that contains all strings with letter 'a'
+    implements(ISource)
+    required_letter = 'a'
+    def __contains__(self, value):
+        return self.required_letter in value
+letter_a_source = LetterASource()
+
+def letter_source_binder(context):
+    source = LetterASource()
+    source.required_letter = context.req_letter
+    return source
+directlyProvides(letter_source_binder, IContextSourceBinder)
+
+class SampleContext(object):
+    req_letter = 'b'
+
+
+
+class TextLineChoiceAsTextLineTest(TextLineTest):
+    # Tests to guarantee TextLine like behaviour of TextLineChoice
+    # i.e. make sure that a TextLineChoice can everything a TextLine can
+    _Field_Factory = TextLineChoice
+
+class TextLineChoice_Values_Tests(unittest.TestCase):
+    # Tests to guarantee values support
+    def test_create_vocabulary(self):
+        choice = TextLineChoice(values=[1, 3])
+        self.assertEqual([term.value for term in choice.vocabulary], [1, 3])
+
+    def test_validate_string(self):
+        choice = TextLineChoice(values=[u'a', u'c'])
+        choice.validate(u'a')
+        #choice.validate('c')
+        choice.validate(u'c')
+        self.assertRaises(InvalidValue, choice.validate, u'd')
+
+    def test_validate_strings(self):
+        choice = TextLineChoice(values=[u'foo', u'bar'])
+        choice.validate(u'foo')
+        choice.validate(u'bar')
+        choice.validate(u'bar')
+        self.assertRaises(InvalidValue, choice.validate, u'baz')
+
+class TextLineChoice_Vocabulary_Tests(unittest.TestCase):
+    # Tests of the TextLineChoice Field using vocabularies.
+    # Most tests were copied (slightly modified) from zope.schema.tests.
+
+    def setUp(self):
+        vocabulary._clear()
+
+    def tearDown(self):
+        vocabulary._clear()
+
+    def check_preconstructed(self, cls, okval, badval):
+        v = SampleVocabulary()
+        field = cls(vocabulary=v)
+        self.assert_(field.vocabulary is v)
+        self.assert_(field.vocabularyName is None)
+        bound = field.bind(None)
+        self.assert_(bound.vocabulary is v)
+        self.assert_(bound.vocabularyName is None)
+        bound.default = okval
+        self.assertEqual(bound.default, okval)
+        self.assertRaises(ValidationError, setattr, bound, "default", badval)
+
+    def test_preconstructed_vocabulary(self):
+        self.check_preconstructed(TextLineChoice, u'a', u'z')
+
+    def check_constructed(self, cls, okval, badval):
+        vocabulary.setVocabularyRegistry(DummyRegistry())
+        field = cls(vocabulary="vocab")
+        self.assert_(field.vocabulary is None)
+        self.assertEqual(field.vocabularyName, "vocab")
+        o = object()
+        bound = field.bind(o)
+        self.assert_(isinstance(bound.vocabulary, SampleVocabulary))
+        bound.default = okval
+        self.assertEqual(bound.default, okval)
+        self.assertRaises(ValidationError, setattr, bound, "default", badval)
+
+    def test_constructed_vocabulary(self):
+        self.check_constructed(TextLineChoice, u'a', u'z')
+
+    def test_create_vocabulary(self):
+        vocabulary.setVocabularyRegistry(DummyRegistry())
+        field = TextLineChoice(vocabulary="vocab")
+        o = object()
+        bound = field.bind(o)
+        self.assertEqual([term.value for term in bound.vocabulary],
+                         SAMPLE_VALUES)
+
+    def test_undefined_vocabulary(self):
+        choice = TextLineChoice(vocabulary="unknown")
+        self.assertRaises(ValueError, choice.validate, u"value")
+
+class TextLineChoice_Source_Tests(unittest.TestCase):
+    # Tests of the TextLineChoice Field using sources.
+
+    def test_simple_source(self):
+        choice = TextLineChoice(__name__='astring', source=letter_a_source)
+        bound = choice.bind(object())
+        self.assertEqual(bound.vocabulary, letter_a_source)
+        return
+
+    def test_validate_simple_source(self):
+        choice = TextLineChoice(__name__='astring', source=letter_a_source)
+        bound = choice.bind(object())
+        bound.validate(u'string_with_letter_a')
+        self.assertRaises(
+            InvalidValue, bound.validate, u'other_string')
+        return
+
+    def test_contextual_source(self):
+        # make sure we can pass contextual sources
+        choice = TextLineChoice(__name__='astring', source=letter_source_binder)
+        bound = choice.bind(SampleContext()) # requires 'b' instead of 'a'
+        self.assertTrue(isinstance(bound.vocabulary, LetterASource))
+        self.assertTrue(bound.vocabulary is not letter_a_source)
+        return
+
+    def test_validate_contextual_source(self):
+        # make sure the values from context are considered
+        choice = TextLineChoice(__name__='astring', source=letter_source_binder)
+        bound = choice.bind(SampleContext()) # requires 'b' instead of 'a'
+        bound.validate(u'string_with_letter_b')
+        self.assertRaises(
+            InvalidValue, bound.validate, u'other_string') # no 'b'
+        return
+
+def test_suite():
+    # We must define a test suite as we do not want the imported
+    # TextLineTest to be collected by testrunner.
+    return unittest.TestSuite((
+        unittest.makeSuite(TextLineChoiceAsTextLineTest),
+        unittest.makeSuite(TextLineChoice_Values_Tests),
+        unittest.makeSuite(TextLineChoice_Vocabulary_Tests),
+        unittest.makeSuite(TextLineChoice_Source_Tests),
+        ))
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/smtp.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/smtp.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/smtp.py	(revision 12677)
@@ -0,0 +1,208 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Email (SMTP) services for Ikoba.
+
+Note About Encodings
+--------------------
+
+All functions in this module expect any raw strings passed in to be
+encoded 'utf-8' (if you pass in unicode objects instead, this is not a
+problem).
+
+This is because we cannot easily tell from a raw string (it is in fact
+only a byte stream) what encoding it has. In latin-1 and utf-8, for
+instance, there exist some chars (byte values) that have different
+meanings in both encodings. If we see such a char in a byte stream:
+what is it meant to represent? The respective character from latin1 or
+the one from utf-8?
+
+We therefore interpret all internally used raw strings to be encoded as
+utf-8.
+
+The functions in here nevertheless try hard to produce output (mail
+parts, headers, etc.) encoded in the least complex manner. For
+instance if you pass in some address or mail body that is
+representable (correctly) as ASCII or latin-1, we will turn the text
+into that encoding (given, you passed it in as utf-8) to stay as
+compatible as possible with old mailers that do not understand utf-8.
+
+"""
+import grok
+import logging
+from email.Header import Header
+from email.Utils import formataddr
+from email.mime.text import MIMEText
+from zope.component import getUtility
+from zope.sendmail.interfaces import IMailDelivery
+from waeup.ikoba.interfaces import IMailService
+
+
+#: The hardcoded from address. Must not by yahoo.com.
+FROM_ADDRESS = 'no-reply@waeup.org'
+
+
+class DefaultMailService(grok.GlobalUtility):
+    """Returns a :class:`zope.sendmail.IMailDelivery`.
+
+    Searches a site from current request (if applicable) and returns
+    the mail delivery set for this site or a fake mailer that does not
+    really send mail (for testing, evaluating, etc.).
+    """
+    grok.implements(IMailService)
+
+    def __call__(self):
+        name = 'No email service'
+        site = grok.getSite()
+        if site is not None:
+            config = site['configuration']
+            name = getattr(config, 'smtp_mailer', name)
+        return getUtility(IMailDelivery, name=name)
+
+
+class FakeSMTPDelivery(grok.GlobalUtility):
+    """A fake mail delivery for testing etc.
+
+    Instead of sending real mails, this mailer only logs received
+    messages to the ``test.smtp`` logger.
+    """
+    grok.implements(IMailDelivery)
+    grok.name('No email service')
+
+    def send(self, fromaddr, toaddrs, message):
+        logger = logging.getLogger('test.smtp')
+        rcpts = ', '.join([x.decode('utf-8') for x in toaddrs])
+        logger.info(
+            u"Sending email from %s to %s:" % (
+                fromaddr.decode('utf-8'), rcpts))
+        logger.info(u"Message:")
+        for line in message.split('\n'):
+            logger.info(u"msg: " + line.decode('utf-8'))
+        return 'fake-message-id@example.com'
+
+CHARSETS = ('US-ASCII', 'ISO-8859-1', 'UTF-8')
+
+
+def encode_header_item(item):
+    """Turns `item`, a string into an SMTP header part string.
+
+    Encodings are checked carefully (we try to encode as ASCII,
+    Latin-1 and UTF-8 in that order).
+
+    If `item` is not a basestring, `None` is returned.
+    """
+    if not isinstance(item, basestring):
+        return None
+    if not isinstance(item, unicode):
+        item = unicode(item, 'utf-8')
+    return str(Header(item, 'iso-8859-1'))  # try ascii, then latin1, utf-8
+
+
+def encode_address(addr, name=u''):
+    """Turn email address parts into a single valid email string.
+
+    The given email address and the name are turned into a single
+    (byte stream) string, suitable for use with ``To:`` or ``From:``
+    headers in emails.
+
+    Any encodings to a mailer-readable format are performed.
+
+    Preferred input format is unicode, although also raw strings (byte
+    streams) work as long as they are decodable from UTF-8.
+
+    That means: if you pass in non-unicode string, take care to
+    deliver utf-8 encoded strings (or plain ASCII).
+
+    Returns a single (raw) string like "My Name <my@sample.com>".
+    """
+    addr = encode_header_item(addr)
+    name = encode_header_item(name)
+    return formataddr((name, addr))
+
+
+def encode_body(text):
+    """Build MIME message part from text.
+
+    You can pass unicode objects or simple strings as text.
+
+    .. warn:: If the input is a simple string, this string is expected
+              to be encoded 'utf-8'!
+
+    Returns a MIMEText object.
+    """
+    if not isinstance(text, unicode):
+        text = unicode(text, 'utf-8')
+    charset = CHARSETS[-1]  # fallback
+    for charset in CHARSETS:
+        try:
+            text = text.encode(charset)
+        except UnicodeError:
+            pass  # try next encoding
+        else:
+            break
+    return MIMEText(text, 'plain', charset)
+
+
+def send_mail(from_name, from_addr, rcpt_name, rcpt_addrs,
+              subject, body, config=None, cc=False):
+    """Send mail.
+
+    Use `IMailService` to send a mail with the parameters
+    delivered.
+
+    Please note: the `from_addr` given will be used for the reply-to
+    (and cc) field only. It will _not_ be used for the `from` field,
+    as yahoo does not allow non-yahoo servers to deliver mail with
+    ``@yahoo.com`` in the from-field.
+
+    The from-address set here will be: `FROM_ADDRESS` as set above.
+
+    ``cc`` tells whether we want the from-address to be CCed. This is
+    not the case by default as we easily act as an open relay
+    otherwise.
+
+    XXX: The hard-coded from-address should be changable or be
+         determined smarter by looking up a FQDN or similar.
+
+    """
+    # format message
+    rcpt_addrs = rcpt_addrs.replace(' ', '').split(',')
+    body_to = ''
+    for email in rcpt_addrs:
+        body_to += '%s, ' % encode_address(email, rcpt_name)
+    body = encode_body(body)
+    sender_addr = encode_address(FROM_ADDRESS, from_name)
+    reply_addr = encode_address(from_addr, from_name)
+    body["From"] = sender_addr
+    body["To"] = body_to.strip(', ')
+    if cc:
+        body["Cc"] = reply_addr
+    body["Reply-To"] = reply_addr
+    body["Subject"] = encode_header_item(subject)
+
+    mailer = getUtility(IMailService)
+    try:
+        email_admin = grok.getSite()['configuration'].email_admin
+        if from_addr != email_admin:
+            rcpt_addrs += [from_addr]
+    except TypeError:
+        # In tests we might not have a site object
+        rcpt_addrs += [from_addr]
+        pass
+    result = mailer().send(FROM_ADDRESS, rcpt_addrs, body.as_string())
+    return result
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/sourcefactory.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/sourcefactory.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/sourcefactory.py	(revision 12677)
@@ -0,0 +1,78 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Smarter contextual sources, that can do faster containment checks.
+"""
+import zc.sourcefactory.policies
+from zc.sourcefactory.factories import ContextualSourceFactory
+from zc.sourcefactory.source import FactoredContextualSource
+
+class SmartFactoredContextualSource(FactoredContextualSource):
+    """A contextual source that can be faster.
+
+    Regular ContextualSources from zc.sourcefactory suffer from very
+    expensive and slow containment checks. __contains__ is executed as
+    a lookup over the complete set of possible values.
+
+    If, however, a source could do this lookup faster, it had no
+    chance to do that with regular ContextualSources.
+
+    This source instead looks for a `contains()` method of a
+    contextual source first and if it has one, this method is called
+    with `value` and `context` as arguments.
+
+    If a source does not provide `contains()`, the expensive default
+    lookup method is done instead.
+    """
+    def __contains__(self, value):
+        """Check whether `value` is part of the source.
+
+        If the factored source provides a `contains(value, context)`
+        method, this method is called for faster containment
+        checks. If not, then the getValues() method of a factored
+        source is used to examine each single item contained in the
+        source.
+        """
+        if hasattr(self.factory, 'contains'):
+            return self.factory.contains(self.context, value)
+        # This is potentially expensive!
+        return super(SmartFactoredContextualSource, self).__contains__(
+            value)
+
+class SmartContextualSourceFactory(ContextualSourceFactory):
+    source_class = SmartFactoredContextualSource
+
+class SmartBasicContextualSourceFactory(
+    SmartContextualSourceFactory,
+    zc.sourcefactory.policies.BasicContextualSourcePolicy):
+    """Abstract base implementation for a basic but smart contextual
+    source factory.
+
+    These factories use any `contains(value, context)` method of
+    factored sources to speed up containment checks.
+
+    Instances of this class can be used as replacement for
+    `zc.sourcefactory.BasicContextualSourceFactory`. If your
+    implementation derived from :class:`SmartBasicContextualFactory`
+    additionally provides a smart `contains()` method, you only get
+    benefits from using this class.
+
+    The basic requirements for a working
+    `SmartBasicContextualSourceFactory` are the same as for regular
+    ones: you have to implement a `getValues(context)` method. You can
+    (optionally) implements a `contains(value, context)` method.
+    """
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/startup.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/startup.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/startup.py	(revision 12677)
@@ -0,0 +1,105 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""WSGI application factories that additionally set environment vars.
+
+The `grokcore.startup` factories for creating WSGI applications
+currently do not support setting of arbitrary environment vars.
+
+The below factories add this feature.
+
+Environment vars can be set in any ``.ini`` file used at startup. In
+the ``[DEFAULT]`` section set the option ``env_vars`` with key/value
+pairs as value like so::
+
+  [DEFAULT]
+  zope_conf = <path-to-zope.conf>
+  env_vars = KEY1 value1
+             KEY2 value2
+
+This would set the env vars ``KEY1`` and ``KEY2`` to the respective
+values on startup on an instance.
+
+To activate these factories, in the ``setup.py`` of your project use::
+
+  [paste.app_factory]
+  main = waeup.ikoba.startup:env_app_factory
+  debug = waeup.ikoba.startup:env_debug_app_factory
+
+in the entry points section (replacing the references to respective
+`grokcore.startup` factories.
+
+Info for developers: paster on startup delivers the options from
+``[DEFAULT]`` section in `.ini` file as a dictionary in the
+`global_conf`.
+"""
+import os
+from ConfigParser import RawConfigParser
+from grokcore.startup import application_factory, debug_application_factory
+
+def _set_env_vars(global_conf):
+    """Set vars from `global_conf['env_vars']` in `os.environ`.
+    """
+    env_vars = global_conf.get('env_vars', None)
+    if not env_vars:
+        return
+    for line in env_vars.split('\n'):
+        key, val = [x.strip() for x in line.strip().split(' ', 1)]
+        os.environ[key] = val
+    return
+
+def env_app_factory(global_conf, **local_conf):
+    """A WSGI application factory that sets environment vars.
+
+    This app factory provides applications as expected by ``paster``
+    and useable as ``[paste.app_factory]`` plugin in setup.py.
+
+    It's a replacement for the stock app factory provided by
+    `grokcore.startup`.
+
+    Additionally it supports extrapolation of the DEFAULT var
+    ``env_vars`` in .ini files used to configure paster.
+
+    With this factory you can set enviroment vars (as in `os.environ`)
+    via the ``env_vars`` keyword set in some `.ini` file::
+
+      env_vars = MY_KEY some_value
+
+    would set the environment variable ``MY_KEY`` to the value
+    ``some_value`` before creating the actual app.
+
+    You can also set multiple keys/values at once like this::
+
+      env_vars = MY_KEY1  Some value
+                 Another_key Anoter_value
+
+    Note, that keys may not contain whitespaces while values
+    may. Both, keys and values, are stripped before being set.
+    """
+    _set_env_vars(global_conf)
+    return application_factory(global_conf, **local_conf)
+
+def env_debug_app_factory(global_conf, **local_conf):
+    """A debugger application factory.
+
+    This is a wrapper around the real factory from `grokcore.startup`
+    that does the same additional things as :func:`env_app_factory`:
+    it sets environment vars given in `env_vars` option of a
+    configuring .ini file for paster.
+    """
+    _set_env_vars(global_conf)
+    return debug_application_factory(global_conf, **local_conf)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/testing.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/testing.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/testing.py	(revision 12677)
@@ -0,0 +1,470 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Testing support for :mod:`waeup.ikoba`.
+"""
+import grok
+import doctest
+import logging
+import os.path
+import re
+import tempfile
+import shutil
+import unittest
+import warnings
+import zope.component
+import waeup.ikoba
+from zc.async.interfaces import COMPLETED
+from zope.app.testing.functional import (
+    ZCMLLayer, FunctionalTestSetup, getRootFolder, sync, FunctionalTestCase)
+from zope.component import getGlobalSiteManager, queryUtility
+from zope.security.testing import addCheckerPublic
+from zope.testing import renormalizing
+from zope.testing.cleanup import cleanUp
+
+ftesting_zcml = os.path.join(
+    os.path.dirname(waeup.ikoba.__file__), 'ftesting.zcml')
+FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer',
+                            allow_teardown=True)
+
+def get_all_loggers():
+    """Get the keys of all logger defined globally.
+    """
+    result = logging.root.manager.loggerDict.keys()
+    ws_loggers = [x for x in result if 'waeup.ikoba' in x]
+    if ws_loggers:
+        # For debugging: show any remaining loggers from w.k. namespace
+        print "\nLOGGERS: ", ws_loggers
+    return result
+
+def remove_new_loggers(old_loggers):
+    """Remove the loggers except `old_loggers`.
+
+    `old_loggers` is a list of logger keys as returned by
+    :func:`get_all_loggers`. All globally registered loggers whose
+    name is not in `old_loggers` is removed.
+    """
+    new_loggers = [key for key in logging.root.manager.loggerDict
+                   if key not in old_loggers]
+    for key in sorted(new_loggers, reverse=True):
+        logger = logging.getLogger(key)
+        for handler in logger.handlers:
+            handler.close()
+        del logger
+        del logging.root.manager.loggerDict[key]
+    return
+
+def remove_logger(name):
+    """Remove logger with name `name`.
+
+    Use is safe. If the logger does not exist nothing happens.
+    """
+    if name in logging.root.manager.loggerDict.keys():
+        del logging.root.manager.loggerDict[name]
+    return
+
+
+def setUpZope(test=None):
+    """Initialize a Zope-compatible environment.
+
+    Currently, we only initialize the event machinery.
+    """
+    zope.component.eventtesting.setUp(test)
+
+def cleanUpZope(test=None):
+    """Clean up Zope-related registrations.
+
+    Cleans up all registrations and the like.
+    """
+    cleanUp()
+
+def maybe_grok():
+    """Try to grok the :mod:`waeup.ikoba` package.
+
+    For many tests, even simple ones, we want the components defined
+    somewhere in the :mod:`waeup.ikoba` 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.ikoba` 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.ikoba.testing import (
+        maybe_grok, setUpZope, cleanUpZope,
+        )
+      from waeup.ikoba.app import Company
+
+      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 = Company()
+              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.ikoba`
+    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.ikoba.testing.IkobaUnitTestLayer`.
+
+    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.ikoba.testing.IkobaUnitTestLayer`
+
+    """
+    gsm =  getGlobalSiteManager()
+    # If there are any event handlers registered already, we assume
+    # that waeup.ikoba 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.ikoba')
+    warnings.simplefilter('default') # reenable warnings
+    return True
+
+def setup_datacenter_conf():
+    """Register a datacenter config utility for non-functional tests.
+    """
+    from waeup.ikoba.interfaces import IDataCenterConfig
+    conf = queryUtility(IDataCenterConfig)
+    if conf is not None:
+        return
+    path = tempfile.mkdtemp()
+    conf = {'path': path}
+    gsm = getGlobalSiteManager()
+    gsm.registerUtility(conf, IDataCenterConfig)
+    return
+
+def teardown_datacenter_conf():
+    """Unregister a datacenter config utility for non-functional tests.
+    """
+    from waeup.ikoba.interfaces import IDataCenterConfig
+    conf = queryUtility(IDataCenterConfig)
+    if conf is None:
+        return
+    path = conf['path']
+    shutil.rmtree(path)
+    gsm = getGlobalSiteManager()
+    gsm.unregisterUtility(conf, IDataCenterConfig)
+    return
+
+class IkobaUnitTestLayer(object):
+    """A layer for tests that groks `waeup.ikoba`.
+
+    A Zope test layer that registers all :mod:`waeup.ikoba` components
+    before attached tests are run and cleans this registrations up
+    afterwards. Also basic (non-waeup.ikoba) 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.ikoba.testing import IkobaUnitTestLayer
+
+      class MyTestCase(unittest.TestCase):
+
+          layer = IkobaUnitTestLayer
+
+          # per-test setups and real tests go here...
+          def test_foo(self):
+              self.assertEqual(1, 1)
+              return
+
+    """
+    @classmethod
+    def setUp(cls):
+        #setUpZope(None)
+        grokked = maybe_grok()
+        setup_datacenter_conf()
+        return
+
+    @classmethod
+    def tearDown(cls):
+        teardown_datacenter_conf()
+        cleanUpZope(None)
+        return
+
+
+#: This extended :class:`doctest.OutputChecker` allows the following
+#: additional matches when looking for output diffs:
+#:
+#: `N.NNN seconds`
+#:    matches strings like ``12.123 seconds``
+#:
+#: `HTTPError:`
+#:    matches ``httperror_seek_wrapper:``. This string is output by some
+#:    virtual browsers you might use in functional browser tests to signal
+#:    HTTP error state.
+#:
+#: `1034h`
+#:    is ignored. This sequence of control chars is output by some
+#:    (buggy) testrunners at beginning of output.
+#:
+#: `<10-DIGITS>`
+#:    matches a sequence of 10 digits. Useful when checking accesscode
+#:    numbers if you don't know the exact (random) code.
+#:
+#: `<6-DIGITS>`
+#:    matches a sequence of 6 digits. Useful when checking accesscode
+#:    numbers if you don't know the exact (random) code.
+#:
+#: `<YYYY-MM-DD hh:mm:ss>`
+#:    matches any date and time like `2011-05-01 12:01:32`.
+#:
+#: `<DATE-AND-TIME>`
+#:    same like ``<YYYY-MM-DD hh:mm:ss>`` but shorter.
+checker = renormalizing.RENormalizing([
+    # Relevant normalizers from zope.testing.testrunner.tests:
+    (re.compile(r'\d+[.]\d\d\d seconds'), 'N.NNN seconds'),
+    # Our own one to work around
+    # http://reinout.vanrees.org/weblog/2009/07/16/invisible-test-diff.html:
+    (re.compile(r'.*1034h'), ''),
+    (re.compile(r'httperror_seek_wrapper:'), 'HTTPError:' ),
+    (re.compile('[\d]{6}'), '<6-DIGITS>'),
+    (re.compile('[\d]{10}'), '<10-DIGITS>'),
+    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d [\+\-]\d\d\d\d [^ ]+'),
+     '<YYYY-MM-DD hh:mm:ss TZ>'),
+    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<YYYY-MM-DD hh:mm:ss>'),
+    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<DATETIME>'),
+    ])
+
+old_loggers = []
+def setUp(test):
+    old_loggers = get_all_loggers()
+    FunctionalTestSetup().setUp()
+
+def tearDown(test):
+    FunctionalTestSetup().tearDown()
+    remove_new_loggers(old_loggers)
+
+def doctestsuite_for_module(dotted_path):
+    """Create a doctest suite for the module at `dotted_path`.
+    """
+    test = doctest.DocTestSuite(
+        dotted_path,
+        setUp = setUp,
+        tearDown = tearDown,
+        checker = checker,
+        extraglobs = dict(
+            getRootFolder=getRootFolder,
+            sync=sync,),
+        optionflags = (doctest.ELLIPSIS +
+                       doctest.NORMALIZE_WHITESPACE +
+                       doctest.REPORT_NDIFF),
+        )
+    test.layer = FunctionalLayer
+    return test
+
+optionflags = (
+    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
+
+def clear_logger_collector():
+    from zope.component import queryUtility, getGlobalSiteManager
+    from waeup.ikoba.interfaces import ILoggerCollector
+    collector = queryUtility(ILoggerCollector)
+    if collector is None:
+        return
+    keys = list(collector.keys())
+    for key in keys:
+        del collector[key]
+    return
+
+class FunctionalTestCase(FunctionalTestCase):
+    """A test case that supports checking output diffs in doctest style.
+    """
+
+    def setUp(self):
+        super(FunctionalTestCase, self).setUp()
+        self.functional_old_loggers = get_all_loggers()
+        return
+
+    def tearDown(self):
+        super(FunctionalTestCase, self).tearDown()
+        remove_new_loggers(self.functional_old_loggers)
+        clear_logger_collector()
+        return
+
+    def assertMatches(self, want, got, checker=checker,
+                      optionflags=optionflags):
+        """Assert that the multiline string `want` matches `got`.
+
+        In `want` you can use shortcuts like ``...`` as in regular doctests.
+
+        If no special `checker` is passed, we use an extended
+        :class:`doctest.OutputChecker` as defined in
+        :mod:`waeup.ikoba.testing`.
+
+        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
+        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
+
+        .. seealso:: :data:`waeup.ikoba.testing.optionflags`
+
+        .. seealso:: :data:`waeup.ikoba.testing.checker`
+        """
+        if checker.check_output(want, got, optionflags):
+            return
+        diff = checker.output_difference(
+            doctest.Example('', want), got, optionflags)
+        self.fail(diff)
+
+class FunctionalTestSetup(FunctionalTestSetup):
+    """A replacement for the zope.app.testing class.
+
+    Removes also loggers.
+    """
+
+    def setUp(self):
+        self.old_loggers = get_all_loggers()
+        super(FunctionalTestSetup, self).setUp()
+        return
+
+    def tearDown(self):
+        super(FunctionalTestSetup, self).tearDown()
+        remove_new_loggers(self.old_loggers)
+        return
+
+def get_doctest_suite(filename_list=[]):
+    """Helper function to create doctest suites for doctests.
+
+    The `filename_list` is a list of filenames relative to the
+    w.k. dir.  So, to get a doctest suite for ``browser.txt`` and
+    ``blah.txt`` in the ``browser/`` subpackage you have to pass
+    ``filename_list=['browser/browser.txt','browser/blah.txt']`` and
+    so on.
+
+    The returned test suite must be registered somewhere locally for
+    instance by something like:
+
+      from waeup.ikoba.testing import get_doctest_suite
+      def test_suite():
+        suite = get_doctest_suite(['mypkg/foo.txt', 'mypkg/bar.txt'])
+        return suite
+
+    and that's it.
+    """
+    suite = unittest.TestSuite()
+    for filename in filename_list:
+        path = os.path.join(
+            os.path.dirname(__file__), filename)
+        test = doctest.DocFileSuite(
+            path,
+            module_relative=False,
+            setUp=setUp, tearDown=tearDown,
+            globs = dict(getRootFolder = getRootFolder),
+            optionflags = doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE,
+            checker = checker,
+            )
+        test.layer = FunctionalLayer
+        suite.addTest(test)
+    return suite
+
+class FakeJob(object):
+    # A job usable for simple async tests
+    status = COMPLETED
+    result = None
+
+    def __init__(self, *args, **kw):
+        self.args = args
+        self.kwargs = kw
+
+class FakeJobManager(object):
+    # A fake job manager for testing async functionality
+
+    def __init__(self):
+        # make sure each instance maintains an own set of jobs/nums.
+        self._jobs = dict()
+        self._curr_num = 1
+
+    def get(self, job_id):
+        if job_id == '3':
+            return FakeJob()
+        return self._jobs.get(job_id, None)
+
+    def put(self, job):
+        num = str(self._curr_num)
+        self._jobs[num] = job
+        self._curr_num += 1
+        return num
+
+    def remove(self, job_id, site):
+        if job_id in self._jobs:
+            del self._jobs[job_id]
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/__init__.py	(revision 12677)
@@ -0,0 +1,2 @@
+# This is a package
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/sample-datacenter.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/sample-datacenter.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/sample-datacenter.zcml	(revision 12677)
@@ -0,0 +1,13 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:ikoba="http://namespaces.waeup.org/ikoba"
+   i18n_domain="waeup.ikoba"
+   package="waeup.ikoba.tests"
+   >
+  <include package="waeup.ikoba" file="meta.zcml" />
+
+  <!-- Sample for the 'datacenter' ZCML directive: -->
+  <ikoba:datacenter
+      path="/some/path" />
+
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_app.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_app.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_app.py	(revision 12677)
@@ -0,0 +1,63 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import tempfile
+import shutil
+from hurry.file.interfaces import IFileRetrieval
+from zope.component import queryUtility
+from zope.component.hooks import setSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import (
+    ICompany, IJobManager, VIRT_JOBS_CONTAINER_NAME)
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+class CompanyTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CompanyTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        self.getRootFolder()['app'] = Company()
+        self.app = self.getRootFolder()['app']
+        return
+
+    def tearDown(self):
+        super(CompanyTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        company = Company()
+        assert verifyClass(ICompany, Company)
+        assert verifyObject(ICompany, company)
+        return
+
+    def test_IFileRetrieval_utility(self):
+        # Make sure we can get a local IFileRetrieval utility
+        setSite(self.app)
+        result = queryUtility(IFileRetrieval, default=None)
+        assert result is not None
+        assert IFileRetrieval.providedBy(result)
+        return
+
+    def test_jobs(self):
+        # We can get the global job manager when traversing to it...
+        result = self.app.traverse(VIRT_JOBS_CONTAINER_NAME)
+        self.assertTrue(IJobManager.providedBy(result))
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_async.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_async.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_async.py	(revision 12677)
@@ -0,0 +1,411 @@
+# Tests for the async module of waeup.ikoba
+import os
+import re
+import shutil
+import tempfile
+import transaction
+import zc.async.configure
+import zc.async.ftesting
+import zc.async.queue
+from zc.async.interfaces import IJob
+from ZODB import DB, FileStorage
+from zope.component import getUtility, provideAdapter
+from zope.component.hooks import setSite
+from zope.container.interfaces import IContainer
+from zope.interface import verify
+from zope.site.folder import rootFolder
+from zope.site.testing import createSiteManager
+from waeup.ikoba.interfaces import (
+    WAEUP_KEY, IProgressable, IJobManager, IJobContainer
+    )
+from waeup.ikoba.testing import FunctionalTestCase, FunctionalLayer
+from waeup.ikoba.async import (
+    get_job_id, AsyncJob, ProgressableJob, JobManager, JobContainer,
+    JobManagerTraverser, JobContainerInstaller,
+    )
+
+def fake_async_func1():
+    return 23
+
+def fake_async_func2():
+    return 42
+
+class FunctionalAsyncTestCase(FunctionalTestCase):
+    """A functional test case that additionally sets up asynchronous
+    machinery.
+
+    These type of test case takes _much_ more time than regular
+    functional tests! So, only use it if really neccessary.
+    """
+
+    def setUp(self):
+        super(FunctionalAsyncTestCase, self).setUp()
+        zc.async.configure.base()
+        provideAdapter(zc.async.queue.getDefaultQueue)
+        connection = self.getRootFolder()._p_jar
+        zc.async.ftesting.setUp(connection=connection)
+        return
+
+    def tearDown(self):
+        zc.async.ftesting.tearDown()
+        super(FunctionalAsyncTestCase, self).tearDown()
+        return
+
+class CommonAsyncFunctionalTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_iface_async_job(self):
+        # AsyncJob implements the promised interfaces
+        job = AsyncJob(fake_async_func1)
+        verify.verifyClass(IJob, AsyncJob)
+        verify.verifyObject(IJob, job)
+        return
+
+    def test_iface_progressable_job(self):
+        # ProgressableJob implements the promised interfaces
+        job = ProgressableJob(fake_async_func1)
+        verify.verifyClass(IJob, ProgressableJob)
+        verify.verifyClass(IProgressable, ProgressableJob)
+        verify.verifyObject(IJob, job)
+        verify.verifyObject(IProgressable, job)
+        return
+
+    def test_get_job_id(self):
+        # for persisted jobs we can get an oid as job id
+        job = AsyncJob(fake_async_func1)
+        self.getRootFolder()['myjob'] = job
+        transaction.commit()
+        oid = job.__repr__().split(' ')[2][:-1]
+        result = get_job_id(job)
+        self.assertEqual(result, oid)
+        return
+
+class JobContainerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_iface_job_container(self):
+        # JobContainers implement the promised interfaces
+        container = JobContainer()
+        verify.verifyClass(IContainer, JobContainer)
+        verify.verifyObject(IContainer, container)
+        verify.verifyClass(IJobContainer, JobContainer)
+        verify.verifyObject(IJobContainer, container)
+        return
+
+class JobManagerTests(FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(JobManagerTests, self).setUp()
+        self.root_folder = self.getRootFolder()
+        self.root = self.root_folder._p_jar.root()
+
+    def set_site(self):
+        # helper to set a site 'externally'.
+        # This is a bit complicated under the given limited setup conditions.
+        # Returns a persisted site.
+        site = rootFolder()
+        self.root_folder['application'] = site
+        createSiteManager(site, setsite=True)
+        transaction.commit()
+        return site
+
+    def test_iface_job_manager(self):
+        # JobManager implements the promised interfaces
+        manager = JobManager()
+        verify.verifyClass(IJobManager, JobManager)
+        verify.verifyObject(IJobManager, manager)
+        return
+
+    def test_get_job_manager_as_util(self):
+        # we can get a job manager as global, unnamed utility
+        manager = getUtility(IJobManager)
+        self.assertTrue(isinstance(manager, JobManager))
+        return
+
+    def test_get_site_no_site(self):
+        # if no site is around, we get a lookup error
+        manager = getUtility(IJobManager)
+        self.assertRaises(LookupError, manager._get_site, None)
+        return
+
+    def test_get_site_invalid_site(self):
+        # if we pass an invalid site, we get a lookup error
+        manager = getUtility(IJobManager)
+        self.assertRaises(LookupError, manager._get_site, object())
+        return
+
+    def test_get_site_valid_site(self):
+        # if we pass a valid site, that will be respected
+        manager = getUtility(IJobManager)
+        result = manager._get_site(self.root_folder)
+        self.assertEqual(result, self.root_folder)
+        return
+
+    def test_get_site_ext_set_none_given(self):
+        # with an externally set site we don't have to pass in a site
+        manager = getUtility(IJobManager)
+        site = self.set_site()
+        result = manager._get_site(None)
+        self.assertEqual(result, site)
+        return
+
+    def test_get_site_ext_set_valid_site(self):
+        # with an externally set site we can pass in this site
+        manager = getUtility(IJobManager)
+        site = self.set_site()
+        result = manager._get_site(site)
+        self.assertEqual(result, site)
+        return
+
+    def test_get_site_ext_set_invalid_site(self):
+        # with an externally set site we can pass in invalid sites
+        manager = getUtility(IJobManager)
+        site = self.set_site()
+        result = manager._get_site(object())
+        self.assertEqual(result, site)
+        return
+
+    def test_get_site_ext_set_valid_obj(self):
+        # valid persisted objects override externally set sites
+        manager = getUtility(IJobManager)
+        site = self.set_site()
+        result = manager._get_site(self.root_folder)
+        self.assertEqual(result, self.root_folder)
+        return
+
+    def test_put(self):
+        # we can put jobs into the manager
+        myjob = AsyncJob(fake_async_func1)
+        manager = getUtility(IJobManager)
+        job_id = manager.put(myjob, site=self.root_folder)
+        # the returned job_id should be a string with a bunch of numbers
+        self.assertTrue(re.match('^\d+$', job_id))
+        # the job is also in the (new) jobs container
+        self.assertTrue(job_id in self.root[WAEUP_KEY].keys())
+        # the job is really stored
+        stored_job =self.root[WAEUP_KEY][job_id]
+        self.assertEqual(myjob, stored_job)
+        return
+
+    def test_put_no_site(self):
+        # if no site is around we cannot store jobs
+        myjob = AsyncJob(fake_async_func1)
+        manager = getUtility(IJobManager)
+        self.assertRaises(LookupError, manager.put, myjob)
+        return
+
+    def test_get(self):
+        # we can get a job back with a valid job_id
+        manager = getUtility(IJobManager)
+        myjob = AsyncJob(fake_async_func1)
+        job_id = manager.put(myjob, site=self.root_folder)
+        stored_job = manager.get(job_id, site=self.root_folder)
+        self.assertEqual(myjob, stored_job)
+        return
+
+    def test_get_no_site(self):
+        # if no site is around we cannot retrieve jobs
+        manager = getUtility(IJobManager)
+        self.assertRaises(LookupError, manager.get, 'some_id')
+        return
+
+    def test_get_invalid_jobid(self):
+        # invalid job_ids will result in `None` (if site is valid)
+        manager = getUtility(IJobManager)
+        result = manager.get('not-a-valid-job-id', site=self.root_folder)
+        self.assertEqual(result, None)
+        return
+
+    def test_jobs(self):
+        # we can get all jobs contained
+        manager = getUtility(IJobManager)
+        myjob1 = AsyncJob(fake_async_func1)
+        job_id1 = manager.put(myjob1, site=self.root_folder)
+        myjob2 = AsyncJob(fake_async_func2)
+        job_id2 = manager.put(myjob2, site=self.root_folder)
+        result = sorted(list(manager.jobs(site=self.root_folder)))
+        self.assertEqual(
+            result,
+            [(job_id1, myjob1), (job_id2, myjob2)]
+            )
+        return
+
+    def test_remove(self):
+        # we can remove jobs from the job container
+        manager = getUtility(IJobManager)
+        myjob = AsyncJob(fake_async_func1)
+        job_id = manager.put(myjob, site=self.root_folder)
+        container = self.root[WAEUP_KEY]
+        self.assertEqual(len(container), 1)
+        manager.remove(job_id, site=self.root_folder)
+        self.assertEqual(len(container), 0)
+        return
+
+    def test_start_test_job(self):
+        # we can start a test job
+        manager = getUtility(IJobManager)
+        job_id = manager.start_test_job(duration=1, site=self.root_folder)
+        container = self.root[WAEUP_KEY]
+        self.assertEqual(len(container), 1)
+        self.assertTrue(job_id is not None)
+        return
+
+class JobManagerTraverserTests(FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(JobManagerTraverserTests, self).setUp()
+        self.root_folder = self.getRootFolder()
+        self.root = self.root_folder._p_jar.root()
+        self.manager = getUtility(IJobManager)
+        setSite(self.getRootFolder())
+
+    def tearDown(self):
+        setSite(None)
+        super(JobManagerTraverserTests, self).tearDown()
+
+    def test_no_jobs(self):
+        # the traverser returns None if no job is available
+        traverser = JobManagerTraverser(self.manager, None)
+        result = traverser.traverse('123')
+        self.assertTrue(result is None)
+        return
+
+    def test_valid_job(self):
+        # the traverser returns the appropriate job if available
+        myjob = AsyncJob(fake_async_func1)
+        job_id = self.manager.put(myjob, site=self.root_folder)
+        traverser = JobManagerTraverser(self.manager, None)
+        result = traverser.traverse(job_id)
+        self.assertEqual(myjob, result)
+        return
+
+class FakeEvent(object):
+    # A faked DatabaseOpenedEvent as triggered at instance startup.
+    # The only attribute we need here is `database` which should
+    # return a valid ZODB.
+
+    _storage = None
+    _db = None
+
+    def __init__(self, workdir, db_name=None):
+        # workdir is the place to create a ZODB in, db_name its name.
+        self.workdir = workdir
+        self.db_name = db_name
+
+    @property
+    def database(self):
+        if self._storage is None:
+            # storage not created yet. Do it now.
+            path = os.path.join(self.workdir, 'testdb.fs')
+            self._storage = FileStorage.FileStorage(path)
+            self._db = DB(self._storage, database_name=self.db_name)
+        return self._db
+
+class FakeDB(object):
+
+    closed = False
+
+    def open(self, transaction_manager=None):
+        return self
+
+    def close(self):
+        self.closed = True
+        return
+
+class JobContainerInstallerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(JobContainerInstallerTests, self).setUp()
+        # setup two ZODBs, one unnamed, one named...
+        self.workdir1 = tempfile.mkdtemp()
+        self.workdir2 = tempfile.mkdtemp()
+        self.fake_event1 = FakeEvent(self.workdir1)
+        self.fake_event2 = FakeEvent(self.workdir2, 'mytestdb')
+        self.tm1 = transaction.TransactionManager()
+        self.tm2 = transaction.TransactionManager()
+        self.db1 = self.fake_event1.database
+        self.db2 = self.fake_event2.database
+        self.conn1 = self.db1.open(self.tm1)
+        self.conn2 = self.db2.open(self.tm2)
+
+    def tearDown(self):
+        self.tm1.abort()
+        self.tm2.abort()
+        shutil.rmtree(self.workdir1)
+        shutil.rmtree(self.workdir2)
+        super(JobContainerInstallerTests, self).tearDown()
+
+    def reopen(self):
+        # reopen connections to the ZODBs (for retrieving fresh data)...
+        self.conn1.close()
+        self.conn2.close()
+        self.conn1 = self.db1.open(self.tm1)
+        self.conn2 = self.db2.open(self.tm2)
+        return
+
+    @property
+    def root1(self):
+        # get root of first (unnamed) database
+        self.reopen()
+        return self.conn1.root()
+
+    @property
+    def root2(self):
+        # get root of second (named 'mytestdb') database
+        self.reopen()
+        return self.conn2.root()
+
+    def test_new_container(self):
+        # we can install a job container if none is available yet
+        installer = JobContainerInstaller()
+        self.assertTrue(WAEUP_KEY not in self.root1)
+        installer(self.fake_event1)
+        self.assertTrue(WAEUP_KEY in self.root1)
+        self.assertTrue(IJobContainer.providedBy(self.root1[WAEUP_KEY]))
+        return
+
+    def test_existing_container(self):
+        # we won't try to install a new container if one is already present
+        installer = JobContainerInstaller()
+        self.assertTrue(WAEUP_KEY not in self.root1)
+        installer(self.fake_event1)
+        self.assertTrue(WAEUP_KEY in self.root1)
+        self.assertTrue(IJobContainer.providedBy(self.root1[WAEUP_KEY]))
+        container = self.root1[WAEUP_KEY]
+        installer(self.fake_event1)
+        self.assertTrue(WAEUP_KEY in self.root1)
+        self.assertTrue(self.root1[WAEUP_KEY] is container)
+        return
+
+    def test_named_db(self):
+        # we can also install containers in named dbs
+        installer = JobContainerInstaller(db_name='mytestdb')
+        self.assertTrue(WAEUP_KEY not in self.root1)
+        self.assertTrue(WAEUP_KEY not in self.root2)
+        installer(self.fake_event2)
+        # now the job container is in database2 (but not database1)
+        self.assertTrue(WAEUP_KEY not in self.root1)
+        self.assertTrue(WAEUP_KEY in self.root2)
+        self.assertTrue(IJobContainer.providedBy(self.root2[WAEUP_KEY]))
+        return
+
+    def test_broken_db(self):
+        # if writing to db fails, we close the connection anyway...
+        installer = JobContainerInstaller()
+        self.conn1.close()
+        # create a fake db
+        db = FakeDB()
+        self.fake_event1._storage = 'dummy'
+        self.fake_event1._db = db
+        self.assertEqual(db.closed, False)
+        self.assertRaises(AttributeError, installer, self.fake_event1)
+        self.assertEqual(db.closed, True)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_authentication.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_authentication.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_authentication.py	(revision 12677)
@@ -0,0 +1,330 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+import logging
+import time
+import unittest
+from cStringIO import StringIO
+from zope.component import getGlobalSiteManager
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+from zope.password.testing import setUpPasswordManagers
+from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.ikoba.testing import FunctionalTestCase, FunctionalLayer
+from waeup.ikoba.authentication import (
+    UserAuthenticatorPlugin, Account, IkobaPrincipalInfo, FailedLoginInfo,
+    get_principal_role_manager, UsersPlugin,)
+from waeup.ikoba.interfaces import (
+    IUserAccount, IFailedLoginInfo, IIkobaPrincipalInfo, IIkobaPluggable)
+
+class FakeSite(grok.Site, grok.Container):
+    #def getSiteManager(self):
+    #    return None
+    #    return getGlobalSiteManager()
+    pass
+
+class UserAuthenticatorPluginTests(FunctionalTestCase):
+    # Must be functional because of various utility lookups and the like
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(UserAuthenticatorPluginTests, self).setUp()
+        self.getRootFolder()['app'] = FakeSite()
+        self.site = self.getRootFolder()['app']
+        self.site['users'] = {'bob': Account('bob', 'secret')}
+        setSite(self.site)
+        return
+
+    def tearDown(self):
+        super(UserAuthenticatorPluginTests, self).tearDown()
+        clearSite()
+        return
+
+    def test_ifaces(self):
+        # make sure, interfaces requirements are met
+        plugin = UserAuthenticatorPlugin()
+        plugin.__parent__ = None # This attribute is required by iface
+        self.assertTrue(
+            verifyClass(IAuthenticatorPlugin, UserAuthenticatorPlugin))
+        self.assertTrue(verifyObject(IAuthenticatorPlugin, plugin))
+        return
+
+    def test_authenticate_credentials(self):
+        # make sure authentication works as expected
+        plugin = UserAuthenticatorPlugin()
+        result1 = plugin.authenticateCredentials(
+            dict(login='bob', password='secret'))
+        result2 = plugin.authenticateCredentials(
+            dict(login='bob', password='nonsense'))
+        self.assertTrue(isinstance(result1, IkobaPrincipalInfo))
+        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, IkobaPrincipalInfo))
+        self.assertTrue(result2 is None)
+        return
+
+    def test_get_principal_role_manager(self):
+        # make sure we get different role managers for different situations
+        prm1 = get_principal_role_manager()
+        clearSite(None)
+        prm2 = get_principal_role_manager()
+        self.assertTrue(IPrincipalRoleManager.providedBy(prm1))
+        self.assertTrue(IPrincipalRoleManager.providedBy(prm2))
+        self.assertTrue(prm1._context is self.site)
+        self.assertTrue(hasattr(prm2, '_context') is False)
+        return
+
+    def make_failed_logins(self, num):
+        # do `num` failed logins and a valid one afterwards
+        del self.site['users']
+        self.site['users'] = {'bob': Account('bob', 'secret')}
+        plugin = UserAuthenticatorPlugin()
+        resultlist = []
+        # reset accounts
+        for x in range(num):
+            resultlist.append(plugin.authenticateCredentials(
+                dict(login='bob', password='wrongsecret')))
+        resultlist.append(plugin.authenticateCredentials(
+            dict(login='bob', password='secret')))
+        return resultlist
+
+    def DISABLED_test_failed_logins(self):
+        # after three failed logins, an account is blocked
+        # XXX: this tests authenticator with time penalty (currently
+        # disabled)
+        results = []
+        succ_principal = IkobaPrincipalInfo(
+            id='bob',
+            title='bob',
+            description=None,
+            email=None,
+            phone=None,
+            public_name=None,
+            user_type=u'user')
+        for x in range(4):
+            results.append(self.make_failed_logins(x))
+        self.assertEqual(results[2], [None, None, succ_principal])
+        # last login was blocked although correctly entered due to
+        # time penalty
+        self.assertEqual(results[3], [None, None, None, None])
+        return
+
+class IkobaPrincipalInfoTests(unittest.TestCase):
+
+    def create_info(self):
+        return IkobaPrincipalInfo(
+            id='bob',
+            title='bob',
+            description=None,
+            email=None,
+            phone=None,
+            public_name=None,
+            user_type=u'user')
+
+    def test_iface(self):
+        # make sure we implement the promised interfaces
+        info = self.create_info()
+        verifyClass(IIkobaPrincipalInfo, IkobaPrincipalInfo)
+        verifyObject(IIkobaPrincipalInfo, info)
+        return
+
+    def test_equality(self):
+        # we can test two infos for equality
+        info1 = self.create_info()
+        info2 = self.create_info()
+        self.assertEqual(info1, info2)
+        self.assertTrue(info1 == info2)
+        info1.id = 'blah'
+        self.assertTrue(info1 != info2)
+        self.assertTrue((info1 == info2) is False)
+        info1.id = 'bob'
+        info2.id = 'blah'
+        self.assertTrue(info1 != info2)
+        self.assertTrue((info1 == info2) is False)
+        return
+
+class FailedLoginInfoTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure we fullfill the promised interfaces
+        info1 = FailedLoginInfo()
+        info2 = FailedLoginInfo(num=1, last=time.time())
+        self.assertTrue(
+            verifyClass(IFailedLoginInfo, FailedLoginInfo))
+        self.assertTrue(verifyObject(IFailedLoginInfo, info1))
+        # make sure the stored values have correct type if not None
+        self.assertTrue(verifyObject(IFailedLoginInfo, info2))
+        return
+
+    def test_default_values(self):
+        # By default we get 0, None
+        info = FailedLoginInfo()
+        self.assertEqual(info.num, 0)
+        self.assertEqual(info.last, None)
+        return
+
+    def test_set_values_by_attribute(self):
+        # we can set values by attribute
+        ts = time.gmtime(0)
+        info = FailedLoginInfo()
+        info.num = 5
+        info.last = ts
+        self.assertEqual(info.num, 5)
+        self.assertEqual(info.last, ts)
+        return
+
+    def test_set_values_by_constructor(self):
+        # we can set values by constructor args
+        ts = time.gmtime(0)
+        info = FailedLoginInfo(5, ts)
+        self.assertEqual(info.num, 5)
+        self.assertEqual(info.last, ts)
+        return
+
+    def test_set_values_by_keywords(self):
+        # we can set values by constructor keywords
+        ts = time.gmtime(0)
+        info = FailedLoginInfo(last=ts, num=3)
+        self.assertEqual(info.num, 3)
+        self.assertEqual(info.last, ts)
+        return
+
+    def test_as_tuple(self):
+        # we can get the info values as tuple
+        ts = time.gmtime(0)
+        info = FailedLoginInfo(last=ts, num=3)
+        self.assertEqual(info.as_tuple(), (3, ts))
+        return
+
+    def test_set_values(self):
+        # we can set the values of a an info instance
+        ts = time.time()
+        info = FailedLoginInfo()
+        info.set_values(num=3, last=ts)
+        self.assertEqual(info.num, 3)
+        self.assertEqual(info.last, ts)
+        return
+
+    def test_increase(self):
+        # we can increase the number of failed logins
+        ts1 = time.time()
+        info = FailedLoginInfo()
+        info.increase()
+        self.assertEqual(info.num, 1)
+        self.assertTrue(info.last > ts1)
+        ts2 = info.last
+        info.increase()
+        self.assertEqual(info.num, 2)
+        self.assertTrue(info.last > ts2)
+        return
+
+    def test_reset(self):
+        # we can reset failed login infos.
+        info = FailedLoginInfo()
+        info.increase()
+        info.reset()
+        self.assertEqual(info.num, 0)
+        self.assertEqual(info.last, None)
+        return
+
+class AccountTests(unittest.TestCase):
+
+    def setUp(self):
+        setUpPasswordManagers()
+        return
+
+    def test_iface(self):
+        acct = Account('bob', 'mypasswd')
+        self.assertTrue(
+            verifyClass(IUserAccount, Account))
+        self.assertTrue(
+            verifyObject(IUserAccount, acct))
+        return
+
+    def test_failed_logins(self):
+        # we can retrieve infos about failed logins
+        ts = time.time()
+        acct = Account('bob', 'mypasswd')
+        self.assertTrue(hasattr(acct, 'failed_logins'))
+        acct.failed_logins.set_values(num=3, last=ts)
+        self.assertEqual(acct.failed_logins.last, ts)
+        self.assertEqual(acct.failed_logins.num, 3)
+        return
+
+    def test_failed_logins_per_inst(self):
+        # we get a different counter for each Account instance
+        acct1 = Account('bob', 'secret')
+        acct2 = Account('alice', 'alsosecret')
+        self.assertTrue(acct1.failed_logins is not acct2.failed_logins)
+        return
+
+class FakeUserAccount(object):
+    pass
+
+class UsersPluginTests(unittest.TestCase):
+
+    def setUp(self):
+        setUpPasswordManagers()
+        self.site = FakeSite()
+        self.site['users'] = grok.Container()
+        return
+
+    def get_logger(self):
+        logger = logging.getLogger('waeup.test')
+        stream = StringIO()
+        handler = logging.StreamHandler(stream)
+        logger.setLevel(logging.DEBUG)
+        logger.propagate = False
+        logger.addHandler(handler)
+        return logger, stream
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces
+        plugin = UsersPlugin()
+        verifyClass(IIkobaPluggable, UsersPlugin)
+        verifyObject(IIkobaPluggable, plugin)
+        return
+
+    def test_update(self):
+        # make sure user accounts are updated properly.
+        plugin = UsersPlugin()
+        logger, stream = self.get_logger()
+        plugin.update(self.site, 'app', logger)
+        stream.seek(0)
+        self.assertEqual(stream.read(), '')
+        self.site['users']['bob'] = FakeUserAccount()
+        logger, stream = self.get_logger()
+        plugin.update(self.site, 'app', logger)
+        stream.seek(0)
+        log_content = stream.read()
+        self.assertTrue(hasattr(self.site['users']['bob'], 'description'))
+        self.assertTrue(hasattr(self.site['users']['bob'], 'failed_logins'))
+        self.assertTrue(
+            isinstance(self.site['users']['bob'].failed_logins,
+                       FailedLoginInfo))
+        self.assertTrue('attribute description added' in log_content)
+        self.assertTrue('attribute failed_logins added' in log_content)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_beaker.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_beaker.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_beaker.py	(revision 12677)
@@ -0,0 +1,22 @@
+from zope.component.hooks import setSite
+from zope.publisher.browser import TestRequest
+from zope.session.interfaces import ISession
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+class BeakerTests(FunctionalTestCase):
+    # Beaker-related tests.
+
+    layer = FunctionalLayer
+
+    def test_beaker_session(self):
+        # Make sure we get a beaker session when asking for ISession.
+        try:
+            import dolmen.beaker
+        except ImportError:
+            # no beaker installed, no test
+            return
+        setSite(self.getRootFolder()) # needed to start transaction
+        request = TestRequest()
+        session = ISession(request)
+        self.assertTrue(session.__module__.startswith('dolmen.beaker'))
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_catalog.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_catalog.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_catalog.py	(revision 12677)
@@ -0,0 +1,186 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Tests for basic catalog components.
+"""
+import grok
+import unittest
+from grok import index
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.interface import Interface, Attribute
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.catalog import FilteredQueryBase, FilteredCatalogQueryBase
+from waeup.ikoba.interfaces import IFilteredQuery, IFilteredCatalogQuery
+
+class FilteredQueryBaseTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure, we correctly implement interface requirements
+        obj = FilteredQueryBase()
+        verifyClass(IFilteredQuery, FilteredQueryBase)
+        verifyObject(IFilteredQuery, obj)
+        return
+
+    def test_kw(self):
+        # keywords are set correctly on construction
+        q1 = FilteredQueryBase()
+        self.assertEqual(q1._kw, dict())
+        q2 = FilteredQueryBase(name='Bob')
+        self.assertEqual(q2._kw, dict(name='Bob'))
+        return
+
+    def test_kw_defaults(self):
+        # defaults are set initially but can be overridden.
+        class MyFilter(FilteredQueryBase):
+            defaults=dict(name='Bob')
+        q1 = MyFilter()
+        self.assertEqual(q1._kw, dict(name='Bob'))
+        q2 = MyFilter(name='Alice', age=29)
+        self.assertEqual(q2._kw, dict(name='Alice', age=29))
+        return
+
+    def test_query_not_implemented(self):
+        # we are warned when using `query()`
+        q = FilteredQueryBase()
+        self.assertRaises(NotImplementedError, q.query)
+        return
+
+
+# A sample catalog environment. Has to be grokked to work
+class Herd(grok.Container, grok.Application):
+    pass
+
+class IMammoth(Interface):
+    age = Attribute('Age')
+    name = Attribute('Name')
+
+    def message():
+        """Message for the world.
+        """
+
+class MammothCatalog(grok.Indexes):
+    grok.site(Herd)
+    grok.context(IMammoth)
+    grok.name('mammoths')
+
+    name = index.Field(attribute='name')
+    age = index.Field(attribute='age')
+
+class Mammoth(grok.Model):
+    grok.implements(IMammoth)
+
+    def __init__(self, name, age, message):
+        self.age = age
+        self.name = name
+        self._message = message
+
+    def message(self):
+        return self._message
+
+setup_done = False
+class FilteredCatalogQueryBaseTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        global setup_done
+        super(FilteredCatalogQueryBaseTests, self).setUp()
+        if not setup_done:
+            # grok this module to register indexes etc. defined above
+            # ...and do this only once
+            # XXX workaround because functional test cases do not support
+            #     classmethod setUp
+            grok.testing.grok(self.__module__)
+            setup_done = True
+        self.root = self.getRootFolder()
+        self.getRootFolder()['herd'] = Herd()
+        self.herd = self.root['herd']
+        self.populate_catalog()
+        self.catalog = getUtility(
+            ICatalog, context=self.herd, name='mammoths')
+        return
+
+    def populate_catalog(self):
+        setSite(self.herd)
+        self.herd['bob'] = Mammoth('Bob', 21, 'Hi there!')
+        self.herd['alice'] = Mammoth('Alice', 22, 'Hello!')
+        return
+
+    def test_iface(self):
+        # make sure, we correctly implement interface requirements
+        obj = FilteredCatalogQueryBase()
+        verifyClass(IFilteredCatalogQuery, FilteredCatalogQueryBase)
+        verifyObject(IFilteredCatalogQuery, obj)
+        return
+
+    def test_cat_name_respected(self):
+        # a set cat_name is respected when doing the query
+        q = FilteredCatalogQueryBase(name='Bob')
+        q.cat_name = 'mammoths' # set a registered catalog name
+        result = q.query()
+        self.assertEqual([x.name for x in result], ['Bob'])
+        return
+
+    def test_query_catalog(self):
+        # we can pass in a catalog instance
+        q = FilteredCatalogQueryBase(name='Bob')
+        result = q.query_catalog(catalog=self.catalog)
+        self.assertEqual([x.name for x in result], ['Bob'])
+        return
+
+    def test_query_catalog_value_ranges(self):
+        # we can ask for value ranges
+        q = FilteredCatalogQueryBase(name=('Alice', 'Bob'))
+        result = q.query_catalog(catalog=self.catalog)
+        self.assertEqual([x.name for x in result], ['Bob', 'Alice'])
+        return
+
+    def test_query_value_ranges(self):
+        # we can ask for value ranges instead of single values
+        q = FilteredCatalogQueryBase(name=('Alice', 'Bob'))
+        q.cat_name = 'mammoths'
+        result = q.query()
+        self.assertEqual([x.name for x in result], ['Bob', 'Alice'])
+        return
+
+    def test_query_mixed_values(self):
+        # we can ask for different values at the same time
+        q = FilteredCatalogQueryBase(name=('Alice', 'Bob'), age=22)
+        q.cat_name = 'mammoths'
+        result = q.query()
+        self.assertEqual([x.name for x in result], ['Alice'])
+        return
+
+    def test_query_invalid_cat_name(self):
+        # with an invalid catalog name we get empty results
+        q = FilteredCatalogQueryBase(name='Bob')
+        q.cat_name = 'NOT-EXISTING'
+        result = q.query()
+        self.assertEqual([x.name for x in result], [])
+        return
+
+    def test_query_catalog_no_site(self):
+        # without a site with catalog we get empty results
+        setSite(None) # unset current site
+        q = FilteredCatalogQueryBase(name='Bob')
+        q.cat_name = 'mammoths'
+        result = q.query()
+        self.assertEqual([x.name for x in result], [])
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_configuration.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_configuration.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_configuration.py	(revision 12677)
@@ -0,0 +1,41 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Tests for configuration containers and related.
+"""
+from zope.component.interfaces import IFactory
+from zope.interface import verify
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.configuration import ConfigurationContainer
+from waeup.ikoba.interfaces import IConfigurationContainer
+
+class ConfigurationTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ConfigurationTest, self).setUp()
+        self.confcontainer = ConfigurationContainer()
+        return
+
+    def tearDown(self):
+        super(ConfigurationTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(IConfigurationContainer, ConfigurationContainer)
+        verify.verifyObject(IConfigurationContainer, self.confcontainer)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_datacenter.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_datacenter.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_datacenter.py	(revision 12677)
@@ -0,0 +1,286 @@
+# -*- coding: utf-8 -*-
+## Tests for datacenter
+##
+## XXX: Most tests for datacenter are still in doctest datacenter.txt
+##
+import os
+import shutil
+import tempfile
+import unittest
+from zope.component import getUtility, getGlobalSiteManager
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.ikoba.datacenter import DataCenter
+from waeup.ikoba.interfaces import (
+    IDataCenter, IDataCenterConfig, IExportJobContainer)
+
+class DataCenterLogQueryTests(unittest.TestCase):
+    # Tests for querying logfiles via datacenter.
+
+    def setUp(self):
+        # create a temporary place to store files
+        self.workdir = tempfile.mkdtemp()
+        self.storage = os.path.join(self.workdir, 'storage')
+        os.mkdir(self.storage)
+        self.logpath = os.path.join(self.storage, 'logs', 'myapp.log')
+        # register a datacenter config that provides the set up location
+        self.config = {'path': self.storage,}
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(self.config, IDataCenterConfig)
+        pass
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.config, IDataCenterConfig)
+        shutil.rmtree(self.workdir)
+        return
+
+    def fill_logfile(self, num=1):
+        # write 100 messages into logfile, with 50 containing 'Msg'
+        path = self.logpath
+        for m in range(num-1,-1,-1):
+            # write messages in order: lowest message in oldest file
+            fp = open(path, 'wb')
+            for n in range(50*m, 50*m+50):
+                fp.write('Msg %d\n' % (n + 1))
+                fp.write('Other Line %d\n' % (n + 1))
+            fp.write('A Message with Ümläüt')
+            fp.close()
+            path = self.logpath + '.%d' % m
+        return
+
+    def test_util_available(self):
+        # a self-test
+        config = getUtility(IDataCenterConfig)
+        self.assertTrue(config is not None)
+        return
+
+    def test_query_logfiles(self):
+        # We can find entries in logfiles
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Message 1\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Message'))
+        self.assertEqual(result, [u'Message 1\n'])
+        return
+
+    def test_query_logfiles_multi_logs(self):
+        # We can find entries in multiple logfiles (backups)
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Msg 3\n')
+        open(self.logpath + '.2', 'wb').write('Msg 2\n')
+        open(self.logpath + '.10', 'wb').write('Msg 1\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Msg'))
+        # entry of logfile .10 comes after entry of logfile .2
+        self.assertEqual(result, [u'Msg 1\n', u'Msg 2\n', u'Msg 3\n'])
+        return
+
+    def test_query_logfiles_ignores_other_logs(self):
+        # We look only for the basename specified
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Msg 1\n')
+        open(self.logpath + '-not-a-real-log', 'wb').write('Msg 2\n')
+        open(self.logpath + '-not-a-real-log.1', 'wb').write('Msg 3\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Msg'))
+        # Msg 2 and 3 won't show up in results.
+        self.assertEqual(result, [u'Msg 1\n'])
+        return
+
+    def test_query_logfiles_not_existant(self):
+        # We make sure only existing logs are searched
+        datacenter = DataCenter()
+        open(self.logpath + '.1', 'wb').write('Msg 1\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Msg'))
+        # works, although there is no myapp.log, only myapp.log.1
+        self.assertEqual(result, ['Msg 1\n'])
+        return
+
+    def test_query_logfiles_invalid_regexp(self):
+        # Invalid regular expressions give a ValueError
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Msg 1\n')
+        result = datacenter.queryLogfiles('myapp.log', '(a')
+        self.assertRaises(ValueError, list, result)
+        return
+
+    def test_query_logfiles_batching_limit(self):
+        # we can use `limit` for batching
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', limit=10))
+        self.assertEqual(len(result), 10)
+        self.assertEqual(result[-1], 'Msg 10\n')
+        return
+
+    def test_query_logfiles_batching_start(self):
+        # `start` is respected when batching
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=25))
+        self.assertEqual(len(result), 25)
+        self.assertEqual(result[0], u'Msg 26\n')
+        return
+
+    def test_query_logfiles_batching_limit_and_start(self):
+        # we can use `start` and `limit` simultanously
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=25, limit=10))
+        self.assertEqual(len(result), 10)
+        self.assertEqual(result[0], u'Msg 26\n')
+        self.assertEqual(result[-1], u'Msg 35\n')
+        return
+
+    def test_query_logfiles_batching_edge_cases(self):
+        # we can find last matches if found num < limit.
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=45, limit=10))
+        self.assertEqual(len(result), 5)
+        self.assertEqual(result[0], u'Msg 46\n')
+        self.assertEqual(result[-1], u'Msg 50\n')
+        return
+
+    def test_query_logfiles_batching_multiple_files(self):
+        # batching works also with multiple log files
+        datacenter = DataCenter()
+        self.fill_logfile(num=2)
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=45, limit=10))
+        self.assertEqual(len(result), 10)
+        self.assertEqual(result[0], u'Msg 46\n')
+        self.assertEqual(result[-1], u'Msg 55\n')
+        return
+
+    def test_query_logfiles_regex_match_inner(self):
+        # we also find lines that match at some inner part
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles('myapp.log', 'sg 1\n'))
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0], u'Msg 1\n')
+        return
+
+    def test_query_logfiles_umlauts(self):
+        # we return results as unicode decoded from utf-8
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', u'Ümläüt'))
+        self.assertTrue(isinstance(result[0], unicode))
+        self.assertEqual(result, [u'A Message with Ümläüt'])
+        return
+
+class DataCenterTests(unittest.TestCase):
+    # General datacenter tests.
+
+    def setUp(self):
+        # create a temporary place to store files
+        self.workdir = tempfile.mkdtemp()
+        self.storage = os.path.join(self.workdir, 'storage')
+        os.mkdir(self.storage)
+        self.logpath = os.path.join(self.storage, 'logs', 'myapp.log')
+        # register a datacenter config that provides the set up location
+        self.config = {'path': self.storage,}
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(self.config, IDataCenterConfig)
+        pass
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.config, IDataCenterConfig)
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        # we comply with interfaces
+        obj = DataCenter()
+        verifyClass(IDataCenter, DataCenter)
+        verifyClass(IExportJobContainer, DataCenter)
+        verifyObject(IDataCenter, obj)
+        verifyObject(IExportJobContainer, obj)
+        return
+
+    def test_get_log_files(self):
+        # We can get lists of logfiles available.
+        # By default, backups are skipped.
+        datacenter = DataCenter()
+        logpath2 = self.logpath + '.1'
+        logpath3 = self.logpath + '.2'
+        for path in self.logpath, logpath2, logpath3:
+            open(path, 'wb').write('some contents')
+        result = datacenter.getLogFiles()
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0].name, os.path.basename(self.logpath))
+        return
+
+    def test_get_log_files_incl_backups(self):
+        # We can get lists of logfiles including backup logs.
+        datacenter = DataCenter()
+        logpath2 = self.logpath + '.1'
+        logpath3 = self.logpath + '.2'
+        for path in self.logpath, logpath2, logpath3:
+            open(path, 'wb').write('some contents')
+        result = datacenter.getLogFiles(exclude_backups=False)
+        self.assertEqual(len(result), 3)
+        names = [x.name for x in result]
+        expected = [os.path.basename(x) for x in [
+            self.logpath, logpath2, logpath3]]
+        self.assertEqual(names, expected)
+        return
+
+    def test_append_csv_file(self):
+        # we can append CSV files to others
+        datacenter = DataCenter()
+        csv_file1 = os.path.join(self.workdir, 'foo.csv')
+        csv_file2 = os.path.join(self.workdir, 'bar.csv')
+        open(csv_file1, 'wb').write('name,age\nBarney,28')
+        open(csv_file2, 'wb').write('name,age\nManfred,28')
+        datacenter._appendCSVFile(csv_file2, csv_file1)
+        result = open(csv_file1, 'rb').read()
+        self.assertEqual(result, 'age,name\r\n28,Barney\r\n28,Manfred\r\n')
+        # The source is deleted afterwards
+        self.assertEqual(os.path.exists(csv_file2), False)
+        return
+
+    def test_append_csv_file_no_dest(self):
+        # a non-existing dest CSV file will result in a simple move
+        datacenter = DataCenter()
+        csv_file1 = os.path.join(self.workdir, 'foo.csv')
+        csv_file2 = os.path.join(self.workdir, 'bar.csv')
+        # csv_file1 does not exist
+        open(csv_file2, 'wb').write('name,age\nManfred,28\n')
+        datacenter._appendCSVFile(csv_file2, csv_file1)
+        result = open(csv_file1, 'rb').read()
+        # raw input, no CSV mangling
+        self.assertEqual(result, 'name,age\nManfred,28\n')
+        # The source is deleted afterwards
+        self.assertEqual(os.path.exists(csv_file2), False)
+        return
+
+    def test_append_csv_file_no_source(self):
+        # a non existing source file will mean no changes at all
+        datacenter = DataCenter()
+        csv_file1 = os.path.join(self.workdir, 'foo.csv')
+        csv_file2 = os.path.join(self.workdir, 'bar.csv')
+        open(csv_file1, 'wb').write('name,age\nManfred,28\n')
+        # csv_file2 does not exist
+        datacenter._appendCSVFile(csv_file2, csv_file1)
+        result = open(csv_file1, 'rb').read()
+        # csv_file1 is the same as before
+        self.assertEqual(result, 'name,age\nManfred,28\n')
+        return
+
+    def test_append_csv_file_same_src_and_dest(self):
+        # if both csv files are the same file, nothing will be changed
+        datacenter = DataCenter()
+        csv_file1 = os.path.join(self.workdir, 'foo.csv')
+        csv_file2 = csv_file1
+        open(csv_file1, 'wb').write('name,age\nManfred,28\n')
+        # csv_file2 does not exist
+        datacenter._appendCSVFile(csv_file2, csv_file1)
+        result = open(csv_file1, 'rb').read()
+        # csv_file1 is the same as before
+        self.assertEqual(result, 'name,age\nManfred,28\n')
+        self.assertEqual(os.path.exists(csv_file2), True)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_doctests.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_doctests.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_doctests.py	(revision 12677)
@@ -0,0 +1,26 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Register doctests for waeup.ikoba.
+"""
+from waeup.ikoba.testing import get_doctest_suite
+
+def test_suite():
+    suite = get_doctest_suite([
+            'authentication.txt',
+            ])
+    return suite
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_fees.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_fees.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_fees.py	(revision 12677)
@@ -0,0 +1,136 @@
+# Tests for fee management.
+import unittest
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.fees import IFeeTable
+from waeup.ikoba.fees import FeeTable, nested_list, nested_tuple
+
+SAMPLE_PARAMS = (('locals', 'non-locals'), ('art', 'science'),
+                 ('fac1', 'fac2'))
+
+SAMPLE_VALUES = (((10, 20),   # locals, art, fac1/fac2
+                  (11, 21)),  #         science, fac1/fac2
+                 ((12, 22),   # non-locals, art, fac1/fac2
+                  (13, 23)))  #         science, fac1/fac2
+
+class HelperTests(unittest.TestCase):
+
+    def test_nested_list(self):
+        # we can turn nested lists into nested tuples
+        self.assertEqual(
+            nested_list(()), [])
+        self.assertEqual(
+            nested_list((1,)), [1])
+        self.assertEqual(
+            nested_list(((1,2), (3,))), [[1,2],[3]])
+        self.assertEqual(
+            nested_list((((1,2),(3,4)),(5,6))), [[[1,2],[3,4]],[5,6]])
+        return
+
+    def test_nested_tuples(self):
+        # we can turn nested tuples into nested lists
+        self.assertEqual(
+            nested_tuple([]), ())
+        self.assertEqual(
+            nested_tuple([1,]), (1,))
+        self.assertEqual(
+            nested_tuple([[1,2], [3,]]), ((1,2),(3,)))
+        self.assertEqual(
+            nested_tuple([[[1,2],[3,4]],[5,6]]), (((1,2),(3,4)),(5,6)))
+        return
+
+class FeeTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure, we comply with interfaces
+        obj = FeeTable()
+        verifyClass(IFeeTable, FeeTable)
+        verifyObject(IFeeTable, obj)
+        return
+
+    def test_constructor(self):
+        # we can set values in constructor
+        fees = FeeTable(params=SAMPLE_PARAMS, values=SAMPLE_VALUES)
+        self.assertEqual(fees.values, SAMPLE_VALUES)
+        self.assertEqual(fees.params, SAMPLE_PARAMS)
+        return
+
+    def test_import_values(self):
+        # we can import a set of values (clearing old stuff)
+        fees = FeeTable()
+        fees.import_values(SAMPLE_PARAMS, SAMPLE_VALUES)
+        self.assertEqual(fees.params, SAMPLE_PARAMS)
+        self.assertEqual(fees.values, SAMPLE_VALUES)
+        return
+
+    def test_import_values_no_values(self):
+        # w/o values the table is emptied
+        fees = FeeTable()
+        fees.import_values()
+        self.assertEqual(fees.params, ())
+        self.assertEqual(fees.values, ())
+        return
+
+    def test_as_dict(self):
+        # we can get a fee table as dict
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        result = fees.as_dict()
+        self.assertEqual(
+            result,
+            {
+                ('locals', 'art', 'fac1'): 10,
+                ('locals', 'science', 'fac1'): 11,
+                ('non-locals', 'art', 'fac1'): 12,
+                ('non-locals', 'science', 'fac1'): 13,
+                ('locals', 'art', 'fac2'): 20,
+                ('locals', 'science', 'fac2'): 21,
+                ('non-locals', 'art', 'fac2'): 22,
+                ('non-locals', 'science', 'fac2'): 23,
+                }
+            )
+
+    def test_get_fee(self):
+        # we can get values
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        result1 = fees.get_fee(('locals', 'science', 'fac2'),)
+        result2 = fees.get_fee(('non-locals', 'art', 'fac1'),)
+        self.assertEqual(result1, 21)
+        self.assertEqual(result2, 12)
+        return
+
+    def test_get_fee_invalid_key(self):
+        # invalid param values will raise a key error
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        self.assertRaises(
+            KeyError, fees.get_fee, ('NONSENSE', 'science', 'fac2'))
+        self.assertRaises(
+            KeyError, fees.get_fee, ('locals', 'NONSENSE', 'fac2'))
+        self.assertRaises(
+            KeyError, fees.get_fee, ('locals', 'science', 'NONSENSE'))
+        # also the order of params matters
+        self.assertRaises( # 'science' and 'locals' is wrong order...
+            KeyError, fees.get_fee, ('science', 'locals', 'fac2'))
+        return
+
+    def test_set_fee(self):
+        # we can set values
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        result1 = fees.get_fee(('locals', 'science', 'fac2'),)
+        fees.set_fee(('locals', 'science', 'fac2'), 42)
+        result2 = fees.get_fee(('locals', 'science', 'fac2'),)
+        self.assertEqual(result1, 21)
+        self.assertEqual(result2, 42)
+        return
+
+    def test_set_fee_invalid(self):
+        # invalid param values will raise a key error
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        self.assertRaises(
+            KeyError, fees.set_fee, ('NONSENSE', 'science', 'fac2'), 12)
+        self.assertRaises(
+            KeyError, fees.set_fee, ('locals', 'NONSENSE', 'fac2'), 12)
+        self.assertRaises(
+            KeyError, fees.set_fee, ('locals', 'science', 'NONSENSE'), 12)
+       # also the order of params matters
+        self.assertRaises( # 'science' and 'locals' is wrong order...
+            KeyError, fees.set_fee, ('science', 'locals', 'fac2'), 12)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_imagestorage.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_imagestorage.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_imagestorage.py	(revision 12677)
@@ -0,0 +1,400 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import os
+import tempfile
+import shutil
+import unittest
+from StringIO import StringIO
+from hurry.file import HurryFile
+from hurry.file.interfaces import IFileRetrieval
+from zope.component import (
+    getUtility, provideUtility, queryUtility, provideAdapter)
+from zope.component.hooks import setSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.app import Company
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.imagestorage import (
+    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
+    DefaultStorage)
+from waeup.ikoba.interfaces import (
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,)
+
+class FileStoreNameChooserTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # we provide the interfaces we promise to do
+        obj = FileStoreNameChooser(None)
+        verifyClass(IFileStoreNameChooser, FileStoreNameChooser)
+        verifyObject(IFileStoreNameChooser, obj)
+        return
+
+    def test_accessible_as_adapter(self):
+        # we can get a file name chooser via adapter
+        chooser = IFileStoreNameChooser(object())
+        self.assertTrue(
+            isinstance(chooser, FileStoreNameChooser))
+        return
+
+    def test_check_name(self):
+        # default file name choosers accept any string
+        chooser = FileStoreNameChooser(object())
+        self.assertEqual(chooser.checkName('Hi there!'), True)
+        self.assertEqual(chooser.checkName(None), False)
+        return
+
+    def test_choose_name(self):
+        # we get a simple string if we do not pass in a valid string
+        chooser = FileStoreNameChooser(object())
+        self.assertEqual(chooser.chooseName('myname'), 'myname')
+        self.assertEqual(chooser.chooseName(None), u'unknown_file')
+        return
+
+class ExtFileStoreTests(unittest.TestCase):
+    # Test external file store (non-functional mode)
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.root = None
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        if self.root is not None:
+            shutil.rmtree(self.root)
+        return
+
+    def test_iface(self):
+        obj = ExtFileStore(None)
+        verifyClass(IExtFileStore, ExtFileStore)
+        verifyObject(IExtFileStore, obj)
+        self.root = obj.root # for cleanup
+        return
+
+    def test_root_setup_wo_site(self):
+        # if no site is available we can use a temporary root
+        fs = ExtFileStore()
+        self.root = fs.root
+        self.assertTrue(isinstance(self.root, basestring))
+        self.assertTrue(os.path.exists(self.root))
+        return
+
+    def test_create_instance(self):
+        # we can create file stores with or without a root dir set
+        storage1 = ExtFileStore()
+        storage2 = ExtFileStore(root=self.workdir)
+        self.root = storage1.root
+        self.assertTrue(storage1.root is not None)
+        self.assertTrue(storage1.root != storage2.root)
+        self.assertEqual(storage2.root, self.workdir)
+        return
+
+    def test_create_file(self):
+        # We can store files
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample', dummy_file)
+        self.assertTrue('mysample' in os.listdir(storage.root))
+        self.assertEqual('mysample', image_file.data)
+        return
+
+    def test_create_file_w_ext(self):
+        # We can store files with filename extension
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample.txt', dummy_file)
+        self.assertTrue('mysample.txt' in os.listdir(storage.root))
+        self.assertEqual('mysample.txt', image_file.data)
+        return
+
+    def test_get_file(self):
+        # We can get files after having them stored
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample', dummy_file)
+        result = storage.getFile(image_file.data)
+        self.assertEqual(result.read(), 'sample file')
+        return
+
+    def test_get_file_w_ext(self):
+        # We can get files with filename extension after having them
+        # stored
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample.txt', dummy_file)
+        result = storage.getFile('mysample')
+        self.assertEqual(result.read(), 'sample file')
+        return
+
+    def test_replace_file_w_new_ext(self):
+        # when we store a file with the same file_id but different
+        # filename extension, the old file will be deleted
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample_file')
+        image_file = storage.createFile('mysample.jpg', dummy_file)
+        file1_path = storage.getFile('mysample').name
+        new_file = storage.createFile(
+            'mysample.png', StringIO('new file'))
+        file2 = storage.getFile('mysample')
+        self.assertEqual(file2.name[-12:], 'mysample.png')
+        self.assertEqual(file2.read(), 'new file')
+        # the old file was deleted
+        self.assertFalse(os.path.exists(file1_path))
+        return
+
+    def test_extract_marker(self):
+        # file stores support extracting markers from filenames
+        storage = ExtFileStore(root=self.workdir)
+        result1 = storage.extractMarker(None)
+        result2 = storage.extractMarker('')
+        result3 = storage.extractMarker('no-marker')
+        result4 = storage.extractMarker('no-marker.txt')
+        result5 = storage.extractMarker('__MARKER__foo.jpg')
+        result6 = storage.extractMarker('__MaRkEr__foo.jpg')
+        result7 = storage.extractMarker('__THE_MARKER__foo.jpg')
+        result8 = storage.extractMarker('__A_MARK__my__foo.jpg')
+
+        self.assertEqual(result1, ('', '', '', ''))
+        self.assertEqual(result2, ('', '', '', ''))
+        self.assertEqual(result3, ('', 'no-marker', 'no-marker', ''))
+        self.assertEqual(result4, ('', 'no-marker.txt', 'no-marker', '.txt'))
+        self.assertEqual(result5, ('marker', 'foo.jpg', 'foo', '.jpg'))
+        self.assertEqual(result6, ('marker', 'foo.jpg', 'foo', '.jpg'))
+        self.assertEqual(result7, ('the_marker', 'foo.jpg', 'foo', '.jpg'))
+        self.assertEqual(result8, ('a_mark', 'my__foo.jpg', 'my__foo', '.jpg'))
+        return
+
+class DefaultFileStoreHandlerTests(unittest.TestCase):
+
+    def test_iface(self):
+        obj = DefaultFileStoreHandler()
+        verifyClass(IFileStoreHandler, DefaultFileStoreHandler)
+        verifyObject(IFileStoreHandler, obj)
+        return
+
+class CustomizedFileHandler(object):
+    def pathFromFileID(self, store, root, file_id):
+        return os.path.join(root, file_id[12:])
+
+    def createFile(self, store, root, filename, file_id, f):
+        ext = os.path.splitext(filename)[1]
+        path = self.pathFromFileID(store, root, file_id) + ext
+        return f, path, HurryFile(filename, file_id + ext)
+
+class CustomContext(object):
+    pass
+
+class CustomContextFileChooser(object):
+    def __init__(self, context):
+        self.context = context
+
+    def chooseName(self, name=None, attr=None):
+        # this name chooser returns different file ids depending on
+        # the `attr` parameter, a simple string.
+        if attr=='img':
+            return '__mymarker__mysample_img.jpg'
+        elif attr=='doc':
+            return '__mymarker__mysample_doc.doc'
+        return '__mymarker__mysample.txt'
+
+class FunctionalExtFileStoreTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FunctionalExtFileStoreTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        self.root = None
+        self.samplefile = os.path.join(self.workdir, 'sample')
+        self.otherfile = os.path.join(self.workdir, 'other')
+        open(self.samplefile, 'wb').write('Hi there!')
+        open(self.otherfile, 'wb').write('Hi from other!')
+        self.fd = open(self.samplefile, 'r')
+        self.fd2 = open(self.otherfile, 'r')
+        self.getRootFolder()['app'] = Company()
+        self.app = self.getRootFolder()['app']
+        self.app['datacenter'].setStoragePath(self.workdir)
+        # register a custom filename mangler
+        provideUtility(
+            CustomizedFileHandler(), IFileStoreHandler, name=u'mymarker')
+        # register a file chooser adapter for CustomContext
+        provideAdapter(
+            CustomContextFileChooser,
+            (CustomContext,), IFileStoreNameChooser)
+        return
+
+
+    def tearDown(self):
+        super(FunctionalExtFileStoreTests, self).tearDown()
+        self.fd.close()
+        self.fd2.close()
+        shutil.rmtree(self.workdir)
+        if self.root is not None and os.path.exists(self.root):
+            shutil.rmtree(self.root)
+        return
+
+    def test_root_setup_w_site(self):
+        # if a site is available we use it to determine the root dir
+        fs = ExtFileStore()
+        setSite(self.app)
+        self.root = fs.root
+        expected_root = os.path.join(
+            self.app['datacenter'].storage, 'media')
+        self.assertTrue(isinstance(self.root, basestring))
+        self.assertEqual(self.root, expected_root)
+        return
+
+    def test_get_utility(self):
+        # we can get an ExtFileStore by global utility lookup
+        fs1 = getUtility(IExtFileStore)
+        fs2 = getUtility(IExtFileStore)
+        self.assertTrue(isinstance(fs1, ExtFileStore))
+        self.assertTrue(fs1 is fs2)
+        return
+
+    def test_default_handler_create_file(self):
+        # we can use the default handler to store files
+        fs = ExtFileStore(root=self.workdir)
+        result = fs.createFile('sample.txt', StringIO('sample text'))
+        self.assertEqual(result.data, 'sample.txt')
+        self.assertTrue('sample.txt' in os.listdir(fs.root))
+        return
+
+    def test_default_handler_get_file(self):
+        # we can get files stored by the default handler
+        fs = ExtFileStore(root=self.workdir)
+        fs.createFile('sample.txt', StringIO('sample text'))
+        result1 = fs.getFile('sample.txt')
+        result2 = fs.getFile('not-existent')
+        self.assertEqual(result1.read(), 'sample text')
+        self.assertTrue(result2 is None)
+        return
+
+    def test_customized_handler_create_file(self):
+        # we can use registered filename handlers
+        fs = ExtFileStore(root=self.workdir)
+        result = fs.createFile(
+            '__MYMARKER__sample.txt', StringIO('sample text'))
+        self.assertEqual(result.data, '__MYMARKER__sample.txt')
+        self.assertTrue('sample.txt' in os.listdir(fs.root))
+        return
+
+    def test_customized_handler_create_file_w_ext(self):
+        # when we create a file of img type, the filename ext is taken
+        # from input file.
+        fs = ExtFileStore(root=self.workdir)
+        result = fs.createFile(
+            '__MYMARKER__sample_img.png', StringIO('sample text'))
+        self.assertEqual(result.data, '__MYMARKER__sample_img.png')
+        self.assertTrue('sample_img.png' in os.listdir(fs.root))
+        return
+
+    def test_customized_handler_get_file(self):
+        # we consider registered filename handlers when asking for
+        # stored files.
+        fs = ExtFileStore(root=self.workdir)
+        fs.createFile('__MYMARKER__sample.txt', StringIO('sample text'))
+        result1 = fs.getFile('__MYMARKER__sample.txt')
+        result2 = fs.getFile('__MYMARKER__not-existent')
+        result3 = fs.getFile('not-existent')
+        self.assertEqual(result1.read(), 'sample text')
+        self.assertTrue(result2 is None)
+        self.assertTrue(result3 is None)
+        return
+
+    def test_get_file_by_context_w_attr(self):
+        # if we register a file name chooser, we can also get a file
+        # by context and attribute
+        context = CustomContext()
+        file_id1 = IFileStoreNameChooser(context).chooseName()
+        file_id2 = IFileStoreNameChooser(context).chooseName(attr='img')
+        file_id3 = IFileStoreNameChooser(context).chooseName(attr='doc')
+        # create three files for a single context, each with different
+        # content
+        fs = ExtFileStore(root=self.workdir)
+        fs.createFile(file_id1, StringIO('my sample 1'))
+        fs.createFile(file_id2, StringIO('my sample 2'))
+        fs.createFile(file_id3, StringIO('my sample 3'))
+        # now get back all files indicated by different `attr` markers
+        result1 = fs.getFileByContext(context)
+        result2 = fs.getFileByContext(context, attr='img')
+        result3 = fs.getFileByContext(context, attr='doc')
+        # each file has a different file id
+        self.assertEqual(file_id1, '__mymarker__mysample.txt')
+        self.assertEqual(file_id2, '__mymarker__mysample_img.jpg')
+        self.assertEqual(file_id3, '__mymarker__mysample_doc.doc')
+        # each file has different content
+        self.assertEqual(result1.read(), 'my sample 1')
+        self.assertEqual(result2.read(), 'my sample 2')
+        self.assertEqual(result3.read(), 'my sample 3')
+        return
+
+    def test_get_default_handler(self):
+        # we can get a default handler
+        result = queryUtility(IFileStoreHandler)
+        self.assertTrue(
+            isinstance(result, DefaultFileStoreHandler))
+        return
+
+    def test_get_default_file_retrieval(self):
+        # we get a file store when requesting a file retrieval
+        result = queryUtility(IFileRetrieval)
+        self.assertTrue(
+            isinstance(result, DefaultStorage))
+
+    def test_delete_file(self):
+        # we can remove stored files from storage
+        fs = ExtFileStore(root=self.workdir)
+        # First, we store a file in file store
+        fs.createFile('sample.txt', StringIO('sample text'))
+        # Then we delete it
+        fs.deleteFile('sample.txt')
+        # 'Deletion' means, next call to getFile should get None
+        result = fs.getFile('sample.txt')
+        self.assertTrue(result is None)
+        # Hm, okay we can also check, whether it was really deleted
+        self.assertTrue('sample.txt' not in os.listdir(fs.root))
+        return
+
+    def test_delete_file_by_context_w_attr(self):
+        # if we register a file name chooser, we can also delete a file
+        # by context and attribute
+        fs = ExtFileStore(root=self.workdir)
+        context = CustomContext()
+        file_id1 = IFileStoreNameChooser(context).chooseName()
+        file_id2 = IFileStoreNameChooser(context).chooseName(attr='img')
+        file_id3 = IFileStoreNameChooser(context).chooseName(attr='doc')
+        fs = ExtFileStore(root=self.workdir)
+        # create three files for a single context, each which
+        # different content
+        fs.createFile(file_id1, StringIO('my sample 1'))
+        fs.createFile(file_id2, StringIO('my sample 2'))
+        fs.createFile(file_id3, StringIO('my sample 3'))
+        # now delete first two of these files
+        fs.deleteFileByContext(context)
+        fs.deleteFileByContext(context, attr='img')
+        # Following getFile calls should give None for two of the
+        # files.
+        result1 = fs.getFileByContext(context)
+        result2 = fs.getFileByContext(context, attr='img')
+        result3 = fs.getFileByContext(context, attr='doc')
+        self.assertEqual(result1, None)
+        self.assertEqual(result2, None)
+        self.assertEqual(result3.read(), 'my sample 3')
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_interfaces.py	(revision 12677)
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+# Tests for interfaces and included components.
+import grok
+import unittest
+from waeup.ikoba.interfaces import (
+    DuplicationError, RoleSource, check_email, check_id, check_uuid)
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+class RegExTests(unittest.TestCase):
+    # Let's test regular expressions here
+
+    def check(self, expression, string):
+        if expression.match(string) is not None:
+            return True
+        return False
+
+    def test_check_email_allowed(self):
+        # the RE check_email does moderate format checks, allowing
+        # also strange looking but valid email addresses
+        self.assertTrue(check_email('manfred@domain.com') is not None)
+        self.assertTrue(check_email('bob@localhost') is not None)
+        self.assertTrue(check_email('bob@h5.waeup.org') is not None)
+        self.assertTrue(check_email('bob@idn_with_ümlaut.com') is not None)
+        # also unicodes with umlauts work
+        self.assertTrue(check_email(u'bob@idn_with_ümlaut.com') is not None)
+        self.assertTrue(
+            check_email('bob_the-complex+mbox@my.place') is not None)
+        return
+
+    def test_check_email_forbidden(self):
+        # the RE check_email does moderate format checks, but doesn't
+        # accept everything.
+        # no domain at all
+        self.assertTrue(check_email('bob') is None)
+        self.assertTrue(check_email('bob@') is None)
+        # each domain part must contain at least something
+        self.assertTrue(check_email('bob@h5.waeup.') is None)
+        # whitespaces in addresses
+        self.assertTrue(check_email('spacey @domain.com') is None)
+        self.assertTrue(check_email('tabby\t@domain.com') is None)
+        # two dots next to each other in domain (empty domain part)
+        self.assertTrue(check_email('bob@some..domain') is None)
+        # commas are forbidden
+        self.assertTrue(check_email('bob@foo,alice@bar') is None)
+        return
+
+    def test_check_id(self):
+        self.assertTrue(check_id('abcABC') is not None)
+        # No spaces
+        self.assertTrue(check_id('abc ABC') is None)
+        # More than 10 characters are not allowed
+        self.assertTrue(check_id('abcdefghijk') is None)
+        # Only underscores and hyphens are allowed
+        self.assertTrue(check_id('a+b') is None)
+        self.assertTrue(check_id('a/b') is None)
+        self.assertTrue(check_id('a-b') is not None)
+        self.assertTrue(check_id('a_b') is not None)
+        # 32 charachters are allowed
+        self.assertTrue(
+            check_uuid('dfceb5c587364faf95e17b3cbaf98d5f') is not None)
+        return
+
+
+class DuplicationErrorTests(unittest.TestCase):
+
+    def test_is_exception(self):
+        exc = DuplicationError('Some message')
+        self.assertEqual(exc.msg, 'Some message')
+        self.assertEqual(exc.entries, [])
+
+    def test_as_string(self):
+        exc = DuplicationError('Some message')
+        self.assertEqual(exc.__str__(), "'Some message'")
+
+
+class RoleSourceTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(RoleSourceTests, self).setUp()
+        # Register a role
+        class SomeRole(grok.Role):
+            grok.name('waeup.testrole')
+            grok.title('RoleWith.DotName')
+            grok.permissions('waeup.Public')
+        # Register a role not visible to waeup portal as its name does
+        # not start with 'waeup.'
+        class NonIkobaRole(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('NonIkobaRole', NonIkobaRole)
+        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.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_maintenance.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_maintenance.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_maintenance.py	(revision 12677)
@@ -0,0 +1,130 @@
+import shutil
+import tempfile
+from grokcore.site.interfaces import IUtilityInstaller
+from zope import schema
+from zope.catalog.catalog import Catalog
+from zope.catalog.field import FieldIndex
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.interface import Interface, implements
+from waeup.ikoba.app import Company
+from waeup.ikoba.maintenance import update_catalog
+from waeup.ikoba.testing import FunctionalTestCase, FunctionalLayer
+
+class ISampleContent(Interface):
+
+    age = schema.Int(
+        title=u'Age')
+    name = schema.TextLine(
+        title=u'Name')
+
+class SampleContent(object):
+    implements(ISampleContent)
+
+    def __init__(self, age=None, name=None):
+        self.age = age
+        self.name = name
+        return
+
+    def __eq__(self, ob):
+        # make sample contents comparable
+        if not hasattr(ob, 'name') or not hasattr(ob, 'age'):
+            return False
+        return self.age == ob.age and self.name == ob.name
+
+class UpdateCatalogTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(UpdateCatalogTests, self).setUp()
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        super(UpdateCatalogTests, self).tearDown()
+        shutil.rmtree(self.dc_root)
+        return
+
+    def create_catalog(self):
+        # install catalog in site
+        self.catalog = Catalog()
+        install = getUtility(IUtilityInstaller)
+        install(self.app, self.catalog, ICatalog, 'my_catalog')
+        self.create_indexes()
+        self.catalog.clear() # make sure cat is empty
+        return
+
+    def create_indexes(self):
+        # create 'age' and 'name' field index in cat if they do not
+        # exist already.
+        for name in ('age', 'name'):
+            if name in self.catalog.keys():
+                continue
+            self.catalog[name] = FieldIndex(
+                field_name=name, interface=ISampleContent,
+                field_callable=False)
+        return
+
+    def add_sample_content(self):
+        # Add sample content in ZODB
+        manfred = SampleContent(42, 'Manfred')
+        self.app['manfred'] = manfred
+        self.manfred = self.app['manfred'] # this one has __parent__ etc.
+        return
+
+    def test_update_catalog_by_list(self):
+        ## We can catalog new objects passed in as list
+        # create object in ZODB before catalog exists
+        self.add_sample_content()
+        self.create_catalog()
+
+        # right now we can't find manfred
+        result1 = list(self.catalog.searchResults(age=(40,43)))
+        self.assertEqual(len(result1), 0)
+
+        # update catalog. We have to feed 'located' objects, which is
+        # normally the case for objects from ZODB. In our case
+        # 'manfred' could not be catalogued while the located version
+        # 'manfred_located' (with a __parent__ and __name__ attribute)
+        # can.
+        update_catalog(self.app, 'my_catalog', [self.manfred])
+
+        # now we can find manfred in catalog
+        result2 = list(self.catalog.searchResults(age=(40,43)))
+        self.assertEqual(result2[0], self.manfred)
+        return
+
+    def test_update_catalog_iterator(self):
+        ## We can catalog new objects via function
+        # create object in ZODB before catalog exists
+        self.add_sample_content()
+        self.create_catalog()
+
+        # right now we can't find manfred
+        result1 = list(self.catalog.searchResults(age=(40,43)))
+        self.assertEqual(len(result1), 0)
+
+        # update catalog. We have to feed 'located' objects, which is
+        # normally the case for objects from ZODB. In our case
+        # 'manfred' could not be catalogued while the located version
+        # 'manfred_located' (with a __parent__ and __name__ attribute)
+        # can.
+        def func():
+            for x in [self.manfred]:
+                yield x
+        update_catalog(self.app, 'my_catalog', func=func)
+
+        # now we can find manfred in catalog
+        result2 = list(self.catalog.searchResults(age=(40,43)))
+        self.assertEqual(result2[0], self.manfred)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_objecthistory.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_objecthistory.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_objecthistory.py	(revision 12677)
@@ -0,0 +1,134 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Test object history.
+"""
+import grok
+import shutil
+import tempfile
+from persistent.list import PersistentList
+from zope.interface.verify import verifyObject, verifyClass
+from zope.security.management import newInteraction, endInteraction
+from zope.security.testing import Principal, Participation
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import IObjectHistory, IIkobaObject
+from waeup.ikoba.testing import FunctionalTestCase, FunctionalLayer
+from waeup.ikoba.objecthistory import ObjectHistory
+
+class SampleObject(grok.Model):
+    grok.implements(IIkobaObject)
+    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 = Company()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.obj = SampleObject()
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(ObjectHistoryTests, self).tearDown()
+        endInteraction() # Just in case, one is still lingering around
+
+    def test_iface(self):
+        # ObjectHistory class and instances provide the promised ifaces
+        hist = IObjectHistory(self.obj)
+        assert verifyObject(IObjectHistory, hist)
+        assert verifyClass(IObjectHistory, ObjectHistory)
+
+    def test_adapter(self):
+        # We can get a history by adapting to IObjectHistory
+        result = IObjectHistory(self.obj)
+        assert isinstance(result, ObjectHistory)
+
+    def test_add_messages(self):
+        # We can add a message
+        hist = IObjectHistory(self.obj)
+        assert hist.messages == []
+        hist.addMessage('blah')
+        assert 'blah' in ''.join(hist.messages)
+
+    def test_add_messages_timestamp_and_user(self):
+        # Messages added get a timestamp and the current user
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('blah')
+        result = ''.join(hist.messages)
+        self.assertMatches('<YYYY-MM-DD hh:mm:ss> UTC - blah by system', result)
+
+    def test_add_messages_existing_principal(self):
+        principal = Principal('bob')
+        principal.title = 'Bob'
+        newInteraction(Participation(principal)) # set current user
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('blah')
+        result = ''.join(hist.messages)
+        self.assertMatches('<YYYY-MM-DD hh:mm:ss> UTC - blah by Bob', result)
+
+    def test_modify_messages(self):
+        principal = Principal('bob')
+        principal.title = 'Bob'
+        newInteraction(Participation(principal)) # set current user
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('blah')
+        hist.modifyMessages('blah', 'blow')
+        result = ''.join(hist.messages)
+        self.assertMatches('<YYYY-MM-DD hh:mm:ss> UTC - blow by Bob', result)
+
+    def test_remove_message(self):
+        principal = Principal('bob')
+        principal.title = 'Bob'
+        newInteraction(Participation(principal)) # set current user
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('blah')
+        hist.addMessage('blow')
+        self.assertEqual(len(hist._getMessages()), 2)
+        result = ' '.join(hist.messages)
+        self.assertTrue('blah by Bob' in result)
+        self.assertTrue('blow by Bob' in result)
+        success, text = hist.removeMessage('xyz')
+        self.assertFalse(success)
+        self.assertEqual(text, 'Not a number')
+        success, text = hist.removeMessage(100)
+        self.assertFalse(success)
+        self.assertEqual(text, 'Number out of range')
+        success, text = hist.removeMessage(1)
+        self.assertTrue(success)
+        self.assertMatches('<YYYY-MM-DD hh:mm:ss> UTC - blow by Bob', text)
+        self.assertEqual(len(hist.messages), 1)
+        result = ' '.join(hist.messages)
+        self.assertFalse('blow by Bob' in result)
+
+    def test_messages(self):
+        # we get messages as a persistent list of strings
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('foo')
+        hist.addMessage('bar')
+        assert isinstance(hist.messages, PersistentList)
+        assert 'foo' in hist.messages[0]
+        assert 'bar' in hist.messages[1]
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_reports.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_reports.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_reports.py	(revision 12677)
@@ -0,0 +1,445 @@
+import datetime
+import grok
+import logging
+import pytz
+import unittest
+from cStringIO import StringIO
+from grok.interfaces import IContainer
+from zc.async.interfaces import IJob
+from zc.async.testing import wait_for_result
+from zope.component import getGlobalSiteManager, getUtility
+from zope.component.hooks import setSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.interfaces import IJobManager, IIkobaPluggable
+from waeup.ikoba.reports import (
+    IReport, IReportGenerator, IReportJob, IReportJobContainer,
+    IReportsContainer,)
+from waeup.ikoba.reports import (
+    Report, ReportGenerator, get_generators, report_job, AsyncReportJob,
+    ReportJobContainer, ReportsContainer, ReportsContainerPlugin)
+from waeup.ikoba.testing import FakeJob, FakeJobManager, FunctionalLayer
+from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
+
+class FakeReportGenerator(ReportGenerator):
+    # fake report generator for tests.
+
+    def __init__(self, name=None, perm_create=None, perm_view=None):
+        self.title = 'Report 1'
+        self.perm_create = perm_create
+        self.perm_view = perm_view
+        return
+
+    def __eq__(self, obj):
+        if getattr(obj, 'title', None) is None:
+            return False
+        return self.title == obj.title
+
+    def generate(self, site, args=[], kw={}):
+        result = Report()
+        result.args = args
+        result.kw = kw
+        return result
+
+class ReportTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure we fullfill the promised contracts
+        obj = Report()
+        verifyClass(IReport, Report)
+        verifyObject(IReport, obj)
+        return
+
+    def test_creation_dt(self):
+        # a report sets a datetime timestamp when created
+        report = Report()
+        self.assertTrue(hasattr(report, 'creation_dt'))
+        self.assertTrue(isinstance(report.creation_dt, datetime.datetime))
+        # the datetime is set with UTC timezone info
+        self.assertEqual(report.creation_dt.tzinfo, pytz.utc)
+        return
+
+    def test_args(self):
+        # a report stores the args and kw used for generation
+        report1 = Report()
+        report2 = Report(args=[1, 2], kwargs=dict(a=1, b=2))
+        self.assertEqual(report1.args, [])
+        self.assertEqual(report1.kwargs, dict())
+        self.assertEqual(report2.args, [1, 2])
+        self.assertEqual(report2.kwargs, dict(a=1, b=2))
+        return
+
+    def test_create_pdf(self):
+        # trying to create a pdf results in an error
+        report = Report()
+        self.assertRaises(NotImplementedError, report.create_pdf)
+        return
+
+class ReportGeneratorTest(unittest.TestCase):
+    def setUp(self):
+        grok.testing.grok('waeup.ikoba.reports') # register utils
+
+    def test_iface(self):
+        # make sure we fullfill the promised contracts
+        obj = ReportGenerator()
+        verifyClass(IReportGenerator, ReportGenerator)
+        verifyObject(IReportGenerator, obj)
+        return
+
+    def test_generate(self):
+        # the base report generator delivers reports
+        obj = ReportGenerator()
+        result = obj.generate(None)
+        self.assertTrue(IReport.providedBy(result))
+        return
+
+class GeneratorRegistrar(object):
+    # Mix-in providing report generator registrations
+    def setUp(self):
+        self.registered = []
+
+    def tearDown(self):
+        # unregister any previously registered report types
+        gsm = getGlobalSiteManager()
+        for report, name in self.registered:
+            gsm.unregisterUtility(report, IReportGenerator, name=name)
+        return
+
+    def register_generator(self, name, perm_create=None, perm_view=None):
+        # helper to register report generators as utils
+        gsm = getGlobalSiteManager()
+        generator = FakeReportGenerator(name)
+        gsm.registerUtility(generator, provided=IReportGenerator, name=name)
+        self.registered.append((generator, name),)
+        return generator
+
+class HelpersTests(GeneratorRegistrar, unittest.TestCase):
+    # Tests for helper functions
+
+    def setUp(self):
+        grok.testing.grok('waeup.ikoba.reports') # register utils
+        super(HelpersTests, self).setUp()
+        return
+
+    def test_get_generators_none(self):
+        # we get no generators if none was registered
+        result = list(get_generators())
+        self.assertEqual(result, [])
+        return
+
+    def test_get_generators_simple(self):
+        # we get a single generator if one is registered
+        self.register_generator('report1')
+        result = list(get_generators())
+        self.assertEqual(
+            result, [('report1', FakeReportGenerator('report1'))])
+        return
+
+    def test_get_generators_multiple(self):
+        # we also get multiple generators if available
+        self.register_generator('report1')
+        self.register_generator('report2')
+        result = list(get_generators())
+        self.assertEqual(
+            result,
+            [(u'report1', FakeReportGenerator('report1')),
+             (u'report2', FakeReportGenerator('report2'))])
+        return
+
+
+class ReportJobTests(FunctionalAsyncTestCase, GeneratorRegistrar):
+    # Test asynchronous report functionality (simple cases)
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ReportJobTests, self).setUp()
+        GeneratorRegistrar.setUp(self)
+        self.root_folder = self.getRootFolder()
+
+    def test_report_job_func(self):
+        # the report_job func really creates reports...
+        self.register_generator('report1')
+        report = report_job(None, 'report1')
+        self.assertTrue(IReport.providedBy(report))
+        self.assertTrue(isinstance(report, Report))
+
+    def test_report_job_interfaces(self):
+        # the AsyncReportJob implements promised interfaces correctly...
+        job = AsyncReportJob(None, None)
+        verifyClass(IJob, AsyncReportJob)
+        verifyObject(IJob, job)
+        verifyClass(IReportJob, AsyncReportJob)
+        verifyObject(IReportJob, job)
+        return
+
+    def test_finished(self):
+        # AsyncReportJobs signal with a bool whether they`re  finished
+        job = AsyncReportJob(self.root_folder, None)
+        setSite(self.root_folder)
+        self.assertEqual(job.finished, False)
+        manager = getUtility(IJobManager)
+        manager.put(job)
+        wait_for_result(job)
+        self.assertEqual(job.finished, True)
+        return
+
+    def test_failed_true(self):
+        # We can test whether a job failed
+        job = AsyncReportJob(self.root_folder, None) # no report generator
+        setSite(self.root_folder)
+        # while a job is not finished, `failed` is ``None``
+        self.assertTrue(job.failed is None)
+        manager = getUtility(IJobManager)
+        manager.put(job)
+        wait_for_result(job)
+        # the finished job failed
+        self.assertEqual(job.failed, True)
+        return
+
+    def test_failed_false(self):
+        # We can test whether a job failed
+        self.register_generator('report1')
+        job = AsyncReportJob(self.root_folder, 'report1')
+        setSite(self.root_folder)
+        # while a job is not finished, `failed` is ``None``
+        self.assertTrue(job.failed is None)
+        manager = getUtility(IJobManager)
+        manager.put(job)
+        wait_for_result(job)
+        # the finished job failed
+        self.assertEqual(job.failed, False)
+        return
+
+class FakeJobWithResult(FakeJob):
+
+    def __init__(self, args=[], kw={}):
+        self.result = Report()
+        self.result.args = args
+        self.result.kw = kw
+        return
+
+class ReportJobContainerTests(unittest.TestCase):
+    # Test ReportJobContainer
+
+    def setUp(self):
+        # register a suitable ICSVExporter as named utility
+        self.generator = FakeReportGenerator('report1')
+        self.job_manager = FakeJobManager()
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(
+            self.generator, IReportGenerator, name='report1')
+        self.gsm.registerUtility(
+            self.job_manager, IJobManager)
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(
+            self.generator, IReportGenerator, name='report1')
+        self.gsm.unregisterUtility(self.job_manager, IJobManager)
+
+    def test_report_job_interfaces(self):
+        # the ExportJobContainer implements promised interfaces correctly...
+        container = ReportJobContainer()
+        verifyClass(IReportJobContainer, ReportJobContainer)
+        verifyObject(IReportJobContainer, container)
+        return
+
+    def test_start_report_job(self):
+        # we can start jobs
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        result = self.job_manager._jobs.values()[0]
+        self.assertTrue(IJob.providedBy(result))
+        self.assertEqual(
+            container.running_report_jobs,
+            [('1', 'report3', 'bob')]
+            )
+        return
+
+    def test_get_running_report_jobs_all(self):
+        # we can get report jobs of all users
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        container.start_report_job('report3', 'alice')
+        result = container.get_running_report_jobs()
+        self.assertEqual(
+            result,
+            [('1', 'report3', 'bob'),
+             ('2', 'report3', 'alice')]
+            )
+        return
+
+    def test_get_running_report_jobs_user(self):
+        # we can get the report jobs running for a certain user
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        container.start_report_job('report3', 'alice')
+        result1 = container.get_running_report_jobs(user_id='alice')
+        result2 = container.get_running_report_jobs(user_id='foo')
+        self.assertEqual(
+            result1, [('2', 'report3', 'alice')])
+        self.assertEqual(
+            result2, [])
+        return
+
+    def test_get_running_report_jobs_only_if_exist(self):
+        # we get only jobs that are accessible through the job manager...
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        container.start_report_job('report3', 'bob')
+        self.assertTrue(
+            ('2', 'report3', 'bob') in container.running_report_jobs)
+        # we remove the second entry from job manager
+        del self.job_manager._jobs['2']
+        result = container.get_running_report_jobs(user_id='bob')
+        self.assertEqual(
+            result, [('1', 'report3', 'bob')])
+        self.assertTrue(
+            ('2', 'report3', 'bob') not in container.running_report_jobs)
+        return
+
+    def test_get_report_job_status(self):
+        # we can get the stati of jobs...
+        container = ReportJobContainer()
+        container.start_report_job('report1', 'alice')
+        container.start_report_job('report1', 'bob')
+        container.start_report_job('report1', 'bob')
+        result = container.get_report_jobs_status(user_id='bob')
+        # we'll get the raw value, a translation and the title of the
+        # exporter
+        self.assertEqual(
+            result,
+            [('new', u'new', u'Report 1'),
+             ('completed', u'completed', u'Report 1')]
+            )
+        return
+
+    def test_delete_report_entry(self):
+        # we can remove report entries in local lists and the job
+        # manager as well...
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        entry = container.running_report_jobs[0]
+        container.delete_report_entry(entry)
+        # both, running_report_jobs list and job manager are empty now
+        self.assertEqual(
+            container.running_report_jobs, [])
+        self.assertEqual(
+            self.job_manager._jobs, {})
+        return
+
+    def test_report_entry_from_job_id(self):
+        # we can get an report entry for a job_id if the id exists
+        container = ReportJobContainer()
+        entry = ('4', 'report3', 'bob')
+        container.running_report_jobs = [entry]
+        fake_job = FakeJobWithResult()
+        self.job_manager._jobs['4'] = fake_job
+        result1 = container.report_entry_from_job_id(None)
+        result2 = container.report_entry_from_job_id('4')
+        result3 = container.report_entry_from_job_id('23')
+        self.assertEqual(result1, None)
+        self.assertEqual(result2, ('4', 'report3', 'bob'))
+        self.assertEqual(result3, None)
+        return
+
+
+class ReportsContainerTests(unittest.TestCase):
+    # Tests for ReportsContainer
+
+    def test_iface(self):
+        # ReportsContainers really provide the promised interfaces
+        obj = ReportsContainer()
+        verifyClass(IReportsContainer, ReportsContainer)
+        verifyClass(IContainer, ReportsContainer)
+        verifyObject(IReportsContainer, obj)
+        verifyObject(IContainer, obj)
+        return
+
+class ReportsContainerPluginTests(unittest.TestCase):
+    # Tests for ReportsContainerPlugin
+
+    def create_logger(self):
+        # create a logger suitable for local tests.
+        test_logger = logging.getLogger('waeup.ikoba.reports.testlogger')
+        log = StringIO()
+        handler = logging.StreamHandler(log)
+        handler.setLevel(logging.DEBUG)
+        test_logger.addHandler(handler)
+        test_logger.setLevel(logging.DEBUG)
+        self.logger = test_logger
+        self.log = log
+        self.handler = handler
+        return self.logger
+
+    def remove_logger(self):
+        del self.handler
+        del self.logger
+        del self.log
+        pass
+
+    def get_log(self):
+        self.log.seek(0)
+        return self.log.read()
+
+    def setUp(self):
+        self.create_logger()
+        return
+
+    def tearDown(self):
+        self.remove_logger()
+        return
+
+    def test_iface(self):
+        # make sure we fullfill the promised interfaces
+        obj = ReportsContainerPlugin()
+        verifyClass(IIkobaPluggable, ReportsContainerPlugin)
+        verifyObject(IIkobaPluggable, obj)
+        return
+
+    def test_get_as_utility(self):
+        # make sure we can get the plugin as utility
+        grok.testing.grok('waeup.ikoba.reports')
+        util = getUtility(IIkobaPluggable, name='reports')
+        self.assertTrue(util is not None)
+        return
+
+    def test_update_no_container(self):
+        # we can update an existing site
+        fake_site = grok.Container()
+        plugin = ReportsContainerPlugin()
+        plugin.update(fake_site, 'app', self.logger)
+        log = self.get_log()
+        self.assertEqual(
+            log, 'Added reports container for site "app"\n')
+        self.assertTrue('reports' in fake_site.keys())
+        self.assertTrue(IReportsContainer.providedBy(fake_site['reports']))
+        return
+
+    def test_update_uptodate_site(self):
+        # we leave already existing reports containers in place
+        fake_site = grok.Container()
+        plugin = ReportsContainerPlugin()
+        fake_site['reports'] = ReportsContainer()
+        plugin.update(fake_site, 'app', self.logger)
+        log = self.get_log()
+        self.assertEqual(log, '') # no log message
+        return
+
+    def test_setup_new_site(self):
+        # if we setup a site, we always install a fresh reports container
+        fake_site = grok.Container()
+        plugin = ReportsContainerPlugin()
+        plugin.setup(fake_site, 'app', self.logger)
+        log1 = self.get_log()
+        result1 = fake_site.get('reports', None)
+        plugin.setup(fake_site, 'app', self.logger) # replace old container
+        log2 = self.get_log()
+        result2 = fake_site.get('reports', None)
+        self.assertTrue(result1 is not result2)
+        self.assertEqual(log1,
+                         'Added reports container for site "app"\n')
+        self.assertEqual(log2,
+                         'Added reports container for site "app"\n'
+                         'Removed reports container for site "app"\n'
+                         'Added reports container for site "app"\n')
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_smtp.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_smtp.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_smtp.py	(revision 12677)
@@ -0,0 +1,414 @@
+# -*- coding: utf-8 -*-
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+# Tests for email-related components
+import base64
+import logging
+import tempfile
+import shutil
+import unittest
+from StringIO import StringIO
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.sendmail.interfaces import IMailDelivery
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import IMailService
+from waeup.ikoba.smtp import (
+    encode_header_item, encode_address, encode_body, FakeSMTPDelivery,
+    send_mail)
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+#
+# SMTP test config
+#
+
+# Also run tests that send mail to external servers?
+#   If you enable this, please make sure the external smtp hosts and receivers
+#   do exist really and are not bothered by being spammed by a test programme.
+EXTERNAL_MAIL_TESTS = False
+
+# Maybe existing receiver of externally sent mail. If this mail account
+# exists really, it might receive mail from the tests!
+EXTERNAL_MAIL_RECEIVER = 'no-reply@waeup.org'
+
+# Names of mail deliveries to use when external mail tests are enabled.
+#   See local mail.zcml for names of available deliveries.
+EXTERNAL_DIRECT_DELIVERY = 'Direct SMTP on localhost'
+EXTERNAL_QUEUED_DELIVERY = 'Queued SMTP on localhost'
+
+#
+# end of SMTP test config
+#
+
+
+def external_mail_test(func):
+    if not EXTERNAL_MAIL_TESTS:
+        myself = __file__
+        if myself.endswith('.pyc'):
+            myself = myself[:-2]
+        print "WARNING: external mail tests are skipped!"
+        print "WARNING: edit %s to enable them." % myself
+        return
+    return func
+
+
+class HelperTests(unittest.TestCase):
+
+    def test_encode_header_item(self):
+        # encoding header items works with strings and unicodes, as
+        # long as we pass in utf-8 encoded stuff.
+        result1 = encode_header_item(u'Plain Name'.encode('utf-8'))
+        result2 = encode_header_item(u'Name with umläut'.encode('utf-8'))
+        result3 = encode_header_item(u'Plain Name')
+        result4 = encode_header_item(u'Name with umläut')
+        result5 = encode_header_item(None)
+        self.assertEqual(result1, u'Plain Name')
+        self.assertEqual(result2, u'=?iso-8859-1?q?Name_with_uml=E4ut?=')
+        self.assertEqual(result3, u'Plain Name')
+        self.assertEqual(result4, u'=?iso-8859-1?q?Name_with_uml=E4ut?=')
+        self.assertTrue(result5 is None)
+        return
+
+    def test_encode_address(self):
+        # we can correctly encode address parts
+        result1 = encode_address('foo@bar.baz')
+        result2 = encode_address(u'foo@bar.baz', 'The Foo')
+        result3 = encode_address('foo@bar.baz', u'The Foo')
+        result4 = encode_address('foo@bar.baz', u'With Umläut')
+        self.assertEqual(result1, 'foo@bar.baz')
+        self.assertEqual(result2, 'The Foo <foo@bar.baz>')
+        self.assertEqual(result3, 'The Foo <foo@bar.baz>')
+        self.assertEqual(result4,
+                         '=?iso-8859-1?q?With_Uml=E4ut?= <foo@bar.baz>')
+        return
+
+    def test_encode_body(self):
+        result1 = encode_body(u'Simple Text')
+        self.assertEqual(
+            str(result1).split('\n')[1:],
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="us-ascii"',
+             'Content-Transfer-Encoding: 7bit',
+             '',
+             'Simple Text'])
+        return
+
+    def test_encode_body_latin1_string(self):
+        # utf-8 encoded byte streams are transferred correctly when
+        # they contain chars unrepresentable in ascii but available in latin1
+        text = u'Simple text with Ümläut'.encode('utf-8')
+        result1 = encode_body(text)
+        result1 = str(result1).split('\n')[1:]
+        self.assertEqual(
+            result1,
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="iso-8859-1"',
+             'Content-Transfer-Encoding: quoted-printable',
+             '',
+             'Simple text with =DCml=E4ut'])
+        return
+
+    def test_encode_body_latin1_unicode(self):
+        # unicode strings are transferred correctly when they contain
+        # chars unrepresentable in ascii but available in latin1
+        text = u'Simple text with Ümläut'
+        result1 = encode_body(text)
+        result1 = str(result1).split('\n')[1:]
+        self.assertEqual(
+            result1,
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="iso-8859-1"',
+             'Content-Transfer-Encoding: quoted-printable',
+             '',
+             'Simple text with =DCml=E4ut'])
+        return
+
+    def test_encode_body_utf8_string(self):
+        # utf-8 encoded byte streams are transferred correctly when
+        # they contain chars unrepresentable in ascii but available in latin1
+        text = u"Simple text with ü and capital pi: " + unichr(0x3a0)
+        text = text.encode('utf-8')  # turn unicode into byte stream
+        result1 = encode_body(text)
+        result1 = str(result1).split('\n')[1:]
+        self.assertEqual(
+            result1,
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="utf-8"',
+             'Content-Transfer-Encoding: base64',
+             '',
+             'U2ltcGxlIHRleHQgd2l0aCDDvCBhbmQgY2FwaXRhbCBwaTogzqA=', ''])
+        self.assertEqual(
+            base64.b64decode(result1[-2]).decode('utf-8'),
+            u"Simple text with ü and capital pi: " + unichr(0x3a0))
+        return
+
+    def test_encode_body_utf8_unicode(self):
+        # utf-8 encoded byte streams are transferred correctly when
+        # they contain chars unrepresentable in latin1
+        text = u"Simple text with ü and capital pi: " + unichr(0x3a0)
+        result1 = encode_body(text)
+        result1 = str(result1).split('\n')[1:]
+        self.assertEqual(
+            result1,
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="utf-8"',
+             'Content-Transfer-Encoding: base64',
+             '',
+             'U2ltcGxlIHRleHQgd2l0aCDDvCBhbmQgY2FwaXRhbCBwaTogzqA=', ''])
+        self.assertEqual(
+            base64.b64decode(result1[-2]).decode('utf-8'),
+            u"Simple text with ü and capital pi: " + unichr(0x3a0))
+        return
+
+
+class FunctionalMailerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FunctionalMailerTests, self).setUp()
+        self.setup_logging()
+        return
+
+    def tearDown(self):
+        super(FunctionalMailerTests, self).tearDown()
+        self.teardown_logging()
+        return
+
+    def setup_logging(self):
+        # setup a log-handler that catches all fake mailer output
+        self.stream = StringIO()
+        handler = logging.StreamHandler(self.stream)
+        logger = logging.getLogger('test.smtp')
+        logger.addHandler(handler)
+        logger.setLevel(logging.INFO)
+        return
+
+    def get_fake_smtp_output(self):
+        # get output generated by fake mailer
+        self.stream.flush()
+        self.stream.seek(0)
+        return self.stream.read()
+
+    def teardown_logging(self):
+        # remove the log handler for fake mailer output
+        logger = logging.getLogger('test.smtp')
+        handlers = [x for x in logger.handlers]
+        for handler in handlers:
+            logger.removeHandler(handler)
+        return
+
+    def test_get_fake_mailer(self):
+        # we can get the fake mailer if we want
+        mailer = getUtility(IMailDelivery, name='No email service')
+        self.assertTrue(isinstance(mailer, FakeSMTPDelivery))
+        return
+
+    def test_get_default_mailer(self):
+        # we can get a default mailer if we want
+        mailer = getUtility(IMailService)
+        self.assertTrue(isinstance(mailer(), FakeSMTPDelivery))
+        return
+
+    def test_send_mail(self):
+        # we can really send mail.
+        mail_id = send_mail(
+            u'A sender', u'sender@example.com',
+            u'A recipient', u'recpt@example.com',
+            u'A subject',
+            u'This is a test mail.')
+        self.assertEqual(mail_id, 'fake-message-id@example.com')
+        self.assertEqual(
+            self.get_fake_smtp_output().split('\n'),
+            [u'Sending email from no-reply@waeup.org to '
+             u'recpt@example.com, sender@example.com:',
+             u'Message:',
+             u'msg: MIME-Version: 1.0',
+             u'msg: Content-Type: text/plain; charset="us-ascii"',
+             u'msg: Content-Transfer-Encoding: 7bit',
+             u'msg: From: A sender <no-reply@waeup.org>',
+             u'msg: To: A recipient <recpt@example.com>',
+             # u'msg: Cc: A sender <sender@example.com>',
+             u'msg: Reply-To: A sender <sender@example.com>',
+             u'msg: Subject: A subject',
+             u'msg: ',
+             u'msg: This is a test mail.',
+             u'']
+            )
+        return
+
+    def test_send_mail_with_cc(self):
+        # with cc=True the message will be CCed to the from address
+        mail_id = send_mail(
+            u'A sender', u'sender@example.com',
+            u'A recipient', u'recpt@example.com',
+            u'A subject',
+            u'This is a test mail.',
+            cc=True)
+        self.assertEqual(mail_id, 'fake-message-id@example.com')
+        self.assertEqual(
+            self.get_fake_smtp_output().split('\n'),
+            [u'Sending email from no-reply@waeup.org to '
+             u'recpt@example.com, sender@example.com:',
+             u'Message:',
+             u'msg: MIME-Version: 1.0',
+             u'msg: Content-Type: text/plain; charset="us-ascii"',
+             u'msg: Content-Transfer-Encoding: 7bit',
+             u'msg: From: A sender <no-reply@waeup.org>',
+             u'msg: To: A recipient <recpt@example.com>',
+             u'msg: Cc: A sender <sender@example.com>',
+             u'msg: Reply-To: A sender <sender@example.com>',
+             u'msg: Subject: A subject',
+             u'msg: ',
+             u'msg: This is a test mail.',
+             u'']
+            )
+        return
+
+    def test_send_mail_utf8_strings(self):
+        # we can send mail with utf-8 encoded strings as input
+        mail_id = send_mail(
+            u'A sender', u'sender@example.com',
+            u'A recipient', u'recpt@example.com',
+            u'A subject',
+            u'This is a test mail with ümläut.'.encode('utf-8'))
+        self.assertEqual(mail_id, 'fake-message-id@example.com')
+        self.assertEqual(
+            self.get_fake_smtp_output().split('\n'),
+            [u'Sending email from no-reply@waeup.org '
+             u'to recpt@example.com, sender@example.com:',
+             u'Message:', u'msg: MIME-Version: 1.0',
+             u'msg: Content-Type: text/plain; charset="iso-8859-1"',
+             u'msg: Content-Transfer-Encoding: quoted-printable',
+             u'msg: From: A sender <no-reply@waeup.org>',
+             u'msg: To: A recipient <recpt@example.com>',
+             # u'msg: Cc: A sender <sender@example.com>',
+             u'msg: Reply-To: A sender <sender@example.com>',
+             u'msg: Subject: A subject',
+             u'msg: ',
+             u'msg: This is a test mail with =FCml=E4ut.',
+             u'']
+            )
+        return
+
+    def test_send_mail_utf8_unicode(self):
+        # we can send mail with utf-8 encoded unicode as input
+        mail_id = send_mail(
+            u'A sender', u'sender@example.com',
+            u'A recipient', u'recpt@example.com',
+            u'A subject',
+            u'This is a test mail with ümläut.')
+        self.assertEqual(mail_id, 'fake-message-id@example.com')
+        self.assertEqual(
+            self.get_fake_smtp_output().split('\n'),
+            [u'Sending email from no-reply@waeup.org '
+             u'to recpt@example.com, sender@example.com:',
+             u'Message:', u'msg: MIME-Version: 1.0',
+             u'msg: Content-Type: text/plain; charset="iso-8859-1"',
+             u'msg: Content-Transfer-Encoding: quoted-printable',
+             u'msg: From: A sender <no-reply@waeup.org>',
+             u'msg: To: A recipient <recpt@example.com>',
+             # u'msg: Cc: A sender <sender@example.com>',
+             u'msg: Reply-To: A sender <sender@example.com>',
+             u'msg: Subject: A subject',
+             u'msg: ',
+             u'msg: This is a test mail with =FCml=E4ut.',
+             u'']
+            )
+        return
+
+
+class ExternalMailerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ExternalMailerTests, self).setUp()
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        return
+
+    def tearDown(self):
+        super(ExternalMailerTests, self).tearDown()
+        shutil.rmtree(self.dc_root)
+        return
+
+    def test_config_default_mailer(self):
+        # The default mailer set in config is 'no mailer'.
+        self.assertEqual(
+            getattr(self.app.get('configuration', None), 'smtp_mailer'),
+            u'No email service')
+        return
+
+    @external_mail_test
+    def test_send_direct_mail(self):
+        # send mail using direct mail delivery
+        self.app['configuration'].smtp_mailer = EXTERNAL_DIRECT_DELIVERY
+        setSite(self.app)
+        result = send_mail(
+            'test program', 'no-reply@waeup.org',
+            'test mail receiver', EXTERNAL_MAIL_RECEIVER,
+            'Test Mail from WAeUP Ikoba',
+            'Hi from test mailer!\n\nRegards,\nTest Programme\n'
+            )
+        import transaction
+        transaction.commit()  # The mail is really sent when transactions is
+                              # committed
+        self.assertTrue('@' in result)
+        return
+
+    @external_mail_test
+    def test_send_direct_mails(self):
+        # send several mails using direct mail delivery
+        self.app['configuration'].smtp_mailer = EXTERNAL_DIRECT_DELIVERY
+        setSite(self.app)
+        result = send_mail(
+            'test program', 'no-reply@waeup.org',
+            'test mail receiver',
+            '%s, %s' % (EXTERNAL_MAIL_RECEIVER, EXTERNAL_MAIL_RECEIVER),
+            'Test Mail from WAeUP Ikoba',
+            'Hi from test mailer!\n\nRegards,\nTest Programme\n'
+            )
+        import transaction
+        transaction.commit()  # The mail is really sent when transactions is
+                              # committed
+        self.assertTrue('@' in result)
+        return
+
+    @external_mail_test
+    def test_send_queued_mail(self):
+        # send mail using queued mail delivery
+        self.app['configuration'].smtp_mailer = EXTERNAL_QUEUED_DELIVERY
+        setSite(self.app)
+        result = send_mail(
+            'test program', 'no-reply@waeup.org',
+            'test mail receiver', EXTERNAL_MAIL_RECEIVER,
+            'Test Mail from WAeUP Ikoba',
+            'Hi from test mailer!\n\nRegards,\nTest Programme\n'
+            )
+        import transaction
+        transaction.commit()  # The mail is really sent when transactions is
+                              # committed
+        self.assertTrue('@' in result)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_sourcefactory.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_sourcefactory.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_sourcefactory.py	(revision 12677)
@@ -0,0 +1,61 @@
+## Tests for waeup.ikoba.sourcefactory.
+import unittest
+from waeup.ikoba.sourcefactory import SmartBasicContextualSourceFactory
+
+class MyContext(object):
+    # a sample context with min and max values
+    minimum = 6
+    maximum = 8
+
+class MyContextualSource(SmartBasicContextualSourceFactory):
+    # a contextual sample source containing all values from context
+    def __init__(self, defaults):
+        super(MyContextualSource, self).__init__()
+        self.defaults = defaults
+
+    def getValues(self, context):
+        # This tuple could become quite large. Bad for containment checks.
+        return self.defaults + tuple(
+            range(context.minimum, context.maximum + 1))
+
+class MySmartContextualSource(MyContextualSource):
+    # a smart contextual sample source that can check a value for
+    # containment fast, even if there are zillions of values in the
+    # source.
+    def contains(self, context, value):
+        if value in self.defaults:
+            return True
+        # The context max is excluded. So we know that this method was
+        # called.
+        if value >= context.minimum and value < context.maximum:
+            return True
+        return False
+
+class SmartBasicContextualSourceFactoryTests(unittest.TestCase):
+
+    def test_default_get_values(self):
+        # smart bcsfs behave like regular ones when asked for values.
+        default_values = (4, 5, 6)
+        source = MyContextualSource(default_values)(MyContext())
+        self.assertEqual(list(source), [4, 5, 6, 6, 7, 8])
+        return
+
+    def test_default_contains(self):
+        # smart bcsfs can behave like regular ones when checking
+        # containment.  The default method is very expensive as it
+        # scans each single value of the source.
+        default_values = (4, 5, 6)
+        source = MyContextualSource(default_values)(MyContext())
+        self.assertEqual(6 in source, True)
+        self.assertEqual(8 in source, True)
+        self.assertEqual(10 in source, False)
+        return
+
+    def test_smart_contains(self):
+        # smart bcfss support smarter containment checks.
+        default_values = (4, 5, 6)
+        source = MySmartContextualSource(default_values)(MyContext())
+        self.assertEqual(6 in source, True)
+        self.assertEqual(8 in source, False) # the special contains()
+        self.assertEqual(10 in source, False)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_startup.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_startup.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_startup.py	(revision 12677)
@@ -0,0 +1,76 @@
+# Tests for local startup functions.
+import os
+import shutil
+import tempfile
+import unittest
+from zope.app.wsgi import WSGIPublisherApplication
+from waeup.ikoba.startup import env_app_factory, env_debug_app_factory
+
+ZOPE_CONF_TEMPL = '''
+site-definition %s
+
+<zodb>
+  <mappingstorage />
+</zodb>
+
+<eventlog>
+  <logfile>
+    path STDOUT
+  </logfile>
+</eventlog>
+'''
+
+class StartUpTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.site_zcml = os.path.join(self.workdir, 'site.zcml')
+        open(self.site_zcml, 'w').write('<configure />')
+        self.zope_conf = os.path.join(self.workdir, 'zope.conf')
+        open(self.zope_conf, 'wb').write(ZOPE_CONF_TEMPL % self.site_zcml)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        for name in ('TEST_FOO', 'TEST_BAR'):
+            if name in os.environ:
+                del os.environ[name]
+        return
+
+    def test_env_app_factory(self):
+        # We can create plain WSGI apps
+        factory = env_app_factory({'zope_conf': self.zope_conf})
+        self.assertTrue(
+            isinstance(factory, WSGIPublisherApplication))
+        return
+
+    def test_env_app_factory_single_env_var(self):
+        # We can create WSGI apps with a single env var set
+        factory = env_app_factory({'zope_conf': self.zope_conf,
+                                   'env_vars': 'TEST_FOO value1'})
+        self.assertEqual(os.environ.get('TEST_FOO', None), 'value1')
+        return
+
+    def test_env_app_factory_multiple_env_vars(self):
+        # We can create WSGI apps with multiple env vars set
+        env_vars = 'TEST_FOO  value1   \n  TEST_BAR  value2'
+        factory = env_app_factory({'zope_conf': self.zope_conf,
+                                   'env_vars': env_vars})
+        self.assertEqual(os.environ.get('TEST_FOO', None), 'value1')
+        self.assertEqual(os.environ.get('TEST_BAR', None), 'value2')
+        return
+
+    def test_env_debug_app_factory(self):
+        # We can create debugger WSGI apps
+        factory = env_debug_app_factory({'zope_conf': self.zope_conf})
+        self.assertTrue(
+            isinstance(factory, WSGIPublisherApplication))
+        return
+        pass
+
+    def test_env_debug_app_factory_single_env_var(self):
+        # We can create debugger WSGI apps with a single env var set
+        factory = env_debug_app_factory({'zope_conf': self.zope_conf,
+                                         'env_vars': 'TEST_FOO value1'})
+        self.assertEqual(os.environ.get('TEST_FOO', None), 'value1')
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_waeup.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_waeup.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_waeup.py	(revision 12677)
@@ -0,0 +1,37 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test setup for the waeup.ikoba package.
+"""
+import grok
+import re
+import zope.component.eventtesting
+from zope.testing import renormalizing
+from waeup.ikoba.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.ikoba package
+test_suite = grok.testing.register_all_tests(
+    'waeup.ikoba', checker=checker, usetup=setUpZope,
+    layer=FunctionalLayer)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_zcml.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_zcml.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/test_zcml.py	(revision 12677)
@@ -0,0 +1,120 @@
+import os
+import tempfile
+import shutil
+import unittest
+from waeup.ikoba import tests
+from waeup.ikoba.interfaces import IDataCenterConfig, IPayPalConfig
+from zope.component import getUtility, queryUtility, getGlobalSiteManager
+from zope.configuration.exceptions import ConfigurationError
+from zope.configuration.xmlconfig import file as zcfile
+from zope.configuration.xmlconfig import string as zcstring
+
+
+def get_zcml_conf(xml_code):
+    """Get a working ZCML configuration file with `xml_code` embedded.
+
+    The returned ZCML declares an ikoba namespace for local ZCML
+    extensions and includes the `waeup.ikoba.meta` declarations before
+    embedding `xml_code`.
+
+    The returned string can be 'executed' with `string` or `file` from
+    `zope.configuration.xmlconfig.
+    """
+    config = (
+        '<configure '
+        '   xmlns="http://namespaces.zope.org/zope"'
+        '   xmlns:ikoba="http://namespaces.waeup.org/ikoba"'
+        '   i18n_domain="waeup.ikoba"'
+        '   package="waeup.ikoba.tests"'
+        '>'
+        '<include package="waeup.ikoba" file="meta.zcml" />'
+        ''
+        '%s'
+        ''
+        '</configure>'
+        ) % xml_code
+    return config
+
+
+class ZCMLTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        # unregister any remaining utils registered during tests
+        gsm = getGlobalSiteManager()
+        for iface in (IDataCenterConfig, IPayPalConfig):
+            conf = queryUtility(iface)
+            if conf is None:
+                continue
+            gsm.unregisterUtility(conf, iface)
+
+    def create_fake_paypal_configs(self):
+        # create two regular files in workdir
+        self.conf1_path = os.path.join(self.workdir, 'paypal1.conf')
+        self.conf2_path = os.path.join(self.workdir, 'paypal2.conf')
+        open(self.conf1_path, 'w').write('[rest-client]\n')
+        open(self.conf2_path, 'w').write('[rest-client]\n')
+
+    def test_datacenter_config_directive(self):
+        # the 'datacenter' directive can be used in ZCML
+        assert queryUtility(IDataCenterConfig) is None
+        zcfile("sample-datacenter.zcml", tests)  # 'execute' the ZCML
+        result = getUtility(IDataCenterConfig)
+        assert result == {'path': u'/some/path'}
+
+    def test_datacenter_config_singleton(self):
+        # we can define only one datacenter config
+        config = get_zcml_conf(
+            '<ikoba:datacenter path="/some/path" />'
+            '<ikoba:datacenter path="/other/path" />'
+            )
+        assert queryUtility(IDataCenterConfig) is None
+        self.assertRaises(
+            ConfigurationError, zcstring, config)
+
+    def test_paypal_config_directive(self):
+        # the 'paypalconf' ZCML directive works
+        assert queryUtility(IPayPalConfig) is None
+        self.create_fake_paypal_configs()
+        config = get_zcml_conf(
+            '<ikoba:paypalconf path="%s" />' % self.conf1_path
+            )
+        zcstring(config)  # execute ZCML
+        result = getUtility(IPayPalConfig)
+        assert result == {
+            'path': self.conf1_path,
+            'client_id': None,
+            'client_secret': None,
+            'mode': 'sandbox'
+            }
+
+    def test_paypal_config_singleton(self):
+        # we can define only one paypal config
+        assert queryUtility(IPayPalConfig) is None
+        self.create_fake_paypal_configs()
+        config = get_zcml_conf(
+            '<ikoba:paypalconf path="%s" />'
+            '<ikoba:paypalconf path="%s" />'
+            ) % (self.conf1_path, self.conf2_path)
+        assert queryUtility(IPayPalConfig) is None
+        self.assertRaises(
+            ConfigurationError, zcstring, config)
+
+    def test_paypal_config_no_such_file(self):
+        # a set paypal config file must exist
+        config = get_zcml_conf(
+            '<ikoba:paypalconf path="/not/eXiStInG/path" />')
+        self.assertRaises(
+            ConfigurationError, zcstring, config)
+
+    def test_paypal_config_not_a_regular_file(self):
+        # a paypal config file must be a regular file
+        dirpath = os.path.join(self.workdir, 'mydir')
+        os.mkdir(dirpath)
+        config = get_zcml_conf(
+            '<ikoba:paypalconf path="%s" />' % dirpath)
+        self.assertRaises(
+            ConfigurationError, zcstring, config)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/util.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/util.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/tests/util.py	(revision 12677)
@@ -0,0 +1,43 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import random
+
+def get_randstring(length):
+    result = ''
+    for i in range(1, length):
+        result += random.choice('abcdefghijklmnopqrstuvwxyz')
+    return result
+
+def get_randname():
+    result = ''
+    n1 = get_randstring(random.randint(4, 15))
+    n2 = get_randstring(random.randint(4, 15))
+    n3 = get_randstring(random.randint(4, 15))
+    #n1[0] = n1[0].upper()
+    #n2[0] = n2[0].upper()
+    #n3[0] = n3[0].upper()
+    return '"%s %s %s"' % (n1, n2, n3) 
+
+def get_namelist(num):
+    return [get_randname() for x in range(0, num)]
+
+random.seed()
+fd = open('namelist.txt', 'w')
+for name in get_namelist(70000):
+    fd.write(name + '\n')
+fd.close()
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/userscontainer.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/userscontainer.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/userscontainer.py	(revision 12677)
@@ -0,0 +1,116 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Users container for the Ikoba portal.
+"""
+import grok
+from zope.event import notify
+from waeup.ikoba.authentication import Account
+from waeup.ikoba.interfaces import IUsersContainer
+from waeup.ikoba.utils.logger import Logger
+from waeup.ikoba.utils.batching import ExporterBase
+from waeup.ikoba.interfaces import ICSVExporter
+from waeup.ikoba.interfaces import MessageFactory as _
+
+
+class UsersContainer(grok.Container, Logger):
+    """A container for principals.
+
+    See interfaces.py and users.txt for extensive description.
+    """
+    grok.implements(IUsersContainer)
+    grok.require('waeup.manageUsers')
+
+    def addUser(self, name, password, title=None,
+                description=None, email=None, phone=None,
+                public_name= None, roles=[]):
+        """Add a new Account instance, created from parameters.
+        """
+        if title is None:
+            title = name
+        #if description is None:
+        #    description = title
+        self[name] = Account(name=name, password=password, title=title,
+                             description=description, public_name=public_name,
+                             email=email, phone=phone, roles=roles)
+
+    def addAccount(self, account):
+        """Add the account passed.
+        """
+        self[account.name] = account
+
+    def delUser(self, name):
+        """Delete user, if an account with the given name exists.
+
+        Do not complain, if the name does not exist.
+        """
+        if name in self.keys():
+            del self[name]
+
+
+class UserExporter(grok.GlobalUtility, ExporterBase):
+    """Exporter for user accounts.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('users')
+
+    #: Fieldnames considered by this exporter
+    fields = ('name', 'title', 'public_name', 'description',
+              'email', 'phone', 'roles', 'local_roles', 'password')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Users')
+
+    def mangle_value(self, value, name, context=None):
+        """Treat location values special.
+        """
+
+        if name == 'local_roles' and context is not None:
+            local_roles = context.getLocalRoles()
+            value = {}
+            for role in local_roles.keys():
+                objects = local_roles[role]
+                object_list = []
+                for object in objects:
+                    obj= object
+                    path = ''
+                    while obj.__class__.__name__ != 'Company':
+                        path = '%s/' % obj.__name__ + path
+                        obj = obj.__parent__
+                    object_list.append(path)
+                value[role] = object_list
+        return super(
+            UserExporter, self).mangle_value(
+            value, name, context=context)
+
+    def export(self, users, filepath=None):
+        """Export `users`, an iterable, as CSV file.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for user in users:
+            self.write_item(user, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export users into filepath as CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        users = site.get('users', {})
+        return self.export(users.values(), filepath)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/userscontainer.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/userscontainer.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/userscontainer.txt	(revision 12677)
@@ -0,0 +1,80 @@
+User container for the Ikoba
+****************************
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+Before we can start, we need some password managers available:
+
+    >>> from zope.app.authentication.placelesssetup import (
+    ...   PlacelessSetup)
+    >>> PlacelessSetup().setUp()
+
+We can create a user container which will hold the useraccounts for
+us:
+
+    >>> from waeup.ikoba.userscontainer import UsersContainer
+    >>> myusers = UsersContainer()
+
+We can add users, just by passing a name, a password, and (optionally)
+a title and a description:
+
+    >>> myusers.addUser('bob', 'bobssecret')
+
+Now, Bob is in the container:
+
+    >>> list(myusers)
+    [u'bob']
+
+We can get Bob's account:
+
+    >>> bob = myusers['bob']
+    >>> bob
+    <waeup.ikoba.authentication.Account object at 0x...>
+
+    >>> bob.name
+    'bob'
+
+    >>> bob.title
+    'bob'
+
+    >>> bob.description
+
+
+As we did not give a title, the name was taken instead
+for title (but not for description). The password, however, is stored encoded:
+
+    >>> bob.password
+    '{SSHA}...'
+
+We can export user accounts:
+
+    >>> from waeup.ikoba.userscontainer import UserExporter
+    >>> import os
+    >>> import tempfile
+    >>> workdir = tempfile.mkdtemp()
+    >>> outfile = os.path.join(workdir, 'myoutput.csv')
+    >>> exporter = UserExporter()
+    >>> site = {'users':myusers}
+    >>> exporter.export_all(site, outfile)
+    >>> result = open(outfile, 'rb').read()
+    >>> 'name,title,public_name,description,email,phone,roles,local_roles,password' in result
+    True
+    >>> '{SSHA}' in result
+    True
+
+We can delete users:
+
+    >>> myusers.delUser('alice')
+
+The container won't complain, although there is no ``alice`` stored
+yet. But we can really delete users:
+
+    >>> myusers.delUser('bob')
+    >>> list(myusers)
+    []
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(workdir)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/api.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/api.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/api.txt	(revision 12677)
@@ -0,0 +1,19 @@
+:mod:`waeup.ikoba.utils` -- misc. helpers
+****************************************
+
+.. module:: waeup.ikoba.utils
+
+Components and other stuff of general interest.
+
+Submodules
+==========
+
+The :mod:`waeup.ikoba.utils` module provides the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   helpers.txt
+   batching.txt
+   importexport.txt
+   converters.txt
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/batching.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/batching.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/batching.py	(revision 12677)
@@ -0,0 +1,777 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Ikoba components for batch processing.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+"""
+import grok
+import datetime
+import os
+import shutil
+import tempfile
+import time
+import unicodecsv
+import zc.async.interfaces
+from cStringIO import StringIO
+from persistent.list import PersistentList
+from zope.component import createObject, getUtility
+from zope.component.hooks import setSite
+from zope.interface import Interface, implementer
+from zope.schema import getFields
+from zope.schema.interfaces import ConstraintNotSatisfied
+from zope.event import notify
+from waeup.ikoba.async import AsyncJob
+from waeup.ikoba.interfaces import (
+    IBatchProcessor, FatalCSVError, IObjectConverter, IJobManager,
+    ICSVExporter, IGNORE_MARKER, DuplicationError, JOB_STATUS_MAP,
+    IExportJobContainer, IExportJob, IExportContainerFinder)
+
+class BatchProcessor(grok.GlobalUtility):
+    """A processor to add, update, or remove data.
+
+    This is a non-active baseclass.
+    """
+    grok.implements(IBatchProcessor)
+    grok.context(Interface)
+    grok.baseclass()
+
+    # Name used in pages and forms...
+    name = u'Non-registered base processor'
+
+    # Internal name...
+    util_name = 'baseprocessor'
+
+    # Items for this processor need an interface with zope.schema fields.
+    iface = Interface
+
+    # The name must be the same as the util_name attribute in order to
+    # register this utility correctly.
+    grok.name(util_name)
+
+    # Headers needed to locate items...
+    location_fields = ['any_id']
+
+    # A factory with this name must be registered...
+    factory_name = 'waeup.any_factory'
+
+    @property
+    def required_fields(self):
+        """Required fields that have no default.
+
+        A list of names of field, whose value cannot be set if not
+        given during creation. Therefore these fields must exist in
+        input.
+
+        Fields with a default != missing_value do not belong to this
+        category.
+        """
+        result = []
+        for key, field in getFields(self.iface).items():
+            if key in self.location_fields:
+                continue
+            if field.default is not field.missing_value:
+                continue
+            if field.required:
+                result.append(key)
+        return result
+
+    @property
+    def req(self):
+        result = dict(
+            create = self.location_fields + self.required_fields,
+            update = self.location_fields,
+            remove = self.location_fields,
+        )
+        return result
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+                    self.location_fields + getFields(self.iface).keys())))
+
+    def getHeaders(self, mode='create'):
+        return self.available_fields
+
+    def checkHeaders(self, headerfields, mode='create'):
+        req = self.req[mode]
+        # Check for required fields...
+        for field in req:
+            if not field in headerfields:
+                raise FatalCSVError(
+                    "Need at least columns %s for import!" %
+                    ', '.join(["'%s'" % x for x in req]))
+        # Check for double fields. Cannot happen because this error is
+        # already catched in views
+        not_ignored_fields = [x for x in headerfields
+                              if not x.startswith('--')]
+        if len(set(not_ignored_fields)) < len(not_ignored_fields):
+            raise FatalCSVError(
+                "Double headers: each column name may only appear once.")
+        return True
+
+    def applyMapping(self, row, mapping):
+        """Apply mapping to a row of CSV data.
+
+        """
+        result = dict()
+        for key, replacement in mapping.items():
+            if replacement == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[replacement] = row[key]
+        return result
+
+    def getMapping(self, path, headerfields, mode):
+        """Get a mapping from CSV file headerfields to actually used fieldnames.
+
+        """
+        result = dict()
+        reader = unicodecsv.reader(open(path, 'rb'))
+        raw_header = reader.next()
+        for num, field in enumerate(headerfields):
+            if field not in self.location_fields and mode == 'remove':
+                # Skip non-location fields when removing.
+                continue
+            if field == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[raw_header[num]] = field
+        return result
+
+    def stringFromErrs(self, errors, inv_errors):
+        result = []
+        for err in errors:
+            fieldname, message = err
+            result.append("%s: %s" % (fieldname, message))
+        for err in inv_errors:
+            result.append("invariant: %s" % err)
+        return '; '.join(result)
+
+    def callFactory(self, *args, **kw):
+        return createObject(self.factory_name)
+
+    def parentsExist(self, row, site):
+        """Tell whether the parent object for data in ``row`` exists.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def entryExists(self, row, site):
+        """Tell whether there already exists an entry for ``row`` data.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def getParent(self, row, site):
+        """Get the parent object for the entry in ``row``.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def getEntry(self, row, site):
+        """Get the parent object for the entry in ``row``.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def addEntry(self, obj, row, site):
+        """Add the entry given given by ``row`` data.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def delEntry(self, row, site):
+        """Delete entry given by ``row`` data.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being updated.
+
+        This method is not used in case of deleting or adding objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        return None
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+
+        Returns a string describing the fields changed.
+        """
+        changed = []
+        for key, value in row.items():
+            # Skip fields to be ignored.
+            if value == IGNORE_MARKER:
+                continue
+            # Skip fields not declared in interface and which are
+            # not yet attributes of existing objects. We can thus not
+            # add non-existing attributes here.
+            if not hasattr(obj, key):
+                continue
+            try:
+                setattr(obj, key, value)
+            except AttributeError:
+                # Computed attributes can't be set.
+                continue
+            log_value = getattr(value, 'product_id', value)
+            log_value = getattr(value, 'document_id', log_value)
+            changed.append('%s=%s' % (key, log_value))
+
+        # If any catalog is involved it must be updated.
+        #
+        # XXX: The event is also triggered when creating objects as
+        # updateEntry is called also when creating entries resulting
+        # in objectAdded and additional objectModified events.
+        if len(changed):
+            notify(grok.ObjectModifiedEvent(obj))
+
+        return ', '.join(changed)
+
+    def createLogfile(self, path, fail_path, num, warnings, mode, user,
+                      timedelta, logger=None):
+        """Write to log file.
+        """
+        if logger is None:
+            return
+        logger.info(
+            "processed: %s, %s mode, %s lines (%s successful/ %s failed), "
+            "%0.3f s (%0.4f s/item)" % (
+            path, mode, num, num - warnings, warnings,
+            timedelta, timedelta/(num or 1)))
+        return
+
+    def writeFailedRow(self, writer, row, warnings):
+        """Write a row with error messages to error CSV.
+
+        If warnings is a list of strings, they will be concatenated.
+        """
+        error_col = warnings
+        if isinstance(warnings, list):
+            error_col = ' / '.join(warnings)
+        row['--ERRORS--'] = error_col
+        writer.writerow(row)
+        return
+
+    def checkConversion(self, row, mode='ignore', ignore_empty=True):
+        """Validates all values in row.
+        """
+        converter = IObjectConverter(self.iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name, mode=mode)
+        return errs, inv_errs, conv_dict
+
+    def doImport(self, path, headerfields, mode='create', user='Unknown',
+                 logger=None, ignore_empty=True):
+        """Perform actual import.
+        """
+        time_start = time.time()
+        self.checkHeaders(headerfields, mode)
+        mapping = self.getMapping(path, headerfields, mode)
+        reader = unicodecsv.DictReader(open(path, 'rb'))
+
+        temp_dir = tempfile.mkdtemp()
+
+        base = os.path.basename(path)
+        (base, ext) = os.path.splitext(base)
+        failed_path = os.path.join(temp_dir, "%s.pending%s" % (base, ext))
+        failed_headers = mapping.values()
+        failed_headers.append('--ERRORS--')
+        failed_writer = unicodecsv.DictWriter(open(failed_path, 'wb'),
+                                              failed_headers)
+        os.chmod(failed_path, 0664)
+        failed_writer.writerow(dict([(x,x) for x in failed_headers]))
+
+        finished_path = os.path.join(temp_dir, "%s.finished%s" % (base, ext))
+        finished_headers = mapping.values()
+        finished_writer = unicodecsv.DictWriter(open(finished_path, 'wb'),
+                                                finished_headers)
+        os.chmod(finished_path, 0664)
+        finished_writer.writerow(dict([(x,x) for x in finished_headers]))
+
+        num =0
+        num_warns = 0
+        site = grok.getSite()
+
+        for raw_row in reader:
+            num += 1
+            string_row = self.applyMapping(raw_row, mapping)
+            if ignore_empty and mode in ('update',):
+                # replace empty strings with ignore-markers
+                for key, val in string_row.items():
+                    if val == '':
+                        string_row[key] = IGNORE_MARKER
+            row = dict(string_row.items()) # create deep copy
+            errs, inv_errs, conv_dict = self.checkConversion(string_row, mode)
+            if errs or inv_errs:
+                num_warns += 1
+                conv_warnings = self.stringFromErrs(errs, inv_errs)
+                self.writeFailedRow(
+                    failed_writer, string_row, conv_warnings)
+                continue
+            row.update(conv_dict)
+
+            if mode == 'create':
+                if not self.parentsExist(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "Not all parents do exist yet. Skipping")
+                    continue
+                if self.entryExists(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "This object already exists. Skipping.")
+                    continue
+                obj = self.callFactory()
+                # Override all values in row, also
+                # ids which have been
+                # generated in the respective __init__ methods before.
+                self.updateEntry(obj, row, site, base)
+                try:
+                    self.addEntry(obj, row, site)
+                except KeyError, error:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "%s Skipping." % error.message)
+                    continue
+                except DuplicationError, error:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "%s Skipping." % error.msg)
+                    continue
+            elif mode == 'remove':
+                if not self.entryExists(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "Cannot remove: no such entry")
+                    continue
+                self.delEntry(row, site)
+            elif mode == 'update':
+                obj = self.getEntry(row, site)
+                if obj is None:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "Cannot update: no such entry")
+                    continue
+                update_errors = self.checkUpdateRequirements(obj, row, site)
+                if update_errors is not None:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row, update_errors)
+                    continue
+                try:
+                    self.updateEntry(obj, row, site, base)
+                except ConstraintNotSatisfied, err:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "ConstraintNotSatisfied: %s" % err)
+                    continue
+            finished_writer.writerow(string_row)
+
+        time_end = time.time()
+        timedelta = time_end - time_start
+
+        self.createLogfile(path, failed_path, num, num_warns, mode, user,
+                           timedelta, logger=logger)
+        failed_path = os.path.abspath(failed_path)
+        if num_warns == 0:
+            del failed_writer
+            os.unlink(failed_path)
+            failed_path = None
+        return (num, num_warns,
+                os.path.abspath(finished_path), failed_path)
+
+    def get_csv_skeleton(self):
+        """Export CSV file only with a header of available fields.
+
+        A raw string with CSV data should be returned.
+        """
+        outfile = StringIO()
+        writer = unicodecsv.DictWriter(outfile, self.available_fields)
+        writer.writerow(
+            dict(zip(self.available_fields, self.available_fields))) # header
+        outfile.seek(0)
+        return outfile.read()
+
+class ExporterBase(object):
+    """A base for exporters.
+    """
+    grok.implements(ICSVExporter)
+
+    #: Fieldnames considered by this exporter
+    fields = ('any_id', 'any_title')
+
+    #: The title under which this exporter will be displayed
+    #: (if registered as a utility)
+    title = 'Override this title'
+
+    def mangle_value(self, value, name, context=None):
+        """Hook for mangling values in derived classes
+        """
+        if isinstance(value, bool):
+            value = value and '1' or '0'
+        elif isinstance(value, unicode):
+            # CSV writers like byte streams better than unicode
+            value = value.encode('utf-8')
+        elif isinstance(value, datetime.datetime):
+            #value = str(value)
+            value = str('%s#' % value) # changed 2014-07-06, see ticket #941
+        elif isinstance(value, datetime.date):
+            # Order is important here: check for date after datetime as
+            # datetimes are also dates.
+            #
+            # Append hash '#' to dates to circumvent unwanted excel automatic
+            value = str('%s#' % value)
+        elif value is None:
+            # None is not really representable in CSV files
+            value = ''
+        return value
+
+    def get_csv_writer(self, filepath=None):
+        """Get a CSV dict writer instance open for writing.
+
+        Returns a tuple (<writer>, <outfile>) where ``<writer>`` is a
+        :class:`csv.DictWriter` instance and outfile is the real file
+        which is written to. The latter is important when writing to
+        StringIO and can normally be ignored otherwise.
+
+        The returned file will already be filled with the header row.
+
+        Please note that if you give a filepath, the returned outfile
+        is open for writing only and you might have to close it before
+        reopening it for reading.
+        """
+        if filepath is None:
+            outfile = StringIO()
+        else:
+            outfile = open(filepath, 'wb')
+        writer = unicodecsv.DictWriter(outfile, self.fields)
+        writer.writerow(dict(zip(self.fields, self.fields))) # header
+        return writer, outfile
+
+    def write_item(self, obj, writer):
+        """Write a row extracted from `obj` into CSV file using `writer`.
+        """
+        row = {}
+        for name in self.fields:
+            value = getattr(obj, name, None)
+            value = self.mangle_value(value, name, obj)
+            row[name] = value
+        writer.writerow(row)
+        return
+
+    def close_outfile(self, filepath, outfile):
+        """Close outfile.
+
+        If filepath is None, the contents of outfile is returned.
+        """
+        outfile.seek(0)
+        if filepath is None:
+            return outfile.read()
+        outfile.close()
+        return
+
+    def get_filtered(self, site, **kw):
+        """Get datasets to export filtered by keyword arguments.
+
+        Returns an iterable.
+        """
+        raise NotImplementedError
+
+    def export(self, iterable, filepath=None):
+        """Export `iterable` as CSV file.
+
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        raise NotImplementedError
+
+    def export_all(self, site, filepath=None):
+        """Export all appropriate objects in `site` into `filepath` as
+        CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        raise NotImplementedError
+
+    def export_filtered(self, site, filepath=None, **kw):
+        """Export items denoted by `args` and `kw`.
+
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        data = self.get_filtered(site, **kw)
+        return self.export(data, filepath=filepath)
+
+def export_job(site, exporter_name, **kw):
+    """Export all entries delivered by exporter and store it in a temp file.
+
+    `site` gives the site to search. It will be passed to the exporter
+    and also be set as 'current site' as the function is used in
+    asynchronous jobs which run in their own threads and have no site
+    set initially. Therefore `site` must also be a valid value for use
+    with `zope.component.hooks.setSite()`.
+
+    `exporter_name` is the utility name under which the desired
+    exporter was registered with the ZCA.
+
+    The resulting CSV file will be stored in a new temporary directory
+    (using :func:`tempfile.mkdtemp`). It will be named after the
+    exporter used with `.csv` filename extension.
+
+    Returns the path to the created CSV file.
+
+    .. note:: It is the callers responsibility to clean up the used
+              file and its parent directory.
+    """
+    setSite(site)
+    exporter = getUtility(ICSVExporter, name=exporter_name)
+    output_dir = tempfile.mkdtemp()
+    filename = '%s.csv' % exporter_name
+    output_path = os.path.join(output_dir, filename)
+    if kw == {}:
+        exporter.export_all(site, filepath=output_path)
+    else:
+        exporter.export_filtered(site, filepath=output_path, **kw)
+    return output_path
+
+class AsyncExportJob(AsyncJob):
+    """An IJob that exports data to CSV files.
+
+    `AsyncExportJob` instances are regular `AsyncJob` instances with a
+    different constructor API. Instead of a callable to execute, you
+    must pass a `site` and some `exporter_name` to trigger an export.
+
+    The real work is done when an instance of this class is put into a
+    queue. See :mod:`waeup.ikoba.async` to learn more about
+    asynchronous jobs.
+
+    The `exporter_name` must be the name under which an ICSVExporter
+    utility was registered with the ZCA.
+
+    The `site` must be a valid site  or ``None``.
+
+    The result of an `AsyncExportJob` is the path to generated CSV
+    file. The file will reside in a temporary directory that should be
+    removed after being used.
+    """
+    grok.implements(IExportJob)
+
+    def __init__(self, site, exporter_name, *args, **kwargs):
+        super(AsyncExportJob, self).__init__(
+            export_job, site, exporter_name, *args, **kwargs)
+
+    @property
+    def finished(self):
+        """A job is marked `finished` if it is completed.
+
+        Please note: a finished report job does not neccessarily
+        provide an IReport result. See meth:`failed`.
+        """
+        return self.status == zc.async.interfaces.COMPLETED
+
+    @property
+    def failed(self):
+        """A report job is marked failed iff it is finished and the
+        result is None.
+
+        While a job is unfinished, the `failed` status is ``None``.
+
+        Failed jobs normally provide a `traceback` to examine reasons.
+        """
+        if not self.finished:
+            return None
+        if getattr(self, 'result', None) is None:
+            return True
+        return False
+
+class ExportJobContainer(object):
+    """A mix-in that provides functionality for asynchronous export jobs.
+    """
+    grok.implements(IExportJobContainer)
+    running_exports = PersistentList()
+
+    def start_export_job(self, exporter_name, user_id, *args, **kwargs):
+        """Start asynchronous export job.
+
+        `exporter_name` is the name of an exporter utility to be used.
+
+        `user_id` is the ID of the user that triggers the export.
+
+        The job_id is stored along with exporter name and user id in a
+        persistent list.
+
+        The method supports additional positional and keyword
+        arguments, which are passed as-is to the respective
+        :class:`AsyncExportJob`.
+
+        Returns the job ID of the job started.
+        """
+        site = grok.getSite()
+        manager = getUtility(IJobManager)
+        job = AsyncExportJob(site, exporter_name, *args, **kwargs)
+        job_id = manager.put(job)
+        # Make sure that the persisted list is stored in ZODB
+        self.running_exports = PersistentList(self.running_exports)
+        self.running_exports.append((job_id, exporter_name, user_id))
+        return job_id
+
+    def get_running_export_jobs(self, user_id=None):
+        """Get export jobs for user with `user_id` as list of tuples.
+
+        Each tuples holds ``<job_id>, <exporter_name>, <user_id>`` in
+        that order. The ``<exporter_name>`` is the utility name of the
+        used exporter.
+
+        If `user_id` is ``None``, all running jobs are returned.
+        """
+        entries = []
+        to_delete = []
+        manager = getUtility(IJobManager)
+        for entry in self.running_exports:
+            if user_id is not None and entry[2] != user_id:
+                continue
+            if manager.get(entry[0]) is None:
+                to_delete.append(entry)
+                continue
+            entries.append(entry)
+        if to_delete:
+            self.running_exports = PersistentList(
+                [x for x in self.running_exports if x not in to_delete])
+        return entries
+
+    def get_export_jobs_status(self, user_id=None):
+        """Get running/completed export jobs for `user_id` as list of tuples.
+
+        Each tuple holds ``<raw status>, <status translated>,
+        <exporter title>`` in that order, where ``<status
+        translated>`` and ``<exporter title>`` are translated strings
+        representing the status of the job and the human readable
+        title of the exporter used.
+        """
+        entries = self.get_running_export_jobs(user_id)
+        result = []
+        manager = getUtility(IJobManager)
+        for entry in entries:
+            job = manager.get(entry[0])
+            if job is None:
+                continue
+            status, status_translated = JOB_STATUS_MAP[job.status]
+            exporter_name = getUtility(ICSVExporter, name=entry[1]).title
+            result.append((status, status_translated, exporter_name))
+        return result
+
+    def delete_export_entry(self, entry):
+        """Delete the export denoted by `entry`.
+
+        Removes given entry from the local `running_exports` list and also
+        removes the regarding job via the local job manager.
+
+        `entry` must be a tuple ``(<job id>, <exporter name>, <user
+        id>)`` as created by :meth:`start_export_job` or returned by
+        :meth:`get_running_export_jobs`.
+        """
+        manager = getUtility(IJobManager)
+        job = manager.get(entry[0])
+        if job is not None:
+            # remove created export file
+            if isinstance(job.result, basestring):
+                if os.path.exists(os.path.dirname(job.result)):
+                    shutil.rmtree(os.path.dirname(job.result))
+        manager.remove(entry[0], self)
+        new_entries = [x for x in self.running_exports
+                       if x != entry]
+        self.running_exports = PersistentList(new_entries)
+        return
+
+    def entry_from_job_id(self, job_id):
+        """Get entry tuple for `job_id`.
+
+        Returns ``None`` if no such entry can be found.
+        """
+        for entry in self.running_exports:
+            if entry[0] == job_id:
+                return entry
+        return None
+
+class VirtualExportJobContainer(ExportJobContainer):
+    """A virtual export job container.
+
+    Virtual ExportJobContainers can be used as a mixin just like real
+    ExportJobContainer.
+
+    They retrieve and store data in the site-wide ExportJobContainer.
+
+    Functionality is currently entirely as for regular
+    ExportJobContainers, except that data is stored elsewhere.
+
+    VirtualExportJobContainers need a registered
+    IExportContainerFinder utility to find a suitable container for
+    storing data.
+    """
+    grok.implements(IExportJobContainer)
+
+    @property
+    def _site_container(self):
+        return getUtility(IExportContainerFinder)()
+
+    # The following is a simple trick. While ExportJobContainers store
+    # only one attribute in ZODB, it is sufficient to replace this
+    # attribute `running_exports` with a suitable manager to make the
+    # whole virtual container work like the original but with the data
+    # stored in the site-wide exports container. This way, virtual
+    # export containers provide the whole functionality of a regular
+    # exports container but store no data at all with themselves.
+    @property
+    def running_exports(self):
+        """Exports stored in the site-wide exports container.
+        """
+        return self._site_container.running_exports
+
+    @running_exports.setter
+    def running_exports(self, value):
+        self._site_container.running_exports = value
+
+    @running_exports.deleter
+    def running_exports(self):
+        del self._site_container.running_exports
+
+    @property
+    def logger(self):
+        return self._site_container.logger
+
+@implementer(IExportContainerFinder)
+class ExportContainerFinder(grok.GlobalUtility):
+    """Finder for local (site-wide) export container.
+    """
+
+    def __call__(self):
+        """Get the local export container-
+
+        If no site can be determined or the site provides no export
+        container, None is returned.
+        """
+        site = grok.getSite()
+        if site is None:
+            return None
+        return site.get('datacenter', None)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/batching.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/batching.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/batching.txt	(revision 12677)
@@ -0,0 +1,556 @@
+:mod:`waeup.ikoba.utils.batching` -- Batch processing
+****************************************************
+
+Batch processing is much more than pure data import.
+
+Overview
+========
+
+Basically, it means processing CSV files in order to mass-create,
+mass-remove, or mass-update data.
+
+So you can feed CSV files to processors, that are part of
+the batch-processing mechanism.
+
+Processors
+----------
+
+Each CSV file processor
+
+* accepts a single data type identified by an interface.
+
+* knows about the places inside a site (Company) 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.ikoba.utils.batching import BatchProcessor
+    >>> from waeup.ikoba.interfaces import IGNORE_MARKER
+    >>> class CaveProcessor(BatchProcessor):
+    ...   util_name = 'caveprocessor'
+    ...   grok.name(util_name)
+    ...   name = 'Cave Processor'
+    ...   iface = ICave
+    ...   location_fields = ['name']
+    ...   factory_name = 'Lovely Cave'
+    ...
+    ...   def parentsExist(self, row, site):
+    ...     return True
+    ...
+    ...   def getParent(self, row, site):
+    ...     return stoneville
+    ...
+    ...   def entryExists(self, row, site):
+    ...     return row['name'] in stoneville.keys()
+    ...
+    ...   def getEntry(self, row, site):
+    ...     if not self.entryExists(row, site):
+    ...       return None
+    ...     return stoneville[row['name']]
+    ...
+    ...   def delEntry(self, row, site):
+    ...     del stoneville[row['name']]
+    ...
+    ...   def addEntry(self, obj, row, site):
+    ...     stoneville[row['name']] = obj
+    ...
+    ...   def updateEntry(self, obj, row, site, filename):
+    ...     # This is not strictly necessary, as the default
+    ...     # updateEntry method does exactly the same
+    ...     for key, value in row.items():
+    ...       if value != IGNORE_MARKER:
+    ...         setattr(obj, key, value)
+
+If we also want the results being logged, we must provide a logger
+(this is optional):
+
+    >>> import logging
+    >>> logger = logging.getLogger('stoneville')
+    >>> logger.setLevel(logging.DEBUG)
+    >>> logger.propagate = False
+    >>> handler = logging.FileHandler('stoneville.log', 'w')
+    >>> logger.addHandler(handler)
+
+Create the fellows:
+
+    >>> processor = CaveProcessor()
+    >>> result = processor.doImport('newcomers.csv',
+    ...                   ['name', 'dinoports', 'owner', 'taxpayer'],
+    ...                    mode='create', user='Bob', logger=logger)
+    >>> result
+    (4, 0, '/.../newcomers.finished.csv', None)
+
+The result means: four entries were processed and no warnings
+occured. Furthermore we get filepath to a CSV file with successfully
+processed entries and a filepath to a CSV file with erraneous entries.
+As everything went well, the latter is ``None``. Let's check:
+
+    >>> sorted(stoneville.keys())
+    [u'Barneys Home', ..., u'Wilmas Asylum']
+
+The values of the Cave instances have correct type:
+
+    >>> barney = stoneville['Barneys Home']
+    >>> barney.dinoports
+    2
+
+which is a number, not a string.
+
+Apparently, when calling the processor, we gave some more info than
+only the CSV filepath. What does it all mean?
+
+While the first argument is the path to the CSV file, we also have to
+give an ordered list of headernames. These replace the header field
+names that are actually in the file. This way we can override faulty
+headers.
+
+The ``mode`` paramter tells what kind of operation we want to perform:
+``create``, ``update``, or ``remove`` data.
+
+The ``user`` parameter finally is optional and only used for logging.
+
+We can, by the way, see the results of our run in a logfile if we
+provided a logger during the call:
+
+    >>> print open('stoneville.log').read()
+    processed: newcomers.csv, create mode, 4 lines (4 successful/ 0 failed), ... s (... s/item)
+
+
+We cleanup the temporay dir created by doImport():
+
+    >>> import shutil
+    >>> import os
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+As we can see, the processing was successful. Otherwise, all problems
+could be read here as we can see, if we do the same operation again:
+
+    >>> result = processor.doImport('newcomers.csv',
+    ...                   ['name', 'dinoports', 'owner', 'taxpayer'],
+    ...                    mode='create', user='Bob', logger=logger)
+    >>> result
+    (4, 4, '/.../newcomers.finished.csv', '/.../newcomers.pending.csv')
+
+This time we also get a path to a .pending file.
+
+The log file will tell us this in more detail:
+
+    >>> print open('stoneville.log').read()
+    processed: newcomers.csv, create mode, 4 lines (4 successful/ 0 failed), ... s (... s/item)
+    processed: newcomers.csv, create mode, 4 lines (0 successful/ 4 failed), ... s (... s/item)
+
+
+This time a new file was created, which keeps all the rows we could not
+process and an additional column with error messages:
+
+    >>> print open(result[3]).read()
+    owner,name,taxpayer,dinoports,--ERRORS--
+    Barney,Barneys Home,1,2,This object already exists. Skipping.
+    Wilma,Wilmas Asylum,1,1,This object already exists. Skipping.
+    Fred,Freds Dinoburgers,0,10,This object already exists. Skipping.
+    Joey,Joeys Drive-in,0,110,This object already exists. Skipping.
+
+This way we can correct the faulty entries and afterwards retry without
+having the already processed rows in the way.
+
+We also notice, that the values of the taxpayer column are returned as
+in the input file. There we wrote '1' for ``True`` and '0' for
+``False`` (which is accepted by the converters).
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+We can also tell to ignore some cols from input by passing
+``--IGNORE--`` as col name:
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             '--IGNORE--', '--IGNORE--'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (4, 0, '...', None)
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+If something goes wrong during processing, the respective --IGNORE--
+cols won't be populated  in the resulting pending file:
+
+    >>> result = processor.doImport('newcomers.csv', ['name', 'dinoports',
+    ...                             '--IGNORE--', '--IGNORE--'],
+    ...                    mode='create', user='Bob')
+    >>> result
+    (4, 4, '...', '...')
+
+    >>> print open(result[3], 'rb').read()
+    name,dinoports,--ERRORS--
+    Barneys Home,2,This object already exists. Skipping.
+    Wilmas Asylum,1,This object already exists. Skipping.
+    Freds Dinoburgers,10,This object already exists. Skipping.
+    Joeys Drive-in,110,This object already exists. Skipping.
+
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+
+
+Updating entries
+----------------
+
+To update entries, we just call the batchprocessor in a different
+mode:
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (4, 0, '...', None)
+
+Now we want to tell, that Wilma got an extra port for her second dino:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... Wilmas Asylum,2,Wilma
+    ... """)
+
+    >>> wilma = stoneville['Wilmas Asylum']
+    >>> wilma.dinoports
+    1
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+We start the processor:
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                    'dinoports', 'owner'], mode='update', user='Bob')
+    >>> result
+    (1, 0, '...', None)
+
+    >>> wilma = stoneville['Wilmas Asylum']
+    >>> wilma.dinoports
+    2
+
+Wilma's number of dinoports raised.
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+If we try to update an unexisting entry, an error occurs:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... NOT-WILMAS-ASYLUM,2,Wilma
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (1, 1, '/.../newcomers.finished.csv', '/.../newcomers.pending.csv')
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+    
+Also invalid values will be spotted:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... Wilmas Asylum,NOT-A-NUMBER,Wilma
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (1, 1, '...', '...')
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+We can also update only some cols, leaving some out. We skip the
+'dinoports' column in the next run:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,owner
+    ... Wilmas Asylum,Barney
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name', 'owner'],
+    ...                             mode='update', user='Bob')
+    >>> result
+    (1, 0, '...', None)
+
+    >>> wilma.owner
+    u'Barney'
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+We can however, not leave out the 'location field' ('name' in our
+case), as this one tells us which entry to update:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... 2,Wilma
+    ... """)
+
+    >>> processor.doImport('newcomers.csv', ['dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    Traceback (most recent call last):
+    ...
+    FatalCSVError: Need at least columns 'name' for import!
+
+This time we get even an exception!
+
+Generally, empty strings are considered as ``None``:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... "Wilmas Asylum","","Wilma"
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (1, 0, '...', None)
+
+    >>> wilma.dinoports
+    2
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+We can tell to set dinoports to ``None`` although this is not a
+number, as we declared the field not required in the interface:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... "Wilmas Asylum","XXX","Wilma"
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob', ignore_empty=False)
+    >>> result
+    (1, 0, '...', None)
+
+    >>> wilma.dinoports is None
+    True
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+Removing entries
+----------------
+
+In 'remove' mode we can delete entries. Here validity of values in
+non-location fields doesn't matter because those fields are ignored.
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... "Wilmas Asylum","ILLEGAL-NUMBER",""
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='remove', user='Bob')
+    >>> result
+    (1, 0, '...', None)
+
+    >>> sorted(stoneville.keys())
+    [u'Barneys Home', "Fred's home", u'Freds Dinoburgers', u'Joeys Drive-in']
+
+Oops! Wilma is gone.
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+Clean up:
+
+    >>> import os
+    >>> os.unlink('newcomers.csv')
+    >>> os.unlink('stoneville.log')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/browser.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/browser.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/browser.py	(revision 12677)
@@ -0,0 +1,57 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""UI components for utilities and helpers.
+"""
+
+import grok
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from waeup.ikoba.browser.layout import UtilityView
+from waeup.ikoba.interfaces import IObjectHistory
+
+from waeup.ikoba.interfaces import ICompany
+
+class ReindexPage(UtilityView, grok.View):
+    """ Reindex view.
+
+    Reindexes a catalog. For managers only.
+    """
+    grok.context(ICompany)
+    grok.name('reindex')
+    grok.require('waeup.managePortal')
+
+    def update(self,ctlg=None):
+        if ctlg is None:
+            self.flash('No catalog name provided.')
+            return
+        cat = queryUtility(ICatalog, name='%s_catalog' % ctlg)
+        if cat is None:
+            self.flash('%s_catalog does not exist' % ctlg)
+            return
+        self.context.logger.info(
+            'Catalog `%s_catalog` re-indexing started.' % ctlg)
+        cat.updateIndexes()
+        no_of_entries = cat.values()[0].documentCount()
+        self.flash('%d %s re-indexed.' % (no_of_entries,ctlg))
+        self.context.logger.info(
+            'Re-indexing of %d objects finished.' % no_of_entries)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/converters.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/converters.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/converters.py	(revision 12677)
@@ -0,0 +1,327 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Converters for zope.schema-based datatypes.
+"""
+import grok
+from zope.component import createObject
+from zope.formlib import form
+from zope.formlib.boolwidgets import CheckBoxWidget
+from zope.formlib.form import (
+    _widgetKey, WidgetInputError, ValidationError, InputErrors,
+    expandPrefix)
+from zope.formlib.interfaces import IInputWidget, ConversionError
+from zope.interface import Interface
+from zope.publisher.browser import TestRequest
+from zope.schema.interfaces import IList
+from waeup.ikoba.interfaces import (
+    IObjectConverter, IFieldConverter,
+    DELETION_MARKER, IGNORE_MARKER)
+from waeup.ikoba.schema.interfaces import IPhoneNumber
+from waeup.ikoba.products.productoptions import (
+    IProductOptionField, ProductOption)
+
+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 first 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).
+    The second difference is that we return only ConversionError objects.
+    """
+    errors = []
+    form_prefix = expandPrefix(form_prefix)
+
+    for input, widget in widgets.__iter_input_and_widget__():
+        if input and IInputWidget.providedBy(widget):
+            name = _widgetKey(widget, form_prefix)
+
+            if not widget.hasInput():
+                continue
+
+            try:
+                data[name] = widget.getInputValue()
+            except ValidationError, error:
+                error = ConversionError(u'Validation failed')
+                errors.append((name, error))
+            except WidgetInputError, error:
+                error = ConversionError(u'Invalid input')
+                errors.append((name, error))
+            except ConversionError, error:
+                errors.append((name, error))
+
+    return errors
+
+class DefaultFieldConverter(grok.Adapter):
+    grok.context(Interface)
+    grok.implements(IFieldConverter)
+
+    def request_data(self, name, value, schema_field, prefix='',
+                     mode='create'):
+        if prefix == 'form.sex' and isinstance(value, basestring):
+            value = value.lower()
+        return {prefix: value}
+
+class ListFieldConverter(grok.Adapter):
+    grok.context(IList)
+    grok.implements(IFieldConverter)
+
+    def request_data(self, name, value, schema_field, prefix='',
+                     mode='create'):
+        value_type = schema_field.value_type
+        try:
+            items = eval(value)
+        except:
+            return {prefix: value}
+        result = {'%s.count' % prefix: len(items)}
+        for num, item in enumerate(items):
+            sub_converter = IFieldConverter(value_type)
+            result.update(sub_converter.request_data(
+                unicode(num), unicode(item),
+                value_type, "%s.%s." % (prefix, num)))
+        return result
+
+class PhoneNumberFieldConverter(grok.Adapter):
+    """Convert strings into dict as expected from forms feeding PhoneWidget.
+
+    If you want strings without extra-checks imported, you can use
+    schema.TextLine in your interface instead of PhoneNumber.
+    """
+    grok.context(IPhoneNumber)
+    grok.implements(IFieldConverter)
+
+    def request_data(self, name, value, schema_field, prefix='',
+                     mode='create'):
+        parts = value.split('-', 2)
+        country = ''
+        area = ''
+        ext = ''
+        if len(parts) == 3:
+            country = parts[0]
+            area = parts[1]
+            ext = parts[2]
+        elif len(parts) == 2:
+            country = parts[0]
+            ext = parts[1]
+        else:
+            ext = value
+        result = {
+            u'%s.country' % prefix: country,
+            u'%s.area' % prefix: area,
+            u'%s.ext' % prefix: ext}
+        return result
+
+class ProductOptionConverter(grok.Adapter):
+    grok.context(IProductOptionField)
+    grok.implements(IFieldConverter)
+
+    def request_data(self, name, value, schema_field, prefix='',
+                     mode='create'):
+        """Turn CSV values into ProductOption-compatible form data.
+
+        Expects as `value` a _string_ like ``(u'mytitle',
+        u'myfee')`` and turns it into some dict like::
+
+          {
+            'form.option.title': u'9234896395...',
+            'form.option.fee': 7698769
+            'form.option.currency': u'7e67e9e777..'
+            }
+
+        where the values are tokens from appropriate sources.
+
+        Such dicts can be transformed into real ProductOption objects by
+        input widgets used in converters.
+        """
+        try:
+            entry = ProductOption.from_string(value)
+            title, fee, currency = entry.title, entry.fee, entry.currency
+        except:
+            return {prefix: value}
+        result = {
+            "%stitle" % (prefix): title,
+            "%sfee" % (prefix): fee,
+            "%scurrency" % (prefix): currency,
+            }
+        return result
+
+class DefaultObjectConverter(grok.Adapter):
+    """Turn string values into real values.
+
+    A converter can convert string values for objects that implement a
+    certain interface into real values based on the given interface.
+    """
+
+    grok.context(Interface)
+    grok.implements(IObjectConverter)
+
+    def __init__(self, iface):
+        self.iface = iface
+        # Omit known dictionaries since there is no widget available
+        # for dictionary schema fields
+        self.default_form_fields = form.Fields(iface).omit('description_dict')
+        return
+
+    def fromStringDict(self, data_dict, context, form_fields=None,
+                       mode='create'):
+        """Convert values in `data_dict`.
+
+        Converts data in `data_dict` into real values based on
+        `context` and `form_fields`.
+
+        `data_dict` is a mapping (dict) from field names to values
+        represented as strings.
+
+        The fields (keys) to convert can be given in optional
+        `form_fields`. If given, form_fields should be an instance of
+        :class:`zope.formlib.form.Fields`. Suitable instances are for
+        example created by :class:`grok.AutoFields`.
+
+        If no `form_fields` are given, a default is computed from the
+        associated interface.
+
+        The `context` can be an existing object (implementing the
+        associated interface) or a factory name. If it is a string, we
+        try to create an object using
+        :func:`zope.component.createObject`.
+
+        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
+        <DATA_DICT>)`` where
+
+        ``<FIELD_ERRORS>``
+           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
+           error that happened when validating the input data in
+           `data_dict`
+
+        ``<INVARIANT_ERRORS>``
+           is a list of invariant errors concerning several fields
+
+        ``<DATA_DICT>``
+           is a dict with the values from input dict converted.
+
+        If mode is ``'create'`` or ``'update'`` then some additional
+        filtering applies:
+
+        - values set to DELETION_MARKER are set to missing_value (or
+          default value if field is required) and
+
+        - values set to IGNORE_MARKER are ignored and thus not part of
+          the returned ``<DATA_DICT>``.
+
+        If errors happen, i.e. the error lists are not empty, always
+        an empty ``<DATA_DICT>`` is returned.
+
+        If ``<DATA_DICT>`` is non-empty, there were no errors.
+        """
+        if form_fields is None:
+            form_fields = self.default_form_fields
+
+        request = TestRequest(form={})
+        new_data = dict()
+        for key, val in data_dict.items():
+            field = form_fields.get(key, None)
+            if field is not None:
+                # let adapters to the respective schema fields do the
+                # further fake-request processing
+                schema_field = field.interface[field.__name__]
+                field_converter = IFieldConverter(schema_field)
+                if mode in ('update', 'create'):
+                    if val == IGNORE_MARKER:
+                        continue
+                    elif val == DELETION_MARKER:
+                        val = schema_field.missing_value
+                        if schema_field.required:
+                            val = schema_field.default
+                        new_data[key] = val
+                        continue
+                request.form.update(
+                    field_converter.request_data(
+                        key, val, schema_field, 'form.%s' % key)
+                    )
+            else:
+                request.form['form.%s' % key] = val
+
+        obj = context
+        if isinstance(context, basestring):
+            # If we log initialization transitions in the __init__
+            # method of objects, a second (misleading) log entry
+            # will be created here.
+            obj = createObject(context)
+
+        widgets = form.setUpInputWidgets(
+            form_fields, 'form', obj, request)
+
+        errors = getWidgetsData(widgets, 'form', new_data)
+
+        invariant_errors = form.checkInvariants(form_fields, new_data)
+
+        if errors or invariant_errors:
+            err_messages = [(key, err.args[0]) for key, err in errors]
+            invariant_errors = [err.message for err in invariant_errors]
+            return err_messages, invariant_errors, {}
+
+        return errors, invariant_errors, new_data
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/converters.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/converters.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/converters.txt	(revision 12677)
@@ -0,0 +1,861 @@
+:mod:`waeup.ikoba.utils.converters` -- Converters
+************************************************
+
+.. module:: waeup.ikoba.utils.converters
+
+Converters for :mod:`zope.schema` based data.
+
+.. contents::
+
+
+.. :NOdoctest:
+.. :NOlayer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+
+Constants
+=========
+
+.. attribute:: NONE_STRING_VALUE
+
+   This value determines, which string represents
+   'non-values'. Current setting is:
+
+      >>> from waeup.ikoba.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.ikoba.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.ikoba.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.ikoba.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.ikoba.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.ikoba.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.ikoba.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.ikoba.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.ikoba.utils.converters import TextConverter
+    >>> converter = TextConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.ikoba.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.ikoba.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.ikoba.utils.converters import BoolConverter
+    >>> converter = BoolConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.ikoba.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.ikoba.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.ikoba.utils.converters import IntConverter
+    >>> converter = IntConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.ikoba.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.ikoba.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.ikoba.utils.converters import ChoiceConverter
+    >>> converter = ChoiceConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.ikoba.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.ikoba.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.ikoba.interfaces import SimpleIkobaVocabulary
+    >>> field = Choice(
+    ...   title = u'Favourite Dish',
+    ...   default = 0,
+    ...   vocabulary = SimpleIkobaVocabulary(
+    ...        ('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.ikoba.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.ikoba.utils.converters import DateConverter
+    >>> converter = DateConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.ikoba.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.ikoba.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.ikoba/branches/uli-payments/src/waeup/ikoba/utils/countries.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/countries.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/countries.py	(revision 12677)
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+#
+# This file was automatically generated. Please do not edit
+#
+from waeup.ikoba.interfaces import MessageFactory as _
+
+COUNTRIES = (
+    (_(u"Afghanistan"), u"AF"),
+    (_(u"Åland Islands"), u"AX"),
+    (_(u"Albania"), u"AL"),
+    (_(u"Algeria"), u"DZ"),
+    (_(u"American Samoa"), u"AS"),
+    (_(u"Andorra"), u"AD"),
+    (_(u"Angola"), u"AO"),
+    (_(u"Anguilla"), u"AI"),
+    (_(u"Antarctica"), u"AQ"),
+    (_(u"Antigua and Barbuda"), u"AG"),
+    (_(u"Argentina"), u"AR"),
+    (_(u"Armenia"), u"AM"),
+    (_(u"Aruba"), u"AW"),
+    (_(u"Australia"), u"AU"),
+    (_(u"Austria"), u"AT"),
+    (_(u"Azerbaijan"), u"AZ"),
+    (_(u"Bahamas"), u"BS"),
+    (_(u"Bahrain"), u"BH"),
+    (_(u"Bangladesh"), u"BD"),
+    (_(u"Barbados"), u"BB"),
+    (_(u"Belarus"), u"BY"),
+    (_(u"Belgium"), u"BE"),
+    (_(u"Belize"), u"BZ"),
+    (_(u"Benin"), u"BJ"),
+    (_(u"Bermuda"), u"BM"),
+    (_(u"Bhutan"), u"BT"),
+    (_(u"Bolivia, Plurinational State of"), u"BO"),
+    (_(u"Bonaire, Sint Eustatius and Saba"), u"BQ"),
+    (_(u"Bosnia and Herzegovina"), u"BA"),
+    (_(u"Botswana"), u"BW"),
+    (_(u"Bouvet Island"), u"BV"),
+    (_(u"Brazil"), u"BR"),
+    (_(u"British Indian Ocean Territory"), u"IO"),
+    (_(u"Brunei Darussalam"), u"BN"),
+    (_(u"Bulgaria"), u"BG"),
+    (_(u"Burkina Faso"), u"BF"),
+    (_(u"Burundi"), u"BI"),
+    (_(u"Cambodia"), u"KH"),
+    (_(u"Cameroon"), u"CM"),
+    (_(u"Canada"), u"CA"),
+    (_(u"Cape Verde"), u"CV"),
+    (_(u"Cayman Islands"), u"KY"),
+    (_(u"Central African Republic"), u"CF"),
+    (_(u"Chad"), u"TD"),
+    (_(u"Chile"), u"CL"),
+    (_(u"China"), u"CN"),
+    (_(u"Christmas Island"), u"CX"),
+    (_(u"Cocos (Keeling) Islands"), u"CC"),
+    (_(u"Colombia"), u"CO"),
+    (_(u"Comoros"), u"KM"),
+    (_(u"Congo"), u"CG"),
+    (_(u"Congo, the Democratic Republic of the"), u"CD"),
+    (_(u"Cook Islands"), u"CK"),
+    (_(u"Costa Rica"), u"CR"),
+    (_(u"Côte d'Ivoire"), u"CI"),
+    (_(u"Croatia"), u"HR"),
+    (_(u"Cuba"), u"CU"),
+    (_(u"Curaçao"), u"CW"),
+    (_(u"Cyprus"), u"CY"),
+    (_(u"Czech Republic"), u"CZ"),
+    (_(u"Denmark"), u"DK"),
+    (_(u"Djibouti"), u"DJ"),
+    (_(u"Dominica"), u"DM"),
+    (_(u"Dominican Republic"), u"DO"),
+    (_(u"Ecuador"), u"EC"),
+    (_(u"Egypt"), u"EG"),
+    (_(u"El Salvador"), u"SV"),
+    (_(u"Equatorial Guinea"), u"GQ"),
+    (_(u"Eritrea"), u"ER"),
+    (_(u"Estonia"), u"EE"),
+    (_(u"Ethiopia"), u"ET"),
+    (_(u"Falkland Islands (Malvinas)"), u"FK"),
+    (_(u"Faroe Islands"), u"FO"),
+    (_(u"Fiji"), u"FJ"),
+    (_(u"Finland"), u"FI"),
+    (_(u"France"), u"FR"),
+    (_(u"French Guiana"), u"GF"),
+    (_(u"French Polynesia"), u"PF"),
+    (_(u"French Southern Territories"), u"TF"),
+    (_(u"Gabon"), u"GA"),
+    (_(u"Gambia"), u"GM"),
+    (_(u"Georgia"), u"GE"),
+    (_(u"Germany"), u"DE"),
+    (_(u"Ghana"), u"GH"),
+    (_(u"Gibraltar"), u"GI"),
+    (_(u"Greece"), u"GR"),
+    (_(u"Greenland"), u"GL"),
+    (_(u"Grenada"), u"GD"),
+    (_(u"Guadeloupe"), u"GP"),
+    (_(u"Guam"), u"GU"),
+    (_(u"Guatemala"), u"GT"),
+    (_(u"Guernsey"), u"GG"),
+    (_(u"Guinea"), u"GN"),
+    (_(u"Guinea-bissau"), u"GW"),
+    (_(u"Guyana"), u"GY"),
+    (_(u"Haiti"), u"HT"),
+    (_(u"Heard Island and Mcdonald Islands"), u"HM"),
+    (_(u"Holy See (Vatican City State)"), u"VA"),
+    (_(u"Honduras"), u"HN"),
+    (_(u"Hong Kong"), u"HK"),
+    (_(u"Hungary"), u"HU"),
+    (_(u"Iceland"), u"IS"),
+    (_(u"India"), u"IN"),
+    (_(u"Indonesia"), u"ID"),
+    (_(u"Iran, Islamic Republic of"), u"IR"),
+    (_(u"Iraq"), u"IQ"),
+    (_(u"Ireland"), u"IE"),
+    (_(u"Isle of Man"), u"IM"),
+    (_(u"Israel"), u"IL"),
+    (_(u"Italy"), u"IT"),
+    (_(u"Jamaica"), u"JM"),
+    (_(u"Japan"), u"JP"),
+    (_(u"Jersey"), u"JE"),
+    (_(u"Jordan"), u"JO"),
+    (_(u"Kazakhstan"), u"KZ"),
+    (_(u"Kenya"), u"KE"),
+    (_(u"Kiribati"), u"KI"),
+    (_(u"Korea, Democratic People's Republic of"), u"KP"),
+    (_(u"Korea, Republic of"), u"KR"),
+    (_(u"Kuwait"), u"KW"),
+    (_(u"Kyrgyzstan"), u"KG"),
+    (_(u"Lao People's Democratic Republic"), u"LA"),
+    (_(u"Latvia"), u"LV"),
+    (_(u"Lebanon"), u"LB"),
+    (_(u"Lesotho"), u"LS"),
+    (_(u"Liberia"), u"LR"),
+    (_(u"Libya"), u"LY"),
+    (_(u"Liechtenstein"), u"LI"),
+    (_(u"Lithuania"), u"LT"),
+    (_(u"Luxembourg"), u"LU"),
+    (_(u"Macao"), u"MO"),
+    (_(u"Macedonia, the Former Yugoslav Republic of"), u"MK"),
+    (_(u"Madagascar"), u"MG"),
+    (_(u"Malawi"), u"MW"),
+    (_(u"Malaysia"), u"MY"),
+    (_(u"Maldives"), u"MV"),
+    (_(u"Mali"), u"ML"),
+    (_(u"Malta"), u"MT"),
+    (_(u"Marshall Islands"), u"MH"),
+    (_(u"Martinique"), u"MQ"),
+    (_(u"Mauritania"), u"MR"),
+    (_(u"Mauritius"), u"MU"),
+    (_(u"Mayotte"), u"YT"),
+    (_(u"Mexico"), u"MX"),
+    (_(u"Micronesia, Federated States of"), u"FM"),
+    (_(u"Moldova, Republic of"), u"MD"),
+    (_(u"Monaco"), u"MC"),
+    (_(u"Mongolia"), u"MN"),
+    (_(u"Montenegro"), u"ME"),
+    (_(u"Montserrat"), u"MS"),
+    (_(u"Morocco"), u"MA"),
+    (_(u"Mozambique"), u"MZ"),
+    (_(u"Myanmar"), u"MM"),
+    (_(u"Namibia"), u"NA"),
+    (_(u"Nauru"), u"NR"),
+    (_(u"Nepal"), u"NP"),
+    (_(u"Netherlands"), u"NL"),
+    (_(u"New Caledonia"), u"NC"),
+    (_(u"New Zealand"), u"NZ"),
+    (_(u"Nicaragua"), u"NI"),
+    (_(u"Niger"), u"NE"),
+    (_(u"Nigeria"), u"NG"),
+    (_(u"Niue"), u"NU"),
+    (_(u"Norfolk Island"), u"NF"),
+    (_(u"Northern Mariana Islands"), u"MP"),
+    (_(u"Norway"), u"NO"),
+    (_(u"Oman"), u"OM"),
+    (_(u"Pakistan"), u"PK"),
+    (_(u"Palau"), u"PW"),
+    (_(u"Palestinian Territory, Occupied"), u"PS"),
+    (_(u"Panama"), u"PA"),
+    (_(u"Papua New Guinea"), u"PG"),
+    (_(u"Paraguay"), u"PY"),
+    (_(u"Peru"), u"PE"),
+    (_(u"Philippines"), u"PH"),
+    (_(u"Pitcairn"), u"PN"),
+    (_(u"Poland"), u"PL"),
+    (_(u"Portugal"), u"PT"),
+    (_(u"Puerto Rico"), u"PR"),
+    (_(u"Qatar"), u"QA"),
+    (_(u"Réunion"), u"RE"),
+    (_(u"Romania"), u"RO"),
+    (_(u"Russian Federation"), u"RU"),
+    (_(u"Rwanda"), u"RW"),
+    (_(u"Saint Barthélemy"), u"BL"),
+    (_(u"Saint Helena, Ascension and Tristan Da Cunha"), u"SH"),
+    (_(u"Saint Kitts and Nevis"), u"KN"),
+    (_(u"Saint Lucia"), u"LC"),
+    (_(u"Saint Martin (French Part)"), u"MF"),
+    (_(u"Saint Pierre and Miquelon"), u"PM"),
+    (_(u"Saint Vincent and the Grenadines"), u"VC"),
+    (_(u"Samoa"), u"WS"),
+    (_(u"San Marino"), u"SM"),
+    (_(u"Sao Tome and Principe"), u"ST"),
+    (_(u"Saudi Arabia"), u"SA"),
+    (_(u"Senegal"), u"SN"),
+    (_(u"Serbia"), u"RS"),
+    (_(u"Seychelles"), u"SC"),
+    (_(u"Sierra Leone"), u"SL"),
+    (_(u"Singapore"), u"SG"),
+    (_(u"Sint Maarten (Dutch Part)"), u"SX"),
+    (_(u"Slovakia"), u"SK"),
+    (_(u"Slovenia"), u"SI"),
+    (_(u"Solomon Islands"), u"SB"),
+    (_(u"Somalia"), u"SO"),
+    (_(u"South Africa"), u"ZA"),
+    (_(u"South Georgia and the South Sandwich Islands"), u"GS"),
+    (_(u"South Sudan"), u"SS"),
+    (_(u"Spain"), u"ES"),
+    (_(u"Sri Lanka"), u"LK"),
+    (_(u"Sudan"), u"SD"),
+    (_(u"Suriname"), u"SR"),
+    (_(u"Svalbard and Jan Mayen"), u"SJ"),
+    (_(u"Swaziland"), u"SZ"),
+    (_(u"Sweden"), u"SE"),
+    (_(u"Switzerland"), u"CH"),
+    (_(u"Syrian Arab Republic"), u"SY"),
+    (_(u"Taiwan, Province of China"), u"TW"),
+    (_(u"Tajikistan"), u"TJ"),
+    (_(u"Tanzania, United Republic of"), u"TZ"),
+    (_(u"Thailand"), u"TH"),
+    (_(u"Timor-leste"), u"TL"),
+    (_(u"Togo"), u"TG"),
+    (_(u"Tokelau"), u"TK"),
+    (_(u"Tonga"), u"TO"),
+    (_(u"Trinidad and Tobago"), u"TT"),
+    (_(u"Tunisia"), u"TN"),
+    (_(u"Turkey"), u"TR"),
+    (_(u"Turkmenistan"), u"TM"),
+    (_(u"Turks and Caicos Islands"), u"TC"),
+    (_(u"Tuvalu"), u"TV"),
+    (_(u"Uganda"), u"UG"),
+    (_(u"Ukraine"), u"UA"),
+    (_(u"United Arab Emirates"), u"AE"),
+    (_(u"United Kingdom"), u"GB"),
+    (_(u"United States"), u"US"),
+    (_(u"United States Minor Outlying Islands"), u"UM"),
+    (_(u"Uruguay"), u"UY"),
+    (_(u"Uzbekistan"), u"UZ"),
+    (_(u"Vanuatu"), u"VU"),
+    (_(u"Venezuela, Bolivarian Republic of"), u"VE"),
+    (_(u"Viet Nam"), u"VN"),
+    (_(u"Virgin Islands, British"), u"VG"),
+    (_(u"Virgin Islands, U.S."), u"VI"),
+    (_(u"Wallis and Futuna"), u"WF"),
+    (_(u"Western Sahara"), u"EH"),
+    (_(u"Yemen"), u"YE"),
+    (_(u"Zambia"), u"ZM"),
+    (_(u"Zimbabwe"), u"ZW"),
+)
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/countries.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/countries.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/countries.txt	(revision 12677)
@@ -0,0 +1,255 @@
+Received on 2012-03-23 00:50:06.989914 
+from http://www.iso.org/iso/list-en1-semic-3.txt
+-----------------
+This list states the country names (official short names in English) in alphabetical order as given in ISO 3166-1 and the corresponding ISO 3166-1-alpha-2 code elements. The list is updated whenever a change to the official code list in ISO 3166-1 is effected by the ISO 3166/MA. It lists 249 official short names and code elements. One line of text contains one entry. A country name and its code element are separated by a semicolon (;).
+
+AFGHANISTAN;AF
+ÅLAND ISLANDS;AX
+ALBANIA;AL
+ALGERIA;DZ
+AMERICAN SAMOA;AS
+ANDORRA;AD
+ANGOLA;AO
+ANGUILLA;AI
+ANTARCTICA;AQ
+ANTIGUA AND BARBUDA;AG
+ARGENTINA;AR
+ARMENIA;AM
+ARUBA;AW
+AUSTRALIA;AU
+AUSTRIA;AT
+AZERBAIJAN;AZ
+BAHAMAS;BS
+BAHRAIN;BH
+BANGLADESH;BD
+BARBADOS;BB
+BELARUS;BY
+BELGIUM;BE
+BELIZE;BZ
+BENIN;BJ
+BERMUDA;BM
+BHUTAN;BT
+BOLIVIA, PLURINATIONAL STATE OF;BO
+BONAIRE, SINT EUSTATIUS AND SABA;BQ
+BOSNIA AND HERZEGOVINA;BA
+BOTSWANA;BW
+BOUVET ISLAND;BV
+BRAZIL;BR
+BRITISH INDIAN OCEAN TERRITORY;IO
+BRUNEI DARUSSALAM;BN
+BULGARIA;BG
+BURKINA FASO;BF
+BURUNDI;BI
+CAMBODIA;KH
+CAMEROON;CM
+CANADA;CA
+CAPE VERDE;CV
+CAYMAN ISLANDS;KY
+CENTRAL AFRICAN REPUBLIC;CF
+CHAD;TD
+CHILE;CL
+CHINA;CN
+CHRISTMAS ISLAND;CX
+COCOS (KEELING) ISLANDS;CC
+COLOMBIA;CO
+COMOROS;KM
+CONGO;CG
+CONGO, THE DEMOCRATIC REPUBLIC OF THE;CD
+COOK ISLANDS;CK
+COSTA RICA;CR
+CÔTE D'IVOIRE;CI
+CROATIA;HR
+CUBA;CU
+CURAÇAO;CW
+CYPRUS;CY
+CZECH REPUBLIC;CZ
+DENMARK;DK
+DJIBOUTI;DJ
+DOMINICA;DM
+DOMINICAN REPUBLIC;DO
+ECUADOR;EC
+EGYPT;EG
+EL SALVADOR;SV
+EQUATORIAL GUINEA;GQ
+ERITREA;ER
+ESTONIA;EE
+ETHIOPIA;ET
+FALKLAND ISLANDS (MALVINAS);FK
+FAROE ISLANDS;FO
+FIJI;FJ
+FINLAND;FI
+FRANCE;FR
+FRENCH GUIANA;GF
+FRENCH POLYNESIA;PF
+FRENCH SOUTHERN TERRITORIES;TF
+GABON;GA
+GAMBIA;GM
+GEORGIA;GE
+GERMANY;DE
+GHANA;GH
+GIBRALTAR;GI
+GREECE;GR
+GREENLAND;GL
+GRENADA;GD
+GUADELOUPE;GP
+GUAM;GU
+GUATEMALA;GT
+GUERNSEY;GG
+GUINEA;GN
+GUINEA-BISSAU;GW
+GUYANA;GY
+HAITI;HT
+HEARD ISLAND AND MCDONALD ISLANDS;HM
+HOLY SEE (VATICAN CITY STATE);VA
+HONDURAS;HN
+HONG KONG;HK
+HUNGARY;HU
+ICELAND;IS
+INDIA;IN
+INDONESIA;ID
+IRAN, ISLAMIC REPUBLIC OF;IR
+IRAQ;IQ
+IRELAND;IE
+ISLE OF MAN;IM
+ISRAEL;IL
+ITALY;IT
+JAMAICA;JM
+JAPAN;JP
+JERSEY;JE
+JORDAN;JO
+KAZAKHSTAN;KZ
+KENYA;KE
+KIRIBATI;KI
+KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF;KP
+KOREA, REPUBLIC OF;KR
+KUWAIT;KW
+KYRGYZSTAN;KG
+LAO PEOPLE'S DEMOCRATIC REPUBLIC;LA
+LATVIA;LV
+LEBANON;LB
+LESOTHO;LS
+LIBERIA;LR
+LIBYA;LY
+LIECHTENSTEIN;LI
+LITHUANIA;LT
+LUXEMBOURG;LU
+MACAO;MO
+MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF;MK
+MADAGASCAR;MG
+MALAWI;MW
+MALAYSIA;MY
+MALDIVES;MV
+MALI;ML
+MALTA;MT
+MARSHALL ISLANDS;MH
+MARTINIQUE;MQ
+MAURITANIA;MR
+MAURITIUS;MU
+MAYOTTE;YT
+MEXICO;MX
+MICRONESIA, FEDERATED STATES OF;FM
+MOLDOVA, REPUBLIC OF;MD
+MONACO;MC
+MONGOLIA;MN
+MONTENEGRO;ME
+MONTSERRAT;MS
+MOROCCO;MA
+MOZAMBIQUE;MZ
+MYANMAR;MM
+NAMIBIA;NA
+NAURU;NR
+NEPAL;NP
+NETHERLANDS;NL
+NEW CALEDONIA;NC
+NEW ZEALAND;NZ
+NICARAGUA;NI
+NIGER;NE
+NIGERIA;NG
+NIUE;NU
+NORFOLK ISLAND;NF
+NORTHERN MARIANA ISLANDS;MP
+NORWAY;NO
+OMAN;OM
+PAKISTAN;PK
+PALAU;PW
+PALESTINIAN TERRITORY, OCCUPIED;PS
+PANAMA;PA
+PAPUA NEW GUINEA;PG
+PARAGUAY;PY
+PERU;PE
+PHILIPPINES;PH
+PITCAIRN;PN
+POLAND;PL
+PORTUGAL;PT
+PUERTO RICO;PR
+QATAR;QA
+RÉUNION;RE
+ROMANIA;RO
+RUSSIAN FEDERATION;RU
+RWANDA;RW
+SAINT BARTHÉLEMY;BL
+SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA;SH
+SAINT KITTS AND NEVIS;KN
+SAINT LUCIA;LC
+SAINT MARTIN (FRENCH PART);MF
+SAINT PIERRE AND MIQUELON;PM
+SAINT VINCENT AND THE GRENADINES;VC
+SAMOA;WS
+SAN MARINO;SM
+SAO TOME AND PRINCIPE;ST
+SAUDI ARABIA;SA
+SENEGAL;SN
+SERBIA;RS
+SEYCHELLES;SC
+SIERRA LEONE;SL
+SINGAPORE;SG
+SINT MAARTEN (DUTCH PART);SX
+SLOVAKIA;SK
+SLOVENIA;SI
+SOLOMON ISLANDS;SB
+SOMALIA;SO
+SOUTH AFRICA;ZA
+SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS;GS
+SOUTH SUDAN;SS
+SPAIN;ES
+SRI LANKA;LK
+SUDAN;SD
+SURINAME;SR
+SVALBARD AND JAN MAYEN;SJ
+SWAZILAND;SZ
+SWEDEN;SE
+SWITZERLAND;CH
+SYRIAN ARAB REPUBLIC;SY
+TAIWAN, PROVINCE OF CHINA;TW
+TAJIKISTAN;TJ
+TANZANIA, UNITED REPUBLIC OF;TZ
+THAILAND;TH
+TIMOR-LESTE;TL
+TOGO;TG
+TOKELAU;TK
+TONGA;TO
+TRINIDAD AND TOBAGO;TT
+TUNISIA;TN
+TURKEY;TR
+TURKMENISTAN;TM
+TURKS AND CAICOS ISLANDS;TC
+TUVALU;TV
+UGANDA;UG
+UKRAINE;UA
+UNITED ARAB EMIRATES;AE
+UNITED KINGDOM;GB
+UNITED STATES;US
+UNITED STATES MINOR OUTLYING ISLANDS;UM
+URUGUAY;UY
+UZBEKISTAN;UZ
+VANUATU;VU
+VENEZUELA, BOLIVARIAN REPUBLIC OF;VE
+VIET NAM;VN
+VIRGIN ISLANDS, BRITISH;VG
+VIRGIN ISLANDS, U.S.;VI
+WALLIS AND FUTUNA;WF
+WESTERN SAHARA;EH
+YEMEN;YE
+ZAMBIA;ZM
+ZIMBABWE;ZW
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/country_helpers.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/country_helpers.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/country_helpers.py	(revision 12677)
@@ -0,0 +1,107 @@
+"""Tools to create country lists.
+
+Helpers that can retrieve files from ISO servers with current ISO
+codes and country names.
+
+Also helpers for turning this lists into a runnable Python module
+(countries.py) are provided.
+
+These helpers are meant to be used from the debug schell. You can use
+them like this::
+
+  $ ./bin/ikobactl debug
+  >>> from waeup.ikoba.utils.country_helpers import update_list
+  >>> from waeup.ikoba.utils.country_helpers import update_module
+  >>> update_list()
+  >>> update_module()
+
+This will retrieve a current list of countries and their ISO codes
+from iso-committee servers, store the file locally (countries.txt) and
+afterwards compile a valid Python module (countries.py) out of the
+list.
+
+"""
+import datetime
+import os
+import urllib
+
+SRC_URL = 'http://www.iso.org/iso/list-en1-semic-3.txt'
+COUNTRY_LIST = os.path.join(os.path.dirname(__file__), 'countries.txt')
+COUNTRY_MODULE = os.path.join(os.path.dirname(__file__), 'countries.py')
+
+TEMPLATE = """# -*- coding: utf-8 -*-
+#
+# This file was automatically generated. Please do not edit
+#
+from waeup.ikoba.interfaces import MessageFactory as _
+
+COUNTRIES = ${COUNTRY_LIST}
+"""
+
+def mangle_country_name(name):
+    """Turn uppercase country names into capitalized.
+
+    Some special cases are handled as well ('of', 'the', etc.)
+    """
+    name = name.replace('(', '( ') # handle letters following brackets
+    name = ' '.join([x.capitalize() for x in name.split(' ')])
+    name = name.replace('( ', '(')
+    for term in ['Of', ' And', 'The']:
+        name = name.replace(term, term.lower())
+    name = name.replace('U.s.', 'U.S.')
+    name = name.replace("D'i", "d'I") # Cote d'Ivoire
+    return name
+
+def update_list():
+    """Update the local country.txt list.
+
+    File is received from iso-servers.
+    """
+    print "Receiving list from %s" % SRC_URL
+    contents = urllib.urlopen(SRC_URL).read()
+    print "Storing list at %s" % COUNTRY_LIST
+    fp = open(COUNTRY_LIST, 'wb')
+    fp.write('Received on %s \r\nfrom %s\r\n-----------------\r\n' % (
+        datetime.datetime.now(), SRC_URL))
+    fp.write(contents)
+    fp.close()
+    print "Done."
+    return
+
+def mangle_list():
+    """Read country list and extract countries and their ISO-codes.
+
+    Returns an iterable of tuples (<COUNTRY>, <ISO-CODE>).
+    """
+    lines = open(COUNTRY_LIST, 'rb').readlines()
+    read_header = False
+    for num, line in enumerate(lines):
+        if read_header is False and line != '\r\n':
+            continue
+        if read_header is False:
+            read_header = True
+            continue
+        if line == '\r\n':
+            continue
+        line = line.decode('iso-8859-1').strip()
+        try:
+            country, code = line.split(';', 1)
+        except:
+            print "Could not process: line %s:", num, line
+            continue
+        yield mangle_country_name(country), code
+
+def update_module(template=TEMPLATE, module_path=COUNTRY_MODULE):
+    """Update local countries.py with data from countries.txt
+    """
+    print "Parsing countries and their ISO codes..."
+    c_list = "(\n"
+    for country, code in mangle_list():
+        c_list = c_list + u'    (_(u"%s"), u"%s"),\n' % (country, code)
+    c_list += ')\n'
+    contents = template.replace('${COUNTRY_LIST}', c_list)
+    contents = contents.encode('utf-8')
+    print "Write new module:", module_path
+    open(module_path, 'wb').write(contents)
+    print "Done."
+    return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/helpers.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/helpers.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/helpers.py	(revision 12677)
@@ -0,0 +1,868 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""General helper functions for Ikoba.
+"""
+import unicodecsv as csv  # XXX: csv ops should move to dedicated module.
+import datetime
+import imghdr
+import logging
+import os
+import pytz
+import re
+import shutil
+import tempfile
+import grok
+from cStringIO import StringIO
+from docutils.core import publish_string
+from zope.i18n import translate
+from zope.component import getUtility
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.interface.interface import Method, Attribute
+from zope.schema import getFieldNames
+from zope.schema.fieldproperty import FieldProperty
+from zope.security.interfaces import NoInteraction
+from zope.security.management import getInteraction
+from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from zope.formlib.widget import renderElement
+from waeup.ikoba.interfaces import MessageFactory as _
+
+BUFSIZE = 8 * 1024
+
+
+def remove_file_or_directory(filepath):
+    """Remove a file or directory.
+
+    Different to :func:`shutil.rmtree` we also accept not existing
+    paths (returning silently) and if a dir turns out to be a regular
+    file, we remove that.
+    """
+    filepath = os.path.abspath(filepath)
+    if not os.path.exists(filepath):
+        return
+    if os.path.isdir(filepath):
+        shutil.rmtree(filepath)
+    else:
+        os.unlink(filepath)
+    return
+
+
+def copy_filesystem_tree(src, dst, overwrite=False, del_old=False):
+    """Copy contents of directory src to directory dst.
+
+    Both directories must exists.
+
+    If `overwrite` is true, any same named objects will be
+    overwritten. Otherwise these files will not be touched.
+
+    If `del_old` is true, copied files and directories will be removed
+    from the src directory.
+
+    This functions returns a list of non-copied files.
+
+    Unix hidden files and directories (starting with '.') are not
+    processed by this function.
+    """
+    if not os.path.exists(src):
+        raise ValueError('source path does not exist: %s' % src)
+    if not os.path.exists(dst):
+        raise ValueError('destination path does not exist: %s' % dst)
+    if not os.path.isdir(src):
+        raise ValueError('source path is not a directory: %s' % src)
+    if not os.path.isdir(dst):
+        raise ValueError('destination path is not a directory: %s' % dst)
+    not_copied = []
+    for item in os.listdir(src):
+        if item.startswith('.'):
+            continue  # We do not copy hidden stuff...
+        itemsrc = os.path.join(src, item)
+        itemdst = os.path.join(dst, item)
+
+        if os.path.exists(itemdst):
+            if overwrite is True:
+                remove_file_or_directory(itemdst)
+            else:
+                not_copied.append(item)
+                continue
+
+        if os.path.isdir(itemsrc):
+            shutil.copytree(itemsrc, itemdst)
+        else:
+            shutil.copy2(itemsrc, itemdst)
+        if del_old:
+            remove_file_or_directory(itemsrc)
+    return not_copied
+
+
+def get_inner_HTML_part(html_code):
+    """Return the 'inner' part of a complete HTML snippet.
+
+    If there is a form part, get this.
+
+    If there is no form part, try to return the body part contents.
+
+    If there is no body, return as-is.
+
+    Let's see how that works. If we deliver some doc with form, we
+    will get that form only:
+
+       >>> doc = '<html><form>My Form</form>Outside the form</html>'
+       >>> get_inner_HTML_part(doc)
+       '<form>My Form</form>'
+
+    No form? Then seek for a body part and get the contents:
+
+       >>> doc = '<html><body>My Body</body>Trailing Trash</html>'
+       >>> get_inner_HTML_part(doc)
+       'My Body'
+
+    If none of these is included, return what we got:
+
+       >>> doc = '<html>without body nor form</html>'
+       >>> get_inner_HTML_part(doc)
+       '<html>without body nor form</html>'
+
+    """
+
+    try:
+        result = re.match('^.+(<form[^\>]*>.*</form>).+$', html_code,
+                          re.DOTALL).groups()[0]
+        return result
+    except AttributeError:
+        # No <form> part included
+        try:
+            result = re.match('^.+<body[^\>]*>(.*)</body>.*$', html_code,
+                              re.DOTALL).groups()[0]
+            return result
+        except AttributeError:
+            # No <form> and no <body> tag...
+            pass
+    return html_code
+
+
+class FactoryBase(grok.GlobalUtility):
+    """A factory for things.
+
+    This is a baseclass for easier creation of factories. Factories
+    are utilities that are registered under a certain name and return
+    instances of certain classes when called.
+
+    In :mod:`waeup.ikoba` we use factories extensively for
+    batching. While processing a batch some processors looks up a
+    factory to create real-world instances that then get filled with
+    data from imported CSV files.
+
+    To get rid of reimplementing the same stuff over and over again,
+    most notably the methods defined here, we offer this base class
+    (which will *not* be registered as a factory itself).
+
+    Real factories can then be created like this:
+
+       >>> import grok
+       >>> from waeup.ikoba.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.ikoba.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.ikoba.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.ikoba`
+    system.
+
+       >>> grok.testing.grok('waeup.ikoba.utils.helpers')
+       >>> grok.testing.grok_component(
+       ...    'MyObjectFactory', MyObjectFactory
+       ...  )
+       True
+
+    After grokking we (and processors) can create objects without
+    knowing about the location of the real class definition, just by
+    the factory name:
+
+       >>> from zope.component import createObject
+       >>> obj = createObject('waeup.ikoba.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.ikoba.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 e.g. 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.ikoba.utils.helpers import ReST2HTML
+        >>> source = '''
+        ... Headline
+        ... ========
+        ...
+        ... - A list item
+        ... - Another item
+        ...
+        ... Thanks for watching!
+        ... '''
+        >>> html, warnings = ReST2HTML_w_warnings(source)
+        >>> print html
+        <div class="document" id="headline">
+        <h1 class="title">Headline</h1>
+        <BLANKLINE>
+        <ul class="simple">
+        <li>A list item</li>
+        <li>Another item</li>
+        </ul>
+        <p>Thanks for watching!</p>
+        </div>
+
+    Here no warnings happened, so the `warnings` are ``None``:
+
+        >>> warnings is None
+        True
+
+    If warnings happen then they can be retrieved in the returned
+    ``warnings``. We try to render an erraneous document:
+
+        >>> source = '''
+        ... Headline
+        ... ======
+        ...
+        ... Thanks for watching!
+        ... '''
+        >>> html, warnings = ReST2HTML_w_warnings(source)
+        >>> print html
+        <div class="document" id="headline">
+        <h1 class="title">Headline</h1>
+        <BLANKLINE>
+        <p>Thanks for watching!</p>
+        </div>
+
+        >>> print warnings
+        <string>:3: (WARNING/2) Title underline too short.
+        <BLANKLINE>
+        Headline
+        ======
+        <BLANKLINE>
+
+    As you can see, the warnings are not displayed inline the document
+    but can be retrieved from the returned warnings, which is a string
+    or ``None``.
+    """
+    warnings = StringIO()
+    fulldoc = publish_string(
+        source_string, writer_name='html4css1',
+        settings_overrides={
+            'report_level': 0,
+            'warning_stream': warnings,
+            })
+    warnings.seek(0)
+    warning_msgs = warnings.read()
+    if warning_msgs:
+        # Render again, this time with no warnings inline...
+        fulldoc = publish_string(
+        source_string, writer_name='html4css1',
+        settings_overrides={
+            'report_level': 10000,
+            'halt_level': 10000,
+            'warning_stream': warnings,
+            })
+    if warning_msgs == '':
+        warning_msgs = None
+    result = get_inner_HTML_part(fulldoc).strip()
+    if not isinstance(result, unicode):
+        result = result.decode('utf-8')
+    return result, warning_msgs
+
+
+def ReST2HTML(source_string):
+    """Render a string containing ReStructuredText to HTML.
+
+    Any warnings about too short headings, etc. are silently
+    discarded. Use :func:`ReST2HTML_w_warnings` if you want to get any
+    warnings.
+
+    The returned string will be unicode.
+
+    A regular document will be rendered like this:
+
+        >>> source = '''
+        ... Headline
+        ... ========
+        ...
+        ... Thanks for watching!
+        ... '''
+        >>> html = ReST2HTML(source)
+        >>> print html
+        <div class="document" id="headline">
+        <h1 class="title">Headline</h1>
+        <BLANKLINE>
+        <p>Thanks for watching!</p>
+        </div>
+
+    A document with markup problems (here: the underline is too short)
+    will look similar:
+
+        >>> source = '''
+        ... Headline
+        ... ======
+        ...
+        ... Thanks for watching!
+        ... '''
+        >>> html = ReST2HTML(source)
+        >>> print html
+        <div class="document" id="headline">
+        <h1 class="title">Headline</h1>
+        <BLANKLINE>
+        <p>Thanks for watching!</p>
+        </div>
+
+    """
+    html, warnings = ReST2HTML_w_warnings(source_string)
+    return html
+
+
+def attrs_to_fields(cls, omit=[]):
+    """Turn the attributes of a class into FieldProperty instances.
+
+    With Python >= 2.6 we can even use this function as a class decorator.
+
+    `omit` is a list of field names that should _not_ be turned into
+    field properties. This is useful for properties and the like.
+    """
+    iface = list(implementedBy(cls))[0]
+    for field_name in getFieldNames(iface):
+        if field_name in omit:
+            continue
+        field_property = FieldProperty(iface[field_name])
+        # Set proper docstring for the API docs.
+        field_property.__doc__ = iface[field_name].title + ' (computed attribute)'
+        setattr(cls, field_name, field_property)
+    return cls
+
+
+def get_current_principal():
+    """Get the 'current' principal.
+
+    This method works without a request. Examining a request is the
+    regular (and recommended) way to get a principal involved
+    'currently'.
+
+    Use this method only if you really have no access to the current
+    request.
+
+    Returns ``None`` when no principal is involved (for instance
+    during tests).
+    """
+    try:
+        principal = getInteraction().participations[0].principal
+    except NoInteraction:
+        return None
+    except IndexError:  # No participations present
+        return None
+    return principal
+
+
+def cmp_files(file_descr1, file_descr2):
+    """Compare two files by their file descriptors.
+
+    Returns ``True`` if both are equal, ``False`` otherwise.
+    """
+    file_descr1.seek(0)
+    file_descr2.seek(0)
+    while True:
+        b1 = file_descr1.read(BUFSIZE)
+        b2 = file_descr2.read(BUFSIZE)
+        if b1 != b2:
+            return False
+        if not b1:
+            return True
+
+
+def string_from_bytes(number):
+    """Turn a number into some textual representation.
+
+      Examples:
+
+        >>> string_from_bytes(1)
+        u'1 byte(s)'
+
+        >>> string_from_bytes(1025)
+        u'1 KB'
+
+        >>> string_from_bytes(1.5 * 1024*1024)
+        u'1.50 MB'
+
+        >>> string_from_bytes(673.286 * 1024**3)
+        u'673.29 GB'
+
+    """
+    if number < 1024:
+        return u'%s byte(s)' % (str(number),)
+    elif number < 1024 ** 2:
+        return u'%s KB' % (number / 1024,)
+    elif number < 1024 ** 3:
+        return u'%.2f MB' % (number / 1024 ** 2,)
+    return u'%.2f GB' % (number / 1024 ** 3,)
+
+
+def file_size(file_like_obj):
+    """Determine file size in most effective manner.
+
+    Returns the number of bytes in a file. This function works for
+    both, real files as well as file-like objects like cStringIO based
+    'files'.
+
+    Example:
+
+      >>> from cStringIO import StringIO
+      >>> file_size(StringIO('my file content'))
+      15
+
+    Please note that this function expects the file-like object passed
+    in to be at first reading position (it does no seek(0)) and that
+    when finished the file pointer might be at end of file.
+    """
+    if hasattr(file_like_obj, 'fileno'):
+        return os.fstat(file_like_obj.fileno())[6]
+    file_like_obj.seek(0, 2)  # seek to last position in file
+    return file_like_obj.tell()
+
+
+def get_user_account(request):
+    """Return local user account.
+    """
+    principal_id = request.principal.id
+    authenticator = getUtility(IAuthenticatorPlugin, name='users')
+    account = authenticator.getAccount(principal_id)
+    return account
+
+
+def iface_names(iface, omit=[], exclude_attribs=True, exclude_methods=True):
+    """Get all attribute names of an interface.
+
+    Searches also base interfaces.
+
+    Names of fields that are pure attributes
+    (i.e. zope.interface.Attribute) or methods are excluded by
+    default.
+
+    Names of typical fields derived from zope.schema are included.
+
+    The `omit` paramter can give a list of names to exclude.
+
+    Returns an unsorted list of strings.
+    """
+    ifaces = set((iface,))
+    # Collect all interfaces (also bases) recursively
+    while True:
+        ext_ifaces = set(ifaces)
+        for iface in ext_ifaces:
+            ext_ifaces = set.union(ext_ifaces, set(iface.getBases()))
+        if ext_ifaces == ifaces:
+            # No new interfaces found, list complete
+            break
+        ifaces = ext_ifaces
+    # Collect (filtered) names of collected interfaces
+    result = []
+    for iface in ifaces:
+        for name, descr in iface.namesAndDescriptions():
+            if name in omit:
+                continue
+            if exclude_attribs and descr.__class__ is Attribute:
+                continue
+            if exclude_methods and isinstance(descr, Method):
+                continue
+            if name in result:
+                continue
+            result.append(name)
+    return result
+
+
+def get_sorted_preferred(tuples_iterable, preferred_list):
+    """Get a list of tuples (<TITLE>,<TOKEN>) with values in
+    `preferred_list` put in front.
+
+    The rest of the tuples iterable is returned in orginal order. This
+    is useful for putting default entries on top of (already sorted)
+    lists of choice values, for instance when sorting countries and
+    their code.
+
+    Sample:
+
+    We have a list of tuples with uppercase 'titles' and lowercase
+    'tokens'. This list is already sorted but we want certain values
+    of this list to show up before other values. For instance we want
+    to see the 'C' entry to come first.
+
+      >>> get_sorted_preferred([('A','a'), ('B','b'), ('C','c')],
+      ...                       ['c'])
+      (('C', 'c'), ('A', 'a'), ('B', 'b'))
+
+    i.e. the entry with 'c' as second value moved to head of result.
+
+    We can also require multiple entries at head of list:
+
+      >>> get_sorted_preferred([('A','a'), ('B','b'), ('C','c')],
+      ...                       ['b', 'c'])
+      (('B', 'b'), ('C', 'c'), ('A', 'a'))
+
+    We required the 'b' entry to come before the 'c' entry and then
+    the rest of the input list. That's what we got.
+
+    The result is returned as a tuple of tuples to keep order of values.
+    """
+    result = [None for x in preferred_list]
+    for title, code in tuples_iterable:
+        if code in preferred_list:
+            index = preferred_list.index(code)
+            result[index] = (title, code)
+        else:
+            result.append((title, code))
+    return tuple(result)
+
+
+def now(tz=None):
+    """Get current datetime in timezone of `tz`.
+
+    If `tz`, a `tzinfo` instance, is None, UTC time is returned.
+
+    `tz` should be a timezone as defined in pytz.
+    """
+    return to_timezone(datetime.datetime.utcnow(), tz=tz)
+
+
+def to_timezone(dt, tz=None):
+    """Shift datetime into timezone `tz`.
+
+    If datetime `dt` contains no `tzinfo` (i.e. it is 'naive'), it is
+    assumed to be UTC.
+
+    If no `tz` is given, shift to UTC is performed.
+
+    If `dt` is not a datetime.datetime, the input value is returned
+    unchanged.
+    """
+    if not isinstance(dt, datetime.datetime):
+        return dt
+    if tz is None:
+        tz = pytz.utc
+    if dt.tzinfo is None:
+        dt = pytz.utc.localize(dt)
+    return tz.normalize(dt.tzinfo.normalize(dt).astimezone(tz))
+
+
+def imghdr_test_fpm(h, f):
+    """FPM fileformat test.
+
+    The `fpm` fileformat is the binary fingerprint data as created by
+    `libfprint`.
+    """
+    if len(h) >= 3 and h[:3] == 'FP1':
+        return 'fpm'
+
+
+#: Add test function in stdlib's imghdr tests.
+imghdr.tests.append(imghdr_test_fpm)
+
+
+def get_fileformat(path, bytestream=None):
+    """Try to determine the file format of a given media file.
+
+    Although checks done here are not done very thoroughly, they make
+    no assumptions about the filetype by looking at its filename
+    extension or similar. Instead they check header data to comply
+    with common known rules (Magic Words).
+
+    If bytestream is not `None` the `path` is ignored.
+
+    Returns filetype as string (something like ``'jpg'``) if
+    file-format can be recognized, ``None`` else.
+
+    Tested recognized filetypes currently are `jpg`, `png`, `fpm`, and
+    `pdf`.
+
+    More filetypes (though untested in waeup.ikoba) are automatically
+    recognized because we deploy the stdlib `imghdr` library. See this
+    module's docs for a complete list of filetypes recognized.
+    """
+    if path is None and bytestream is None:
+        return None
+
+    img_type = None
+    if bytestream is not None:
+        img_type = imghdr.what(path, bytestream)
+    else:
+        img_type = imghdr.what(path)
+    for name, replacement in (('jpeg', 'jpg'), ('tiff', 'tif')):
+        if img_type == name:
+            img_type = replacement
+    return img_type
+
+
+def check_pdf(bytestream, file):
+    """Tell whether a file or bytestream is a PDF file.
+
+    Works as a test/plugin for the stdlib `imghdr` library.
+    """
+    if file is not None:
+        file.seek(0)
+        bytestream = file.read(4)
+        file.seek(0)
+
+    if bytestream.startswith('%PDF'):
+        return 'pdf'
+    return None
+
+# register check_pdf as header check function with `imghdr`
+if check_pdf not in imghdr.tests:
+    imghdr.tests.append(check_pdf)
+
+
+def merge_csv_files(path1, path2):
+    """Merge two CSV files into one (appending).
+
+    CSV data from `path2` will be merged into `path1` csv file. This
+    is a bit like 'appending' data from path2 to data from path1.
+
+    The path of the resulting temporary file will be returned.
+
+    In the result file data from `path2` will always come _after_ data
+    from `path1`.
+
+    **Caution**: It is the _callers_ responsibility to remove the
+    result file (which is created by tempfile.mkstemp) after usage.
+
+    This CSV file merging copes with different column orders in both
+    CSV files and even with different column sets in both files.
+
+    Also broken/empty CSV files can be handled.
+    """
+    # sniff the col names
+    try:
+        row10 = csv.DictReader(open(path1, 'rb')).next()
+    except StopIteration:
+        row10 = dict()
+    try:
+        row20 = csv.DictReader(open(path2, 'rb')).next()
+    except StopIteration:
+        row20 = dict()
+    fieldnames = sorted(list(set(row10.keys() + row20.keys())))
+    # now read/write the real data
+    reader1 = csv.DictReader(open(path1, 'rb'))
+    reader2 = csv.DictReader(open(path2, 'rb'))
+    wp, tmp_path = tempfile.mkstemp()
+    writer = csv.DictWriter(os.fdopen(wp, 'wb'), fieldnames)
+    writer.writerow(dict((x, x) for x in fieldnames))  # header
+    for row in reader1:
+        writer.writerow(row)
+    for row in reader2:
+        writer.writerow(row)
+    return tmp_path
+
+
+def product(sequence, start=1):
+    """Returns the product of a sequence of numbers (_not_ strings)
+    multiplied by the parameter `start` (defaults to 1). If the
+    sequence is empty, returns 0.
+    """
+    if not len(sequence):
+        return 0
+    result = start
+    for item in sequence:
+        result *= item
+    return result
+
+
+class NullHandler(logging.Handler):
+    """A logging NullHandler.
+
+    Does not log anything. Useful if you want to shut up a log.
+
+    Defined here for backwards compatibility with Python < 2.7.
+    """
+    def emit(self, record):
+        pass
+
+
+def check_csv_charset(iterable):
+    """Check contents of `iterable` regarding valid CSV encoding.
+
+    `iterable` is expected to be an iterable on _rows_ (not
+    chars). This is true for instance for
+    filehandlers. `zope.publisher.browser.FileUpload` instances are
+    _not_ iterable, unfortunately.
+
+    Returns line num of first illegal char or ``None``. Line nums
+    start counting with 1 (not zero).
+    """
+    linenum = 1
+    reader = csv.DictReader(iterable)
+    try:
+        for row in reader:
+            linenum += 1
+    except UnicodeDecodeError:
+        return linenum
+    except:
+        return linenum + 1
+    return None
+
+
+class MemInfo(dict):
+    """A dict with access to its items like if they are attributes.
+    """
+    __getattr__ = dict.__getitem__
+    __setattr__ = dict.__setitem__
+    __delattr__ = dict.__delitem__
+
+
+def get_meminfo(src="/proc/meminfo"):
+    """Get local memory info as provided in /proc/meminfo.
+
+    Entries in /proc/meminfo are available as MemInfo attributes.
+
+    By default we lookup a file /proc/meminfo. Another path can be
+    lines = open(src, 'r').read()passed in as `src` parameter. In this
+    case `src` must be a regular file and contain meminfo-style data.
+
+    If the given `src` (or `/proc/meminfo`) are not available, `None`
+    lines = open(src, 'r').read()is returned.
+    """
+    if not os.path.isfile(src):
+        return None
+    lines = open(src, 'r').read().splitlines()
+    result = MemInfo()
+    for line in lines:
+        key, value = line.split(':', 1)
+        value = int(value.split(' kB', 1)[0])
+        result[key] = value
+    return result
+
+def html2dict(value=None,portal_language='en'):
+    """Transforms a localized HTML text string into a dictionary.
+
+    Different languages must be separated by `>>xy<<` whereas
+    xy is the language code. Text parts without correct leading
+    language separator - usually the first part has no language
+    descriptor - are interpreted as texts in the portal's language.
+    The latter can be configured in waeup.srp.utils.utils.IkobaUtils.
+    """
+    try:
+        parts = value.split('>>')
+    except:
+        return {}
+    elements = {}
+    lang = portal_language
+    for part in parts:
+        if part[2:4] == u'<<':
+            lang = str(part[0:2].lower())
+            text = part[4:]
+            elements[lang] = renderElement(u'div id="html"',
+                contents=text)
+        else:
+            text = part
+            elements[lang] = renderElement(u'div id="html"',
+                contents=text)
+    return elements
+
+def rest2dict(value=None,portal_language='en'):
+    """Transforms a localized REST text string into a dictionary.
+
+    Different languages must be separated by `>>xy<<` whereas
+    xy is the language code. Text parts without correct leading
+    language separator - usually the first part has no language
+    descriptor - are interpreted as texts in the portal's language.
+    The latter can be configured in waeup.srp.utils.utils.IkobaUtils.
+    """
+    try:
+        parts = value.split('>>')
+    except:
+        return {}
+    elements = {}
+    lang = portal_language
+    for part in parts:
+        if part[2:4] == u'<<':
+            lang = str(part[0:2].lower())
+            text = part[4:]
+            elements[lang] = renderElement(u'div id="rest"',
+                contents=ReST2HTML(text))
+        else:
+            text = part
+            elements[lang] = renderElement(u'div id="rest"',
+                contents=ReST2HTML(text))
+    return elements
+
+def format_date(dateobj, request):
+    if isinstance(dateobj, datetime.date):
+        return dateobj.strftime("%d/%m/%Y")
+    else:
+        return translate(_('indefinite'), context=request)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/helpers.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/helpers.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/helpers.txt	(revision 12677)
@@ -0,0 +1,228 @@
+:mod:`waeup.ikoba.utils.helpers` -- Helpers for Ikoba
+***************************************************
+
+.. module:: waeup.ikoba.utils.helpers
+
+Helper functions for Ikoba.
+
+.. :doctest:
+
+:func:`remove_file_or_directory`
+================================
+
+.. function:: remove_file_or_directory(path)
+
+   Removes a file or directory given by a path. We can remove files:
+
+     >>> import os
+     >>> import tempfile
+     >>> old_location = os.getcwd()
+     >>> new_location = tempfile.mkdtemp()
+     >>> os.chdir(new_location)
+
+     >>> from waeup.ikoba.utils.helpers import remove_file_or_directory
+     >>> open('blah', 'wb').write('nonsense')
+     >>> 'blah' in os.listdir('.')
+     True
+
+     >>> remove_file_or_directory('blah')
+     >>> 'blah' in os.listdir('.')
+     False
+
+   We can remove directories:
+
+     >>> os.mkdir('blah')
+     >>> 'blah' in os.listdir('.')
+     True
+
+     >>> remove_file_or_directory('blah')
+     >>> 'blah' in os.listdir('.')
+     False
+
+
+:func:`copy_filesystem_tree`
+============================
+
+.. function:: ccopy_filesystem_tree(src_path, dst_path[, overwrite=False[, del_old=False]])
+
+   Copies the contents of an (existing) directory to another
+   (existing) directory.
+
+   :param src_path: filesystem path to copy from
+   :type  src_path: string
+   :param dst_path: filesystem path to copy to
+   :type  dst_path: string
+   :keyword overwrite: Whether exiting files with same names should be
+                     overwritten.
+   :type  overwrite: bool
+   :keyword del_old: Whether old contents in destination path should be 
+                   removed.
+   :type  del_old: bool
+   :return: List of non-copied files
+ 
+   Both directories must exist.
+
+   Unix hidden files and directories (starting with '.') are not
+   processed by this function.
+
+   Without any further parameters, we can copy complete file trees:
+
+     >>> os.mkdir('src')
+     >>> os.mkdir('dst')
+     >>> open(os.path.join('src', 'blah'), 'wb').write('nonsense')
+
+     >>> from waeup.ikoba.utils.helpers import copy_filesystem_tree
+     >>> result = copy_filesystem_tree('src', 'dst')
+
+   As a result we get a list of non-copied files:
+
+     >>> result
+     []
+
+   The created file was indeed copied:
+
+     >>> 'blah' in os.listdir('dst')
+     True
+
+   Hidden files (i.e. such starting with a dot) are not copied:
+
+     >>> open(os.path.join('src', '.blah'), 'wb').write('nonsense')
+     >>> result = copy_filesystem_tree('src', 'dst')
+     >>> '.blah' in os.listdir('dst')
+     False
+
+   This function supports some keyword parameters as explained below.
+
+Using ``overwrite``
+-------------------
+
+Boolean. If set to ``True``, any existing and same named files and
+directories in the destination dir are overwritten with copies from
+the source. Default is `False`.
+
+Normally, existing same named files in the destination are not
+overwritten:
+
+    >>> open(os.path.join('src', 'blah'), 'wb').write('newnonsense')
+    >>> result = copy_filesystem_tree('src', 'dst')
+    >>> open(os.path.join('dst', 'blah'), 'rb').read()
+    'nonsense'
+
+Instead the filename is added to the result (a list of non-copied
+files):
+
+    >>> result
+    ['blah']
+
+If, however, we use `overwrite`, the existing file will be
+overwritten:
+
+    >>> result = copy_filesystem_tree('src', 'dst', overwrite=True)
+    >>> open(os.path.join('dst', 'blah'), 'rb').read()
+    'newnonsense'
+
+    >>> result
+    []
+
+This also works for complete directories:
+
+    >>> os.mkdir(os.path.join('src', 'mydir'))
+    >>> os.mkdir(os.path.join('dst', 'mydir'))
+    >>> open(os.path.join(
+    ...   'src', 'mydir', 'blah'), 'wb').write('srcblah')
+    >>> open(os.path.join(
+    ...   'dst', 'mydir', 'blah'), 'wb').write('dstblah')
+
+    >>> result = copy_filesystem_tree('src', 'dst', overwrite=True)
+    >>> open(os.path.join('dst', 'mydir', 'blah'), 'rb').read()
+    'srcblah'
+
+
+Using ``del_old``
+-----------------
+
+Boolean. If set to ``True``, any copied files and directories will be
+removed from the src dir. Default is `False`.
+
+    >>> result = copy_filesystem_tree('src', 'dst', overwrite=True,
+    ...                                           del_old=True)
+    >>> os.listdir('src')
+    ['.blah']
+
+All files and directories are removed from src, except the hidden file
+we created in the beginning.
+
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(new_location)
+    >>> os.chdir(old_location)
+    
+
+
+:func:`get_inner_HTML_part()`
+=============================
+
+.. function:: get_inner_HTML_part(html_code)
+
+   Get the 'inner' part out of a piece of HTML code.
+
+   Helper function mainly to extract 'real content' from already
+   rendered forms.
+
+   The term 'inner part' here means the ``<form>`` part of an HTML
+   snippet. If this cannot be found, we look for a ``<body>`` part and
+   if this cannot be found as well, we simply return the whole input.
+
+   If a ``<form>`` part can be found in an HTML snippet, this is
+   returned with all preceeding/following stuff stripped:
+
+     >>> from waeup.ikoba.utils.helpers import get_inner_HTML_part
+     >>> print get_inner_HTML_part("""<html>
+     ... <head>
+     ... </head>
+     ... <BLANKLINE>
+     ... <body>
+     ... <form action="http://localhost/mycompany/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/mycompany/faculties/TF/add"
+           method="post" class="edit-form"
+           enctype="multipart/form-data">
+     <BLANKLINE>
+       <h1>Add a department</h1>
+     </form>
+     <BLANKLINE>
+     <BLANKLINE>
+
+   If there is no ``<form>`` part, try to find any ``<body>`` part:
+
+     >>> print get_inner_HTML_part("""<html>
+     ... <head>
+     ... </head>
+     ... <BLANKLINE>
+     ... <body>
+     ...  <div>Some content</div>
+     ... </body>
+     ... </html>
+     ... """)
+     <BLANKLINE>
+      <div>Some content</div>
+     <BLANKLINE>
+
+   If there is also no ``<body>`` tag, we return the input as-is:
+
+     >>> print get_inner_HTML_part("""<div>
+     ...  <div>Some content</div>
+     ... </div>
+     ... """)
+     <div>
+      <div>Some content</div>
+     </div>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/idgen.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/idgen.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/idgen.py	(revision 12677)
@@ -0,0 +1,50 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""ID generators.
+
+Give you unique IDs for objects.
+"""
+import grok
+import uuid
+from waeup.ikoba.interfaces import IIDSource
+
+
+class IDSource(grok.GlobalUtility):
+    """This utility provides 'Universally Unique IDentifiers'.
+
+    It is registered without name on startup. You can get an instance
+    of this utility with
+
+      >>> from zope.component import getUtility
+      >>> from waeup.ikoba.interfaces import IIDSource
+      >>> source = getUtility(IIDSource)
+
+    """
+    grok.implements(IIDSource)
+
+    @classmethod
+    def get_uuid(self):
+        """Get a UUID string of 36 hexadecimal chars and hyphens.
+        """
+        return str(uuid.uuid4())
+
+    @classmethod
+    def get_hex_uuid(self):
+        """Get a UUID string of 32 hexadecimal chars.
+        """
+        return uuid.uuid4().hex
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/importexport.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/importexport.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/importexport.py	(revision 12677)
@@ -0,0 +1,101 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import unicodecsv as csv # XXX: csv ops should move to dedicated module
+import grok
+import cPickle
+import zope.xmlpickle
+from cStringIO import StringIO
+from zope.interface import Interface
+from waeup.ikoba.interfaces import (IIkobaObject, IIkobaExporter,
+                                   IIkobaXMLExporter, IIkobaXMLImporter)
+
+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 Ikoba object as pickle.
+
+    This all-purpose exporter exports attributes defined in schemata
+    and contained objects (if the exported thing is a container).
+    """
+    grok.context(IIkobaObject)
+    grok.provides(IIkobaExporter)
+
+    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 Ikoba object as XML.
+
+    This all-purpose exporter exports XML representations of pickable
+    objects.
+    """
+    grok.context(Interface)
+    grok.provides(IIkobaXMLExporter)
+
+    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 Ikoba object from XML.
+    """
+    grok.context(Interface)
+    grok.provides(IIkobaXMLImporter)
+
+    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.ikoba/branches/uli-payments/src/waeup/ikoba/utils/importexport.txt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/importexport.txt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/importexport.txt	(revision 12677)
@@ -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.ikoba.testing.IkobaUnitTestLayer
+
+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.ikoba.interfaces import IIkobaXMLExporter
+    >>> exporter = IIkobaXMLExporter(mycave)
+    >>> exporter
+    <waeup.ikoba.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.ikoba.interfaces import IIkobaXMLImporter
+    >>> importer = IIkobaXMLImporter(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.ikoba/branches/uli-payments/src/waeup/ikoba/utils/logger.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/logger.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/logger.py	(revision 12677)
@@ -0,0 +1,398 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Convenience stuff for logging.
+
+Main component of :mod:`waeup.ikoba.utils.logging` is a mix-in class
+:class:`waeup.ikoba.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.ikoba.${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.ikoba.utils.logger import Logger
+
+  class MyComponent(object, Logger):
+      # Yes that's a complete working class
+      logger_name = 'waeup.ikoba.${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.ikoba.app.Company`. This also means that
+logging with the `Logger` mix-in will work only inside so-called sites
+(`Company` instances put into ZODB are such `sites`).
+
+Other components in this module help to make everything work.
+"""
+import os
+import grok
+import logging
+from logging.handlers import WatchedFileHandler
+from string import Template
+from ulif.loghandlers import DatedRotatingFileHandler
+from zope import schema
+from zope.component import queryUtility
+from zope.interface import Interface, Attribute, implements
+from waeup.ikoba.interfaces import (
+    IDataCenter, IDataCenterStorageMovedEvent, ILoggerCollector)
+from waeup.ikoba.utils.helpers import get_current_principal
+
+#: Default logfile size (5 KB, not relevant for DatedRotatingFileHandlers)
+MAX_BYTES = 5 * 1024 ** 2
+
+#: Default num of backup files (-1 = indefinite)
+BACKUP_COUNT = -1
+
+#: Default logging level (`logging.INFO')
+LEVEL = logging.INFO
+
+class ILogger(Interface):
+    logger_name = schema.TextLine(
+        title = u'A Python logger name')
+    logger_filename = schema.TextLine(
+        title = u'A filename for the log file to use (basename)')
+    logger = Attribute("Get a Python logger instance already set up")
+    def logger_setup(logger):
+        """Setup a logger.
+
+        `logger` is an instance of :class:`logging.Logger`.
+        """
+
+    def logger_get_logfile_path():
+        """Get path to logfile used.
+
+        Return `None` if the file path cannot be computed.
+        """
+
+    def logger_get_logdir():
+        """Get the directory of the logfile.
+
+        Return `None` if the directory path cannot be computed.
+        """
+
+class ContextFilter(logging.Filter):
+    """
+    This is a filter which injects contextual information into the log.
+
+    """
+
+    def filter(self, record):
+        user = get_current_principal()
+        record.user = getattr(user, 'id', 'system')
+        return True
+
+class Logger(object):
+    """Mixin-class that for logging support.
+
+    Classes that (also) inherit from this class provide support for
+    logging with waeup sites.
+
+    By default a `logger` attribute is provided which returns a
+    regular Python logger. This logger has already registered a file
+    rotating log handler that writes log messages to a file `main.log`
+    in datacenters ``log/`` directory. This is the main log file also
+    used by other components. Therefore you can pick another filename
+    by setting the `logger_filename` attribute.
+
+    All methods and attributes of this mix-in start with ``logger_``
+    in order not to interfere with already existing names of a class.
+
+    Method names do not follow the usual Zope habit (CamelCase) but
+    PEP8 convention (lower_case_with_underscores).
+    """
+
+    #: The filename to use when logging.
+    logger_filename = 'main.log'
+
+    #: The Python logger name used when
+    #: logging. ``'waeup.ikoba.${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.ikoba.${sitename}'
+
+    #: The format to use when logging.
+    logger_format_str = '%(asctime)s - %(levelname)s - %(user)s - %(message)s'
+
+    implements(ILogger)
+
+    @property
+    def logger(self):
+        """Get a logger instance.
+
+        Returns a standard logger object as provided by :mod:`logging`
+        module from the standard library.
+
+        Other components can use this logger to perform log entries
+        into the logfile of this component.
+
+        The logger is initialized the first time it is called.
+
+        The logger is only available when used inside a site.
+
+        .. note:: The logger default level is
+                  :data:`logging.WARN`. Use
+                  :meth:`logging.Logger.setLevel` to set a different level.
+        """
+        site = grok.getSite()
+        sitename = getattr(site, '__name__', None)
+        loggername = Template(self.logger_name).substitute(
+            dict(sitename='%s' % sitename))
+        logger = logging.getLogger(loggername)
+        if site is None or sitename is None:
+            # Site not added to ZODB yet. Log to commandline
+            return logger
+        if len(logger.handlers) != 1:
+            handlers = [x for x in logger.handlers]
+            for handler in handlers:
+                handler.flush()
+                handler.close()
+                logger.removeHandler(handler)
+            logger = self.logger_setup(logger)
+        if logger is None:
+            # It might happen, that we have no logger now.
+            logger = logging.getLogger(loggername)
+        return logger
+
+    def logger_setup(self, logger):
+        """Setup logger.
+
+        The logfile will be stored in the datacenter logs/ dir.
+        """
+        filename = self.logger_get_logfile_path()
+        if filename is None:
+            return
+        collector = queryUtility(ILoggerCollector)
+        if collector is not None:
+            site = grok.getSite()
+            collector.registerLogger(site, self)
+
+        # Create a rotating file handler logger.
+        handler = WatchedFileHandler(
+            filename, encoding='utf-8')
+        #handler = DatedRotatingFileHandler(
+        #    filename, when='MON', backupCount=BACKUP_COUNT)
+        handler.setLevel(LEVEL)
+        formatter = logging.Formatter(self.logger_format_str)
+        handler.setFormatter(formatter)
+
+        # Don't send log msgs to ancestors. This stops displaying
+        # logmessages on the commandline.
+        logger.propagate = False
+        logger.addHandler(handler)
+        logger.setLevel(LEVEL)
+
+        flt = ContextFilter()
+        logger.addFilter(flt)
+        return logger
+
+    def logger_get_logfile_path(self):
+        """Get the path to the logfile used.
+
+        Returns the path to a file in local sites datacenter ``log/``
+        directory (dependent on :meth:`logger_get_logdir`) and with
+        :attr:`logger_filename` as basename.
+
+        Override this method if you want a complete different
+        computation of the logfile path. If you only want a different
+        logfile name, set :attr:`logger_filename`. If you only want a
+        different path to the logfile override
+        :meth:`logger_get_logdir` instead.
+
+        Returns ``None`` if no logdir can be fetched.
+
+        .. note:: creates the logfile dir if it does not exist.
+
+        """
+        logdir = self.logger_get_logdir()
+        if logdir is None:
+            return None
+        return os.path.join(logdir, self.logger_filename)
+
+    def logger_get_logdir(self):
+        """Get log dir where logfile should be put.
+
+        Returns the path to the logfile directory. If no site is set,
+        ``None`` is returned. The same applies, if the site has no
+        datacenter.
+
+        If the dir dies not exist already it will be created. Only the
+        last part of the directory path will be created.
+        """
+        site = grok.getSite()
+        if site is None:
+            return None
+        datacenter = site.get('datacenter', None)
+        if datacenter is None:
+            return None
+        logdir = os.path.join(datacenter.storage, 'logs')
+        if not os.path.exists(logdir):
+            os.mkdir(logdir)
+        return logdir
+
+    def logger_logfile_changed(self):
+        """React on logfile location change.
+
+        If the logfile changed, we can set a different logfile. While
+        changing the logfile is a rather critical operation you might
+        not do often in production use, we have to cope with that
+        especially in tests.
+
+        What this method does by default (unless you override it):
+
+        - It fetches the current logger and
+
+          - Removes flushes, closes, and removes all handlers
+
+          - Sets up new handler(s).
+
+        All this, of course, requires to be 'in a site'.
+
+        Use this method to handle moves of datacenters, for instance
+        by writing an appropriate event handler.
+        """
+        logger = self.logger
+        self.logger_shutdown()
+        self.logger_setup(logger)
+        return
+
+    def logger_shutdown(self):
+        """Remove all specific logger setup.
+        """
+        logger = self.logger
+        handlers = [x for x in logger.handlers]
+        for handler in handlers:
+            handler.flush()
+            handler.close()
+            logger.removeHandler(handler)
+        collector = queryUtility(ILoggerCollector)
+        if collector is not None:
+            collector.unregisterLogger(grok.getSite(), self)
+        return
+
+
+class LoggerCollector(dict, grok.GlobalUtility):
+    """A global utility providing `ILoggerCollector`.
+
+    A logging collector collects logging components. This helps to
+    inform them when a logfile location changes.
+
+    Logging components are registered per site they belong to.
+    """
+
+    implements(ILoggerCollector)
+
+    def getLoggers(self, site):
+        name = getattr(site, '__name__', None)
+        if name is None:
+            return []
+        if name not in self.keys():
+            return []
+        return self[name]
+
+    def registerLogger(self, site, logging_component):
+        name = getattr(site, '__name__', None)
+        if name is None:
+            return
+        if not name in self.keys():
+            # new component
+            self[name] = []
+        if logging_component in self[name]:
+            # already registered
+            return
+        self[name].append(logging_component)
+        return
+
+    def unregisterLogger(self, site, logging_component):
+        name = getattr(site, '__name__', None)
+        if name is None or name not in self.keys():
+            return
+        if logging_component not in self[name]:
+            return
+        self[name].remove(logging_component)
+        return
+
+@grok.subscribe(IDataCenter, IDataCenterStorageMovedEvent)
+def handle_datacenter_storage_moved(obj, event):
+    """Event handler, in case datacenter storage moves.
+
+    By default all our logfiles (yes, we produce a whole bunch of it)
+    are located in a ``log/`` dir of a local datacenter, the
+    datacenter 'storage'. If this path changes because the datacenter
+    is moved an appropriate event is triggered and we can react.
+
+    Via the global ILoggerCollector utility, a small piece that allows
+    self-registering of logging components, we can lookup components
+    whose logfile path has to be set up anew.
+
+    Each component we call has to provide ILogger or, more specific,
+    the :meth:`logger_logfile_changed` method of this interface.
+    """
+    site = grok.getSite()
+    if site is None:
+        return
+    collector = queryUtility(ILoggerCollector)
+    loggers = collector.getLoggers(site)
+    for logger in loggers:
+        if hasattr(logger, 'logger_logfile_changed'):
+            logger.logger_logfile_changed()
+    return
+
+from waeup.ikoba.interfaces import ICompany
+@grok.subscribe(ICompany, grok.IObjectRemovedEvent)
+def handle_site_removed(obj, event):
+    collector = queryUtility(ILoggerCollector)
+    name = getattr(obj, '__name__', None)
+    if name is None:
+        return
+    if name not in collector.keys():
+        return
+    del collector[name]
+    return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/overrides.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/overrides.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/overrides.zcml	(revision 12677)
@@ -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.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/sample_meminfo
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/sample_meminfo	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/sample_meminfo	(revision 12677)
@@ -0,0 +1,42 @@
+MemTotal:       16345792 kB
+MemFree:        13431496 kB
+Buffers:          218528 kB
+Cached:          1013816 kB
+SwapCached:            0 kB
+Active:          1661796 kB
+Inactive:         839776 kB
+Active(anon):    1270232 kB
+Inactive(anon):     2292 kB
+Active(file):     391564 kB
+Inactive(file):   837484 kB
+Unevictable:          16 kB
+Mlocked:              16 kB
+SwapTotal:       7999484 kB
+SwapFree:        7999484 kB
+Dirty:                40 kB
+Writeback:             0 kB
+AnonPages:       1269248 kB
+Mapped:           236544 kB
+Shmem:              3296 kB
+Slab:             131344 kB
+SReclaimable:      95972 kB
+SUnreclaim:        35372 kB
+KernelStack:        4376 kB
+PageTables:        31384 kB
+NFS_Unstable:          0 kB
+Bounce:                0 kB
+WritebackTmp:          0 kB
+CommitLimit:    16172380 kB
+Committed_AS:    4243140 kB
+VmallocTotal:   34359738367 kB
+VmallocUsed:      610564 kB
+VmallocChunk:   34359120892 kB
+HardwareCorrupted:     0 kB
+AnonHugePages:         0 kB
+HugePages_Total:       0
+HugePages_Free:        0
+HugePages_Rsvd:        0
+HugePages_Surp:        0
+Hugepagesize:       2048 kB
+DirectMap4k:      167936 kB
+DirectMap2M:    16525312 kB
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_batching.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_batching.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_batching.py	(revision 12677)
@@ -0,0 +1,614 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import datetime
+import doctest
+import logging
+import os
+import shutil
+import tempfile
+import unittest
+from zc.async.interfaces import IJob
+from zope import schema
+from zope.component import provideUtility, getGlobalSiteManager, getUtility
+from zope.component.factory import Factory
+from zope.component.hooks import clearSite, setSite
+from zope.component.interfaces import IFactory
+from zope.interface import Interface, implements, verify
+from waeup.ikoba.app import Company
+from waeup.ikoba.interfaces import (
+    ICSVExporter, IBatchProcessor, IExportJobContainer, IJobManager,
+    IExportJob, IExportContainerFinder)
+from waeup.ikoba.testing import (
+    FunctionalLayer, FunctionalTestCase, FakeJob, FakeJobManager)
+from waeup.ikoba.utils.batching import (
+    ExporterBase, BatchProcessor, export_job, AsyncExportJob,
+    ExportJobContainer, VirtualExportJobContainer, ExportContainerFinder)
+
+optionflags = (
+    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
+
+
+class ICave(Interface):
+    """A cave."""
+    id_num = schema.TextLine(
+        title = u'internal id',
+        default = u'default',
+        required = True,
+        readonly = True,
+        )
+    name = schema.TextLine(
+        title = u'Cave name',
+        default = u'Unnamed',
+        required = True)
+    dinoports = schema.Int(
+        title = u'Number of DinoPorts (tm)',
+        required = False,
+        default = 1)
+    owner = schema.TextLine(
+        title = u'Owner name',
+        required = True,
+        missing_value = 'Fred Estates Inc.')
+    taxpayer = schema.Bool(
+        title = u'Payes taxes',
+        required = True,
+        default = False)
+
+class Cave(object):
+    implements(ICave)
+    def __init__(self, name=u'Unnamed', dinoports=2,
+                 owner='Fred Estates Inc.', taxpayer=False):
+        self.name = name
+        self.dinoports = 2
+        self.owner = owner
+        self.taxpayer = taxpayer
+
+stoneville = dict
+
+SAMPLE_DATA = """name,dinoports,owner,taxpayer
+Barneys Home,2,Barney,1
+Wilmas Asylum,1,Wilma,1
+Freds Dinoburgers,10,Fred,0
+Joeys Drive-in,110,Joey,0
+"""
+
+SAMPLE_FILTERED_DATA = """name,dinoports,owner,taxpayer
+Barneys Home,2,Barney,1
+Wilmas Asylum,1,Wilma,1
+"""
+
+class CaveProcessor(BatchProcessor):
+    util_name = 'caveprocessor'
+    name = 'Cave Processor'
+    iface = ICave
+    location_fields = ['name']
+    factory_name = 'Lovely Cave'
+
+    def parentsExist(self, row, site):
+        return True
+
+    def getParent(self, row, site):
+        return stoneville
+
+    def entryExists(self, row, site):
+        return row['name'] in stoneville.keys()
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        return stoneville[row['name']]
+
+    def delEntry(self, row, site):
+        del stoneville[row['name']]
+
+    def addEntry(self, obj, row, site):
+        stoneville[row['name']] = obj
+
+class BatchProcessorTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setupLogger(self):
+
+        self.logger = logging.getLogger('stoneville')
+        self.logger.setLevel(logging.DEBUG)
+        self.logger.propagate = False
+        self.logfile = os.path.join(self.workdir, 'stoneville.log')
+        self.handler = logging.FileHandler(self.logfile, 'w')
+        self.logger.addHandler(self.handler)
+
+    def setUp(self):
+        global stoneville
+        super(BatchProcessorTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        self.workdir = tempfile.mkdtemp()
+        factory = Factory(Cave)
+        provideUtility(factory, IFactory, 'Lovely Cave')
+
+        # Provide sample data
+        self.newcomers_csv = os.path.join(self.workdir, 'newcomers.csv')
+        open(self.newcomers_csv, 'wb').write(SAMPLE_DATA)
+        self.setupLogger()
+        self.stoneville = stoneville
+        stoneville = dict()
+        self.resultpath = None
+        return
+
+    def tearDown(self):
+        super(BatchProcessorTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        self.logger.removeHandler(self.handler)
+        clearSite()
+        if not isinstance(self.resultpath, list):
+            self.resultpath = [self.resultpath]
+        for path in self.resultpath:
+            if not isinstance(path, basestring):
+                continue
+            if not os.path.isdir(path):
+                path = os.path.dirname(path)
+            if os.path.exists(path):
+                shutil.rmtree(path)
+        return
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = BatchProcessor()
+        verify.verifyClass(IBatchProcessor, BatchProcessor)
+        verify.verifyObject(IBatchProcessor, obj)
+        return
+
+    def test_import(self):
+        processor = CaveProcessor()
+        result = processor.doImport(
+            self.newcomers_csv,
+            ['name', 'dinoports', 'owner', 'taxpayer'],
+            mode='create', user='Bob', logger=self.logger)
+        num, num_fail, finished_path, failed_path = result
+        self.resultpath = [finished_path, failed_path]
+        assert num == 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, 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, num_fail, finished_path, failed_path = result
+        self.resultpath = [finished_path, failed_path]
+        assert isinstance(self.stoneville['Barneys Home'].dinoports, int)
+
+
+    def test_log(self):
+        """
+           >>> print log_contents
+           processed: /.../newcomers.csv, create mode, 4 lines (4 successful/ 0 failed), ... s (... s/item)
+
+        """
+        processor = CaveProcessor()
+        result = processor.doImport(
+            self.newcomers_csv,
+            ['name', 'dinoports', 'owner', 'taxpayer'],
+            mode='create', user='Bob', logger=self.logger)
+        num, num_fail, finished_path, failed_path = result
+        self.resultpath = [finished_path, failed_path]
+        log_contents = open(self.logfile, 'rb').read()
+        doctest.run_docstring_examples(
+            self.test_log, locals(), False, 'test_log', None, optionflags)
+        return
+
+class ExporterBaseTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.workfile = os.path.join(self.workdir, 'testfile.csv')
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_iface(self):
+        # ExporterBase really implements the promised interface.
+        obj = ExporterBase()
+        verify.verifyClass(ICSVExporter, ExporterBase)
+        verify.verifyObject(ICSVExporter, obj)
+        return
+
+    def test_unimplemented(self):
+        # make sure the not implemented methods signal that.
+        exporter = ExporterBase()
+        self.assertRaises(NotImplementedError, exporter.export_all, None)
+        self.assertRaises(NotImplementedError, exporter.export, None)
+        return
+
+    def test_mangle_value(self):
+        # some basic types are mangled correctly
+        exporter = ExporterBase()
+        result1 = exporter.mangle_value(True, 'foo')
+        result2 = exporter.mangle_value(False, 'foo')
+        result3 = exporter.mangle_value('string', 'foo')
+        result4 = exporter.mangle_value(u'string', 'foo')
+        result5 = exporter.mangle_value(None, 'foo')
+        result6 = exporter.mangle_value(datetime.date(2012, 4, 1), 'foo')
+        result7 = exporter.mangle_value(
+            datetime.datetime(2012, 4, 1, 12, 1, 1), 'foo')
+        self.assertEqual(
+            (result1, result2, result3, result4, result5),
+            ('1', '0', u'string', u'string', ''))
+        self.assertEqual(type(result3), type('string'))
+        self.assertEqual(type(result4), type('string'))
+        # dates are formatted with trailing hash
+        self.assertEqual(result6, '2012-04-01#')
+        # datetimes are formatted as yyyy-mm-dd hh:mm:ss
+        self.assertEqual(result7, '2012-04-01 12:01:01#')
+        return
+
+    def test_get_csv_writer(self):
+        # we can get a CSV writer to a memory file
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer()
+        writer.writerow(dict(any_id='A', any_title='B'))
+        outfile.seek(0)
+        self.assertEqual(
+            outfile.read(),
+            'any_id,any_title\r\nA,B\r\n')
+        return
+
+    def test_get_csv_writer_with_file(self):
+        # we can get CSV writer that writes to a real file
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer(filepath=self.workfile)
+        writer.writerow(dict(any_id='A', any_title='B'))
+        outfile.close()
+        resultfile = open(self.workfile, 'rb')
+        self.assertEqual(
+            resultfile.read(),
+            'any_id,any_title\r\nA,B\r\n')
+        return
+
+    def test_write_item(self):
+        # we can write items to opened exporter files.
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer()
+        class Sample(object):
+            any_id = 'A'
+            any_title = u'B'
+        exporter.write_item(Sample(), writer)
+        outfile.seek(0)
+        self.assertEqual(
+            outfile.read(),
+            'any_id,any_title\r\nA,B\r\n')
+        return
+
+    def test_close_outfile(self):
+        # exporters can help to close outfiles.
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer()
+        result = exporter.close_outfile(None, outfile)
+        self.assertEqual(result, 'any_id,any_title\r\n')
+        return
+
+    def test_close_outfile_real(self):
+        # we can also close outfiles in real files.
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer(filepath=self.workfile)
+        result = exporter.close_outfile(self.workfile, outfile)
+        self.assertEqual(result, None)
+        return
+
+    def test_export_filtered(self):
+        # we can pass in positional and keyword args
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer(filepath=self.workfile)
+        self.assertRaises(NotImplementedError, exporter.export_filtered,
+                          'foo', bar='bar')
+        return
+
+class CaveExporter(ExporterBase):
+    # A minimal fake exporter suitable to be called by export_jobs
+    fields = ('name', 'dinoports', 'owner', 'taxpayer')
+    title = u'Dummy cave exporter'
+
+    def export_all(self, site, filepath=None):
+        if filepath is None:
+            return SAMPLE_DATA
+        open(filepath, 'wb').write(SAMPLE_DATA)
+        return
+
+    def export_filtered(self, site, filepath=None, foo=None, bar=None):
+        if foo or bar:
+            open(filepath, 'wb').write(SAMPLE_FILTERED_DATA)
+            return
+        self.export_all(site, filepath=filepath)
+        return
+
+class ExportJobTests(unittest.TestCase):
+    # Test asynchronous export functionality (simple cases)
+
+    def setUp(self):
+        # register a suitable ICSVExporter as named utility
+        self.exporter = CaveExporter()
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(
+            self.exporter, ICSVExporter, name='cave_exporter')
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.exporter)
+
+    def test_export_job_func(self):
+        # the export_job func does really export data...
+        result_path = export_job(None, 'cave_exporter')
+        self.assertTrue(os.path.isfile(result_path))
+        contents = open(result_path, 'rb').read()
+        shutil.rmtree(os.path.dirname(result_path))
+        self.assertEqual(contents, SAMPLE_DATA)
+        return
+
+    def test_export_job_interfaces(self):
+        # the AsyncExportJob implements promised interfaces correctly...
+        job = AsyncExportJob(None, None)
+        verify.verifyClass(IJob, AsyncExportJob)
+        verify.verifyObject(IJob, job)
+        verify.verifyClass(IExportJob, AsyncExportJob)
+        verify.verifyObject(IExportJob, job)
+        return
+
+    def test_export_job_with_args(self):
+        # we export filtered sets
+        result_path = export_job(None, 'cave_exporter', foo='foo')
+        contents = open(result_path, 'rb').read()
+        shutil.rmtree(os.path.dirname(result_path))
+        self.assertEqual(contents, SAMPLE_FILTERED_DATA)
+        return
+
+
+class FakeJobWithResult(FakeJob):
+
+    def __init__(self):
+        self.dir_path = tempfile.mkdtemp()
+        self.result = os.path.join(self.dir_path, 'fake.csv')
+        open(self.result, 'wb').write('a fake result')
+        return
+
+class ExportJobContainerTests(unittest.TestCase):
+    # Test ExportJobContainer
+
+    TestedClass = ExportJobContainer
+
+    def setUp(self):
+        # register a suitable ICSVExporter as named utility
+        self.exporter = CaveExporter()
+        self.job_manager = FakeJobManager()
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(
+            self.exporter, ICSVExporter, name='cave_exporter')
+        self.gsm.registerUtility(
+            self.job_manager, IJobManager)
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.exporter)
+        self.gsm.unregisterUtility(self.job_manager, IJobManager)
+
+    def test_export_job_interfaces(self):
+        # the ExportJobContainer implements promised interfaces correctly...
+        container = self.TestedClass()
+        verify.verifyClass(IExportJobContainer, self.TestedClass)
+        verify.verifyObject(IExportJobContainer, container)
+        return
+
+    def test_start_export_job(self):
+        # we can start jobs
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        result = self.job_manager._jobs.values()[0]
+        self.assertTrue(IJob.providedBy(result))
+        self.assertEqual(
+            container.running_exports,
+            [('1', 'cave_exporter', 'bob')]
+            )
+        return
+
+    def test_get_running_export_jobs_all(self):
+        # we can get export jobs of all users
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        container.start_export_job('cave_exporter', 'alice')
+        result = container.get_running_export_jobs()
+        self.assertEqual(
+            result,
+            [('1', 'cave_exporter', 'bob'),
+             ('2', 'cave_exporter', 'alice')]
+            )
+        return
+
+    def test_get_running_export_jobs_user(self):
+        # we can get the export jobs running for a certain user
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        container.start_export_job('cave_exporter', 'alice')
+        result1 = container.get_running_export_jobs(user_id='alice')
+        result2 = container.get_running_export_jobs(user_id='foo')
+        self.assertEqual(
+            result1, [('2', 'cave_exporter', 'alice')])
+        self.assertEqual(
+            result2, [])
+        return
+
+    def test_get_running_export_jobs_only_if_exist(self):
+        # we get only jobs that are accessible through the job manager...
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        container.start_export_job('cave_exporter', 'bob')
+        self.assertTrue(
+            ('2', 'cave_exporter', 'bob') in container.running_exports)
+        # we remove the second entry from job manager
+        del self.job_manager._jobs['2']
+        result = container.get_running_export_jobs(user_id='bob')
+        self.assertEqual(
+            result, [('1', 'cave_exporter', 'bob')])
+        self.assertTrue(
+            ('2', 'cave_exporter', 'bob') not in container.running_exports)
+        return
+
+    def test_get_export_job_status(self):
+        # we can get the stati of jobs...
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'alice')
+        container.start_export_job('cave_exporter', 'bob')
+        container.start_export_job('cave_exporter', 'bob')
+        result = container.get_export_jobs_status(user_id='bob')
+        # we'll get the raw value, a translation and the title of the
+        # exporter
+        self.assertEqual(
+            result,
+            [('new', u'new', u'Dummy cave exporter'),
+             ('completed', u'completed', u'Dummy cave exporter')]
+            )
+        return
+
+    def test_delete_export_entry(self):
+        # we can remove export entries in local lists and the job
+        # manager as well...
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        entry = container.running_exports[0]
+        container.delete_export_entry(entry)
+        # both, running_exports list and job manager are empty now
+        self.assertEqual(
+            container.running_exports, [])
+        self.assertEqual(
+            self.job_manager._jobs, {})
+        return
+
+    def test_delete_export_entry_remove_file(self):
+        # any result files of exports are deleted as well
+        container = self.TestedClass()
+        entry = ('4', 'cave_exporter', 'bob')
+        container.running_exports = [entry]
+        fake_job = FakeJobWithResult()
+        self.job_manager._jobs['4'] = fake_job
+        self.assertTrue(os.path.isfile(fake_job.result))
+        container.delete_export_entry(entry)
+        self.assertTrue(not os.path.exists(fake_job.result))
+        return
+
+    def test_entry_from_job_id(self):
+        # we can get an entry for a job_id if the id exists
+        container = self.TestedClass()
+        entry = ('4', 'cave_exporter', 'bob')
+        container.running_exports = [entry]
+        fake_job = FakeJobWithResult()
+        self.job_manager._jobs['4'] = fake_job
+        result1 = container.entry_from_job_id(None)
+        result2 = container.entry_from_job_id('4')
+        result3 = container.entry_from_job_id('23')
+        self.assertEqual(result1, None)
+        self.assertEqual(result2, ('4', 'cave_exporter', 'bob'))
+        self.assertEqual(result3, None)
+        shutil.rmtree(fake_job.dir_path)
+        return
+
+class VirtualExportJobContainerTests(ExportJobContainerTests):
+    # VirtualExportJobContainers should provide the
+    # same functionality as regular ones.
+
+    TestedClass = VirtualExportJobContainer
+
+    def setUp(self):
+        super(VirtualExportJobContainerTests, self).setUp()
+        self.root_job_container = ExportJobContainer()
+        def fake_finder():
+            return self.root_job_container
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(fake_finder, IExportContainerFinder)
+        return
+
+class ExportContainerFinderTests(FunctionalTestCase):
+    # Tests for export container finder.
+
+    layer = FunctionalLayer
+
+    def test_get_finder_as_util(self):
+        # we can get a finder by utility lookup
+        finder = getUtility(IExportContainerFinder)
+        self.assertTrue(finder is not None)
+        self.assertEqual(
+            IExportContainerFinder.providedBy(finder),
+            True)
+        return
+
+    def test_iface(self):
+        # the finder complies with the promised interface
+        finder = ExportContainerFinder()
+        verify.verifyClass(IExportContainerFinder,
+                           ExportContainerFinder)
+        verify.verifyObject(IExportContainerFinder, finder)
+        return
+
+    def test_no_site(self):
+        # a finder returns None if no site is available
+        finder = ExportContainerFinder()
+        self.assertEqual(
+            finder(), None)
+        return
+
+    def test_active_site(self):
+        # we get the datafinder if one is installed and site set
+        self.getRootFolder()['app'] = Company()
+        finder = getUtility(IExportContainerFinder)
+        setSite(self.getRootFolder()['app'])
+        container = finder()
+        self.assertTrue(container is not None)
+        return
+
+    def test_broken_site(self):
+        # if the current site has no ExportContainer, we get None
+        self.getRootFolder()['app'] = Company()
+        app = self.getRootFolder()['app']
+        del app['datacenter'] # datacenter _is_ the export container
+        setSite(app)
+        finder = getUtility(IExportContainerFinder)
+        container = finder()
+        self.assertTrue(container is None)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_converters.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_converters.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_converters.py	(revision 12677)
@@ -0,0 +1,426 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for converterts.
+"""
+import datetime
+import shutil
+import tempfile
+import unittest
+from decimal import Decimal
+from zope import schema
+from zope.component import provideUtility
+from zope.component.factory import Factory
+from zope.component.hooks import clearSite
+from zope.component.interfaces import IFactory
+from zope.formlib import form
+from zope.interface import (
+    Interface, implements, invariant, Invalid, implementedBy, verify)
+
+from waeup.ikoba.app import Company
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.interfaces import (
+    SimpleIkobaVocabulary, IFieldConverter,
+    DELETION_MARKER, IGNORE_MARKER)
+from waeup.ikoba.products.productoptions import ProductOptionField
+from waeup.ikoba.utils.converters import (
+    IObjectConverter, IFieldConverter, DefaultFieldConverter,
+    ListFieldConverter, PhoneNumberFieldConverter, ProductOptionConverter,
+    DefaultObjectConverter)
+from waeup.ikoba.utils.helpers import attrs_to_fields
+
+colors = SimpleIkobaVocabulary(
+    ('Red', u'red'),
+    ('Green', u'green'),
+    ('Blue', u'blue'),
+    )
+car_nums = SimpleIkobaVocabulary(
+    ('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,
+        )
+    options = schema.List(
+        title = u'Options',
+        value_type = ProductOptionField(),
+        required = True,
+        default = [],
+        )
+    friends = schema.List(
+        title = u'Friends',
+        value_type = schema.TextLine(
+            title = u'Name',
+            )
+        )
+
+    @invariant
+    def kevinIsYoung(contact):
+        if contact.age > 16 and contact.name == 'Kevin':
+            raise Invalid('Kevins are age 16 or below.')
+
+class Contact(object):
+    """Sample content type.
+    """
+    implements(IContact)
+Contact = attrs_to_fields(Contact)
+
+form_fields_select = form.Fields(IContact).select('name', 'vip')
+form_fields_omit = form.Fields(IContact).omit('name', 'vip')
+
+class ContactFactory(object):
+    """A factory for faculty containers.
+    """
+    implements(IContact)
+
+    def __call__(self, *args, **kw):
+        return Faculty()
+
+    def getInterfaces(self):
+        return implementedBy(Faculty)
+
+class FieldConverterTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj1 = DefaultFieldConverter(None)
+        obj2 = ListFieldConverter(None)
+        obj3 = PhoneNumberFieldConverter(None)
+        obj4 = ProductOptionConverter(None)
+        verify.verifyObject(IFieldConverter, obj1)
+        verify.verifyObject(IFieldConverter, obj2)
+        verify.verifyObject(IFieldConverter, obj3)
+        verify.verifyObject(IFieldConverter, obj4)
+        verify.verifyClass(IFieldConverter, DefaultFieldConverter)
+        verify.verifyClass(IFieldConverter, ListFieldConverter)
+        verify.verifyClass(IFieldConverter, PhoneNumberFieldConverter)
+        verify.verifyClass(IFieldConverter, ProductOptionConverter)
+        return
+
+class ConverterTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ConverterTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        self.workdir = tempfile.mkdtemp()
+
+        # Create a factory for contacts and register it as global utility
+        factory = Factory(Contact)
+        provideUtility(factory, IFactory, 'contact')
+        return
+
+    def tearDown(self):
+        super(ConverterTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = DefaultObjectConverter(IContact)
+        verify.verifyObject(IObjectConverter, obj)
+        verify.verifyClass(IObjectConverter, DefaultObjectConverter)
+        return
+
+    def test_valid_data(self):
+        contact = Contact()
+        contact.age = 33
+        input_data = dict(name='Rudi', age='99')
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact)
+        assert data['name'] == 'Rudi'
+        assert data['age'] == 99
+        return
+
+    def test_bool(self):
+        contact1 = Contact()
+        contact2 = Contact()
+        input_data1 = dict(vip='on')
+        input_data2 = dict(vip='')
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err1, inv_err1, data1 = converter.fromStringDict(
+            input_data1, contact1)
+        err2, inv_err2, data2 = converter.fromStringDict(
+            input_data2, contact2)
+        assert data1['vip'] is True
+        assert data2['vip'] is False
+
+    def test_bool_nonstandard_values1(self):
+        # We accept 'true', 'True', 'tRuE', 'faLSE' and similar.
+        contact1 = Contact()
+        contact2 = Contact()
+        input_data1 = dict(vip='True')
+        input_data2 = dict(vip='false')
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err1, inv_err1, data1 = converter.fromStringDict(
+            input_data1, contact1)
+        err2, inv_err2, data2 = converter.fromStringDict(
+            input_data2, contact2)
+        assert data1['vip'] is True
+        assert data2['vip'] is False
+
+    def test_bool_nonstandard_values2(self):
+        # We accept '1' and '0' as bool values.
+        contact1 = Contact()
+        contact2 = Contact()
+        input_data1 = dict(vip='1')
+        input_data2 = dict(vip='0')
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err1, inv_err1, data1 = converter.fromStringDict(
+            input_data1, contact1)
+        err2, inv_err2, data2 = converter.fromStringDict(
+            input_data2, contact2)
+        assert data1['vip'] is True
+        assert data2['vip'] is False
+
+    def test_bool_nonstandard_values3(self):
+        # We accept 'yEs', 'no' and similar as bool values.
+        contact1 = Contact()
+        contact2 = Contact()
+        input_data1 = dict(vip='Yes')
+        input_data2 = dict(vip='no')
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err1, inv_err1, data1 = converter.fromStringDict(
+            input_data1, contact1)
+        err2, inv_err2, data2 = converter.fromStringDict(
+            input_data2, contact2)
+        assert data1['vip'] is True
+        assert data2['vip'] is False
+
+    def test_int(self):
+        contact = Contact()
+        input_data = dict(age='99')
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact)
+        assert data['age'] == 99
+        return
+
+    def test_int_invalid(self):
+        contact = Contact()
+        input_data = dict(age='sweet sixteen')
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, new_contact = converter.fromStringDict(
+            input_data, contact)
+        self.assertEqual(err, [('age', u'Invalid integer data')])
+        return
+
+    def test_textline(self):
+        contact = Contact()
+        input_data = dict(name='Rudi')
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact)
+        self.assertEqual(data['name'], u'Rudi')
+        assert isinstance(data['name'], unicode)
+        return
+
+    def test_invariant(self):
+        contact = Contact()
+        input_data = dict(name='Kevin', age='22')
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, new_contact = converter.fromStringDict(
+            input_data, contact)
+        self.assertEqual(inv_err, ['Kevins are age 16 or below.'])
+        return
+
+    def test_date(self):
+        contact = Contact()
+        converter = IObjectConverter(IContact) # a converter to IContact
+
+        # The input format for dates: YYYY-MM-DD
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='1945-12-23'), contact)
+        assert data['birthday'] == datetime.date(1945, 12, 23)
+        assert isinstance(data['birthday'], datetime.date)
+
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='1945-23-12'), contact)
+        #assert data['birthday'] == datetime.date(1945, 12, 23)
+        assert err[0][1] =='Invalid datetime data'
+
+        # '08' is not interpreted as octal number
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='1945-12-08'), contact)
+        assert data['birthday'] == datetime.date(1945, 12, 8)
+        return
+
+    def test_date_invalid(self):
+        contact = Contact()
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='not-a-date'), contact)
+        self.assertEqual(err, [('birthday', u'Invalid datetime data')])
+
+    def test_inject_formfields_select(self):
+        # We can use our own formfields and select only a subset of fields
+        contact = Contact()
+        converter = IObjectConverter(IContact) # a converter to IContact
+        input_data = dict(name='Bruno', age='99', vip='on')
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact, form_fields=form_fields_select)
+        self.assertEqual(data['name'], 'Bruno')
+        assert 'age' not in data.keys()
+        assert data['vip'] is True
+        return
+
+    def test_inject_formfields_omit(self):
+        # We can use our own formfields and omit some fields
+        contact = Contact()
+        converter = IObjectConverter(IContact) # a converter to IContact
+        input_data = dict(name='Bruno', age='99', vip='on')
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact, form_fields=form_fields_omit)
+        self.assertEqual(data['age'], 99)
+        assert 'name' not in data.keys()
+        assert 'vip' not in data.keys()
+        return
+
+    def test_factory(self):
+        # We can use factories to convert values
+        converter = IObjectConverter(IContact) # a converter to IContact
+        # pass string ``contact`` instead of a real object
+        err, inv_err, data = converter.fromStringDict(
+            dict(name='Gabi', age='23'), 'contact')
+        self.assertEqual(data['age'], 23)
+        self.assertEqual(data['name'], u'Gabi')
+        return
+
+    def test_choice_vocab(self):
+        # We can handle vocabularies
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(fav_color='blue'), 'contact')
+        assert data['fav_color'] == u'blue'
+        assert isinstance(data['fav_color'], unicode)
+        return
+
+    def test_choice_vocab_invalid_value(self):
+        # We can handle vocabularies
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(fav_color='magenta'), 'contact')
+        self.assertEqual(err, [('fav_color', u'Invalid value')])
+        assert 'fav_color' not in data.keys()
+        return
+
+    def test_non_string_choice(self):
+        # We can handle vocabs with non-string values
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(num_cars='1'), 'contact')
+        assert data['num_cars'] == 1
+        return
+
+    def test_list_of_textlines(self):
+        # We can convert lists of text lines
+        converter = IObjectConverter(IContact)
+        err, inv_err, data = converter.fromStringDict(
+            {"friends": "['Fred', 'Wilma']"}, 'contact')
+        self.assertEqual(
+            data, {'friends': ['Fred', 'Wilma']})
+        return
+
+    def test_list_of_optionentries(self):
+        # We can handle lists of result entries
+        converter = IObjectConverter(IContact)
+        # get currently valid values
+        req_string = u"[(u'T1', u'55.6', u'USD'), (u'T2', u'66.6', u'EUR')]"
+        err, inv_err, data = converter.fromStringDict(
+            {"options": req_string,}, 'contact')
+        result_options = data['options']
+        self.assertTrue(isinstance(result_options, list))
+        self.assertEqual(len(result_options), 2)
+        self.assertEqual(result_options[0].title, 'T1')
+        self.assertEqual(result_options[0].fee, Decimal('55.6'))
+        self.assertEqual(result_options[0].currency, 'USD')
+        self.assertEqual(result_options[1].title, 'T2')
+        self.assertEqual(result_options[1].fee, Decimal('66.6'))
+        self.assertEqual(result_options[1].currency, 'EUR')
+        return
+
+    def test_ignore_values(self):
+        # in update mode we ignore marked values
+        converter = IObjectConverter(IContact)
+        err, inv_err, data = converter.fromStringDict(
+            {"friends": IGNORE_MARKER},
+            Contact(),
+            mode='update')
+        # the ignored key/value are not part of the result
+        self.assertEqual(data, {})
+        return
+
+    def test_delete_values(self):
+        # in update mode we delete values marked accordingly
+        # 'deleting' means setting to missing_value or to default if required.
+        converter = IObjectConverter(IContact)
+        err, inv_err, data = converter.fromStringDict(
+            {"options": DELETION_MARKER,
+             "friends": DELETION_MARKER},
+            'contact', mode='update')
+        # grades are about to be set to default, friends to None
+        self.assertEqual(data, {'options': [], 'friends': None})
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_doctests.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_doctests.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_doctests.py	(revision 12677)
@@ -0,0 +1,24 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Register doctests from utils subpackage.
+"""
+from waeup.ikoba.testing import get_doctest_suite
+
+def test_suite():
+    suite = get_doctest_suite(['utils/batching.txt'])
+    return suite
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_helpers.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_helpers.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_helpers.py	(revision 12677)
@@ -0,0 +1,650 @@
+# -*- coding: utf-8 -*-
+
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+import datetime
+import os
+import pytz
+import shutil
+import tempfile
+import unittest
+import doctest
+from cStringIO import StringIO
+from zope import schema
+from zope.interface import Interface, Attribute, implements, implementer
+from zope.security.testing import Principal, Participation
+from zope.security.management import newInteraction, endInteraction
+from waeup.ikoba.utils import helpers
+
+class IFakeObject(Interface):
+    """Some marker interface."""
+
+class FakeObject(object):
+    implements(IFakeObject)
+
+class SimpleHelpersTestCase(unittest.TestCase):
+    # Tests for simple functions in `helpers`.
+    def test_product(self):
+        # the product will return zero without input
+        result1 = helpers.product([])
+        result2 = helpers.product([1,2,3])
+        result3 = helpers.product([], start=5)
+        result4 = helpers.product([1,2,3], start=5)
+        self.assertEqual(result1, 0)
+        self.assertEqual(result2, 6)
+        self.assertEqual(result3, 0)
+        self.assertEqual(result4, 30)
+        return
+
+    def test_attrs_to_fields_properties(self):
+        # we can omit single fields in order to retrieve properties
+        class IMyInterface(Interface):
+            attr1 = schema.Int(
+                title = u'Attribute 1', readonly = True, default = 110,
+                )
+
+        @helpers.attrs_to_fields
+        @implementer(IMyInterface)
+        class MyClass1(object):
+            @property
+            def attr1(self):
+                return 42
+
+        @implementer(IMyInterface)
+        class MyClass2(object):
+            @property
+            def attr1(self):
+                return 42
+        MyClass2 = helpers.attrs_to_fields(MyClass2, omit=['attr1'])
+
+        obj1 = MyClass1()
+        obj2 = MyClass2()
+
+        self.assertEqual(obj1.attr1, 110)
+        self.assertEqual(obj2.attr1, 42)
+        return
+
+
+class RemoveFileOrDirectoryTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.dirpath = tempfile.mkdtemp()
+        self.filepath = os.path.join(self.dirpath, 'somefile')
+        self.non_file = os.path.join(self.dirpath, 'nonfile')
+        open(self.filepath, 'wb').write('Hi!')
+        return
+
+    def tearDown(self):
+        if os.path.exists(self.dirpath):
+            shutil.rmtree(self.dirpath)
+        return
+
+    def test_handle_not_existing_path(self):
+        result = helpers.remove_file_or_directory(self.non_file)
+        self.assertTrue(result is None)
+        return
+
+    def test_handle_dir(self):
+        helpers.remove_file_or_directory(self.dirpath)
+        self.assertFalse(
+            os.path.exists(self.dirpath)
+            )
+        return
+
+    def test_handle_file(self):
+        helpers.remove_file_or_directory(self.filepath)
+        self.assertFalse(
+            os.path.exists(self.filepath)
+            )
+        return
+
+class CopyFileSystemTreeTestCase(unittest.TestCase):
+    # Test edge cases of copy_filesystem_tree().
+    #
+    # This is a typical case of tests not written as doctest as it is
+    # normally not interesting for developers and we only want to make
+    # sure everything works as expected.
+    def setUp(self):
+        self.existing_src = tempfile.mkdtemp()
+        self.filepath = os.path.join(self.existing_src, 'somefile')
+        open(self.filepath, 'wb').write('Hi!')
+        self.existing_dst = tempfile.mkdtemp()
+        self.not_existing_dir = tempfile.mkdtemp()
+        shutil.rmtree(self.not_existing_dir)
+
+        pass
+
+    def tearDown(self):
+        shutil.rmtree(self.existing_src)
+        shutil.rmtree(self.existing_dst)
+        pass
+
+    def test_source_and_dst_existing(self):
+        helpers.copy_filesystem_tree(self.existing_src, self.existing_dst)
+        self.assertTrue(
+            os.path.exists(
+                os.path.join(self.existing_dst, 'somefile')
+                )
+            )
+        return
+
+    def test_source_not_existing(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copy_filesystem_tree,
+            self.not_existing_dir,
+            self.existing_dst
+            )
+        return
+
+    def test_dest_not_existing(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copy_filesystem_tree,
+            self.existing_src,
+            self.not_existing_dir
+            )
+        return
+
+    def test_src_not_a_dir(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copy_filesystem_tree,
+            self.filepath,
+            self.existing_dst
+            )
+        return
+
+    def test_dst_not_a_dir(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copy_filesystem_tree,
+            self.existing_src,
+            self.filepath
+            )
+        return
+
+class ReST2HTMLTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.expected = u'<div class="document">\n\n\n<p>Some '
+        self.expected += u'test with \xfcmlaut</p>\n</div>'
+        return
+
+    def test_ascii_umlauts(self):
+        # Make sure we convert umlauts correctly to unicode.
+        source = 'Some test with ümlaut'
+        result = helpers.ReST2HTML(source)
+        self.assertEqual(result, self.expected)
+
+    def test_unicode_umlauts(self):
+        # Make sure we convert umlauts correctly to unicode.
+        source = u'Some test with ümlaut'
+        result = helpers.ReST2HTML(source)
+        self.assertEqual(result, self.expected)
+
+    def test_unicode_output_from_ascii(self):
+        source = 'Some test with ümlaut'
+        self.assertTrue(isinstance(helpers.ReST2HTML(source), unicode))
+
+    def test_unicode_output_from_unicode(self):
+        source = u'Some test with ümlaut'
+        self.assertTrue(isinstance(helpers.ReST2HTML(source), unicode))
+
+
+class FactoryBaseTestCase(unittest.TestCase):
+
+    def test_ifaces(self):
+        # We test all relevant parts in the docstring. But the interfaces
+        # method has to be tested to please the coverage report as well.
+        factory = helpers.FactoryBase()
+        factory.factory = FakeObject
+        self.assertTrue(factory.getInterfaces()(IFakeObject))
+        return
+
+class CurrentPrincipalTestCase(unittest.TestCase):
+
+    def tearDown(test):
+        endInteraction() # Just in case, one is still lingering around
+
+    def test_existing_principal(self):
+        # We can get the current principal if one is involved
+        principal = Principal('myprincipal')
+        newInteraction(Participation(principal))
+        result = helpers.get_current_principal()
+        self.assertTrue(result is principal)
+
+    def test_no_participation(self):
+        # Interactions without participation are handled correctly
+        newInteraction()
+        result = helpers.get_current_principal()
+        self.assertTrue(result is None)
+
+    def test_not_existing_principal(self):
+        # Missing interactions do not raise errors.
+        result = helpers.get_current_principal()
+        self.assertTrue(result is None)
+
+class CmpFilesTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+
+    def test_equal(self):
+        p1 = os.path.join(self.workdir, 'sample1')
+        p2 = os.path.join(self.workdir, 'sample2')
+        open(p1, 'wb').write('Hi!')
+        open(p2, 'wb').write('Hi!')
+        assert helpers.cmp_files(open(p1, 'r'), open(p2, 'r')) is True
+
+    def test_unequal(self):
+        p1 = os.path.join(self.workdir, 'sample1')
+        p2 = os.path.join(self.workdir, 'sample2')
+        open(p1, 'wb').write('Hi!')
+        open(p2, 'wb').write('Ho!')
+        assert helpers.cmp_files(open(p1, 'r'), open(p2, 'r')) is False
+
+class FileSizeTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+
+    def test_real_file(self):
+        # we can get the size of real files
+        path = os.path.join(self.workdir, 'sample.txt')
+        open(path, 'wb').write('My content')
+        self.assertEqual(
+            int(helpers.file_size(open(path, 'rb'))), 10)
+        return
+
+    def test_stringio_file(self):
+        # we can get the size of file-like objects
+        self.assertEqual(
+            helpers.file_size(StringIO('my sample content')), 17)
+
+class IfaceNamesTestCase(unittest.TestCase):
+
+    def test_iface_names(self):
+        class I1(Interface):
+            foo = Attribute("""Some Foo""")
+            def bar(blah):
+                pass
+            i1_name = schema.TextLine(title=u'i1 name')
+        class I2(I1):
+            baz = schema.TextLine(title=u'some baz')
+        class I3(I2):
+            pass
+
+        result1 = helpers.iface_names(I3)
+        result2 = helpers.iface_names(I2)
+        result3 = helpers.iface_names(I1)
+        result4 = helpers.iface_names(I3, exclude_attribs=False)
+        result5 = helpers.iface_names(I3, exclude_methods=False)
+        result6 = helpers.iface_names(I3, omit='i1_name')
+        self.assertEqual(sorted(result1), ['baz', 'i1_name'])
+        self.assertEqual(sorted(result2), ['baz', 'i1_name'])
+        self.assertEqual(sorted(result3), ['i1_name'])
+        self.assertEqual(sorted(result4), ['baz', 'foo', 'i1_name'])
+        self.assertEqual(sorted(result5), ['bar', 'baz', 'i1_name'])
+        self.assertEqual(sorted(result6), ['baz'])
+        return
+
+class DateTimeHelpersTestCase(unittest.TestCase):
+
+    def test_now(self):
+        tz_berlin = pytz.timezone('Europe/Berlin')
+        result1 = helpers.now()
+        result2 = helpers.now(tz_berlin)
+        self.assertEqual(result1.tzinfo, pytz.utc)
+        self.assertFalse(result2.tzinfo == pytz.utc)
+        self.assertFalse(result2.tzinfo is None)
+        return
+
+    def test_to_timezone(self):
+        fmt = '%Y-%m-%d %H:%M:%S %Z%z'
+        tz_berlin = pytz.timezone('Europe/Berlin')
+        tz_lagos = pytz.timezone('Africa/Lagos')
+        dt1 = datetime.datetime(2012, 1, 1, 0, 0)
+        dt2 = datetime.datetime(2012, 1, 1, 0, 0, tzinfo=tz_berlin)
+        dt3 = datetime.datetime(2012, 6, 1, 0, 0, tzinfo=tz_lagos)
+        dt4 = datetime.datetime(2012, 6, 1, 0, 0, tzinfo=tz_berlin)
+        result1 = helpers.to_timezone(dt1)
+        result2 = helpers.to_timezone(dt1, pytz.utc)
+        result3 = helpers.to_timezone(dt2)
+        result4 = helpers.to_timezone(dt2, tz_lagos)
+        result5 = helpers.to_timezone(dt3, tz_berlin)
+        result6 = helpers.to_timezone(dt4, tz_lagos)
+        self.assertEqual(
+            result1.strftime(fmt), '2012-01-01 00:00:00 UTC+0000')
+        self.assertEqual(
+            result2.strftime(fmt), '2012-01-01 00:00:00 UTC+0000')
+        self.assertEqual(
+            result3.strftime(fmt), '2011-12-31 23:00:00 UTC+0000')
+        self.assertEqual(
+            result4.strftime(fmt), '2012-01-01 00:00:00 WAT+0100')
+        self.assertEqual(
+            result5.strftime(fmt), '2012-06-01 01:46:00 CEST+0200')
+        self.assertEqual(
+            result6.strftime(fmt), '2012-06-01 00:00:00 WAT+0100')
+        return
+
+    def test_to_timezone_no_dt(self):
+        # the to_timezone function copes with dates (!= datetimes)
+        d = datetime.date(2012, 12, 1)
+        result1 = helpers.to_timezone(d)
+        result2 = helpers.to_timezone(d, pytz.utc)
+        self.assertEqual(result1, d)
+        self.assertEqual(result2, d)
+        return
+
+class GetFileFormatTestCase(unittest.TestCase):
+    # Tests for the get_fileformat helper.
+
+    def setUp(self):
+        self.valid_jpg_path = os.path.join(
+            os.path.dirname(__file__), 'sample_jpg_valid.jpg')
+        self.valid_jpg = open(self.valid_jpg_path, 'rb').read()
+        self.valid_png_path = os.path.join(
+            os.path.dirname(__file__), 'sample_png_valid.png')
+        self.valid_png = open(self.valid_png_path, 'rb').read()
+        self.valid_pdf_path = os.path.join(
+            os.path.dirname(__file__), 'sample_pdf_valid.pdf')
+        self.valid_pdf = open(self.valid_pdf_path, 'rb').read()
+        self.valid_fpm_path = os.path.join(
+            os.path.dirname(__file__), 'sample_fpm_valid.fpm')
+        self.valid_fpm = open(self.valid_fpm_path, 'rb').read()
+        return
+
+    def test_none(self):
+        # ``None`` is not a file and not a valid file format
+        self.assertEqual(helpers.get_fileformat(None), None)
+        return
+
+    def test_path_and_bytestream(self):
+        # get_fileformat accepts bytestreams and paths as arg.
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_jpg), 'jpg')
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_jpg_path), 'jpg')
+        # path is ignored when giving a bytestream
+        self.assertEqual(
+            helpers.get_fileformat('blah', self.valid_jpg), 'jpg')
+        return
+
+    def test_jpg(self):
+        # we recognize jpeg images.
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_jpg_path), 'jpg')
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_jpg), 'jpg')
+        return
+
+    def test_png(self):
+        # we recognize png images.
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_png_path), 'png')
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_png), 'png')
+        return
+
+    def test_pdf(self):
+        # we recognize pdf documents.
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_pdf_path), 'pdf')
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_pdf), 'pdf')
+        return
+
+    def test_fpm(self):
+        # we recognize fpm files.
+        # fpm files are binary fingerprint data produced by libfprint.
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_fpm_path), 'fpm')
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_fpm), 'fpm')
+        return
+
+class MergeCSVFileTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.path1 = os.path.join(self.workdir, 'myfile1')
+        self.path2 = os.path.join(self.workdir, 'myfile2')
+        self.result_path = None
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        if self.result_path is not None and os.path.exists(self.result_path):
+            os.unlink(self.result_path)
+        return
+
+    def test_basic(self):
+        # we can merge very basic CSV files
+        open(self.path1, 'wb').write('name,age\nManfred,32\n')
+        open(self.path2, 'wb').write('name,age\nBarney,28\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents,
+            'age,name\r\n'
+            '32,Manfred\r\n'
+            '28,Barney\r\n')
+        return
+
+    def test_different_col_order(self):
+        # if cols of both files have different order, that won't stop us
+        open(self.path1, 'wb').write('name,age\nManfred,32\n')
+        open(self.path2, 'wb').write('age,name\n28,Barney\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents,
+            'age,name\r\n'
+            '32,Manfred\r\n'
+            '28,Barney\r\n')
+        return
+
+    def test_different_cols_at_all(self):
+        # also cols available only in one file will work.
+        open(self.path1, 'wb').write('name,age\nManfred,32\n')
+        open(self.path2, 'wb').write('name,age,buddy\nBarney,28,Manfred\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents,
+            'age,buddy,name\r\n'
+            '32,,Manfred\r\n'
+            '28,Manfred,Barney\r\n')
+        return
+
+    def test_one_empty_input(self):
+        # we cope even with nearly empty input (one file with no data)
+        open(self.path1, 'wb').write('\n')
+        open(self.path2, 'wb').write('name,age\nManfred,32\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents,
+            'age,name\r\n'
+            '32,Manfred\r\n')
+        return
+
+    def test_two_empty_inputs(self):
+        # we cope even with empty input (two files with no data)
+        open(self.path1, 'wb').write('\n')
+        open(self.path2, 'wb').write('\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents, '\r\n')
+        return
+
+
+class CheckCSVCharsetTestCase(unittest.TestCase):
+
+    def test_valid_data1(self):
+        csv = (
+            'col1,col2,col3\n'
+            'val1,val2,val3\n'
+            ).splitlines()
+        self.assertEqual(helpers.check_csv_charset(csv), None)
+
+    def test_valid_data2(self):
+        csv = (
+            "code,title,title_prefix\n"
+            "FAC1,Faculty 1,faculty\n"
+            "FAC2,Faculty 2,institute\n"
+            "FAC3,Fäcülty 3,school\n"
+            ).splitlines()
+        self.assertEqual(helpers.check_csv_charset(csv), None)
+
+    def test_invalid_data1(self):
+        csv = (
+            'col1,col2,col3\n' +
+            chr(0x92) + 'val1,val2,val3\n'
+            ).splitlines()
+        self.assertEqual(helpers.check_csv_charset(csv), 1)
+
+    def test_invalid_data2(self):
+        csv = (
+            'some text that \n'
+            '\n'      # this empty line will break
+            'is not a csv file \n' + chr(0x92) + '\n'
+            ).splitlines()
+        self.assertEqual(helpers.check_csv_charset(csv), 2)
+
+
+class MemInfoTestCase(unittest.TestCase):
+
+    def test_getsetattrs(self):
+        # we can set/get attributes of MemInfos
+        info = helpers.MemInfo()
+        self.assertRaises(
+            KeyError, info.__getattr__, 'foo')
+        info['foo'] = 'bar'
+        assert info.foo == 'bar'
+        info.bar = 'baz'
+        assert info.bar == 'baz'
+
+    def test_getattrs(self):
+        # we can del attributes
+        info = helpers.MemInfo()
+        info['foo'] = 'bar'
+        del info['foo']
+        self.assertRaises(
+            KeyError, info.__getattr__, 'foo')
+        info['bar'] = 'baz'
+        del info.bar
+        self.assertRaises(
+            KeyError, info.__getattr__, 'bar')
+
+
+class GetMemInfoTestCase(unittest.TestCase):
+
+    @unittest.skipIf(
+        not os.path.exists('/proc/meminfo'),
+        reason="No /proc/meminfo found.")
+    def test_system(self):
+        info = helpers.get_meminfo()
+        assert isinstance(info, helpers.MemInfo)
+
+    def test_values(self):
+        sample_meminfo = os.path.join(
+            os.path.dirname(__file__), 'sample_meminfo')
+        info = helpers.get_meminfo(src=sample_meminfo)
+        assert info.Cached == 1013816
+
+    def test_invalid_src(self):
+        # we cope with invalid src files
+        info = helpers.get_meminfo(src="nOt-ExIsTiNg-FiLe")
+        assert info is None
+
+
+class Html2dictTestCase(unittest.TestCase):
+
+    def test_html2dict(self):
+        assert helpers.html2dict(None) == {}
+        assert helpers.html2dict() == {}
+        assert helpers.html2dict(9) == {}
+        assert helpers.html2dict('Hello world') == {
+            'en': u'<div id="html">Hello world</div id="html">'}
+        assert helpers.html2dict('Hello world>>de<<Hallo Welt') == {
+            'de': u'<div id="html">Hallo Welt</div id="html">',
+            'en': u'<div id="html">Hello world</div id="html">'}
+
+
+class Rest2dictTestCase(unittest.TestCase):
+
+    def test_rest2dict(self):
+        assert helpers.rest2dict(None) == {}
+        assert helpers.rest2dict() == {}
+        assert helpers.rest2dict(9) == {}
+        assert helpers.rest2dict('Hello world') == {
+            'en': u'<div id="rest"><div class="document">\n\n\n<p>Hello world'
+                   '</p>\n</div></div id="rest">'}
+        assert helpers.rest2dict('Hello world>>de<<Hallo Welt') == {
+            'de': u'<div id="rest"><div class="document">\n\n\n<p>Hallo Welt'
+                   '</p>\n</div></div id="rest">',
+            'en': u'<div id="rest"><div class="document">\n\n\n<p>Hello world'
+                   '</p>\n</div></div id="rest">'}
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    # Register local test cases...
+    for testcase in [
+        ReST2HTMLTestCase,
+        FactoryBaseTestCase,
+        CopyFileSystemTreeTestCase,
+        RemoveFileOrDirectoryTestCase,
+        CurrentPrincipalTestCase,
+        CmpFilesTestCase,
+        FileSizeTestCase,
+        IfaceNamesTestCase,
+        DateTimeHelpersTestCase,
+        GetFileFormatTestCase,
+        MergeCSVFileTestCase,
+        SimpleHelpersTestCase,
+        CheckCSVCharsetTestCase,
+        MemInfoTestCase,
+        GetMemInfoTestCase,
+        Html2dictTestCase,
+        Rest2dictTestCase,
+        ]:
+        suite.addTests(
+            unittest.TestLoader().loadTestsFromTestCase(testcase)
+            )
+    # Add tests from docstrings in helpers.py...
+    suite.addTests(
+        doctest.DocTestSuite(
+            helpers,
+            optionflags = doctest.ELLIPSIS + doctest.REPORT_NDIFF,
+            )
+        )
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_idgen.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_idgen.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_idgen.py	(revision 12677)
@@ -0,0 +1,48 @@
+import re
+import unittest
+from zope.component import queryUtility
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+from waeup.ikoba.utils.idgen import IIDSource, IDSource
+
+
+# format of raw uuids (36 chars including 4 hyphens)
+RE_FORMAT_UUID = re.compile(
+    "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
+    )
+
+# format of hex uuids (32 chars)
+RE_FORMAT_HEX_UUID = re.compile("^[0-9a-f]{32}$")
+
+
+class IDGeneratorTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts
+        src = IDSource()
+        verifyClass(IIDSource, IDSource)
+        verifyObject(IIDSource, src)
+
+    def test_get_uuid(self):
+        # we can get universally unique identifiers
+        src = IDSource()
+        result = src.get_uuid()
+        assert(RE_FORMAT_UUID.match(result))
+        assert src.get_uuid() != result
+
+    def test_get_hex_uuid(self):
+        # we can get UUIDs as hex strings
+        src = IDSource()
+        result = src.get_hex_uuid()
+        assert (RE_FORMAT_HEX_UUID.match(result))
+        assert src.get_hex_uuid() != result
+
+
+class FunctionalIDSourceTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_IIDSource_utility_registered(self):
+        # an IIDSource is registered on startup
+        util = queryUtility(IIDSource)
+        assert util is not None
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_logger.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_logger.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_logger.py	(revision 12677)
@@ -0,0 +1,426 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+# Tests for waeup.ikoba.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.ikoba.app import Company
+from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
+from waeup.ikoba.utils.logger import (
+    Logger, MAX_BYTES, BACKUP_COUNT, ILoggerCollector, LoggerCollector,
+    ILogger)
+
+class FakeComponent(object):
+    # A component that can use the Logger mixin-class
+    def do_something(self):
+        self.logger.info('msg from do_something')
+
+class LoggingComponent(FakeComponent, Logger):
+    # A component that supports logging by mixing in Logger
+    logger_name = 'sample.${sitename}'
+    logger_filename = 'sample.log'
+
+class LoggersTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoggersTests, self).setUp()
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        self.dc_root2 = None
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(LoggersTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def clear_logger(self, logger):
+        del_list = [x for x in logger.handlers]
+        for handler in del_list:
+            logger.removeHandler(handler)
+        return
+
+    def test_iface(self):
+        setSite(self.app)
+        logger = LoggingComponent()
+        verifyClass(ILogger, Logger)
+        verifyObject(ILogger, logger)
+        return
+
+    def test_logger_prop_no_site(self):
+        # If not in a site, we will get a logger without handlers
+        logger = Logger().logger
+        self.assertTrue(isinstance(logger, logging.Logger))
+        self.assertEqual(len(logger.handlers), 0)
+        return
+
+    def test_site_no_site_name(self):
+        # If a site has not name we will get a logger anyway
+        setSite(self.app)
+        del self.app.__name__
+        logger = Logger().logger
+        self.assertTrue(logger is not None)
+        return
+
+    def test_get_logdir(self):
+        # We can get a logdir
+        setSite(self.app)
+        logger = Logger()
+        logdir = logger.logger_get_logdir()
+        expected_logdir = os.path.join(
+            self.app['datacenter'].storage, 'logs')
+        self.assertEqual(logdir, expected_logdir)
+        return
+
+    def test_get_logdir_no_site(self):
+        # We cannot get a logdir without a site
+        logger = Logger()
+        logdir = logger.logger_get_logdir()
+        self.assertTrue(logdir is None)
+        return
+
+    def test_get_logdir_no_datacenter(self):
+        # Having a site, is not enough. We also need a datacenter in it.
+        setSite(self.app)
+        del self.app['datacenter']
+        logger = Logger()
+        logdir = logger.logger_get_logdir()
+        self.assertTrue(logdir is None)
+        return
+
+    def test_get_logdir_create_dir(self):
+        # If the logdirectory does not exist, we will create it
+        setSite(self.app)
+        logger = Logger()
+        expected_logdir = os.path.join(
+            self.app['datacenter'].storage, 'logs')
+        if os.path.exists(expected_logdir):
+            shutil.rmtree(expected_logdir)
+        logdir = logger.logger_get_logdir()
+        exists_after = os.path.exists(expected_logdir)
+        self.assertEqual(logdir, expected_logdir)
+        self.assertTrue(exists_after is True)
+        return
+
+    def test_get_logfile(self):
+        # We can get a logfile
+        setSite(self.app)
+        logger = Logger()
+        expected_filepath = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        path = logger.logger_get_logfile_path()
+        self.assertEqual(expected_filepath, path)
+        return
+
+    def test_get_logfile_no_dir(self):
+        # We cannot get a logfilepath if there is no dir for it
+        setSite(self.app)
+        logger = Logger()
+        del self.app['datacenter']
+        path = logger.logger_get_logfile_path()
+        self.assertTrue(path is None)
+        return
+
+    def test_setup(self):
+        # We can setup a logger.
+        mylogger = logging.getLogger('test.sample')
+        setSite(self.app)
+        logger = Logger()
+        result = logger.logger_setup(mylogger)
+        self.assertEqual(len(result.handlers), 1)
+        handler = result.handlers[0]
+        if hasattr(handler, 'maxBytes'):
+            self.assertEqual(handler.maxBytes, MAX_BYTES)
+        if hasattr(handler, 'backupCount'):
+            self.assertEqual(handler.backupCount, BACKUP_COUNT)
+        self.assertTrue(result.propagate is False)
+        return
+
+    def test_setup_no_site(self):
+        # Without a site we get no logger from setup
+        mylogger = logging.getLogger('test.sample')
+        logger = Logger()
+        result = logger.logger_setup(mylogger)
+        self.assertTrue(result is None)
+        return
+
+    def test_logfile_change(self):
+        # When the logfile location changes, logger_logfile_changed can react
+        setSite(self.app)
+        logger = Logger()
+        logger.logger.warn('Warning 1')  # Log something to old logfile
+        filename1 = logger.logger.handlers[0].baseFilename
+        content1 = open(filename1, 'r').read()
+        # Now move the logfile
+        self.dc_root2 = tempfile.mkdtemp()
+        self.app['datacenter'].setStoragePath(self.dc_root2)
+        logger.logger_logfile_changed()
+        logger.logger.warn('Warning 2')  # Log something to new logfile
+        filename2 = logger.logger.handlers[0].baseFilename
+        content2 = open(filename2, 'r').read()
+        self.assertTrue('Warning 1' in content1)
+        self.assertTrue('Warning 2' in content2)
+        self.assertTrue('Warning 1' not in content2)
+        self.assertTrue('Warning 2' not in content1)
+        self.assertTrue(filename1 != filename2)
+        shutil.rmtree(self.dc_root2)
+        return
+
+    def test_logger_additional_handlers(self):
+        # When we detect additional handlers in a logger, setup anew.
+        setSite(self.app)
+        logger = Logger()
+        old_py_logger = logger.logger
+        handler = logging.StreamHandler()
+        logger.logger.addHandler(handler)
+        old_handler_num = len(old_py_logger.handlers)
+        new_py_logger = logger.logger  # This should detect new handler
+        new_handler_num = len(new_py_logger.handlers)
+        self.assertEqual(new_handler_num, 1)
+        self.assertTrue(isinstance(
+            new_py_logger.handlers[0],
+            logging.handlers.WatchedFileHandler))
+
+class LoggersFunctionalTests(FunctionalTestCase):
+    # Check loggers with real components using them
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoggersFunctionalTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+
+        self.app['mycomponent'] = LoggingComponent()
+        self.component = self.app['mycomponent']
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        super(LoggersFunctionalTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_component_can_get_logger(self):
+        # Components can get a logger
+        logger = self.component.logger
+        self.assertTrue(logger is not None)
+        self.assertEqual(len(logger.handlers), 1)
+        self.assertEqual(logger.name, 'sample.app')
+        return
+
+    def test_component_handler_setup(self):
+        # handlers of components are setup
+        handler = self.component.logger.handlers[0]
+        self.assertTrue(isinstance(
+                handler, logging.handlers.WatchedFileHandler))
+        if hasattr(handler, 'maxBytes'):
+            self.assertEqual(handler.maxBytes, MAX_BYTES)
+        if hasattr(handler, 'backupCount'):
+            self.assertEqual(handler.backupCount, BACKUP_COUNT)
+        self.assertTrue(handler.baseFilename.endswith('logs/sample.log'))
+        return
+
+    def test_component_can_log(self):
+        # a method of some 'loggerized' instance can log like this.
+        self.component.logger.setLevel(logging.INFO)
+        self.component.do_something()
+        handler = logging.getLogger('sample.app').handlers[0]
+        logfile = handler.baseFilename
+        content = open(logfile, 'rb').read()
+        self.assertTrue(content.endswith(' - INFO - system - msg from do_something\n'))
+        return
+
+class LoggerCollectorTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoggerCollectorTests, self).setUp()
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+
+        self.app['mycomponent'] = LoggingComponent()
+        self.component = self.app['mycomponent']
+        setSite(self.app)
+
+        self.component = LoggingComponent()
+
+
+        return
+
+    def tearDown(self):
+        super(LoggerCollectorTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_ifaces(self):
+        collector = LoggerCollector()
+        verifyClass(ILoggerCollector, LoggerCollector)
+        verifyObject(ILoggerCollector, collector)
+        return
+
+    def test_get_utility(self):
+        # We can get a logger collector via utility lookup
+        util1 = queryUtility(ILoggerCollector, default=None)
+        util2 = queryUtility(ILoggerCollector, default=None)
+        self.assertTrue(util1 is util2)
+        self.assertTrue(isinstance(util1, LoggerCollector))
+        return
+
+    def test_get_loggers(self):
+        # We can get loggers from a logger collector
+        collector = LoggerCollector()
+        result0 = collector.getLoggers(None)
+        result1 = collector.getLoggers('not-a-site')
+        result2 = collector.getLoggers(self.app)
+        collector.registerLogger(self.app, self.component)
+        result3 = collector.getLoggers(self.app)
+        self.assertEqual(result0, [])
+        self.assertEqual(result1, [])
+        self.assertEqual(result2, [])
+        self.assertEqual(result3, [self.component])
+        self.assertTrue(result3[0] is self.component)
+        return
+
+    def test_register_logger(self):
+        # We can register loggers by name
+        collector = LoggerCollector()
+        collector.registerLogger(None, self.component) # Should have no effect
+        collector.registerLogger(object(), self.component) # Same here
+        collector.registerLogger(self.app, self.component)
+        self.assertEqual(['app'], collector.keys())
+        return
+
+    def test_register_logger_double(self):
+        # Registering the same component twice gives one entry, not two
+        collector = LoggerCollector()
+        collector.registerLogger(self.app, self.component)
+        collector.registerLogger(self.app, self.component)
+        self.assertEqual(len(collector['app']), 1)
+        return
+
+    def test_unregister_logger(self):
+        # We can also unregister loggers
+        collector = LoggerCollector()
+        collector.registerLogger(self.app, self.component)
+        collector.unregisterLogger(self.app, self.component)
+        result = collector.getLoggers(self.app)
+        self.assertEqual(result, [])
+        return
+
+    def test_unregister_logger_invalid(self):
+        # We can cope with unregistration of non-existing entries.
+        collector = LoggerCollector()
+        collector.registerLogger(self.app, self.component)
+        collector.unregisterLogger(None, self.component)
+        collector.unregisterLogger(self.app, 'nonsense')
+        self.assertEqual(len(collector), 1)
+        return
+
+class LogfileChangeTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LogfileChangeTests, self).setUp()
+        # Setup a sample site for each test
+        app = Company()
+        self.dc_root = tempfile.mkdtemp()
+        self.dc_root_new = None
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+
+        self.app['mycomponent'] = LoggingComponent()
+        self.component = self.app['mycomponent']
+        setSite(self.app)
+        self.component = LoggingComponent()
+        return
+
+    def tearDown(self):
+        super(LogfileChangeTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        if self.dc_root_new is not None:
+            shutil.rmtree(self.dc_root_new)
+        clearSite()
+        return
+
+    def test_storage_move(self):
+        # When a datacenter storage (its path) changes, we have to
+        # move the logfiles as well.
+        filename1 = self.component.logger.handlers[0].baseFilename
+        self.dc_root_new = tempfile.mkdtemp()
+        self.app['datacenter'].setStoragePath(self.dc_root_new)
+        filename2 = self.component.logger.handlers[0].baseFilename
+        self.assertTrue(filename1 != filename2)
+        self.assertTrue(filename2.startswith(self.dc_root_new))
+        return
+
+    def test_storage_deletion(self):
+        # When a site is deleted, the loggers should go away as well
+        self.dc_root_new = tempfile.mkdtemp()
+        logger1 = self.component.logger.handlers # create entry in collector
+        collector = queryUtility(ILoggerCollector)
+        loggers1 = collector.keys()
+        root = self.app.__parent__
+        del root['app']
+        loggers2 = collector.keys()
+        self.assertEqual(loggers1, ['app'])
+        self.assertEqual(loggers2, [])
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_utils.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_utils.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/tests/test_utils.py	(revision 12677)
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import psutil
+import sys
+import unittest
+from waeup.ikoba.interfaces import IIkobaUtils
+from waeup.ikoba.utils.utils import IkobaUtils
+from zope.interface import verify
+
+
+class IkobaUtilsTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.max_vmem = psutil.virtual_memory().total
+        self.max_smem = psutil.swap_memory().total
+
+    def get_cleared_util(self):
+        # Helper: get a `KofUtil` instance with all values of
+        # SYSTEM_MAX_LOAD dict set to ``None``
+        util = IkobaUtils()
+        for key, val in util.SYSTEM_MAX_LOAD.items():
+            util.SYSTEM_MAX_LOAD[key] = None
+        return util
+
+    def test_iface(self):
+        # IkobaUtils fullfill IIkobaUtils expectations
+        utils = IkobaUtils()
+        verify.verifyClass(IIkobaUtils, IkobaUtils)
+        verify.verifyObject(IIkobaUtils, utils)
+
+    def test_expensive_actions_allowed_swap_none(self):
+        # unset swap maximum values make KofUtils ignore swap values
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = None
+        assert utils.expensive_actions_allowed() == True
+        # even not-set values won't block us
+        del utils.SYSTEM_MAX_LOAD['swap-mem']
+        assert utils.expensive_actions_allowed() == True
+
+    @unittest.skipIf(
+        psutil.swap_memory().percent >= 99.0,
+        reason="System swap use over 99%. Cannot set higher allowed value.")
+    def test_expensive_actions_allowed_swap_ok(self):
+        # We can react to high swap values
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = 99.0           # positive float
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = -1.0           # negative float
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = sys.maxint     # positive int
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = -1             # negative int
+        assert utils.expensive_actions_allowed() == True
+
+    @unittest.skipIf(
+        not psutil.swap_memory().percent,
+        reason="Can test swapping behavior only if actually swapping")
+    def test_expensive_actions_allowed_swap_too_much(self):
+        # We can react if too much swap is used
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = 0.0            # positive float
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = -100.0         # negative float
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = 0              # positive int
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = -(sys.maxint)  # negative int
+        assert utils.expensive_actions_allowed() == False
+
+    def test_expensive_actions_allowed_virtmem_none(self):
+        # unset virtmem maximum values make KofUtils ignore virtmem values
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = None
+        assert utils.expensive_actions_allowed() == True
+        # even not-set values won't block us
+        del utils.SYSTEM_MAX_LOAD['virt-mem']
+        assert utils.expensive_actions_allowed() == True
+
+    @unittest.skipIf(
+        psutil.virtual_memory().percent >= 99.0,
+        reason="System virtmem use over 99%. Cannot set higher allowed value.")
+    def test_expensive_actions_allowed_virtmem_ok(self):
+        # We can react to high virtmem values
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = 99.0           # positive float
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = -1.0           # negative float
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = sys.maxint     # positive int
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = -1             # negative int
+        assert utils.expensive_actions_allowed() == True
+
+    @unittest.skipIf(
+        not psutil.virtual_memory().percent,
+        reason="Can test virtmem behavior only if actually using some")
+    def test_expensive_actions_allowed_virtmem_too_much(self):
+        # We can react if too much virtmem is used
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = 0.0            # positive float
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = -100.0         # negative float
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = 0              # positive int
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = -(sys.maxint)  # negative int
+        assert utils.expensive_actions_allowed() == False
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/utils.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/utils.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/utils/utils.py	(revision 12677)
@@ -0,0 +1,338 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""General helper utilities for Ikoba.
+"""
+import grok
+import psutil
+import string
+import pytz
+from random import SystemRandom as r
+from zope.i18n import translate
+from waeup.ikoba.interfaces import IIkobaUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.smtp import send_mail as send_mail_internally
+from waeup.ikoba.utils.helpers import (
+    get_sorted_preferred, get_current_principal)
+from waeup.ikoba.payments.currencies import ISO_4217_CURRENCIES
+
+
+def send_mail(from_name, from_addr,
+              rcpt_name, rcpt_addr,
+              subject, body, config):
+    """Wrapper for the real SMTP functionality in :mod:`waeup.ikoba.smtp`.
+
+    Merely here to stay compatible with lots of calls to this place.
+    """
+    mail_id = send_mail_internally(
+        from_name, from_addr, rcpt_name, rcpt_addr,
+        subject, body, config)
+    return True
+
+
+#: A list of phone prefixes (order num, country, prefix).
+#: Items with same order num will be sorted alphabetically.
+#: The lower the order num, the higher the precedence.
+INT_PHONE_PREFIXES = [
+    (99, _('Germany'), '49'),
+    (1, _('Nigeria'), '234'),
+    (99, _('U.S.'), '1'),
+    ]
+
+
+def sorted_phone_prefixes(data=INT_PHONE_PREFIXES, request=None):
+    """Sorted tuples of phone prefixes.
+
+    Ordered as shown above and formatted for use in select boxes.
+
+    If request is given, we'll try to translate all country names in
+    order to sort alphabetically correctly.
+
+    XXX: This is a function (and not a constant) as different
+    languages might give different orders. This is not tested yet.
+
+    XXX: If we really want to use alphabetic ordering here, we might
+    think about caching results of translations.
+    """
+    if request is not None:
+        data = [
+            (x, translate(y, context=request), z)
+            for x, y, z in data]
+    return tuple([
+        ('%s (+%s)' % (x[1], x[2]), '+%s' % x[2])
+        for x in sorted(data)
+        ])
+
+
+class IkobaUtils(grok.GlobalUtility):
+    """A collection of parameters and methods subject to customization.
+
+    """
+    grok.implements(IIkobaUtils)
+    # This the only place where we define the portal language
+    # which is used for the translation of system messages
+    # (e.g. object histories).
+    PORTAL_LANGUAGE = 'en'
+
+    PREFERRED_LANGUAGES_DICT = {
+        'en': (1, u'English'),
+        'fr': (2, u'Fran&ccedil;ais'),
+        'de': (3, u'Deutsch'),
+        'ha': (4, u'Hausa'),
+        'yo': (5, u'Yoruba'),
+        'ig': (6, u'Igbo'),
+        }
+
+    # CURRENCIES must be equal to or an excerpt of ISO_4217_CURRENCIES
+    CURRENCIES = ISO_4217_CURRENCIES
+
+    #: A function to return
+    @classmethod
+    def sorted_phone_prefixes(cls, data=INT_PHONE_PREFIXES, request=None):
+        return sorted_phone_prefixes(data, request)
+
+    CON_CATS_DICT = {
+        'sample': 'Sample Category',
+        'license': 'License',
+        'no': _('no contract'),
+        }
+
+    PAYMENT_CATEGORIES = {
+        'license': 'License Fee',
+        }
+
+    #: Set positive number for allowed max, negative for required min
+    #: avail.
+    #:
+    #: Use integer for bytes value, float for percent
+    #: value. `cpu-load`, of course, accepts float values only.
+    #: `swap-mem` = Swap Memory, `virt-mem` = Virtual Memory,
+    #: `cpu-load` = CPU load in percent.
+    SYSTEM_MAX_LOAD = {
+        'swap-mem': None,
+        'virt-mem': None,
+        'cpu-load': 100.0,
+        }
+
+    EXPORTER_NAMES = (
+        'pdfdocuments',
+        'htmldocuments',
+        'restdocuments',
+        'users',
+        'products',
+        'customers',
+        'customersampledocuments',
+        'samplecontracts')
+
+    BATCH_PROCESSOR_NAMES = (
+        'customerprocessor',
+        'customersampledocumentprocessor',
+        'samplecontractprocessor',
+        'productprocessor',
+        'pdfdocumentprocessor',
+        'htmldocumentprocessor',
+        'restdocumentprocessor',
+        'userprocessor')
+
+    def sendContactForm(self, from_name, from_addr, rcpt_name, rcpt_addr,
+                        from_username, usertype, portal, body, subject):
+        """Send an email with data provided by forms.
+        """
+        config = grok.getSite()['configuration']
+        text = _(u"""Fullname: ${a}
+User Id: ${b}
+User Type: ${c}
+Portal: ${d}
+
+${e}
+""")
+        text = _(text, mapping={
+            'a': from_name,
+            'b': from_username,
+            'c': usertype,
+            'd': portal,
+            'e': body})
+        body = translate(text, 'waeup.ikoba',
+            target_language=self.PORTAL_LANGUAGE)
+        if not (from_addr and rcpt_addr):
+            return False
+        return send_mail(
+            from_name, from_addr, rcpt_name, rcpt_addr,
+            subject, body, config)
+
+    @property
+    def tzinfo(self):
+        # For Nigeria: pytz.timezone('Africa/Lagos')
+        # For Germany: pytz.timezone('Europe/Berlin')
+        return pytz.utc
+
+    def fullname(self, firstname, lastname, middlename=None):
+        """Full name constructor.
+        """
+        # We do not necessarily have the middlename attribute
+        if middlename:
+            name = '%s %s %s' % (firstname, middlename, lastname)
+        else:
+            name = '%s %s' % (firstname, lastname)
+        return string.capwords(
+            name.replace('-', ' - ')).replace(' - ', '-')
+
+    def genPassword(self, length=8, chars=string.letters + string.digits):
+        """Generate a random password.
+        """
+        return ''.join([r().choice(chars) for i in range(length)])
+
+    def sendCredentials(self, user, password=None, url_info=None, msg=None):
+        """Send credentials as email.
+
+        Input is the customer for which credentials are sent and the
+        password.
+
+        Returns True or False to indicate successful operation.
+        """
+        subject = 'Your Ikoba credentials'
+        text = _(u"""Dear ${a},
+
+${b}
+Registration and Application Portal of
+${c}.
+
+Your user name: ${d}
+Your password: ${e}
+${f}
+
+Please remember your user name and keep
+your password secret!
+
+Please also note that passwords are case-sensitive.
+
+Regards
+""")
+        config = grok.getSite()['configuration']
+        from_name = config.name_admin
+        from_addr = config.email_admin
+        rcpt_name = user.title
+        rcpt_addr = user.email
+        text = _(text, mapping={
+            'a': rcpt_name,
+            'b': msg,
+            'c': config.name,
+            'd': user.name,
+            'e': password,
+            'f': url_info})
+
+        body = translate(text, 'waeup.ikoba',
+            target_language=self.PORTAL_LANGUAGE)
+        return send_mail(
+            from_name, from_addr, rcpt_name, rcpt_addr,
+            subject, body, config)
+
+    def sendTransitionInfo(self, customer, obj, msg=None):
+        """Send transition information as email.
+
+        Input is the customer for which credentials are sent and the object
+        which was transitioned.
+
+        Returns True or False to indicate successful operation.
+        """
+        config = grok.getSite()['configuration']
+        if not config.email_notification:
+            return
+        subject = 'Ikoba status change information'
+        text = _(u"""Dear ${a},
+
+The status of the following object has been changed:
+
+Object Id: ${c}
+Title: ${d}
+Transition: ${e}
+New state: ${f}
+
+Regards,
+
+${b}
+
+--
+${g}
+""")
+        from_name = config.name_admin
+        from_addr = config.email_admin
+        rcpt_name = customer.title
+        rcpt_addr = customer.email
+
+        user = get_current_principal()
+        if user is None:
+            usertitle = 'system'
+        elif user.id == 'zope.anybody':
+            usertitle = 'Anonymous'
+        else:
+            usertitle = getattr(user, 'public_name', None)
+            if not usertitle:
+                usertitle = user.title
+
+        msg = translate(msg,'waeup.ikoba', target_language=self.PORTAL_LANGUAGE)
+        new_state = translate(obj.translated_state,'waeup.ikoba',
+            target_language=self.PORTAL_LANGUAGE)
+
+        text = _(text, mapping={
+            'a': rcpt_name,
+            'b': usertitle,
+            'c': obj.__name__,
+            'd': obj.title,
+            'e': msg.lower(),
+            'f': new_state,
+            'g': config.name})
+
+        body = translate(text, 'waeup.ikoba',
+            target_language=self.PORTAL_LANGUAGE)
+
+        return send_mail(
+            from_name, from_addr, rcpt_name, rcpt_addr,
+            subject, body, config)
+
+    def expensive_actions_allowed(self, type=None, request=None):
+        """Tell, whether expensive actions are currently allowed.
+
+        Check system load/health (or other external circumstances) and
+        locally set values to see, whether expensive actions should be
+        allowed (`True`) or better avoided (`False`).
+
+        Use this to allow or forbid exports, report generations, or
+        similar actions.
+        """
+        max_values = self.SYSTEM_MAX_LOAD
+        for (key, func) in (
+            ('swap-mem', psutil.swap_memory),
+            ('virt-mem', psutil.virtual_memory),
+            ):
+            max_val = max_values.get(key, None)
+            if max_val is None:
+                continue
+            mem_val = func()
+            if isinstance(max_val, float):
+                # percents
+                if max_val < 0.0:
+                    max_val = 100.0 + max_val
+                if mem_val.percent > max_val:
+                    return False
+            else:
+                # number of bytes
+                if max_val < 0:
+                    max_val = mem_val.total + max_val
+                if mem_val.used > max_val:
+                    return False
+        return True
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/webservices.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/webservices.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/webservices.py	(revision 12677)
@@ -0,0 +1,48 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+import grok
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.ikoba.interfaces import ICompany
+
+class XMLRPCPermission(grok.Permission):
+    """Permission for using XMLRPC functions.
+    """
+    grok.name('waeup.xmlrpc')
+
+class XMLRPCUsers1(grok.Role):
+    """Usergroup 1
+    """
+    grok.name('waeup.xmlrpcusers1')
+    grok.title('XMLRPC Users Group 1')
+    grok.permissions('waeup.xmlrpc',)
+
+class CompanyXMLRPC(grok.XMLRPC):
+    """XMLRPC webservices for Ikoba portals.
+
+    Please note, that XMLRPC does not support real keyword arguments
+    but positional arguments only.
+    """
+    grok.context(ICompany)
+
+    @grok.require('waeup.Public')
+    def xmlrpc_api_version(self):
+        """Return the current API version for XMLRPC clients.
+        """
+        return u'0.1'
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/__init__.py	(revision 12677)
@@ -0,0 +1,2 @@
+"""JS-driven widgets for Ikoba.
+"""
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/datetimewidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/datetimewidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/datetimewidget.py	(revision 12677)
@@ -0,0 +1,89 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Datetimewidgets that are compatible with pytz.
+"""
+import datetime
+import pytz
+from zope.component import queryUtility
+from zope.datetime import parseDatetimetz
+from zope.datetime import DateTimeError
+from zope.formlib.i18n import _
+from zope.formlib.interfaces import ConversionError
+from zope.formlib.textwidgets import DatetimeWidget
+from waeup.ikoba.utils.helpers import to_timezone
+from waeup.ikoba.interfaces import IIkobaUtils
+
+class _DummyUtils(object):
+    tzinfo = pytz.utc
+
+# A fallback, if no IIkobaUtils can be found
+_DUMMY_UTILS = _DummyUtils()
+
+class PytzDatetimeWidget(DatetimeWidget):
+    """pytz-conform, non-ambigous UTC datetimes.
+
+    While the standard formlib datetime widget makes use of
+    zope.datetime and creates also not too reliable (or ambigous)
+    datetimes based on local servertime, this widget provides
+    non-ambigous UTC datetimes with a pytz timezone (pytz.utc).
+
+    Using this widget for datetime data we always get clean UTC
+    datetimes compatible with other Python packages (as pytz is more
+    wide-spread than zope.datetime).
+
+    For datetimes in other timezones we compute the correct UTC value
+    and store this. A way to help making sure, only UTC-based values
+    go into the DB.
+
+    For datetimes without any timezone set, we interpret the input to
+    be meant as local app-time. I.e. if application TZ is
+    ``Africa/Lagos``, we assume that a string like '2012-02-01 12:13'
+    (apparently not providing TZ info) was meant as 12:13 h Lagos
+    time.
+
+    From zope.datetime, however, we save the parser abilities to
+    interpret even bizarre entered data as some datetime.
+    """
+    def _toFieldValue(self, string):
+        """Turn string into a UTC-based datetime.
+
+        The TZ data is guaranteed to be pytz.utc.
+
+        """
+        # In import files we can use the hash symbol at the end of a
+        # datetime and date strings to avoid annoying automatic date
+        # transformation by Excel or Calc.
+        string = string.strip('#')
+        if string == self._missing:
+            return self.context.missing_value
+        else:
+            try:
+                # Different to original implementation we do not
+                # automatically request local server time if no TZ was
+                # set in `string`. In this case we want a datetime with
+                # tzinfo set to `None` instead.
+                value = parseDatetimetz(string, local=False)
+            except (DateTimeError, ValueError, IndexError), v:
+                raise ConversionError(_("Invalid datetime data"), v)
+
+        if not isinstance(value, datetime.datetime):
+            return value
+        if value.tzinfo is None:
+            utils = queryUtility(IIkobaUtils, default=_DUMMY_UTILS)
+            value = utils.tzinfo.localize(value)
+        return value.astimezone(pytz.utc)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/datewidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/datewidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/datewidget.py	(revision 12677)
@@ -0,0 +1,248 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+A datewidget with customizable date format.
+"""
+import pytz
+from datetime import datetime
+from zope.component import queryUtility
+from zope.formlib.interfaces import ConversionError, IDisplayWidget
+from zope.formlib.textwidgets import DateWidget, DateDisplayWidget, escape
+from zope.formlib.widget import renderElement, CustomWidgetFactory
+from zope.interface import implements
+from waeup.ikoba.interfaces import IIkobaUtils
+from waeup.ikoba.utils.helpers import to_timezone
+
+
+#: A dictionary of supported date formats.
+#:
+#: The following formats are supported:
+#:
+#: ``iso``
+#:    ISO format ``YYYY-MM-DD``
+#: ``le``
+#:    little endian with slashes: ``DD/MM/YYYY``
+#: ``de``
+#:    german date format: ``DD.MM.YYYY``
+#: ``us``
+#:    middle endian format common in the U.S.: ``MM/DD/YYYY``
+#:
+#: Furthermore we support for input widgets an additional year
+#: marker. Input date widgets with this marker provide also a year
+#: selector, handy for dates of birth etc.
+#:
+#: The year-supporting formats are similar to the basic versions above:
+#:
+#: ``iso-year``
+#:    ISO format ``YYYY-MM-DD``
+#: ``le-year``
+#:    little endian with slashes: ``DD/MM/YYYY``
+#: ``de-year``
+#:    german date format: ``DD.MM.YYYY``
+#: ``us-year``
+#:    middle endian format common in the U.S.: ``MM/DD/YYYY``
+#:
+#: For date display widgets there is naturally no difference between a
+#: year and non-year setting (you can for instance use 'le' or 'le-year'
+#: with the same output).
+DATE_FORMATS = {
+    'iso': ('datepicker', '%Y-%m-%d'),
+    'le':  ('datepicker-le', '%d/%m/%Y'),
+    'de':  ('datepicker-de', '%d.%m.%Y'),
+    'us':  ('datepicker-us', '%m/%d/%Y'),
+    'iso-year': ('datepicker-year', '%Y-%m-%d'),
+    'le-year':  ('datepicker-le-year', '%d/%m/%Y'),
+    'de-year':  ('datepicker-de-year', '%d.%m.%Y'),
+    'us-year':  ('datepicker-us-year', '%m/%d/%Y'),
+    }
+
+#: a dict containing tuples (<FORMAT>, <SHOW_YEAR>) as keys and
+#: suitable CSS tags as values.
+FORMATS_BY_VALUE = dict(
+    [((val[1], 'year' in key), val[0]) for key, val in DATE_FORMATS.items()])
+
+class FormattedDateWidget(DateWidget):
+    """A date widget that supports different (and _explicit_) date formats.
+
+    If the widget is bound to a schema field with respective
+    attributes, it reads its `show_year` and `date_format` attributes
+    (see waeup.ikoba.schema.FormattedDate for an example) and sets a
+    CSS tag according to these values.
+
+    The widget also accepts ISO format as a fallback, even if a
+    different format was set. This should help with imports.
+
+    This is an input widget.
+    """
+    date_format = '%Y-%m-%d'
+    show_year = False
+
+    def __init__(self, context, request, *args, **kw):
+        # try to grab date_format and show_year from bound schema field.
+        date_format = getattr(context, 'date_format', self.date_format)
+        if date_format is not None:
+            self.date_format = date_format
+        self.show_year = getattr(context, 'show_year', self.show_year)
+        # add css class determined by date_format and show_year
+        css_cls = FORMATS_BY_VALUE.get((self.date_format, self.show_year), '')
+        self.cssClass = ' '.join([self.cssClass, css_cls]).strip()
+        return super(FormattedDateWidget, self).__init__(
+            context, request, *args, **kw)
+
+    def _toFieldValue(self, input):
+        # In import files we can use the hash symbol at the end of a
+        # date string to avoid annoying automatic date transformation
+        # by Excel or Calc
+        input = input.strip('#')
+        if input == self._missing:
+            return self.context.missing_value
+        else:
+            try:
+                value = datetime.strptime(input, self.date_format)
+            except (ValueError, IndexError), v:
+                try:
+                    # Try ISO format as fallback.
+                    # This is needed for instance during imports.
+                    value = datetime.strptime(
+                        input, FormattedDateWidget.date_format)
+                except (ValueError, IndexError), v:
+                    raise ConversionError("Invalid datetime data", v)
+        return value.date()
+
+    def _toFormValue(self, value):
+        if value:
+            try:
+                value = value.strftime(self.date_format)
+            except ValueError:
+                return value
+        return value
+
+
+class FormattedDateDisplayWidget(DateDisplayWidget):
+    """A date widget that supports different (and _explicit_) date formats.
+
+    This is a display widget.
+
+    It can also be used for displaying datetimes. If used to display a
+    datetime (not a date), the widget returns local datetime with
+    timezone set according to IkobaUtils.
+    """
+    date_format = '%Y-%m-%d'
+    show_year = False
+
+    implements(IDisplayWidget)
+
+    def __init__(self, context, request, *args, **kw):
+        # try to grab date_format and show_year from bound schema field.
+        date_format = getattr(context, 'date_format', self.date_format)
+        if date_format is not None:
+            self.date_format = date_format
+        self.show_year = getattr(context, 'show_year', self.show_year)
+        return super(FormattedDateDisplayWidget, self).__init__(
+            context, request, *args, **kw)
+
+    def __call__(self):
+        if self._renderedValueSet():
+            content = self._data
+        else:
+            content = self.context.default
+        if content == self.context.missing_value:
+            return ""
+        if isinstance(content, datetime):
+            # shift value to local timezone
+            tz = pytz.utc
+            utils = queryUtility(IIkobaUtils)
+            if utils is not None:
+                tz = utils.tzinfo
+            content = to_timezone(content, tz)
+        try:
+            content = content.strftime(self.date_format)
+        except ValueError:
+            return None
+        return renderElement("span", contents=escape(content),
+                             cssClass=self.cssClass)
+
+class DateLEWidget(FormattedDateWidget):
+    date_format = '%d/%m/%Y'
+
+class DateDEWidget(FormattedDateWidget):
+    date_format = '%d.%m.%Y'
+
+class DateUSWidget(FormattedDateWidget):
+    date_format = '%m/%d/%Y'
+
+class DateLEDisplayWidget(FormattedDateDisplayWidget):
+    date_format = '%d/%m/%Y'
+
+class DateDEDisplayWidget(FormattedDateDisplayWidget):
+    date_format = '%d.%m.%Y'
+
+class DateUSDisplayWidget(FormattedDateDisplayWidget):
+    date_format = '%m/%d/%Y'
+
+def FriendlyDateWidget(format):
+    """Get a friendly date input widget for `format`.
+
+    This widget is suitable for edit and add forms.
+
+    Valid `format` values are the keys of `DATE_FORMATS`
+    dict. Default is ``le`` (little endian; DD/MM/YYYY).
+
+    Friendly date widgets are rendered with a specialized CSS tag for
+    enabling JavaScript datepickers.
+    """
+    css_class, date_format = DATE_FORMATS.get(format, DATE_FORMATS['le'])
+    return CustomWidgetFactory(
+        FormattedDateWidget,
+        cssClass=css_class,
+        date_format=date_format)
+
+def FriendlyDateDisplayWidget(format):
+    """Get a friendly date display widget for `format`.
+
+    This widget is suitable for display forms.
+
+    Valid `format` values are the keys of `DATE_FORMATS`
+    dict. Default is ``le`` (little endian; DD/MM/YYYY).
+
+    This widget is not rendered with a specialized CSS tag for
+    enabling JavaScript datepickers. `css_class` is ignored which means
+    there is nor difference between e.g. ``le`` and ``le-year``.`
+    """
+    css_class, date_format = DATE_FORMATS.get(format, DATE_FORMATS['le'])
+    return CustomWidgetFactory(
+        FormattedDateDisplayWidget,
+        date_format=date_format)
+
+def FriendlyDatetimeDisplayWidget(format):
+    """Get a friendly datetime display widget for `format`.
+
+    This widget is suitable for display forms.
+
+    Valid `format` values are the keys of `DATE_FORMATS`
+    dict. Default is ``le`` (little endian; DD/MM/YYYY %H:%M:%S).
+
+    This widget is not rendered with a specialized CSS tag for
+    enabling JavaScript datepickers. `css_class` is ignored which means
+    there is no difference between e.g. ``le`` and ``le-year``.`
+    """
+    css_class, date_format = DATE_FORMATS.get(format, DATE_FORMATS['le'])
+    datetime_format = date_format + ' %H:%M:%S'
+    return CustomWidgetFactory(
+        FormattedDateDisplayWidget,
+        date_format=datetime_format)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/hrefwidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/hrefwidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/hrefwidget.py	(revision 12677)
@@ -0,0 +1,36 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""A widget that renders a persistent object title inside an anchor element
+referring to the object.
+"""
+import grok
+from zope.formlib.widget import DisplayWidget
+
+
+class HREFDisplayWidget(DisplayWidget):
+
+    def __call__(self):
+        if self._renderedValueSet():
+            title = getattr(self._data, 'title', '')
+            name = getattr(self._data, '__name__', '')
+            if not name:
+                return ''
+            url = grok.url(self.request, self._data)
+        else:
+            return ''
+        return '<a href="%s">%s - %s</a>' % (url, name, title)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/interfaces.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/interfaces.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/interfaces.py	(revision 12677)
@@ -0,0 +1,21 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Interfaces for widgets.
+"""
+from zope.interface import Interface
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/objectwidget.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/objectwidget.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/objectwidget.pt	(revision 12677)
@@ -0,0 +1,18 @@
+<div class="form-inline">
+  <span tal:repeat="widget context/subwidgets">
+    <div class="form-group">
+      <span for="field.name" title="The widget's hint"
+        tal:attributes="for widget/name; title widget/hint"
+        tal:replace="widget/label">
+          LABEL
+        </span>
+      <tal:block define="error widget/error"
+        condition="error" content="structure error">
+        ERROR
+      </tal:block>
+      <div tal:replace="structure widget">
+        WIDGET
+      </div>
+    </div>
+  </span>
+</div>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/objectwidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/objectwidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/objectwidget.py	(revision 12677)
@@ -0,0 +1,86 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""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 IkobaObjectWidgetView(ObjectWidgetView):
+    template = ViewPageTemplateFile('objectwidget.pt')
+
+class IProductOptionWidgetDisplayView(ObjectWidgetView):
+    template = ViewPageTemplateFile('productoptiondisplaywidget.pt')
+
+class IkobaObjectWidget(ObjectWidget):
+
+    def __init__(self, context, request, factory, **kw):
+        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):
+        self._setUpEditWidgets()
+        # enlarge title widgets
+        for subwidget in self.subwidgets():
+            if 'title' in subwidget.name:
+                subwidget.displayWidth = '50%'
+        return 
+
+    def _getView(self, request):
+        return IkobaObjectWidgetView(self, request)
+
+
+class IkobaObjectDisplayWidget(IkobaObjectWidget):
+
+    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()
+
+    def _getView(self, request):
+        if self.context.schema.__name__ == 'IProductOption':
+            return IProductOptionWidgetDisplayView(self, request)
+        return IkobaObjectWidgetView(self, request)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/orderedSelectionList.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/orderedSelectionList.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/orderedSelectionList.pt	(revision 12677)
@@ -0,0 +1,198 @@
+<script type="text/javascript">
+
+function moveItems(from, to)
+  {
+  // shortcuts for selection fields
+  var src = document.getElementById(from);
+  var tgt = document.getElementById(to);
+
+  if (src.selectedIndex == -1) selectionError();
+  else
+    {
+    // iterate over all selected items
+    // --> attribute "selectedIndex" doesn't support multiple selection.
+    // Anyway, it works here, as a moved item isn't selected anymore,
+    // thus "selectedIndex" indicating the "next" selected item :)
+    while (src.selectedIndex > -1)
+      if (src.options[src.selectedIndex].selected)
+        {
+        // create a new virtal object with values of item to copy
+        temp = new Option(src.options[src.selectedIndex].text,
+                      src.options[src.selectedIndex].value);
+        // append virtual object to targe
+        tgt.options[tgt.length] = temp;
+        // want to select newly created item
+        temp.selected = true;
+        // delete moved item in source
+        src.options[src.selectedIndex] = null;
+      }
+    }
+  }
+
+// move item from "from" selection to "to" selection
+function from2to(name)
+  {
+  moveItems(name+".from", name+".to");
+  copyDataForSubmit(name);
+  }
+
+// move item from "to" selection back to "from" selection
+function to2from(name)
+  {
+  moveItems(name+".to", name+".from");
+  copyDataForSubmit(name);
+  }
+
+function swapFields(a, b)
+  {
+  // swap text
+  var temp = a.text;
+  a.text = b.text;
+  b.text = temp;
+  // swap value
+  temp = a.value;
+  a.value = b.value;
+  b.value = temp;
+  // swap selection
+  temp = a.selected;
+  a.selected = b.selected;
+  b.selected = temp;
+  }
+
+// move selected item in "to" selection one up
+function moveUp(name)
+  {
+  // shortcuts for selection field
+  var toSel = document.getElementById(name+".to");
+
+  if (toSel.selectedIndex == -1)
+      selectionError();
+  else if (toSel.options[0].selected)
+      alert("Cannot move further up!");
+  else for (var i = 0; toSel.length > i; i++)
+    if (toSel.options[i].selected)
+      {
+      swapFields(toSel.options[i-1], toSel.options[i]);
+      copyDataForSubmit(name);
+      }
+  }
+
+// move selected item in "to" selection one down
+function moveDown(name)
+  {
+    // shortcuts for selection field
+    var toSel = document.getElementById(name+".to");
+
+    if (toSel.selectedIndex == -1) {
+        selectionError();
+    } else if (toSel.options[toSel.length-1].selected) {
+        alert("Cannot move further down!");
+    } else {
+      for (var i = toSel.length-1; i >= 0; i--) {
+        if (toSel.options[i].selected) {
+          swapFields(toSel.options[i+1], toSel.options[i]);
+        }
+      }
+      copyDataForSubmit(name);
+    }
+  }
+
+// copy each item of "toSel" into one hidden input field
+function copyDataForSubmit(name)
+  {
+  // shortcuts for selection field and hidden data field
+  var toSel = document.getElementById(name+".to");
+  var toDataContainer = document.getElementById(name+".toDataContainer");
+
+  // delete all child nodes (--> complete content) of "toDataContainer" span
+  while (toDataContainer.hasChildNodes())
+      toDataContainer.removeChild(toDataContainer.firstChild);
+
+  // create new hidden input fields - one for each selection item of
+  // "to" selection
+  for (var i = 0; toSel.options.length > i; i++)
+    {
+    // create virtual node with suitable attributes
+    var newNode = document.createElement("input");
+    var newAttr = document.createAttribute("name");
+    newAttr.nodeValue = name;
+    newNode.setAttributeNode(newAttr);
+
+    newAttr = document.createAttribute("type");
+    newAttr.nodeValue = "hidden";
+    newNode.setAttributeNode(newAttr);
+
+    newAttr = document.createAttribute("value");
+    newAttr.nodeValue = toSel.options[i].value;
+    newNode.setAttributeNode(newAttr);
+
+    // actually append virtual node to DOM tree
+    toDataContainer.appendChild(newNode);
+    }
+  }
+
+// error message for missing selection
+function selectionError()
+  {alert("Must select something!")}
+
+</script>
+
+<table border="0" class="ordered-selection-field">
+  <tr>
+    <td>
+      <select id="from" name="from" size="5" multiple=""
+          tal:attributes="name string:${view/name}.from;
+                          id string:${view/name}.from;
+                          size view/size"
+                          >
+        <option tal:repeat="entry view/choices"
+                tal:attributes="value entry/value"
+                tal:content="entry/text"/>
+      </select>
+    </td>
+    <td>
+      <button class="btn btn-default" name="from2toButton" type="button" value=" -&gt;"
+          onclick="javascript:from2to()"
+          tal:attributes="onClick string:javascript:from2to('${view/name}')"
+          ><img src="/static/img/arrow-right.png" alt="Add" /></button>
+      <br />
+      <button class="btn btn-default" name="to2fromButton" type="button"
+          onclick="javascript:to2from()"
+          tal:attributes="onClick string:javascript:to2from('${view/name}')"
+          ><img src="/static/img/arrow-left.png" alt="Remove" /></button>
+    </td>
+    <td>
+      <select id="to" name="to" size="5" multiple=""
+          tal:attributes="name string:${view/name}.to;
+                          id string:${view/name}.to;
+                          size view/size">
+        <option tal:repeat="entry view/selected"
+                tal:attributes="value entry/value"
+                tal:content="entry/text"/>
+      </select>
+      <input name="foo-empty-marker" type="hidden"
+        tal:attributes="name string:${view/name}-empty-marker"/>
+      <span id="toDataContainer"
+            tal:attributes="id string:${view/name}.toDataContainer">
+        <script type="text/javascript" tal:content="string:
+          copyDataForSubmit('${view/name}');">
+          // initial copying of field "field.to" --> "field"
+          copyDataForSubmit("<i tal:replace="${view/name}"/>");
+        </script>
+      </span>
+    </td>
+    <td>
+      <button class="btn btn-default"
+          name="upButton" type="button" value="^"
+          onclick="javascript:moveUp()"
+          tal:attributes="onClick string:javascript:moveUp('${view/name}')"
+          ><img src="/static/img/arrow-up.png" alt="Up" /></button>
+      <br />
+      <button class="btn btn-default"
+          name="downButton" type="button" value="v"
+          onclick="javascript:moveDown()"
+          tal:attributes="onClick string:javascript:moveDown('${view/name}')"
+          ><img src="/static/img/arrow-down.png" alt="Down" /></button>
+    </td>
+  </tr>
+</table>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/overrides.zcml
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/overrides.zcml	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/overrides.zcml	(revision 12677)
@@ -0,0 +1,65 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+
+  <adapter
+      for="zope.schema.interfaces.IDate
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.ikoba.widgets.datewidget.FormattedDateWidget"
+      permission="zope.Public"
+      />
+
+  <adapter
+      for="waeup.ikoba.schema.interfaces.IFormattedDate
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.ikoba.widgets.datewidget.DateLEWidget"
+      permission="zope.Public"
+      />
+
+  <adapter
+      for="waeup.ikoba.schema.interfaces.IFormattedDate
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.IDisplayWidget"
+      factory="waeup.ikoba.widgets.datewidget.DateLEDisplayWidget"
+      permission="zope.Public"
+      />
+
+  <!-- Register edit widget for PhoneNumbers -->
+  <adapter
+      for="waeup.ikoba.schema.interfaces.IPhoneNumber
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.ikoba.widgets.phonewidget.PhoneWidget"
+      permission="zope.Public"
+      />
+
+  <!-- Register pytz-aware edit widget for datetimes -->
+  <adapter
+      for="zope.schema.interfaces.IDatetime
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.ikoba.widgets.datetimewidget.PytzDatetimeWidget"
+      permission="zope.Public"
+      />
+
+  <!-- Register IkobaSequenceWidget for ProductOptionSource -->
+  <adapter
+      for="zope.schema.interfaces.IList
+           waeup.ikoba.customers.vocabularies.IProductOptionSource
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.ikoba.widgets.sequencewidget.IkobaSequenceWidget"
+      permission="zope.Public"
+      />      
+
+  <!-- Register IkobaSourceOrderedMultiSelectWidget for IterableSource -->
+  <adapter
+      for="zope.schema.interfaces.IList
+           zope.schema.interfaces.IIterableSource
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.ikoba.widgets.source.IkobaSourceOrderedMultiSelectWidget"
+      permission="zope.Public"
+      />
+
+</configure>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/phonewidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/phonewidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/phonewidget.py	(revision 12677)
@@ -0,0 +1,171 @@
+"""A phone number widget.
+
+This widget is an input widget (not made for display forms but for
+edit and add forms).
+
+It can be used for :class:`zope.schema.TextLine` fields but has to be
+requested by the form manually (otherwise the regular TextLine widget
+will be used for rendering).
+
+If you use the PhoneWidget for rendering regular TextLine attributes
+(preferably in edit forms or add forms), the phone number is displayed
+by three input fields representing the international code, the area
+code and the extension line.
+
+When the entered input is stored with a context object, it is stored
+as a single unicode string with the numbers divided by single hyphen.
+
+So, input <+12>, <111>, <444> becomes the string ``'+12-111-444'`` for
+the context object.
+"""
+import grok
+import re
+from zope.component import queryUtility
+from zope.formlib.interfaces import MissingInputError, InputErrors
+from zope.interface import Interface
+from zope.formlib.textwidgets import (
+    TextWidget, renderElement, ConversionError)
+from waeup.ikoba.interfaces import IIkobaUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.utils.utils import IkobaUtils
+
+RE_INT_PREFIX = re.compile('^\+\d+')
+RE_NUMBERS = re.compile('^\d+$')
+RE_NUMBERS_AND_HYPHENS = re.compile('^[\d\-]+$')
+
+class PhoneWidget(TextWidget):
+
+    subwidget_names = ('country', 'area', 'ext')
+    cssClass = 'phone-input'
+
+    def _renderPrefixWidget(self, value):
+        prefix_func = getattr(
+            queryUtility(IIkobaUtils), 'sorted_phone_prefixes',
+            IkobaUtils.sorted_phone_prefixes)
+        options = []
+        for ptitle, pval in prefix_func(request=self.request):
+            selected = ''
+            if value == pval:
+                selected = ' selected="selected" '
+            options.append(
+                '<option value="%s"%s>%s</option>' % (pval, selected, ptitle))
+        options = '\n'.join(options)
+        return '<select id="%s" name="%s" size="1" class="%s">\n%s\n</select>' % (
+            '%s.%s' % (self.name, 'country'),
+            '%s.%s' % (self.name, 'country'),
+            self.cssClass,
+            options)
+
+    def __call__(self):
+        value = self._getFormValue()
+        if value is None or value == self.context.missing_value:
+            value = ''
+        if len(value.split('-')) < 2:
+            value = '--' + value
+        subvalues = value.split('-', 2)
+
+        kwargs = {'type': self.type,
+                  'name': self.name,
+                  'id': self.name,
+                  'value': value,
+                  'cssClass': self.cssClass,
+                  'style': self.style,
+                  'size': self.displayWidth,
+                  'extra': self.extra}
+        if self.displayMaxWidth:
+            kwargs['maxlength'] = self.displayMaxWidth # TODO This is untested.
+        fields = []
+        for num, subname in enumerate(self.subwidget_names):
+            if num == 0:
+                select = self._renderPrefixWidget(subvalues[num])
+                fields.append(select)
+                continue
+                print select
+            kwargs.update(name = '%s.%s' % (self.name, subname))
+            kwargs.update(id=kwargs['name'])
+            kwargs.update(cssClass = '%s %s' % ('', self.cssClass))
+            kwargs.update(value = subvalues[num])
+            fields.append(renderElement(self.tag, **kwargs))
+        return '-'.join(fields)
+
+    def _getFormInput(self):
+        """Returns current form input.
+
+        The value returned must be in a format that can be used as the 'input'
+        argument to `_toFieldValue`.
+
+        The default implementation returns the form value that corresponds to
+        the widget's name. Subclasses may override this method if their form
+        input consists of more than one form element or use an alternative
+        naming convention.
+        """
+        result = '-'.join(
+            [self.request.get('%s.%s' % (self.name, name), '')
+             for name in self.subwidget_names])
+        return result
+
+    def _toFieldValue(self, input):
+        """Check value entered in form further.
+
+        Raises ConversionError if values entered contain non-numbers.
+
+        For the extension line we silently allow slashes as well.
+        """
+        # In import files we can use the hash symbol at the end of a
+        # date string to avoid annoying automatic number transformation
+        # by Excel or Calc
+        input = input.strip('#')
+        result = super(PhoneWidget, self)._toFieldValue(input)
+        parts = input.split('-', 2)
+        if '' in parts and self.context.required:
+            raise ConversionError(
+                _("Empty phone field(s)."), MissingInputError(
+                    self.name, self.label, None))
+        if parts[0] != '' and not RE_INT_PREFIX.match(parts[0]):
+            raise ConversionError(
+                _("Int. prefix requires format '+NNN'"),
+                ValueError('invalid international prefix'))
+        # Make sure there are only numbers in parts 1..N. We do not allow
+        # dashes in last field any more.
+        errors = [(x != '' and not RE_NUMBERS.match(x)) for x in parts[1:]]
+        error = True in errors
+        if error:
+            raise ConversionError(
+                _("Phone numbers may contain numbers only."),
+                ValueError('non numbers in phone number'))
+        # We consider also values ending with empty ending as missing
+        # values.  An 'empty ending' is a form where the fields
+        # following the prefix are all empty.  This means that any
+        # prefix setting in the form will switch back to default upon
+        # submit if no further phone fields are filled.  As advantage
+        # we get only valid phone numbers or missing value.
+        empty_ending = '-'*(len(parts) - 1)
+        if result in ('', None) or result.endswith(empty_ending):
+            result = self.context.missing_value
+        return result
+
+    def _getFormValue(self):
+        """Returns a value suitable for use in an HTML form.
+
+        Detects the status of the widget and selects either the input value
+        that came from the request, the value from the _data attribute or the
+        default value.
+        """
+        try:
+            input_value = self._getCurrentValueHelper()
+        except InputErrors:
+            form_value = '-'.join(
+                [self.request.form.get('%s.%s' % (self.name, name), '')
+                 for name in self.subwidget_names]
+                )
+        else:
+            form_value = self._toFormValue(input_value)
+        return form_value
+
+    def hasInput(self):
+        """A phone widget has input if all three subfields have input.
+        """
+        for name in self.subwidget_names:
+            if '%s.%s' % (self.name, name) not in self.request.form:
+                return False
+        return True
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/productoptiondisplaywidget.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/productoptiondisplaywidget.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/productoptiondisplaywidget.pt	(revision 12677)
@@ -0,0 +1,6 @@
+<span tal:repeat="widget context/subwidgets">
+    <div tal:replace="structure widget">
+      WIDGET
+    </div>
+  <span tal:condition="python: not 'fee' in widget.name and not 'currency' in widget.name">@</span>
+</span>
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/sequencewidget.pt
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/sequencewidget.pt	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/sequencewidget.pt	(revision 12677)
@@ -0,0 +1,32 @@
+<table i18n:domain="waeup.ikoba">
+  <tr tal:repeat="widget view/widgets">
+    <td class="swcheckbox">
+      <input type="checkbox"
+             tal:attributes="
+                 name string:${view/name}.remove_${repeat/widget/index}"
+             tal:condition="view/need_delete" />
+    </td>
+    <td>
+      <input tal:replace="structure widget" />
+    </td>
+  </tr>
+  <tr>
+    <td colspan="2">
+      <p class="hint" i18n:translate="">
+      Don't forget to save the form after adding or deleting items.
+      </p>
+      <button type="submit"
+             class="btn btn-default"
+             tal:condition="view/need_add"
+             tal:attributes="name string:${view/name}.add"
+             tal:content="view/addButtonLabel" />
+      <button type="submit" class="btn btn-default"
+             tal:condition="view/need_delete"
+             tal:attributes="name string:${view/name}.remove"
+             i18n:translate="remove-selected-items">Remove selected items</button>
+      <a id="ikoba-scroll-target"
+         tal:condition="view/_ikoba_seq_len_changed" />
+    </td>
+  </tr>
+</table>
+<input tal:replace="structure view/marker" />
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/sequencewidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/sequencewidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/sequencewidget.py	(revision 12677)
@@ -0,0 +1,108 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""An improved sequence widget.
+
+We provide a sequence widget that better suits our needs layout-wise
+than the default implementation from zope.formlib.
+"""
+import grok
+from zope.i18n import translate
+from zope.browserpage import ViewPageTemplateFile
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from zope.formlib.widgets import (
+    ListSequenceWidget, SequenceDisplayWidget)
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.schema.interfaces import IField, IList
+
+
+class IkobaSequenceWidget(ListSequenceWidget):
+    """A sequence widget for lists.
+
+    This is basically a plain copy from zope.formlib. We have,
+    however, the possibility to tweak the attached template.
+    """
+    template = ViewPageTemplateFile('sequencewidget.pt')
+    _ikoba_seq_len_changed = False
+
+    def _generateSequence(self):
+        result = super(IkobaSequenceWidget, self)._generateSequence()
+        if ((self.name + '.add' in self.request.form) or
+            (self.name + '.remove' in self.request.form)):
+            self._ikoba_seq_len_changed = True
+        return result
+
+
+class IkobaSequenceDisplayWidget(SequenceDisplayWidget):
+    """A sequence widget for lists.
+
+    This is basically a plain copy from zope.formlib. We have,
+    however, the possibility to tweak html tags.
+    """
+
+    tag = None
+    itemTag = 'div'
+
+    def __call__(self):
+        """Patch for the orginal __call__ method.
+
+        The orginal __call__ method doesn't call
+        the translate function properly.
+        """
+
+        # get the data to display:
+        if self._renderedValueSet():
+            data = self._data
+        else:
+            data = self.context.get(self.context.context)
+
+        # deal with special cases:
+        if data == self.context.missing_value:
+            return translate(self._missingValueMessage, context=self.request)
+        data = list(data)
+        if not data:
+            return translate(self._emptySequenceMessage, context=self.request)
+
+        parts = []
+        for i, item in enumerate(data):
+            widget = self._getWidget(i)
+            widget.setRenderedValue(item)
+            s = widget()
+            if self.itemTag:
+                s = "<%s>%s</%s>" % (self.itemTag, s, self.itemTag)
+            parts.append(s)
+        contents = "\n".join(parts)
+        if self.tag:
+            contents = "\n%s\n" % contents
+            contents = renderElement(self.tag,
+                                     cssClass=self.cssClass,
+                                     extra=self.extra,
+                                     contents=contents)
+        return contents
+
+
+# Register our sequence widgets as default for lists.
+@grok.adapter(IList, IField, IBrowserRequest)
+@grok.implementer(IInputWidget)
+def seq_input_widget(obj, field, req, *args, **kw):
+    return IkobaSequenceWidget(obj, field, req, *args, **kw)
+
+
+@grok.adapter(IList, IField, IBrowserRequest)
+@grok.implementer(IDisplayWidget)
+def seq_display_widget(obj, field, req, *args, **kw):
+    return IkobaSequenceDisplayWidget(obj, field, req, *args, **kw)
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/source.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/source.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/source.py	(revision 12677)
@@ -0,0 +1,31 @@
+## $Id$
+##
+## Copyright (C) 2015 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Improved source widgets.
+
+"""
+
+from zope.browserpage import ViewPageTemplateFile
+from zope.formlib.source import SourceOrderedMultiSelectWidget
+
+class IkobaSourceOrderedMultiSelectWidget(SourceOrderedMultiSelectWidget):
+    """A multi-selection widget with ordering support."""
+
+    template = ViewPageTemplateFile('orderedSelectionList.pt')
+
+    def __call__(self):
+        return self.template()
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/__init__.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/__init__.py	(revision 12677)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_datetimewidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_datetimewidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_datetimewidget.py	(revision 12677)
@@ -0,0 +1,102 @@
+# Tests for datetimewidgets
+import datetime
+import pytz
+from zope import schema
+from zope.component import getGlobalSiteManager
+from zope.formlib import form
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from zope.formlib.tests.test_functional_textwidget import(
+    FunctionalWidgetTestCase,)
+from zope.formlib.textwidgets import TextWidget
+from zope.interface import Interface, implements
+from zope.interface.verify import verifyClass, verifyObject
+from zope.publisher.browser import TestRequest
+from waeup.ikoba.interfaces import IIkobaUtils
+from waeup.ikoba.widgets.datetimewidget import PytzDatetimeWidget
+
+class IContent(Interface):
+    my_dt = schema.Datetime(
+        title = u'A datetime.',
+        )
+
+class Content(object):
+    implements(IContent)
+    my_dt = None
+
+class SampleForm(form.EditForm):
+    form_fields = form.fields(IContent)
+    form_fields['my_dt'].custom_widget = PytzDatetimeWidget
+
+class FakeUtils(object):
+    # Fake app-wide set timezone.
+    implements(IIkobaUtils)
+    tzinfo = pytz.timezone('America/Sao_Paulo')
+
+
+class PytzDatetimeWidgetTests(FunctionalWidgetTestCase):
+
+    widgets = [
+        (schema.interfaces.IDatetime, TextWidget),
+        ]
+
+    def setUp(self):
+        super(PytzDatetimeWidgetTests, self).setUp()
+        self.gsm = getGlobalSiteManager()
+        self.utils = FakeUtils()
+        self.gsm.registerUtility(self.utils)
+        return
+
+    def tearDown(self):
+        super(PytzDatetimeWidgetTests, self).tearDown()
+        self.gsm.unregisterUtility(self.utils)
+        return
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = PytzDatetimeWidget(IContent['my_dt'], TestRequest())
+        verifyClass(IInputWidget, PytzDatetimeWidget)
+        verifyObject(IInputWidget, obj)
+        return
+
+    def test_to_value(self):
+        # we get always UTC-based values
+        content = Content()
+        request = TestRequest()
+
+        request.form['form.my_dt'] = u'2012-01-02 12:10:19 GMT+1'
+        request.form['form.actions.apply'] = u''
+        SampleForm(content, request)()
+
+        self.assertEqual( content.my_dt, datetime.datetime(
+            2012, 1, 2, 11, 10, 19, tzinfo=pytz.utc))
+        self.assertTrue(content.my_dt.tzinfo is pytz.utc)
+        return
+
+    def test_export_values(self):
+        # make sure we understand values as exported
+        content = Content()
+        request = TestRequest()
+
+        request.form['form.my_dt'] = u'2012-01-02 12:10:19+00:00'
+        request.form['form.actions.apply'] = u''
+        SampleForm(content, request)()
+
+        self.assertEqual( content.my_dt, datetime.datetime(
+            2012, 1, 2, 12, 10, 19, tzinfo=pytz.utc))
+        self.assertTrue(content.my_dt.tzinfo is pytz.utc)
+        return
+
+    def test_datetimes_wo_tz(self):
+        # Datetimes w/o tz are considered to be meant in app-wide timezone.
+        content = Content()
+        request = TestRequest()
+
+        request.form['form.my_dt'] = u'2011-11-11 11:11:11'
+        request.form['form.actions.apply'] = u''
+        SampleForm(content, request)()
+
+        # Sao Paulo was two hours back UTC on 2011-11-11
+        self.assertEqual(content.my_dt, datetime.datetime(
+            2011, 11, 11, 13, 11, 11, tzinfo=pytz.utc))
+        self.assertTrue(content.my_dt.tzinfo is pytz.utc)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_datewidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_datewidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_datewidget.py	(revision 12677)
@@ -0,0 +1,192 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Formatted Date widget tests.
+"""
+import datetime
+import unittest
+import doctest
+from zope.schema import Date
+from zope.interface import Interface, implementer
+from zope.interface.verify import verifyClass
+from zope.publisher.browser import TestRequest
+
+from zope.formlib.tests.test_browserwidget import (
+    SimpleInputWidgetTest, BrowserWidgetTest, )
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from waeup.ikoba.widgets.datewidget import (
+    FormattedDateWidget, FormattedDateDisplayWidget, )
+from zope.formlib.widgets import DateI18nWidget
+from zope.formlib.interfaces import ConversionError, WidgetInputError
+
+
+class FormattedDateWidgetTest(SimpleInputWidgetTest):
+    """Documents and tests the formatted date widget.
+
+        >>> verifyClass(IInputWidget, FormattedDateWidget)
+        True
+    """
+
+    _FieldFactory = Date
+    _WidgetFactory = FormattedDateWidget
+
+    def setUpContent(self, desc=u'', title=u'Foo Title'):
+        # same as in base class but with a min value of date(1910, 1, 1)
+        field = self._FieldFactory(
+            __name__='foo', title=title, description=desc,
+            min=datetime.date(1910, 1, 1))
+        class ITestContent(Interface):
+            foo = field
+        @implementer(ITestContent)
+        class TestObject:
+            pass
+        self.content = TestObject()
+        field = ITestContent['foo']
+        self.field = field.bind(self.content)
+        request = TestRequest(HTTP_ACCEPT_LANGUAGE='ru')
+        request.form['field.foo'] = u'Foo Value'
+        self._widget = self._WidgetFactory(field, request)
+
+    def testRender(self):
+        super(FormattedDateWidgetTest, self).testRender(
+            datetime.date(2003, 3, 26),
+            ('type="text"', 'id="field.foo"', 'name="field.foo"',
+                'value="2003-03-26"'))
+
+    def testRenderCustomFormat(self):
+        self._widget.date_format = "%d/%m/%y"
+        super(FormattedDateWidgetTest, self).testRender(
+            datetime.datetime(2004, 3, 26, 12, 58, 59),
+            ('type="text"', 'id="field.foo"', 'name="field.foo"',
+                'value="26/03/04"'))
+
+    def testProperties(self):
+        self.assertEqual(self._widget.tag, 'input')
+        self.assertEqual(self._widget.type, 'text')
+        # By default the date format is ISO and the rendered CSS 'datepicker'
+        self.assertEqual(self._widget.cssClass, 'datepicker')
+        self.assertEqual(self._widget.extra, '')
+
+    def test_hasInput(self):
+        del self._widget.request.form['field.foo']
+        self.assertFalse(self._widget.hasInput())
+        self._widget.request.form['field.foo'] = u''
+        self.assertTrue(self._widget.hasInput())
+        self._widget.request.form['field.foo'] = u'2003-03-26'
+        self.assertTrue(self._widget.hasInput())
+
+    def test_getDefaultInputValue(self,
+            value=u'2004-03-26',
+            check_value=datetime.date(2004, 3, 26)):
+        self._widget.request.form['field.foo'] = u''
+        self.assertRaises(WidgetInputError, self._widget.getInputValue)
+        self._widget.request.form['field.foo'] = value
+        self.assertEquals(self._widget.getInputValue(), check_value)
+        self._widget.request.form['field.foo'] = u'abc'
+        self.assertRaises(ConversionError, self._widget.getInputValue)
+
+    def test_getCustomInputValue_de(self):
+        self._widget.date_format = "%d.%m.%y"
+        self.test_getDefaultInputValue(u'26.03.04')
+
+    def test_getCustomInputValue_de2(self):
+        self._widget.date_format = "%d.%m.%Y"
+        self.test_getDefaultInputValue(u'26.03.2004')
+
+    def test_getCustomInputValue_us(self):
+        self._widget.date_format = "%m/%d/%Y"
+        self.test_getDefaultInputValue(u'03/26/2004')
+
+    def test_minimal_value_respected(self):
+        # we did set up the date field to require dates >= 1900-01-01
+        request = TestRequest()
+        # setting a date > 1900-01-01 is okay
+        request.form['field.foo'] = '1912-03-27'
+        widget = FormattedDateWidget(self.field, request)
+        self.assertEqual(
+            widget.getInputValue(), datetime.date(1912, 3, 27))
+
+        # setting a date < 1900-01-01 will fail
+        request.form['field.foo'] = '1812-03-27'
+        widget = FormattedDateWidget(self.field, request)
+        self.assertRaises(
+            WidgetInputError,
+            widget.getInputValue)
+        # check the correct exception message
+        try:
+            widget.getInputValue()
+        except WidgetInputError as exc:
+            # just catch the exception
+            pass
+        exc = '%r' % exc  # turn exception into string
+        self.assertEqual(
+            exc,
+            "WidgetInputError('foo', u'Foo Title', "
+            "TooSmall(datetime.date(1812, 3, 27), datetime.date(1910, 1, 1)))"
+            )
+        return
+
+class FormattedDateDisplayWidgetTest(BrowserWidgetTest):
+    """The FormatterdDisplayDateWidget complies with IDisplayWidget.
+
+        >>> verifyClass(IDisplayWidget, FormattedDateDisplayWidget)
+        True
+    """
+
+    _WidgetFactory = FormattedDateDisplayWidget
+    expected_class = "date"
+
+    def setUp(self):
+        super(FormattedDateDisplayWidgetTest, self).setUp()
+        self._value = datetime.date(2004, 12, 01)
+
+    def testDefaultDisplayStyle(self):
+        self.assertFalse(self._widget.displayStyle)
+
+    def testRenderDefault(self):
+        self._widget.setRenderedValue(self._value)
+        self.verifyResult(self._widget(),
+                          ["<span",
+                           'class="%s"' % self.expected_class,
+                           "2004-12-01",
+                           "</span"])
+
+    def testRenderCustom(self):
+        self._widget.setRenderedValue(self._value)
+        self._widget.date_format = '%m/%d/%Y'
+        self.verifyResult(self._widget(),
+                          ["<span",
+                           'class="%s"' % self.expected_class,
+                           "12/01/2004",
+                           "</span"])
+
+    def testRenderNone(self):
+        self._widget.setRenderedValue(None)
+        self._widget.date_format = '%m/%d/%Y'
+        self.assertEqual(self._widget(), '')
+
+    def testNoValueSet(self):
+        self._widget.date_format = '%m/%d/%Y'
+        self.assertEqual(self._widget(), '')
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(FormattedDateWidgetTest),
+        unittest.makeSuite(FormattedDateDisplayWidgetTest),
+        doctest.DocTestSuite(),
+        ))
+
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_objectwidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_objectwidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_objectwidget.py	(revision 12677)
@@ -0,0 +1,234 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Tests for objectwidget.
+"""
+import doctest
+import unittest
+import sys
+from zope.interface import Interface, implements
+from zope.interface.verify import verifyClass
+from zope.publisher.browser import TestRequest
+from zope.schema import Object, TextLine
+from zope.schema.interfaces import ITextLine
+from zope.traversing.adapters import DefaultTraversable
+from zope.traversing.interfaces import ITraversable
+from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+from zope.component import provideAdapter
+
+from zope.formlib.interfaces import (
+    IInputWidget, IDisplayWidget,MissingInputError)
+from zope.formlib.widgets import TextWidget, DisplayWidget
+from zope.formlib.tests.test_browserwidget import BrowserWidgetTest
+from zope.formlib.interfaces import IWidgetInputErrorView
+
+from waeup.ikoba.widgets.objectwidget import IkobaObjectWidget as ObjectWidget
+from waeup.ikoba.widgets.objectwidget import (
+    IkobaObjectDisplayWidget as ObjectDisplayWidget)
+
+class ITestContact(Interface):
+    name = TextLine()
+    email = TextLine()
+
+class TestContact(object):
+    implements(ITestContact)
+
+class ITestContent(Interface):
+    foo = Object(
+        ITestContact,
+        title = u'Foo Title',
+        description = u'',
+        )
+
+class ObjectWidgetInputErrorView(object):
+    implements(IWidgetInputErrorView)
+
+    def __init__(self, error, request):
+        self.error = error
+        self.request = request
+
+    def snippet(self):
+        return repr(self.error)
+
+class ObjectWidgetTest(BrowserWidgetTest):
+    """Documents and tests the object widget.
+
+        >>> from zope.interface.verify import verifyClass
+        >>> verifyClass(IInputWidget, ObjectWidget)
+        True
+    """
+
+    _FieldFactory = Object
+    def _WidgetFactory(self, context, request, **kw):
+        kw.update({'factory': TestContact})
+        return ObjectWidget(context, request, **kw)
+
+    def setUpContent(self, desc=u'', title=u'Foo Title'):
+        provideAdapter(TextWidget, (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+
+        class TestObject(object):
+            implements(ITestContent)
+
+        self.content = TestObject()
+        self.field = ITestContent['foo']
+        self.request = TestRequest(HTTP_ACCEPT_LANGUAGE='pl')
+        self.request.form['field.foo'] = u'Foo Value'
+        self._widget = self._WidgetFactory(self.field, self.request)
+
+    def setUp(self):
+        super(ObjectWidgetTest, self).setUp()
+        self.field = Object(ITestContact, __name__=u'foo')
+        provideAdapter(TextWidget,
+                       (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+
+    def test_applyChanges(self):
+        self.request.form['field.foo.name'] = u'Foo Name'
+        self.request.form['field.foo.email'] = u'foo@foo.test'
+        widget = self._WidgetFactory(self.field, self.request)
+
+        self.assertEqual(widget.applyChanges(self.content), True)
+        self.assertEqual(hasattr(self.content, 'foo'), True)
+        self.assertEqual(isinstance(self.content.foo, TestContact), True)
+        self.assertEqual(self.content.foo.name, u'Foo Name')
+        self.assertEqual(self.content.foo.email, u'foo@foo.test')
+
+    def test_error(self):
+        provideAdapter(
+            ObjectWidgetInputErrorView,
+            (MissingInputError, TestRequest),
+            IWidgetInputErrorView)
+
+        widget = self._WidgetFactory(self.field, self.request)
+        self.assertRaises(MissingInputError, widget.getInputValue)
+        error_html = widget.error()
+        if sys.version_info < (2, 5):
+            self.assertTrue("email: <zope.formlib.interfaces.Mis"
+                             in error_html)
+            self.assertTrue("name: <zope.formlib.interfaces.Miss"
+                             in error_html)
+        else:
+            self.assertTrue(
+                "email: MissingInputError(u'field.foo.email', u'', None)"
+                in error_html)
+            self.assertTrue(
+                "name: MissingInputError(u'field.foo.name', u'', None)"
+                in error_html)
+
+    def test_applyChangesNoChange(self):
+        self.content.foo = TestContact()
+        self.content.foo.name = u'Foo Name'
+        self.content.foo.email = u'foo@foo.test'
+
+        self.request.form['field.foo.name'] = u'Foo Name'
+        self.request.form['field.foo.email'] = u'foo@foo.test'
+        widget = self._WidgetFactory(self.field, self.request)
+        widget.setRenderedValue(self.content.foo)
+
+        self.assertEqual(widget.applyChanges(self.content), False)
+        self.assertEqual(hasattr(self.content, 'foo'), True)
+        self.assertEqual(isinstance(self.content.foo, TestContact), True)
+        self.assertEqual(self.content.foo.name, u'Foo Name')
+        self.assertEqual(self.content.foo.email, u'foo@foo.test')
+
+    def test_respect_custom_widgets(self):
+        # We can use our own subwidgets when creating ObjectWidgets
+        class CustomTextWidget(TextWidget):
+            pass
+
+        # We create a custom widget for the `name` field of ITestContact
+        name_widget = CustomTextWidget(ITestContact['name'], self.request)
+
+        # Custom widgets are passed by <fieldname>_widget keyword:
+        widget = self._WidgetFactory(self.field, self.request,
+                                     name_widget=name_widget)
+        widget1, widget2 = widget.subwidgets()
+        self.assertTrue(widget1 is name_widget)
+
+
+class ObjectDisplayWidgetTest(BrowserWidgetTest):
+    """Documents and tests the display variant of object widget.
+
+        >>> from zope.interface.verify import verifyClass
+        >>> verifyClass(IDisplayWidget, ObjectDisplayWidget)
+        True
+
+   """
+    _FieldFactory = Object
+    def _WidgetFactory(self, context, request, **kw):
+        kw.update({'factory': TestContact})
+        return ObjectDisplayWidget(context, request, **kw)
+
+    def setUpContent(self, desc=u'', title=u'Foo Title'):
+        provideAdapter(TextWidget, (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+        provideAdapter(DisplayWidget, (ITextLine, IDefaultBrowserLayer),
+                       IDisplayWidget)
+        # The widget must be traversable. We register the default
+        # adapter that can turn nearly any object into an ITraversable.
+        provideAdapter(DefaultTraversable, (None,), ITraversable)
+
+        class TestObject(object):
+            implements(ITestContent)
+
+        self.content = TestObject()
+        self.content.name = u'Foo Name'
+        self.content.email = u'foo@foo.test'
+        self.field = ITestContent['foo']
+        self.request = TestRequest(HTTP_ACCEPT_LANGUAGE='pl')
+        self.request.form['field.foo'] = u'Foo Value'
+        self._widget = self._WidgetFactory(self.field, self.request)
+
+    def setUp(self):
+        super(ObjectDisplayWidgetTest, self).setUp()
+        self.field = Object(ITestContact, __name__=u'foo')
+        provideAdapter(TextWidget,
+                       (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+        provideAdapter(DisplayWidget,
+                       (ITextLine, IDefaultBrowserLayer),
+                       IDisplayWidget)
+
+
+    def test_interfaces(self):
+        self.assertTrue(IDisplayWidget.providedBy(self._widget))
+        self.assertFalse(IInputWidget.providedBy(self._widget))
+        self.assertTrue(verifyClass(IDisplayWidget, ObjectDisplayWidget))
+
+    def test_render(self):
+        widget = ObjectDisplayWidget(
+            self.field, self.request, TestContact)
+        widget.setRenderedValue(self.content)
+
+        check_list = [
+            '<span',
+            'Foo Name',
+            '</span>', '<span',
+            'foo@foo.test',
+            '</span>']
+        self.verifyResult(widget(), check_list, inorder=True)
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(ObjectWidgetTest),
+        unittest.makeSuite(ObjectDisplayWidgetTest),
+        doctest.DocTestSuite(),
+        ))
+
+if __name__=='__main__':
+    unittest.main(defaultTest='test_suite')
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_phonewidget.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_phonewidget.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/widgets/tests/test_phonewidget.py	(revision 12677)
@@ -0,0 +1,191 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for PhoneWidget.
+
+Most tests are reimplementation of similar tests in zope.formlib.
+"""
+from zope import schema
+from zope.component import getGlobalSiteManager
+from zope.formlib import form
+from zope.formlib.tests.test_functional_textwidget import(
+    FunctionalWidgetTestCase, patternExists)
+from zope.formlib.textwidgets import TextWidget
+from zope.interface import Interface, implements
+from zope.publisher.browser import TestRequest
+from zope.schema.interfaces import ITextLine
+from waeup.ikoba.widgets.phonewidget import PhoneWidget
+
+# Dummy content
+class ISampleContent(Interface):
+    foo = schema.TextLine(
+        title = u'Phone',
+        description = u'Phone number',
+        required = True,
+        default=u'+234-1-1')
+
+    bar = schema.TextLine(
+        title = u'Phone',
+        description = u'Phone number (not required)',
+        required = False,
+        missing_value = u'')
+
+    baz = schema.TextLine(
+        title = u'Phone',
+        description = u'Required phone with a default',
+        required = False,
+        #default=u'+234--'
+        )
+
+class SampleContent:
+    implements(ISampleContent)
+
+    def __init__(self):
+        self.foo = None
+        self.bar = 'bar'
+        self.baz = None
+
+class SampleForm(form.EditForm):
+    form_fields = form.fields(ISampleContent)
+    form_fields['foo'].custom_widget = PhoneWidget
+    form_fields['bar'].custom_widget = PhoneWidget
+    form_fields['baz'].custom_widget = PhoneWidget
+
+class PhoneWidgetTests(FunctionalWidgetTestCase):
+
+    widgets = [
+        (ITextLine, TextWidget),
+        (ITextLine, TextWidget),
+        (ITextLine, TextWidget),
+        ]
+
+    def test_display_editform(self):
+        content = SampleContent()
+        request = TestRequest()
+        html = SampleForm(content, request)()
+        # foo.country, foo.area and foo.ext exist
+        self.assert_(patternExists(
+            '<select .* name="form.foo.country".*>', html))
+        self.assert_(patternExists(
+            '<input .* name="form.foo.area".* value="".*>', html))
+        self.assert_(patternExists(
+            '<input .* name="form.foo.ext".* value="".*>', html))
+        return
+
+    def test_submit_editform(self):
+        # we can submit an edit form
+        content = SampleContent()
+        request = TestRequest()
+
+        # submit edit view
+        request.form['form.foo.country'] = u'+123'
+        request.form['form.foo.area'] = u'456'
+        request.form['form.foo.ext'] = u'7890'
+        request.form['form.actions.apply'] = u''
+        SampleForm(content, request)()
+
+        # check new values in object
+        self.assertEqual(content.foo, u'+123-456-7890')
+        return
+
+    def test_invalid_type(self):
+        # there is no invalid type for textline-based input
+        content = SampleContent()
+        request = TestRequest()
+
+        # submit invalid type for text line
+        request.form['form.foo.country'] = '+123'
+        request.form['form.foo.area'] = '456'
+        request.form['form.foo.ext'] = '7890'
+        request.form['form.actions.apply'] = u''
+        html = SampleForm(content, request)()
+
+        # We don't have a invalid field value
+        # since we convert the value to unicode
+        self.assert_('Object is of wrong type.' not in html)
+        return
+
+    def test_missing_value(self):
+        content = SampleContent()
+        request = TestRequest()
+
+        request.form['form.foo.country'] = u'+123'
+        request.form['form.foo.area'] = u'456'
+        request.form['form.foo.ext'] = u'7890'
+        request.form['form.bar.country'] = u''
+        request.form['form.bar.area'] = u''
+        request.form['form.bar.ext'] = u''
+        request.form['form.baz.country'] = u''
+        request.form['form.baz.area'] = u''
+        request.form['form.baz.ext'] = u''
+        request.form['form.actions.apply'] = u''
+
+        SampleForm(content, request)()
+
+        # check new values in object
+        self.assertEqual(content.foo, u'+123-456-7890')
+        self.assertEqual(content.bar, u'') # default missing value
+        self.assertEqual(content.baz, None)
+        return
+
+    def test_partial_values(self):
+        # make sure partial numbers will not be enough
+        # XXX: we have to test each single field alone as an error
+        #      in one field will stop setting the other ones.
+        content = SampleContent()
+        request = TestRequest()
+
+        request.form['form.foo.country'] = u'+123'
+        request.form['form.foo.area'] = u'456'
+        request.form['form.foo.ext'] = u''
+        request.form['form.bar.country'] = u'+123'
+        request.form['form.bar.area'] = u'12'
+        request.form['form.bar.ext'] = u'789'
+        request.form['form.baz.country'] = u'+123'
+        request.form['form.baz.area'] = u'456'
+        request.form['form.baz.ext'] = u'789'
+        request.form['form.actions.apply'] = u''
+
+        SampleForm(content, request)()
+
+        # check new values in object
+        # as there were errors in the form, no value was set at all
+        self.assertEqual(content.foo, None)
+        self.assertEqual(content.bar, 'bar')
+        self.assertEqual(content.baz, None)
+        return
+
+    def test_no_values(self):
+        # if the last two subfields contain no value, no phone will be set
+        content = SampleContent()
+        request = TestRequest()
+
+        request.form['form.bar.country'] = u'+123'
+        request.form['form.bar.area'] = u''
+        request.form['form.bar.ext'] = u''
+        request.form['form.baz.country'] = u'+124'
+        request.form['form.baz.area'] = u''
+        request.form['form.baz.ext'] = u''
+        request.form['form.actions.apply'] = u''
+
+        SampleForm(content, request)()
+
+        # check new values in object
+        self.assertEqual(content.bar, u'') # default missing value
+        self.assertEqual(content.baz, None)
+        return
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/workflow.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/workflow.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/workflow.py	(revision 12677)
@@ -0,0 +1,92 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Workflow components of general use.
+"""
+import grok
+from hurry.workflow.interfaces import IWorkflow, MANUAL, InvalidTransitionError
+from hurry.workflow.workflow import (
+    Transition, Workflow, WorkflowVersions, WorkflowInfo,
+    NullCondition, nullCheckPermission)
+from zope.security.interfaces import NoInteraction
+from zope.security.management import getInteraction
+from waeup.ikoba.interfaces import IIkobaWorkflowInfo
+
+class IkobaWorkflow(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 IkobaWorkflowInfo(WorkflowInfo):
+    """A workflow info that provides a convenience transition getter.
+    """
+
+    grok.provides(IIkobaWorkflowInfo)
+
+    def getManualTransitions(self):
+        """Get allowed manual transitions.
+
+        Get a sorted list of tuples containing the `transition_id` and
+        `title` of each allowed transition.
+        """
+        try:
+            checkPermission = getInteraction().checkPermission
+        except NoInteraction:
+            checkPermission = nullCheckPermission
+        return [(transition.transition_id, transition.title)
+                for transition in sorted(self._getTransitions(MANUAL))
+                if transition.condition(self, self.context) and
+                checkPermission(transition.permission, self.context)]
Index: /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/zcml.py
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/zcml.py	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/zcml.py	(revision 12677)
@@ -0,0 +1,94 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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
+from zope.component.zcml import handler
+from zope.configuration.exceptions import ConfigurationError
+from waeup.ikoba.interfaces import IDataCenterConfig, IPayPalConfig
+from waeup.ikoba.payments.paypal import configure_sdk
+
+
+def data_center_conf(context, path):
+    """Handler for ZCML ``datacenter`` directive.
+
+    Registers a global utility under IDataCenterConfig and containing
+    a dictionary with (currently) only one entry: `path`.
+
+    The directive can be put into site.zcml like this:
+
+    - Add to the header:
+        ``xmlns:ikoba="http://namespaces.waeup.org/ikoba"``
+
+    - Then, after including waeup.ikoba:
+        ``<ikoba:datacenter path="some/existing/file/path" />``
+
+    In a running instance (where some directive like above was
+    processed during startup), one can then ask for the
+    IDataCenterConfig utility:
+
+      >>> from waeup.ikoba.interfaces import IDataCenterConfig
+      >>> from zope.component import getUtility
+      >>> getUtility(IDataCenterConfig)
+      {'path': 'some/existing/file/path'}
+
+    """
+    context.action(
+        discriminator=('utility', IDataCenterConfig, ''),
+        callable=handler,
+        args=('registerUtility',
+              {'path': path}, IDataCenterConfig, '')
+        )
+
+
+def paypal_handler(path):
+    """ZCML handler that registers paypal configuration.
+
+    We expect paypal credentials written down in a config file. The
+    path to this config file can be set with the ZCML `paypalconf`
+    directive, which is handled here and looks like this::
+
+      <ikoba:paypalconf path='/some/path/to/paypal.conf' />
+
+    This handler requires the given path to exist and to be a file.
+
+    If the file exists and is readable, a dict with ``path`` and other
+    values read from the configuration file is registered as a global
+    unnamed utility for the `IPayPalConfig` interface.
+
+    See :func:`waeup.ikoba.payments.paypal.configure_sdk for details
+    of the set dict.
+    """
+    if not os.path.exists(path):
+        raise ConfigurationError("No such file: %s" % path)
+    if not os.path.isfile(path):
+        raise ConfigurationError("Not a regular file: %s" % path)
+    config_dict = configure_sdk(path)
+    return handler(
+        'registerUtility', config_dict, IPayPalConfig, '')
+
+
+def paypal_conf(context, path):
+    """Handler for ZCML paypalconf directive.
+
+    This handler registers the `paypal_handler` above to perform the
+    real configuration action.
+    """
+    context.action(
+        discriminator=('utility', IPayPalConfig, ''),
+        callable=paypal_handler,
+        args=(path, )
+        )
Index: /main/waeup.ikoba/branches/uli-payments/versions.cfg
===================================================================
--- /main/waeup.ikoba/branches/uli-payments/versions.cfg	(revision 12677)
+++ /main/waeup.ikoba/branches/uli-payments/versions.cfg	(revision 12677)
@@ -0,0 +1,184 @@
+# Versions of packages used by waeup.ikoba
+[buildout]
+# default set of versions we use.
+extends= http://grok.zope.org/releaseinfo/grok-1.1.cfg
+
+versions = versions
+
+[versions]
+setuptools = 7.0
+collective.recipe.sphinxbuilder = 0.7.0
+collective.recipe.template = 1.10
+hurry.workflow = 0.11
+# Pinned to circumvent breakage in 0.4.x
+hurry.zoperesource = 0.6
+# Pinned to prevent buildout svn-error.
+lovely.recipe = 1.0.0
+megrok.layout = 1.0.2
+reportlab = 2.5
+transaction = 1.1.0
+z3c.testsetup = 0.6.1
+zc.buildout = 2.2.5
+zc.recipe.egg = 2.0.0a3
+zc.recipe.testrunner = 2.0.0
+zope.app.testing = 3.8.1
+# for support of @provider directive
+zope.interface = 3.6.3
+# for support of contextual default values
+zope.schema = 3.8.0
+zope.testing = 3.10.2
+zope.xmlpickle = 3.4.0
+# Require latest version...
+Sphinx = 1.0.7
+ZODB3 = 3.10.3
+docutils = 0.7
+Jinja2 = 2.3
+# for support of unicode encoded passwords
+zope.password = 3.6.1
+# include bugfix for hanging xmlrpcrequests under paster
+zope.publisher = 3.12.2
+# Pillow >= 2.0 supports Python 2.6, 2.7, 3.x
+# Pillow < 1.x supports Python 2.4, 2.5, 2.6, 2.7
+Pillow = 2.0.0
+mock = 1.0.1
+
+# Added by buildout at 2013-04-29 11:28:31.957221
+Beaker = 1.6.4
+Twisted = 13.0.0
+dolmen.beaker = 0.1
+hurry.resource = 0.10
+repoze.profile = 2.0
+repoze.sphinx.autointerface = 0.7.1
+unittest2 = 0.5.1
+# z3c.coverage 2.0 is out but does not cope with non-ASCII in sources
+z3c.coverage = 1.3.1
+zc.async = 1.5.4
+zc.dict = 1.2.1
+zc.twist = 1.3.1
+zc.z3monitor = 0.8.0
+zope.bforest = 1.2
+
+# Required by:
+# z3c.coverage==2.0.0
+coverage = 3.6
+
+# Required by:
+# waeup.ikoba==0.2dev
+hurry.file = 1.2.1
+
+# Required by:
+# waeup.ikoba==0.2dev
+hurry.jquery = 1.4.3.1
+
+# Required by:
+# waeup.ikoba==0.2dev
+hurry.jqueryui = 1.8.5.1
+
+# Required by:
+# waeup.ikoba==0.2dev
+hurry.query = 1.1.1
+
+# Required by:
+# grokui.base==0.2
+megrok.menu = 0.4
+
+# Required by:
+# dolmen.beaker==0.1
+pycryptopp = 0.6.0.1206569328141510525648634803928199668821045408958
+
+# Required by:
+# repoze.profile==2.0
+pyprof2calltree = 1.1.0
+
+# Required by:
+# zc.async==1.5.4
+rwproperty = 1.0
+
+# Required by:
+# zope.testrunner==4.3.3
+six = 1.3.0
+
+# Required by:
+# waeup.ikoba==0.2dev
+ulif.loghandlers = 0.1.1
+
+# Required by:
+# waeup.ikoba==0.2dev
+unicodecsv = 0.9.4
+
+# Required by:
+# zc.async==1.5.4
+uuid = 1.30
+
+# Required by:
+# zc.z3monitor==0.8.0
+zc.monitor = 0.3.1
+
+# Required by:
+# zc.monitor==0.3.1
+zc.ngi = 2.0.1
+
+# Required by:
+# zc.async==1.5.4
+zc.queue = 1.3
+
+# Required by:
+# waeup.ikoba==0.2dev
+zope.errorview = 0.11
+
+# Required by:
+# zc.recipe.testrunner==2.0.0
+zope.testrunner = 4.3.3
+
+# Added by buildout at 2014-01-13 12:49:22.642212
+mr.developer = 1.27
+plone.recipe.command = 1.1
+z3c.recipe.staticlxml = 0.10
+
+# Required by:
+# z3c.recipe.staticlxml==0.10
+zc.recipe.cmmi = 1.3.5
+
+# Added by buildout at 2014-02-04 15:17:58.515390
+zc.zodbrecipes = 0.6.2
+
+# Added by buildout at 2014-02-13 07:53:24.402989
+WebOb = 1.3.1
+diazo = 1.0.5
+repoze.xmliter = 0.5
+
+# Required by:
+# diazo==1.0.5
+experimental.cssselect = 0.3
+
+# Required by:
+# diazo==1.0.5
+# experimental.cssselect==0.3
+lxml = 3.3.1
+
+# Required by:
+# waeup.ikoba==0.2dev
+psutil = 2.1.1
+six = 1.5.2
+# Added by buildout at 2014-11-18 17:09:18.443792
+cffi = 0.8.6
+cryptography = 0.6.1
+pyOpenSSL = 0.14
+
+# Required by:
+# waeup.ikoba==0.2dev
+paypalrestsdk = 1.6.1
+
+# Required by:
+# cffi==0.8.6
+pycparser = 2.10
+
+# Required by:
+# paypalrestsdk==1.6.1
+requests = 2.4.3
+
+# Added by buildout at 2014-12-09 08:56:22.978680
+
+# Required by:
+# waeup.ikoba==0.2dev
+PyPDF2 = 1.23
