diff --git a/importer/.gitignore b/importer/.gitignore
new file mode 100644
index 0000000..4ae66f6
--- /dev/null
+++ b/importer/.gitignore
@@ -0,0 +1,6 @@
+.idea/
+.webassets-cache/
+*.min.css
+*.min.js
+*.pyc
+*.egg-info
diff --git a/importer/LICENSE b/importer/LICENSE
new file mode 100644
index 0000000..0058b50
--- /dev/null
+++ b/importer/LICENSE
@@ -0,0 +1,626 @@
+In applying this licence, CERN does not waive the privileges and immunities
+granted to it by virtue of its status as an Intergovernmental Organization
+or submit itself to any jurisdiction.
+
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ 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
diff --git a/importer/MANIFEST.in b/importer/MANIFEST.in
new file mode 100644
index 0000000..700c15e
--- /dev/null
+++ b/importer/MANIFEST.in
@@ -0,0 +1 @@
+graft indico_importer/static
diff --git a/importer/indico_importer/__init__.py b/importer/indico_importer/__init__.py
new file mode 100644
index 0000000..bfed2ad
--- /dev/null
+++ b/importer/indico_importer/__init__.py
@@ -0,0 +1,19 @@
+# This file is part of Indico.
+# Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
+#
+# Indico is free software; you can 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.
+#
+# Indico is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Indico; if not, see .
+
+__all__ = ('ImporterSourcePluginBase', 'ImporterEngineBase')
+
+from .base import ImporterSourcePluginBase, ImporterEngineBase
diff --git a/importer/indico_importer/base.py b/importer/indico_importer/base.py
new file mode 100644
index 0000000..9f03ea4
--- /dev/null
+++ b/importer/indico_importer/base.py
@@ -0,0 +1,74 @@
+# This file is part of Indico.
+# Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
+#
+# Indico is free software; you can 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.
+#
+# Indico is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Indico; if not, see .
+
+from __future__ import unicode_literals
+
+from flask_pluginengine import depends
+
+from indico.core.plugins import IndicoPlugin
+from indico_importer.plugin import ImporterPlugin
+
+
+@depends('importer')
+class ImporterSourcePluginBase(IndicoPlugin):
+ """Base class for importer engine plugins"""
+
+ importer_engine_classes = None
+
+ def init(self):
+ super(ImporterSourcePluginBase, self).init()
+ for engine_class in self.importer_engine_classes:
+ importer_engine = engine_class()
+ ImporterPlugin.instance.register_importer_engine(importer_engine, self)
+
+
+class ImporterEngineBase(object):
+ """Base class for data importers"""
+
+ _id = ''
+ name = ''
+
+ def import_data(self, query, size):
+ """Fetch and converts data from external source.
+
+ :param query: A search phrase send to the importer.
+ :param size: Number of records to fetch from external source.
+ :return: List of dictionaries with the following format (all keys are optional)
+ [{"recordId" : idOfTheRecordInExternalSource,
+ "title": eventTitle,
+ "primaryAuthor": {"firstName": primaryAuthorFirstName,
+ "familyName": primaryAuthorFamilyName,
+ "affiliation": primaryAuthorAffiliation},
+ "speaker": {"firstName": speakerFirstName,
+ "familyName": speakerFamilyName,
+ "affiliation": speakerAffiliation},
+ "secondaryAuthor": {"firstName": secondaryAuthorFirstName,
+ "familyName": secondaryAuthorFamilyName,
+ "affiliation": secondaryAuthorAffiliation},
+ "summary": eventSummary,
+ "meetingName": nameOfTheEventMeeting,
+ "materials": [{"name": nameOfTheLink,
+ "url": linkDestination},
+ ...],
+ "reportNumbers": [reportNumber, ...]
+ "startDateTime": {"time" : eventStartTime,
+ "date" : eventStartDate},
+ "endDateTime": {"time" : eventEndTime,
+ "date" : eventEndDate}
+ "place": eventPlace},
+ ...]
+ """
+ raise NotImplementedError
diff --git a/importer/indico_importer/controllers.py b/importer/indico_importer/controllers.py
new file mode 100644
index 0000000..ad9eb25
--- /dev/null
+++ b/importer/indico_importer/controllers.py
@@ -0,0 +1,38 @@
+# This file is part of Indico.
+# Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
+#
+# Indico is free software; you can 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.
+#
+# Indico is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Indico; if not, see .
+
+from __future__ import unicode_literals
+
+from flask import jsonify, request
+from flask_pluginengine import current_plugin
+
+from MaKaC.webinterface.rh.base import RHProtected
+
+
+class RHGetImporters(RHProtected):
+ def _process(self):
+ importers = {k: importer.name for k, (importer, _) in current_plugin.importer_engines.iteritems()}
+ return jsonify(importers)
+
+
+class RHImportData(RHProtected):
+ def _process(self):
+ size = request.args.get('size', 10)
+ query = request.args.get('query')
+ importer, plugin = current_plugin.importer_engines.get(request.view_args['importer_name'])
+ with plugin.plugin_context():
+ data = {'records': importer.import_data(query, size)}
+ return jsonify(data)
diff --git a/importer/indico_importer/converter.py b/importer/indico_importer/converter.py
new file mode 100644
index 0000000..4901c91
--- /dev/null
+++ b/importer/indico_importer/converter.py
@@ -0,0 +1,111 @@
+# This file is part of Indico.
+# Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
+#
+# Indico is free software; you can 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.
+#
+# Indico is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Indico; if not, see .
+
+from __future__ import unicode_literals
+
+APPEND = object()
+
+
+class RecordConverter(object):
+ """
+ Converts a dictionary or list of dictionaries into another list of dictionaries. The goal
+ is to alter data fetched from connector class into a format that can be easily read by importer
+ plugin. The way dictionaries are converted depends on the 'conversion' variable.
+
+ conversion = [ (sourceKey, destinationKey, conversionFuncion(optional), converter(optional))... ]
+
+ It's a list tuples in which a single element represents a translation that will be made. Every
+ element of the list is a tuple that consists of from 1 to 4 entries.
+
+ The first one is the key name in the source dictionary, the value that applies to this key will
+ be the subject of the translation. The second is the key in the destination dictionary at which
+ translated value will be put. If not specified its value will be equal the value of the first
+ element. If the second element is equal *append* and the converted element is a dictionary or a
+ list of dictionaries, destination dictionary will be updated by the converted element.Third,
+ optional, element is the function that will take the value from the source dictionary and return
+ the value which will be inserted into result dictionary. If the third element is empty
+ defaultConversionMethod will be called. Fourth, optional, element is a RecordConverter class
+ which will be executed with converted value as an argument.
+ """
+
+ conversion = []
+
+ @staticmethod
+ def default_conversion_method(attr):
+ """
+ Method that will be used to convert an entry in dictionary unless other method is specified.
+ """
+ return attr
+
+ @classmethod
+ def convert(cls, record):
+ """
+ Converts a single dictionary or list of dictionaries into converted list of dictionaries.
+ """
+ if isinstance(record, list):
+ return [cls._convert(r) for r in record]
+ else:
+ return [cls._convert(record)]
+
+ @classmethod
+ def _convert_internal(cls, record):
+ """
+ Converts a single dictionary into converted dictionary or list of dictionaries into converted
+ list of dictionaries. Used while passing dictionaries to another converter.
+ """
+ if isinstance(record, list):
+ return [cls._convert(r) for r in record]
+ else:
+ return cls._convert(record)
+
+ @classmethod
+ def _convert(cls, record):
+ """
+ Core method of the converter. Converts a single dictionary into another dictionary.
+ """
+ if not record:
+ return {}
+
+ converted_dict = {}
+ for field in cls.conversion:
+ key = field[0]
+ if len(field) >= 2 and field[1]:
+ converted_key = field[1]
+ else:
+ converted_key = key
+ if len(field) >= 3 and field[2]:
+ conversion_method = field[2]
+ else:
+ conversion_method = cls.default_conversion_method
+ if len(field) >= 4:
+ converter = field[3]
+ else:
+ converter = None
+ try:
+ value = conversion_method(record[key])
+ except KeyError:
+ continue
+ if converter:
+ value = converter._convert_internal(value)
+ if converted_key is APPEND:
+ if isinstance(value, list):
+ for v in value:
+ converted_dict.update(v)
+ else:
+ converted_dict.update(value)
+ else:
+ converted_dict[converted_key] = value
+ return converted_dict
diff --git a/importer/indico_importer/plugin.py b/importer/indico_importer/plugin.py
new file mode 100644
index 0000000..9d4baf0
--- /dev/null
+++ b/importer/indico_importer/plugin.py
@@ -0,0 +1,63 @@
+# This file is part of Indico.
+# Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
+#
+# Indico is free software; you can 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.
+#
+# Indico is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Indico; if not, see .
+
+from __future__ import unicode_literals
+
+from indico.core import signals
+from indico.core.plugins import IndicoPlugin, IndicoPluginBlueprint, plugin_url_rule_to_js
+from MaKaC.webinterface.pages.conferences import WPConfModifScheduleGraphic
+from indico.util.i18n import _
+
+from .controllers import RHGetImporters, RHImportData
+
+
+class ImporterPlugin(IndicoPlugin):
+ """Importer
+
+ Extends Indico for other plugins to import data from external sources to
+ the timetable.
+ """
+
+ hidden = True
+
+ def init(self):
+ super(ImporterPlugin, self).init()
+ self.inject_js('importer_js', WPConfModifScheduleGraphic)
+ self.inject_css('importer_css', WPConfModifScheduleGraphic)
+ self.connect(signals.timetable_buttons, self.get_timetable_buttons)
+ self.importer_engines = {}
+
+ def get_blueprints(self):
+ return blueprint
+
+ def get_timetable_buttons(self, *args, **kwargs):
+ yield (_('Importer'), 'create-importer-dialog')
+
+ def get_vars_js(self):
+ return {'urls': {'import_data': plugin_url_rule_to_js('importer.import_data'),
+ 'importers': plugin_url_rule_to_js('importer.importers')}}
+
+ def register_assets(self):
+ self.register_js_bundle('importer_js', 'js/importer.js')
+ self.register_css_bundle('importer_css', 'css/importer.css')
+
+ def register_importer_engine(self, importer_engine, plugin):
+ self.importer_engines[importer_engine._id] = (importer_engine, plugin)
+
+
+blueprint = IndicoPluginBlueprint('importer', __name__)
+blueprint.add_url_rule('/importers//search', 'import_data', RHImportData, methods=('POST',))
+blueprint.add_url_rule('/importers/', 'importers', RHGetImporters)
diff --git a/importer/indico_importer/static/css/importer.css b/importer/indico_importer/static/css/importer.css
new file mode 100644
index 0000000..33480a6
--- /dev/null
+++ b/importer/indico_importer/static/css/importer.css
@@ -0,0 +1,168 @@
+/*
+ * This file is part of Indico.
+ * Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
+ *
+ * Indico is free software; you can 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.
+ *
+ * Indico is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Indico; if not, see .
+ */
+
+ul.treeList {
+ list-style: none;
+}
+
+div.treeListContainer {
+ background: #F8F8F8;
+ -moz-border-radius: 5%;
+ border-radius: 5%;
+ margin-top: 15px;
+ overflow: auto;
+}
+
+div.treeListHeader {
+ color: #4E4C46;
+ font-family: "Times New Roman",Verdana,Arial;
+ font-size: 20px;
+ height: 20px;
+ letter-spacing: 1px;
+ padding: 15px;
+ text-align: center;
+}
+
+div.treeListDescription{
+ text-align: center;
+ color: #777777;
+ font-size: 12px;
+ font-style: italic;
+ font-weight: normal;
+ padding: 7px 0;
+ height: 12px;
+}
+
+.treeListDayName {
+ -moz-border-radius: 20px;
+ border-radius: 20px;
+ border: 1px solid #CCCCCC;
+ padding: 10px;
+ background-color: #FFFFFF;
+ cursor: pointer;
+ float: left;
+}
+
+.treeListEntry {
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+ border: 1px solid #CCCCCC;
+ padding: 7px;
+ width: 80%;
+ background-color: #FFFFFF;
+ cursor: pointer;
+}
+
+div.entryListContainer{
+ background: #F8F8F8;
+ -moz-border-radius: 5%;
+ border-radius: 5%;
+ margin-top: 15px;
+ overflow: auto;
+}
+
+div.entryListHeader{
+ color: #4E4C46;
+ font-family: "Times New Roman",Verdana,Arial;
+ font-size: 20px;
+ height: 20px;
+ letter-spacing: 1px;
+ padding: 15px;
+ text-align: center;
+}
+
+div.entryListDesctiption{
+ text-align: center;
+ color: #777777;
+ font-size: 12px;
+ font-style: italic;
+ font-weight: normal;
+ padding: 7px 0;
+ height: 12px;
+}
+
+ul.entryList li{
+ -moz-border-radius: 20px;
+ border-radius: 20px;
+ border: 1px solid #CCCCCC;
+ padding: 7px;
+ width: 90%;
+ margin-bottom: 10px;
+ list-style: none;
+ background-color: #FFFFFF;
+ cursor: pointer;
+}
+
+ul.entryList li div{
+ padding: 5px;
+}
+
+ul.entryList em{
+ font-weight: bold;
+ font-style: normal;
+}
+.entryListSelected {
+ background-color: #CDEB8B !important;
+ box-shadow: 3px 3px 5px #000000;
+ -moz-box-shadow: 3px 3px 5px #000000;
+ -webkit-box-shadow: 3px 3px 5px #000000;
+}
+
+div.importDialogHeader {
+ margin-left: auto;
+ margin-right: auto;
+ padding: 5px;
+ text-align: center;
+}
+
+div.importDialogHeader input[type=text]{
+ width: 35%;
+ min-width: 250px;
+ height: 20px;
+ font-size: 17px;
+}
+
+
+.entryListIndex {
+ -moz-border-radius: 100% 100% 100% 100%;
+ border-radius: 100% 100% 100% 100%;
+ border: 1px solid #CCCCCC;
+ left: -51px;
+ position: relative;
+ text-align: center;
+ width: 23px;
+}
+
+div.expandButtonsDiv{
+ float: left;
+ left: -30px;
+ position: relative;
+ top: 2px;
+ width: 0;
+}
+
+div.presearchContainer{
+ -moz-border-radius: 20px 20px 20px 20px;
+ border-radius: 20px 20px 20px 20px;
+ border: 1px solid #CCCCCC;
+ color: #777777;
+ font-size: 14px;
+ margin-top: 25px;
+ padding: 15px;
+ text-align: center;
+}
diff --git a/importer/indico_importer/static/js/importer.js b/importer/indico_importer/static/js/importer.js
new file mode 100644
index 0000000..08cf0aa
--- /dev/null
+++ b/importer/indico_importer/static/js/importer.js
@@ -0,0 +1,1360 @@
+/*
+ * This file is part of Indico.
+ * Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
+ *
+ * Indico is free software; you can 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.
+ *
+ * Indico is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Indico; if not, see .
+ */
+
+
+/** Namespace for importer utility functions and variables */
+ImporterUtils = {
+ /** Possible extensions for resources */
+ resourcesExtensionList : {'pdf' : 1, 'doc' : 1, 'docx' : 1, 'ppt' : 1},
+
+ /** Maps importer name into report number system name */
+ reportNumberSystems: {"invenio" : "cds",
+ "dummy" : "Dummy"},
+
+ /** Short names of the months. */
+ shortMonthsNames : [$T("Jan"),
+ $T("Feb"),
+ $T("Mar"),
+ $T("Apr"),
+ $T("May"),
+ $T("Jun"),
+ $T("Jul"),
+ $T("Aug"),
+ $T("Sep"),
+ $T("Oct"),
+ $T("Nov"),
+ $T("Dec")],
+ /**
+ * Converts minutes to the hour string (format HH:MM).
+ * If minutes are greater than 1440 (24:00) value '00:00' is returned.
+ * @param minutes Integer containing number of minutes.
+ * @return hour string.
+ */
+ minutesToTime: function(minutes) {
+ if (minutes <= 1440) {
+ return ((minutes - minutes % 60)/ 60 < 10 ? "0" + (minutes - minutes % 60) / 60:(minutes - minutes % 60) / 60)
+ + ":" + (minutes % 60 < 10 ? "0" + minutes % 60:minutes % 60);
+ } else {
+ return '00:00';
+ }
+ },
+
+ /**
+ * Standard sorting function comparing start times of events.
+ * @param a first event.
+ * @param b second event.
+ * @return true if the first event starts later than the second. If not false.
+ */
+ compareStartTime: function (a, b) {
+ return a.startDate.time > b.startDate.time;
+ },
+
+ /**
+ * Returns array containing sorted keys of the dictionary.
+ * @param dict Dictionary to be sorted.
+ * @param sortFunc Function comparing keys of the dictionary.
+ * @return Array containg sorted keys.
+ */
+ sortedKeys: function(dict, sortFunc) {
+ var array = [];
+ each(dict, function(item) {
+ array.push(item);
+ });
+ return array.sort(sortFunc);
+ },
+
+ /**
+ * Checks if a dictionary contains empty person data.
+ */
+ isPersonEmpty: function(person) {
+ return person && (person.firstName || person.familyName);
+ },
+
+ /**
+ * Handles multiple Indico requests. Requests are sent to the server sequentially.
+ * When a request is finished the next one is sent to the server.
+ * @param reqList List of dictionaries. Each element of the list represents a single request.
+ * Each request has a following format {'method' : nameOfTheMetod(string), 'args': dictionaryOfArguments}
+ * @param successCallback Function executed after every successful request.
+ * @param errorCallback Function executed after every failed request.
+ * @param finalCallback Function executed after finishing all requests.
+ */
+ multipleIndicoRequest: function(reqList, successCallback, errorCallback, finalCallback) {
+ if (reqList.length > 0) {
+ indicoRequest( reqList[0].method, reqList[0].args, function(result, error) {
+ if (result && successCallback) {
+ successCallback(result);
+ }
+ if (error && errorCallback) {
+ errorCallback(error);
+ }
+ reqList.splice(0, 1);
+ ImporterUtils.multipleIndicoRequest(reqList, successCallback, errorCallback, finalCallback);
+ });
+ } else if (finalCallback) {
+ finalCallback();
+ }
+ }
+};
+
+
+/**
+ * Imitates dictionary with keys ordered by the time element insertion.
+ */
+type("QueueDict", [], {
+
+ /**
+ * Inserts new element to the dictionary. If element's value is null removes an element.
+ * @param key element's key
+ * @param value element's value
+ */
+ set: function(key, value) {
+ var existed = false;
+ for (var i in this.keySequence) {
+ if (key == this.keySequence[i]) {
+ existed = true;
+ if (value !== null) {
+ this.keySequence[i] = value;
+ } else {
+ this.keySequence.splice(i, 1);
+ }
+ }
+ }
+ if (!existed) {
+ this.keySequence.push(key);
+ }
+ this.dict[key] = value;
+ },
+
+ /**
+ * Gets list of keys. The list is sorted by an element insertion time.
+ */
+ getKeys: function() {
+ return this.keySequence;
+ },
+
+ /**
+ * Gets elements with the specified key.
+ */
+ get: function(key) {
+ return this.dict[key];
+ },
+
+ /**
+ * Gets list of values. The list is sorted by an element insertion time.
+ */
+ getValues: function() {
+ var tmp = [];
+ for (var index in this.keySequence) {
+ tmp.push(this.get(this.keySequence[index]));
+ }
+ return tmp;
+ },
+
+ /**
+ * Returns number of elements in the dictionary.
+ */
+ getLength: function() {
+ return this.keySequence.length;
+ },
+
+ /**
+ * Removes all elements from the dictionary
+ */
+ clear: function() {
+ this.keySequence = [];
+ this.dict = {};
+ },
+
+ /**
+ * Moves the key one position towards the begining of the key list.
+ */
+ shiftTop: function(idx) {
+ if (idx > 0) {
+ var tmp = this.keySequence[idx];
+ this.keySequence[idx] = this.keySequence[idx - 1];
+ this.keySequence[idx - 1] = tmp;
+ }
+ },
+
+ /**
+ * Moves the key one position towards the end of the key list.
+ */
+ shiftBottom: function(idx) {
+ if (idx < this.keySequence.length - 1) {
+ var tmp = this.keySequence[idx];
+ this.keySequence[idx] = this.keySequence[idx + 1];
+ this.keySequence[idx + 1] = tmp;
+ }
+ }
+ },
+
+ function() {
+ this.keySequence = [];
+ this.dict = {};
+ }
+);
+
+
+type("ImportDialog", ["ExclusivePopupWithButtons", "PreLoadHandler"], {
+ _preload: [
+ /** Loads a list of importers from the server */
+ function(hook) {
+ var self = this;
+ $.ajax({
+ url: build_url(ImporterPlugin.urls.importers, {}),
+ type: 'GET',
+ dataType: 'json',
+ success: function(data) {
+ if (handleAjaxError(data)) {
+ return;
+ }
+ self.importers = data;
+ hook.set(true);
+ }
+ });
+ }
+ ],
+
+ /**
+ * Hides importer list and timetable list and shows information to type a new query.
+ */
+ _hideLists: function() {
+ this.importerList.hide();
+ this.timetableList.hide();
+ this.emptySearchDiv.show();
+ },
+
+ /**
+ * Shows importer list and timetable list and hides information to type a new query.
+ */
+ _showLists: function() {
+ this.importerList.show();
+ this.timetableList.refresh();
+ this.timetableList.show();
+ this.emptySearchDiv.hide();
+ },
+
+ /**
+ * Draws the content of the dialog.
+ */
+ drawContent : function() {
+ var self = this;
+ var search = function() {
+ self.importerList.search(query.dom.value, importFrom.dom.value, 20, [function() {
+ self._showLists();
+ }]);
+ };
+ var searchButton = Html.input('button', {}, $T('search'));
+ searchButton.observeClick(search);
+ var importFrom = Html.select({});
+ for (var importer in this.importers)
+ importFrom.append(Html.option({value:importer}, this.importers[importer]));
+ var query = Html.input('text', {});
+ query.observeEvent('keypress', function(event) {
+ if (event.keyCode == 13) {
+ search();
+ }
+ });
+
+ this.emptySearchDiv = new PresearchContainer(this.height, function() {
+ self._showLists();
+ });
+
+ /** Enables insert button whether some elements are selected at both importer and timetable list */
+ var _observeInsertButton = function() {
+ if (self.importerList.getSelectedList().getLength() > 0 && self.timetableList.getSelection()) {
+ self.insertButton.disabledButtonWithTooltip('enable');
+ } else {
+ self.insertButton.disabledButtonWithTooltip('disable');
+ }
+ };
+ this.importerList = new ImporterList([],
+ {"height" : this.height - 80, "width" : this.width / 2 - 20, "cssFloat" : "left"},
+ 'entryList', 'entryListSelected', true, _observeInsertButton);
+ this.timetableList = new TableTreeList(this.topTimetable,
+ {"height" : this.height - 80, "width" : this.width / 2 - 20, "cssFloat" : "right"},
+ 'treeList', 'treeListDayName', 'treeListEntry', true, _observeInsertButton);
+ return Html.div({},
+ Html.div({className:'importDialogHeader', style:{width:pixels(this.width * 0.9)}}, query, searchButton, $T(" in "), importFrom),
+ this.emptySearchDiv.draw(), this.importerList.draw(), this.timetableList.draw());
+ },
+
+ _getButtons: function() {
+ var self = this;
+ return [
+ [$T('Proceed...'), function() {
+ var destination = self.timetableList.getSelection();
+ var entries = self.importerList.getSelectedList();
+ var importer = self.importerList.getLastImporter();
+ new ImporterDurationDialog(entries, destination, self.confId, self.timetable, importer, function(redirect) {
+ if (!redirect) {
+ self._hideLists();
+ self.timetableList.clearSelection();
+ self.importerList.clearSelection();
+ self.emptySearchDiv.showAfterSearch();
+ } else {
+ self.close();
+ }
+ });
+ }],
+ [$T('Close'), function() {
+ self.close();
+ }]
+ ];
+ },
+
+ draw: function() {
+ this.insertButton = this.buttons.eq(0);
+ this.insertButton.disabledButtonWithTooltip({
+ tooltip: $T('Please select contributions to be added and their destination.'),
+ disabled: true
+ });
+ return this.ExclusivePopupWithButtons.prototype.draw.call(this, this.drawContent());
+ }
+ },
+
+ /**
+ * Importer's main tab. Contains inputs for typing a query and select the importer type.
+ * After making a query imported entries are displayed at the left side of the dialog, while
+ * at the right side list of all entries in the event's timetable will be shown. User can add
+ * new contributions to the timetable's entry by simply selecting them and clicking at 'proceed'
+ * button.
+ * @param timetable Indico timetable object. If it's undefined constructor will try to fetch
+ * window.timetable object.
+ */
+ function(timetable) {
+ var self = this;
+ this.ExclusivePopupWithButtons($T("Import Entries"));
+ this.timetable = timetable?timetable:window.timetable;
+ this.topTimetable = this.timetable.parentTimetable ? this.timetable.parentTimetable : this.timetable;
+ this.confId = this.topTimetable.contextInfo.id;
+ this.height = document.body.clientHeight - 200;
+ this.width = document.body.clientWidth - 200;
+ this.PreLoadHandler(
+ this._preload,
+ function() {
+ self.open();
+ });
+ this.execute();
+ }
+);
+
+
+type("PresearchContainer", [], {
+ /**
+ * Shows a widget.
+ */
+ show: function() {
+ this.contentDiv.dom.style.display = 'block';
+ },
+
+ /**
+ * Hides a widget.
+ */
+ hide: function() {
+ this.contentDiv.dom.style.display = 'none';
+ },
+
+ /**
+ * Changes a content of the widget. It should be used after making a first successful import.
+ */
+ showAfterSearch: function() {
+ this.firstSearch.dom.style.display = 'none';
+ this.afterSearch.dom.style.display = 'inline';
+ },
+
+ draw: function() {
+ this.firstSearch = Html.span({style:{display:"inline"}}, $T("Please type your search phrase and press 'search'."));
+ var hereLink = Html.span({className: 'fakeLink'}, $T("here"));
+ hereLink.observeClick(this.afterSearchAction);
+ this.afterSearch = Html.span({style:{display:"none"}}, $T("Your entries were inserted "), Html.span({style:{fontWeight:'bold'}}, $T("successfully")), $T(". Please specify a new query or click "), hereLink, $T(" to see the previous results."));
+ this.contentDiv = Html.div({className:'presearchContainer', style:{"height" : pixels(this.height - 130)}}, this.firstSearch, this.afterSearch);
+ return this.contentDiv;
+ }
+ },
+
+ /**
+ * A placeholder for importer and timetable list widgets. Contains user's tips about what to do right now.
+ * @param widget's height
+ * @param function executed afer clicking 'here' link.
+ */
+ function(height, afterSearchAction) {
+ this.height = height;
+ this.afterSearchAction = afterSearchAction;
+ }
+);
+
+
+type("ImporterDurationDialog", ["ExclusivePopupWithButtons", "PreLoadHandler"], {
+ _preload: [
+ /**
+ * Fetches the default start time of the first inserted contribution.
+ * Different requests are used for days, sessions and contributions.
+ */
+ function(hook) {
+ var self = this;
+ //If the destination is a contribution, simply fetch the contribution's start time.
+ if (this.destination.entryType == 'Contribution') {
+ self.info.set("startTime", this.destination.startDate.time.substr(0, 5));
+ hook.set(true);
+ } else {
+ var method;
+ if (this.destination.entryType == 'Day') {
+ method = 'schedule.event.getDayEndDate';
+ }
+ if (this.destination.entryType == 'Session') {
+ method = 'schedule.slot.getDayEndDate';
+ }
+ indicoRequest(method, {
+ 'confId': this.confId,
+ 'sessionId': this.destination.sessionId,
+ 'slotId': this.destination.sessionSlotId,
+ 'selectedDay': this.destination.startDate.date.replace(/-/g, '/')},
+ function(result, error) {
+ if (!error) {
+ self.info.set("startTime", result.substr(11, 5));
+ }
+ hook.set(true);
+ }
+ );
+ }
+ }
+ ],
+
+ drawContent: function() {
+ var durationField = Html.input('text', {}, this.destination.contribDuration?this.destination.contribDuration:20);
+ var timeField = Html.input('text', {});
+ var redirectCheckbox = Html.input('checkbox', {});
+
+ this.parameterManager.add(durationField, 'unsigned_int', false);
+ this.parameterManager.add(timeField, 'time', false);
+
+ $B(this.info.accessor("duration"), durationField);
+ $B(timeField, this.info.accessor("startTime"));
+ $B(this.info.accessor("redirect"), redirectCheckbox);
+
+ return IndicoUtil.createFormFromMap([
+ [$T("Duration time of every inserted contribution:"), durationField],
+ [$T("Start time of the first contribution:"), timeField],
+ [$T("Show me the destination:"), redirectCheckbox]
+ ]);
+ },
+
+ /**
+ * Returns an insert method name based on the destintion type.
+ */
+ _extractMethod: function() {
+ switch (this.destination.entryType) {
+ case "Day":
+ return "schedule.event.addContribution";
+ case "Contribution":
+ return "contribution.addSubContribution";
+ case "Session":
+ return "schedule.slot.addContribution";
+ default:
+ return null;
+ }
+ },
+
+ /**
+ * Returns an url of the destination's timetable.
+ */
+ _extractRedirectUrl: function() {
+ switch (this.destination.entryType) {
+ case "Day":
+ return build_url(Indico.Urls.ConfModifSchedule, {confId: this.confId}, this.destination.startDate.date.replace(/-/g, ''));
+ case "Contribution":
+ return build_url(Indico.Urls.SubcontrModif, {contribId: this.destination.contributionId, confId: this.confId});
+ case "Session":
+ return build_url(Indico.Urls.ConfModifSchedule, {confId: this.confId}, this.destination.startDate.date.replace(/-/g, '') + '.' + this.destination.id);
+ default:
+ return null;
+ }
+ },
+
+ _getButtons: function() {
+ var self = this;
+ return [
+ [$T('Insert'), function() {
+ if (!self.parameterManager.check()) {
+ return;
+ }
+ //Converts string containing contribution's start date(HH:MM) into a number of minutes.
+ //Using parseFloat because parseInt('08') = 0.
+ var time = parseFloat(self.info.get('startTime').split(':')[0]) * 60 + parseFloat(self.info.get('startTime').split(':')[1]);
+ var duration = parseInt(self.info.get('duration'));
+ var method = self._extractMethod();
+ //If last contribution finishes before 24:00
+ if (time + duration * self.entries.getLength() <= 1440) {
+ var killProgress = IndicoUI.Dialogs.Util.progress();
+ var date = self.destination.startDate.date.replace(/-/g, '/');
+ var args = [];
+ each(self.entries.getValues(), function(entry) {
+ entry = entry.getAll();
+ var timeStr = ImporterUtils.minutesToTime(time);
+ args.push({'method' : method,
+ 'args' : {'conference' : self.confId,
+ 'duration' : duration,
+ 'title' : entry.title?entry.title:"Untitled",
+ 'sessionId' : self.destination.sessionId,
+ 'slotId' : self.destination.sessionSlotId,
+ 'contribId' : self.destination.contributionId,
+ 'startDate' : date + " " + timeStr,
+ 'keywords' : [],
+ 'authors':ImporterUtils.isPersonEmpty(entry.primaryAuthor)?[entry.primaryAuthor]:[],
+ 'coauthors' : ImporterUtils.isPersonEmpty(entry.secondaryAuthor)?[entry.secondaryAuthor]:[],
+ 'presenters' : ImporterUtils.isPersonEmpty(entry.speaker)?[entry.speaker]:[],
+ 'roomInfo' : {},
+ 'field_content': entry.summary,
+ 'reportNumbers': entry.reportNumbers?
+ [{'system': ImporterUtils.reportNumberSystems[self.importer], 'number': entry.reportNumbers}]:[],
+ 'materials': entry.materials}
+ });
+ time += duration;
+ });
+ var successCallback = function(result) {
+ if (exists(result.slotEntry) && self.timetable.contextInfo.id == result.slotEntry.id) {
+ self.timetable._updateEntry(result, result.id);
+ } else{
+ var timetable = self.timetable.parentTimetable?self.timetable.parentTimetable:self.timetable;
+ timetable._updateEntry(result, result.id);
+ }
+ };
+ var errorCallback = function(error) {
+ if (error) {
+ IndicoUtil.errorReport(error);
+ }
+ };
+ var finalCallback = function() {
+ if (self.successFunction) {
+ self.successFunction(self.info.get('redirect'));
+ }
+ if (self.info.get('redirect')) {
+ window.location = self._extractRedirectUrl();
+ }
+ self.close();
+ killProgress();
+ };
+ ImporterUtils.multipleIndicoRequest(args, successCallback , errorCallback, finalCallback);
+ }
+ else {
+ new WarningPopup("Warning", "Some contributions will end after 24:00. Please modify start time and duration.").open();
+ }
+ }],
+ [$T('Cancel'), function() {
+ self.close();
+ }]
+ ];
+ },
+
+ draw: function() {
+ return this.ExclusivePopupWithButtons.prototype.draw.call(this, this.drawContent());
+ }
+ },
+
+ /**
+ * Dialog used to set the duration of the each contribution and the start time of the first on.
+ * @param entries List of imported entries
+ * @param destination Place into which entries will be inserted
+ * @param confIf Id of the current conference
+ * @param timetable Indico timetable object of the current conference.
+ * @param importer Name of the used importer.
+ * @param successFunction Function executed after inserting events.
+ */
+ function(entries, destination, confId, timetable, importer, successFunction) {
+ var self = this;
+ this.ExclusivePopupWithButtons($T('Adjust entries'));
+ this.confId = confId;
+ this.entries = entries;
+ this.destination = destination;
+ this.timetable = timetable;
+ this.successFunction = successFunction;
+ this.importer = importer;
+ this.parameterManager = new IndicoUtil.parameterManager();
+ this.info = new WatchObject();
+ this.PreLoadHandler(
+ this._preload,
+ function() {
+ self.open();
+ });
+ this.execute();
+ }
+);
+
+type("ImporterListWidget", ["SelectableListWidget"], {
+ /**
+ * Removes all entries from the list
+ */
+ clearList: function() {
+ this.SelectableListWidget.prototype.clearList.call(this);
+ this.recordDivs = [];
+ },
+
+ /**
+ * Removes all selections.
+ */
+ clearSelection: function() {
+ this.SelectableListWidget.prototype.clearSelection.call(this);
+ this.selectedObserver(this.selectedList);
+ },
+
+ /**
+ * Returns number of entries in the list.
+ */
+ getLength: function() {
+ return this.recordDivs.length;
+ },
+
+ /**
+ * Returns the last query.
+ */
+ getLastQuery: function() {
+ return this.lastSearchQuery;
+ },
+
+ /**
+ * Returns the name of the last used importer.
+ */
+ getLastImporter: function() {
+ return this.lastSearchImporter;
+ },
+
+ /**
+ * Returns true if it's possible to import more entries, otherwise false.
+ */
+ isMoreToImport: function() {
+ return this.moreToImport;
+ },
+
+ /**
+ * Base search method. Sends a query to the importer.
+ * @param query A query string send to the importer
+ * @param importer A name of the used importer.
+ * @param size Number of fetched objects.
+ * @param successFunction Method executed after successful request.
+ * @param callbacks List of methods executed after request (doesn't matter if successful).
+ */
+ _searchBase: function(query, importer, size, successFunc, callbacks) {
+ var self = this;
+ $.ajax({
+ // One more entry is fetched to be able to check if it's possible to fetch
+ // more entries in case of further requests.
+ url: build_url(ImporterPlugin.urls.import_data, {'importer_name': importer,
+ 'query': query,
+ 'size': size + 1}),
+ type: 'POST',
+ dataType: 'json',
+ complete: IndicoUI.Dialogs.Util.progress(),
+ success: function(data) {
+ if (handleAjaxError(data)) {
+ return;
+ }
+ successFunc(data.records);
+ _.each(callbacks, function(callback) {
+ callback();
+ });
+ }
+ });
+ //Saves last request data
+ this.lastSearchImporter = importer;
+ this.lastSearchQuery = query;
+ this.lastSearchSize = size;
+ },
+
+ /**
+ * Clears the list and inserts new imported entries.
+ * @param query A query string send to the importer
+ * @param importer A name of the used importer.
+ * @param size Number of fetched objects.
+ * @param callbacks List of methods executed after request (doesn't matter if successful).
+ */
+ search: function(query, importer, size, callbacks) {
+ var self = this;
+ var successFunc = function(result) {
+ self.clearList();
+ var importedRecords = 0;
+ self.moreToImport = false;
+ for (var record in result) {
+ //checks if it's possible to import more entries
+ if (size == importedRecords++) {
+ self.moreToImport = true;
+ break;
+ }
+ self.set(record, $O(result[record]));
+ }
+ };
+ this._searchBase(query, importer, size, successFunc, callbacks);
+ },
+
+ /**
+ * Adds more entries to the current list. Uses previous query and importer.
+ * @param size Number of fetched objects.
+ * @param callbacks List of methods executed after request (doesn't matter if successful).
+ */
+ append: function(size, callbacks) {
+ var self = this;
+ var lastLength = this.getLength();
+ var successFunc = function(result) {
+ var importedRecords = 0;
+ self.moreToImport = false;
+ for (var record in result) {
+ // checks if it's possible to import more entries
+ if (lastLength + size == importedRecords) {
+ self.moreToImport = true;
+ break;
+ }
+ // Some entries are already in the list so we don't want to insert them twice.
+ // Entries with indexes greater or equal lastLength - 1 are not yet in the list.
+ if (lastLength - 1 < importedRecords) {
+ self.set(record, $O(result[record]));
+ }
+ ++importedRecords;
+ }
+ };
+ this._searchBase(this.getLastQuery(), this.getLastImporter(), this.getLength() + size, successFunc, callbacks);
+ },
+
+ /**
+ * Extracts person's name, surname and affiliation
+ */
+ _getPersonString: function(person) {
+ return person.firstName + " " + person.familyName +
+ (person.affiliation? " (" + person.affiliation + ")" : "");
+ },
+
+ /**
+ * Draws sequence number attached to the item div
+ */
+ _drawSelectionIndex: function() {
+ var self = this;
+ var selectionIndex = Html.div({className:'entryListIndex', style:{display:'none', cssFloat:'left'}});
+ //Removes standard mouse events to enable custom right click event
+ var stopMouseEvent = function(event) {
+ event.cancelBubble = true;
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
+ }
+ return false;
+ };
+ selectionIndex.observeEvent('contextmenu', stopMouseEvent);
+ selectionIndex.observeEvent('mousedown', stopMouseEvent);
+ selectionIndex.observeEvent('click', stopMouseEvent);
+ selectionIndex.observeEvent('mouseup', function(event) {
+ //Preventing event propagation
+ event.cancelBubble = true;
+ if (event.which === null) {
+ /* IE case */
+ var button = event.button == 1 ? "left" : "notLeft";
+ } else {
+ /* All others */
+ var button = event.which == 1 ? "left" : "notLeft";
+ }
+ var idx = parseInt(selectionIndex.dom.innerHTML.substr(0, selectionIndex.dom.innerHTML.length - 2) - 1);
+ if (button == "left") {
+ self.selectedList.shiftTop(idx);
+ } else {
+ self.selectedList.shiftBottom(idx);
+ }
+ self.observeSelection(self.selectedList);
+ });
+ return selectionIndex;
+ },
+
+ /**
+ * Converts list of materials into a dictionary
+ */
+ _convertMaterials: function(materials) {
+ var materialsDict = {};
+ each(materials, function(mat) {
+ if (!materialsDict[mat.name]) {
+ materialsDict[mat.name] = [];
+ }
+ materialsDict[mat.name].push(mat.url);
+ });
+ return materialsDict;
+ },
+
+ /**
+ * Converts resource link into a name.
+ */
+ _getResourceName: function(resource) {
+ var splittedName = resource.split('.');
+ if (splittedName[splittedName.length - 1] in ImporterUtils.resourcesExtensionList) {
+ return splittedName[splittedName.length - 1];
+ } else {
+ return 'resource';
+ }
+ },
+
+ /**
+ * Draws a div containing entry's data.
+ */
+ _drawItem : function(record) {
+ var self = this;
+ var recordDiv = Html.div({});
+ var key = record.key;
+ record = record.get();
+ // Empty fields are not displayed.
+ if (record.get("reportNumbers")) {
+ var reportNumber = Html.div({}, Html.em({}, $T("Report number(s)")), ":");
+ each(record.get("reportNumbers"), function(id) {
+ reportNumber.append(" " + id);
+ });
+ recordDiv.append(reportNumber);
+ }
+ if (record.get("title")) {
+ recordDiv.append(Html.div({}, Html.em({}, $T("Title")), ": ", record.get("title")));
+ }
+ if (record.get("meetingName")) {
+ recordDiv.append(Html.div({}, Html.em({}, $T("Meeting")), ": ", record.get("meetingName")));
+ }
+ // Speaker, primary and secondary authors are stored in dictionaries. Their property have to be checked.
+ if (ImporterUtils.isPersonEmpty(record.get("primaryAuthor"))) {
+ recordDiv.append(Html.div({}, Html.em({}, $T("Primary author")), ": ", this._getPersonString(record.get("primaryAuthor"))));
+ }
+ if (ImporterUtils.isPersonEmpty(record.get("secondaryAuthor"))) {
+ recordDiv.append(Html.div({}, Html.em({}, $T("Secondary author")), ": ", this._getPersonString(record.get("secondaryAuthor"))));
+ }
+ if (ImporterUtils.isPersonEmpty(record.get("speaker"))) {
+ recordDiv.append(Html.div({}, Html.em({}, $T("Speaker")), ": ", this._getPersonString(record.get("speaker"))));
+ }
+ if (record.get("summary")) {
+ var summary = record.get("summary");
+ //If summary is too long it need to be truncated.
+ if (summary.length < 200) {
+ recordDiv.append(Html.div({}, Html.em({}, $T("Summary")), ": " , summary));
+ } else {
+ var summaryBeg = Html.span({}, summary.substr(0, 200));
+ var summaryEnd = Html.span({style:{display:'none'}}, summary.substr(200));
+ var showLink = Html.span({className:'fakeLink'}, $T(" (show all)"));
+ showLink.observeClick(function(evt) {
+ summaryEnd.dom.style.display = "inline";
+ showLink.dom.style.display = "none";
+ hideLink.dom.style.display = "inline";
+ //Preventing event propagation
+ evt.cancelBubble=true;
+ //Recalculating position of the selection number
+ self.observeSelection(self.selectedList);
+ });
+ var hideLink = Html.span({className:'fakeLink', style:{display:'none'}}, $T(" (hide)"));
+ hideLink.observeClick(function(evt) {
+ summaryEnd.dom.style.display = "none";
+ showLink.dom.style.display = "inline";
+ hideLink.dom.style.display = "none";
+ //Preventing event propagation
+ evt.cancelBubble=true;
+ //Recalculating position of the selection number
+ self.observeSelection(self.selectedList);
+ });
+ var sumamaryDiv = Html.div({}, Html.em({}, $T("Summary")), ": " , summaryBeg, showLink, summaryEnd, hideLink);
+ recordDiv.append(sumamaryDiv);
+ }
+ }
+ if (record.get("place")) {
+ recordDiv.append(Html.div({}, Html.em({}, $T("Place")), ": ", record.get("place")));
+ }
+ if (record.get("materials")) {
+ record.set("materials", this._convertMaterials(record.get("materials")));
+ var materials = Html.div({}, Html.em({}, $T("Materials")), ":");
+ for (var mat in record.get("materials")) {
+ var materialType = Html.div({}, mat + ":");
+ each(record.get("materials")[mat], function(resource) {
+ var link = Html.a({href:resource, target: "_new"}, self._getResourceName(resource));
+ link.observeClick(function(evt) {
+ //Preventing event propagation
+ evt.cancelBubble = true;
+ });
+ materialType.append(" ");
+ materialType.append(link);
+ });
+ materials.append(materialType);
+ }
+ recordDiv.append(materials);
+ }
+ recordDiv.append(this._drawSelectionIndex());
+ this.recordDivs[key] = recordDiv;
+ return recordDiv;
+ },
+
+ /**
+ * Observer function executed when selection is made. Draws a number next to the item div, which
+ * represents insertion sequence of entries.
+ */
+ observeSelection: function(list) {
+ var self = this;
+ //Clears numbers next to the all divs
+ for (var entry in this.recordDivs) {
+ var record = this.recordDivs[entry];
+ record.dom.lastChild.style.display = 'none';
+ record.dom.lastChild.innerHTML = '';
+ }
+ var seq = 1;
+ each(list.getKeys(), function(entry) {
+ var record = self.recordDivs[entry];
+ record.dom.lastChild.style.display = 'block';
+ record.dom.lastChild.style.top = pixels(-(record.dom.clientHeight + 23) / 2);
+ record.dom.lastChild.innerHTML = seq;
+ switch(seq) {
+ case 1:
+ record.dom.lastChild.innerHTML += $T('st');
+ break;
+ case 2:
+ record.dom.lastChild.innerHTML += $T('nd');
+ break;
+ case 3:
+ record.dom.lastChild.innerHTML += $T('rd');
+ break;
+ default:
+ record.dom.lastChild.innerHTML += $T('th');
+ break;
+ }
+ ++seq;
+ });
+ }
+ },
+
+ /**
+ * Widget containing a list of imported contributions. Supports multiple selections of results and
+ * keeps selection order.
+ * @param events List of events to be inserted during initialization.
+ * @param listStyle Css class name of the list.
+ * @param selectedStyle Css class name of a selected element.
+ * @param customObserver Function executed while selection is made.
+ */
+ function(events, listStyle, selectedStyle, customObserver) {
+ var self = this;
+ // After selecting/deselecting an element two observers are executed.
+ // The first is a default one, used to keep selected elements order.
+ // The second one is a custom observer passed in the arguments list.
+ var observer = function(list) {
+ this.observeSelection(list);
+ if (customObserver) {
+ customObserver(list);
+ }
+ };
+ this.SelectableListWidget(observer, false, listStyle, selectedStyle);
+ this.selectedList = new QueueDict();
+ this.recordDivs = {};
+ for (var record in events) {
+ this.set(record, $O(events[record]));
+ }
+ }
+);
+
+
+type("ImporterList", [], {
+ /**
+ * Show the widget.
+ */
+ show: function() {
+ this.contentDiv.dom.style.display = 'block';
+ },
+
+ /**
+ * Hides the widget.
+ */
+ hide: function() {
+ this.contentDiv.dom.style.display = 'none';
+ },
+
+ /**
+ * Returns list of the selected entries.
+ */
+ getSelectedList: function() {
+ return this.importerWidget.getSelectedList();
+ },
+
+ /**
+ * Removes all entries from the selection list.
+ */
+ clearSelection: function() {
+ this.importerWidget.clearSelection();
+ },
+
+ /**
+ * Returns last used importer.
+ */
+ getLastImporter: function() {
+ return this.importerWidget.getLastImporter();
+ },
+
+ /**
+ * Changes widget's header depending on the number of results in the list.
+ */
+ handleContent: function() {
+ if (this.descriptionDiv && this.emptyDescriptionDiv) {
+ if (this.importerWidget.getLength() === 0) {
+ this.descriptionDiv.dom.style.display = 'none';
+ this.emptyDescriptionDiv.dom.style.display = 'block';
+ this.moreEntriesDiv.dom.style.display = 'none';
+ } else {
+ this.entriesCount.dom.innerHTML = this.importerWidget.getLength() == 1?
+ $T("1 entry was found. "):this.importerWidget.getLength() + $T(" entries were found. ");
+ this.descriptionDiv.dom.style.display = 'block';
+ this.emptyDescriptionDiv.dom.style.display = 'none';
+ if (this.importerWidget.isMoreToImport()) {
+ this.moreEntriesDiv.dom.style.display = 'block';
+ } else {
+ this.moreEntriesDiv.dom.style.display = 'none';
+ }
+ }
+ }
+ },
+
+ /**
+ * Adds handleContent method to the callback list. If callback list is empty, creates a new one
+ * containing only handleContent method.
+ * @return list with inserted handleContent method.
+ */
+ _appendCallbacks: function(callbacks) {
+ var self = this;
+ if (callbacks) {
+ callbacks.push(function() {
+ self.handleContent();
+ });
+ } else {
+ callbacks = [function() {
+ self.handleContent();
+ }];
+ }
+ return callbacks;
+ },
+
+ /**
+ * Calls search method from ImporterListWidget object.
+ */
+ search: function(query, importer, size, callbacks) {
+ this.importerWidget.search(query, importer, size, this._appendCallbacks(callbacks));
+ },
+
+ /**
+ * Calls append method from ImporterListWidget object.
+ */
+ append: function(size, callbacks) {
+ this.importerWidget.append(size, this._appendCallbacks(callbacks));
+ },
+
+ draw: function() {
+ var importerDiv = this._drawImporterDiv();
+ this.contentDiv = Html.div({className:'entryListContainer'}, this._drawHeader(), importerDiv);
+
+ for (var style in this.style) {
+ this.contentDiv.setStyle(style, this.style[style]);
+ if (style == 'height') {
+ importerDiv.setStyle('height', this.style[style] - 76); //76 = height of the header
+ }
+ }
+
+ if (this.hidden) {
+ this.contentDiv.dom.style.display = 'none';
+ }
+
+ return this.contentDiv;
+ },
+
+ _drawHeader: function() {
+ this.entriesCount = Html.span({}, '0');
+ this.descriptionDiv = Html.div({className:'entryListDesctiption'}, this.entriesCount, $T("Please select the results you want to insert."));
+ this.emptyDescriptionDiv = Html.div({className:'entryListDesctiption'}, $T("No result were found. Please change the search phrase."));
+ return Html.div({}, Html.div({className:'entryListHeader'}, $T("Step 1: Search results:")), this.descriptionDiv, this.emptyDescriptionDiv);
+ },
+
+ _drawImporterDiv: function() {
+ var self = this;
+ this.moreEntriesDiv = Html.div({className:'fakeLink', style:{paddingBottom:pixels(15), textAlign:'center', clear: 'both', marginTop: pixels(15)}}, $T("more results"));
+ this.moreEntriesDiv.observeClick(function() {
+ self.append(20);
+ });
+ return Html.div({style:{overflow:'auto'}}, this.importerWidget.draw(), this.moreEntriesDiv);
+ }
+ },
+
+ /**
+ * Encapsulates ImporterListWidget. Adds a header depending on the number of entries in the least.
+ * Adds a button to fetch more entries from selected importer.
+ * @param events List of events to be inserted during initialization.
+ * @param style Dictionary of css styles applied to the div containing the list. IMPORTANT pass 'height'
+ * attribute as an integer not a string, because some further calculations will be made.
+ * @param listStyle Css class name of the list.
+ * @param selectedStyle Css class name of a selected element.
+ * @param hidden If true widget will not be displayed after being initialized.
+ * @param observer Function executed while selection is made.
+ */
+ function(events, style, listStyle, selectedStyle, hidden, observer) {
+ this.importerWidget = new ImporterListWidget(events, listStyle, selectedStyle, observer);
+ this.style = style;
+ this.hidden = hidden;
+ }
+);
+
+
+type("TimetableListWidget", ["ListWidget"], {
+ /**
+ * Highlights selected entry and calls an observer method.
+ */
+ setSelection: function(selected, div) {
+ if (this.selectedDiv) {
+ this.selectedDiv.dom.style.fontWeight = "normal";
+ this.selectedDiv.dom.style.boxShadow = "";
+ this.selectedDiv.dom.style.MozBoxShadow = "";
+ }
+ if (this.selected != selected) {
+ this.selectedDiv = div;
+ this.selected = selected;
+ this.selectedDiv.dom.style.fontWeight = "bold";
+ this.selectedDiv.dom.style.boxShadow = "3px 3px 15px #000000";
+ this.selectedDiv.dom.style.MozBoxShadow = "3px 3px 15px #000000";
+ } else {
+ this.selected = null;
+ this.selectedDiv = null;
+ }
+ if (this.observeSelection) {
+ this.observeSelection();
+ }
+ },
+
+ /**
+ * Deselects current entry.
+ */
+ clearSelection: function() {
+ if (this.selectedDiv) {
+ this.selectedDiv.dom.style.backgroundColor = "";
+ }
+ this.selected = null;
+ this.selectedDiv = null;
+ if (this.observeSelection) {
+ this.observeSelection();
+ }
+ },
+
+ /**
+ * Returns selected entry
+ */
+ getSelection: function() {
+ return this.selected;
+ },
+
+ /**
+ * Recursive function drawing timetable hierarchy.
+ * @param item Entry to be displayed
+ * @param level Recursion level. Used to set margins properly.
+ */
+ _drawItem : function(item, level) {
+ var self = this;
+ level = level?level:0;
+ // entry is a Day
+ switch(item.entryType) {
+ case 'Contribution':
+ item.color = "#F8F2E8";
+ item.textColor = "#000000";
+ case 'Session':
+ var titleDiv = Html.div({className:"treeListEntry", style:{backgroundColor: item.color, color: item.textColor}},
+ item.title + (item.startDate && item.endDate?" (" + item.startDate.time.substr(0, 5) + " - " + item.endDate.time.substr(0, 5) + ")":""));
+ var entries = ImporterUtils.sortedKeys(item.entries, ImporterUtils.compareStartTime);
+ break;
+ case 'Break':
+ if (this.displayBreaks) {
+ var titleDiv = Html.div({className:"treeListEntry", style:{backgroundColor: item.color, color: item.textColor}},
+ item.title + (item.startDate && item.endDate?" (" + item.startDate.time.substr(0, 5) + " - " + item.endDate.time.substr(0, 5) + ")":""));
+ var entries = ImporterUtils.sortedKeys(item.entries, ImporterUtils.compareStartTime);
+ } else {
+ return null;
+ }
+ break;
+ case undefined:
+ item.entryType = 'Day';
+ item.startDate = {date : item.key.substr(0, 4) + "-" + item.key.substr(4, 2) + "-" + item.key.substr(6, 2)};
+ item.color = "#FFFFFF";
+ item.textColor = "#000000";
+ var titleDiv = Html.div({className:"treeListDayName"},
+ item.key.substr(6, 2) + " " + ImporterUtils.shortMonthsNames[parseFloat(item.key.substr(4, 2)) - 1] +
+ " " + item.key.substr(0, 4));
+ var entries = ImporterUtils.sortedKeys(item.get().getAll(), ImporterUtils.compareStartTime);
+ break;
+ }
+ titleDiv.observeClick(function() {
+ self.setSelection(item, titleDiv);
+ });
+ var itemDiv = Html.div({style:{marginLeft:pixels(level * 20), clear:"both", padding:pixels(5)}}, titleDiv);
+ var entriesDiv = Html.div({style:{display:"none"}});
+
+ //Draws subentries
+ for (var entry in entries) {
+ entriesDiv.append(this._drawItem(entries[entry], level + 1));
+ }
+
+ //If there are any subentries, draws buttons to show/hide them on demand.
+ if (entries.length) {
+ titleDiv.append(this._drawShowHideButtons(entriesDiv));
+ itemDiv.append(entriesDiv);
+ }
+
+ return itemDiv;
+ },
+
+ /**
+ * Attaches buttons to the dom object which hide/show it when clicked.
+ */
+ _drawShowHideButtons: function(div) {
+ var self = this;
+ var showButton = Html.img({src:imageSrc("collapsd.png"), style:{display:'block'}});
+ var hideButton = Html.img({src:imageSrc("exploded.png"), style:{display:"none"}});
+ showButton.observeClick(function(evt) {
+ div.dom.style.display = "block";
+ showButton.dom.style.display = "none";
+ hideButton.dom.style.display = "block";
+ evt.cancelBubble = true;
+ });
+ hideButton.observeClick(function(evt) {
+ div.dom.style.display = "none";
+ showButton.dom.style.display = "block";
+ hideButton.dom.style.display = "none";
+ evt.cancelBubble = true;
+ });
+ return Html.div({className: 'expandButtonsDiv'}, showButton, hideButton);
+ },
+
+ /**
+ * Inserts entries from the timetable inside the widget.
+ */
+ _insertFromTimetable: function() {
+ var self = this;
+ var timetableData = this.timetable.getData();
+ each(this.timetable.sortedKeys, function(day) {
+ self.set(day, $O(timetableData[day]));
+ });
+ },
+
+ /**
+ * Clears the list and inserts entries from the timetable inside the widget.
+ */
+ refresh: function() {
+ this.clear();
+ this._insertFromTimetable();
+ }
+ },
+
+ /**
+ * Draws event's timetable as a hierarchical expandable list.
+ * @param timetable Indico timetable object to be drawn
+ * @param listStyle Css class name of the list.
+ * @param dayStyle Css class name of day entries.
+ * @param eventStyle Css class name of session and contributions entries.
+ * @param observeSelection Funtcion executed after changing selection state.
+ * @param displayBreaks If true breaks will be displayed in the list. If false breaks are hidden.
+ */
+ function(timetable, listStyle, dayStyle, eventStyle, observeSelection, displayBreaks) {
+ this.timetable = timetable;
+ this.displayBreaks = displayBreaks;
+ this.observeSelection = observeSelection;
+ var self = this;
+ this.ListWidget(listStyle);
+ this._insertFromTimetable();
+ }
+);
+
+
+type("TableTreeList", [], {
+
+ /**
+ * Show the widget.
+ */
+ show: function() {
+ this.contentDiv.dom.style.display = 'block';
+ },
+
+ /**
+ * Hides the widget
+ */
+ hide: function() {
+ this.contentDiv.dom.style.display = 'none';
+ },
+
+ /**
+ * Returns selected entry. TimetableListWidget method wrapper.
+ */
+ getSelection: function() {
+ return this.timetableList.getSelection();
+ },
+
+ /**
+ * Deselects current entry. TimetableListWidget method wrapper.
+ */
+ clearSelection: function() {
+ return this.timetableList.clearSelection();
+ },
+
+ /**
+ * Highlights selected entry and calls an observer method. TimetableListWidget method wrapper.
+ */
+ setSelection: function(selected, div) {
+ return this.timetableList.setSelection(selected, div);
+ },
+
+ /**
+ * Clears the list and inserts entries from the timetable inside the widget.
+ */
+ refresh: function() {
+ this.timetableList.refresh();
+ },
+
+ draw: function() {
+ this.contentDiv = Html.div({className:'treeListContainer'}, Html.div({className:'treeListHeader'}, $T("Step 2: Choose destination:")),
+ Html.div({className:'treeListDescription'}, $T("Please select the place in which the contributions will be inserted.")));
+ var treeDiv = Html.div({style:{overflow:'auto'}}, this.timetableList.draw());
+ for (var style in this.style) {
+ this.contentDiv.setStyle(style, this.style[style]);
+ if (style == 'height') {
+ treeDiv.setStyle('height', this.style[style] - 76);
+ }
+ }
+ this.contentDiv.append(treeDiv);
+ if (this.hidden) {
+ this.contentDiv.dom.style.display = 'none';
+ }
+ return this.contentDiv;
+ }
+ },
+
+ /**
+ * Draws event's timetable as a hierarchical expandable list.
+ * @param timetable Indico timetable object to be drawn
+ * @param style Dictionary of css styles applied to the div containing the list. IMPORTANT pass 'height'
+ * attribute as an integer not a string, because some further calculations will be made.
+ * @param listStyle Css class name of the list.
+ * @param dayStyle Css class name of day entries.
+ * @param eventStyle Css class name of session and contributions entries.
+ * @param observer Funtcion executed after changing selection state.
+ */
+ function(timetable, style, listStyle, dayStyle, eventStyle, hidden, observer) {
+ this.timetableList = new TimetableListWidget(timetable, listStyle, dayStyle, eventStyle, observer);
+ this.style = style;
+ this.hidden = hidden;
+ }
+);
+
+
+$(function() {
+ $('#timetableDiv').on('click', '.js-create-importer-dialog', function(event) {
+ var timetable = $(this).data('timetable');
+ new ImportDialog(timetable);
+ });
+});
diff --git a/importer/indico_importer/util.py b/importer/indico_importer/util.py
new file mode 100644
index 0000000..897a931
--- /dev/null
+++ b/importer/indico_importer/util.py
@@ -0,0 +1,27 @@
+# This file is part of Indico.
+# Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
+#
+# Indico is free software; you can 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.
+#
+# Indico is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Indico; if not, see .
+
+from __future__ import unicode_literals
+
+
+def convert_dt_tuple(dt_tuple):
+ split_datetime = dt_tuple[0].split('T')
+ if len(split_datetime) > 1:
+ return {'date': dt_tuple[0].split('T')[0],
+ 'time': dt_tuple[0].split('T')[1]}
+ else:
+ return {'date': dt_tuple[0].split('T')[0],
+ 'time': '00:00'}
diff --git a/importer/setup.py b/importer/setup.py
new file mode 100644
index 0000000..758565f
--- /dev/null
+++ b/importer/setup.py
@@ -0,0 +1,43 @@
+# This file is part of Indico.
+# Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
+#
+# Indico is free software; you can 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.
+#
+# Indico is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Indico; if not, see .
+
+from __future__ import unicode_literals
+
+from setuptools import setup, find_packages
+
+
+setup(
+ name='indico_importer',
+ version='0.1',
+ url='https://github.com/indico/indico-plugin-importer',
+ license='https://www.gnu.org/licenses/gpl-3.0.txt',
+ author='Indico Team',
+ author_email='indico-team@cern.ch',
+ packages=find_packages(),
+ zip_safe=False,
+ include_package_data=True,
+ platforms='any',
+ install_requires=[
+ 'indico>=1.9.1'
+ ],
+ classifiers=[
+ 'Environment :: Plugins',
+ 'Environment :: Web Environment',
+ 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
+ 'Programming Language :: Python :: 2.7'
+ ],
+ entry_points={'indico.plugins': {'importer = indico_importer.plugin:ImporterPlugin'}}
+)