commit 042b5ffd984674690e281579eca767a391cf49da Author: Stefan Kögl Date: Sat Mar 31 17:45:24 2012 +0200 initial commit diff --git a/TracRendezVous/AUTHORS b/TracRendezVous/AUTHORS new file mode 100644 index 0000000..14c53c8 --- /dev/null +++ b/TracRendezVous/AUTHORS @@ -0,0 +1 @@ +Stefan Kögl diff --git a/TracRendezVous/CHANGELOG b/TracRendezVous/CHANGELOG new file mode 100644 index 0000000..93ed074 --- /dev/null +++ b/TracRendezVous/CHANGELOG @@ -0,0 +1,64 @@ += changes for 0.3 = +== bug fixes and refactoring == + * fixed index error for elected date + * fixed ugly date display when scheduling a RendezVous + * added missing conversion to RendezVousVote.exists() + * sorting RendezVousDates in rendezvous matrix + * renamed permissions. Now all rendezvous specific permissions have a prefix + 'RendezVous_' + * renamed DateVote to RendezVousVote + * refactored begin and end into date and time parts for better usability + +== new features == + * added begin and end description to graphs + * added comments (RendezVousComment) to RendezVous + * blocking changes if RendezVous is scheduled. See comments in + [source:trunk/TracRendezVous/tracrendezvous/web_ui.py] + * reduced fatal error messages to verbose error warnings and + using more notices on success + * extented workflow + * automatic workflow status changes for "expire" and "start" + * new macro "ScheduledRendezVouses" + +changes for 0.2 +------------------------------------------------------------------------ +r67 | hotshelf | 2009-01-12 00:54:32 +0100 (Mo, 12. Jan 2009) | 1 line + +* fixing typo +------------------------------------------------------------------------ +r66 | hotshelf | 2009-01-12 00:52:37 +0100 (Mo, 12. Jan 2009) | 1 line + +* fixing permission 2 +------------------------------------------------------------------------ +r65 | hotshelf | 2009-01-12 00:47:53 +0100 (Mo, 12. Jan 2009) | 1 line + +* fixing permission +------------------------------------------------------------------------ +r64 | hotshelf | 2008-11-30 14:58:04 +0100 (So, 30. Nov 2008) | 1 line + +* fixed type +------------------------------------------------------------------------ +r63 | hotshelf | 2008-11-26 17:33:09 +0100 (Mi, 26. Nov 2008) | 1 line + +* silly bug +------------------------------------------------------------------------ +r62 | hotshelf | 2008-11-26 17:01:32 +0100 (Mi, 26. Nov 2008) | 1 line + +* catching missing input before it get's dirty +------------------------------------------------------------------------ +r61 | hotshelf | 2008-11-24 15:44:03 +0100 (Mo, 24. Nov 2008) | 1 line + +* print statements +------------------------------------------------------------------------ +r60 | hotshelf | 2008-11-24 15:43:28 +0100 (Mo, 24. Nov 2008) | 2 lines + +* better label +* dates now have a default begin and end, which will be used as default values for new votes +------------------------------------------------------------------------ +r59 | hotshelf | 2008-11-20 16:22:56 +0100 (Do, 20. Nov 2008) | 1 line + +* deleted unused file +------------------------------------------------------------------------ + +changes for 0.1 +* initial release - beginning changelog diff --git a/TracRendezVous/LICENSE b/TracRendezVous/LICENSE new file mode 100644 index 0000000..a4979c6 --- /dev/null +++ b/TracRendezVous/LICENSE @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/TracRendezVous/__init__.py b/TracRendezVous/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/TracRendezVous/babel.ini b/TracRendezVous/babel.ini new file mode 100644 index 0000000..2bc47a7 --- /dev/null +++ b/TracRendezVous/babel.ini @@ -0,0 +1,8 @@ +# Extraction from Python source files +[python: **.py] +# Extraction from Genshi HTML and text templates +[genshi: **/templates/**.html] +ignore_tags = script,style +include_attrs = alt title summary +[genshi: **/templates/**.txt] +template_class = genshi.template:TextTemplate diff --git a/TracRendezVous/docs/epydoc.conf b/TracRendezVous/docs/epydoc.conf new file mode 100644 index 0000000..dfd610a --- /dev/null +++ b/TracRendezVous/docs/epydoc.conf @@ -0,0 +1,19 @@ +[epydoc] +modules: tracrendezvous + +verbosity: 1 +parse: yes +introspect: yes + +output: html +simple-term: no +target: docs/apidocs/ + +sourcecode: no + +graph: all +dotpath: /usr/bin/dot +graph: all +dotpath: /usr/bin/dot +graph-font: Helvetica +graph-font-size: 10 diff --git a/TracRendezVous/setup.cfg b/TracRendezVous/setup.cfg new file mode 100644 index 0000000..b711394 --- /dev/null +++ b/TracRendezVous/setup.cfg @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +[extract_messages] +add_comments = TRANSLATOR: +copyright_holder = Stefan Koegl +msgid_bugs_address = hotshelf@ctdo.de +output_file = tracrendezvous/locale/messages.pot +keywords = _ ngettext:1,2 N_ tag_ + +[init_catalog] +input_file = tracrendezvous/locale/messages.pot +output_dir = tracrendezvous/locale + +[compile_catalog] +directory = tracrendezvous/locale + +[update_catalog] +input_file = tracrendezvous/locale/messages.pot +output_dir = tracrendezvous/locale + + +[extract_messages_js] +add_comments = TRANSLATOR: +copyright_holder = Stefan Koegl +msgid_bugs_address = hotshelf@ctdo.de +output_file = tracrendezvous/locale/messages-js.pot +keywords = _ ngettext:1,2 N_ +mapping_file = messages-js.cfg + +[init_catalog_js] +domain = tracrendezvous-js +input_file = tracrendezvous/locale/messages-js.pot +output_dir = tracrendezvous/locale + +[compile_catalog_js] +domain = tracrendezvous-js +directory = tracrendezvous/locale + +[update_catalog_js] +domain = tracrendezvous-js +input_file = tracrendezvous/locale/messages-js.pot +output_dir = tracrendezvous/locale + +[generate_messages_js] +domain = tracrendezvous-js +input_dir = tracrendezvous/locale +output_dir = tracrendezvous/htdocs/tracrendezvous diff --git a/TracRendezVous/setup.py b/TracRendezVous/setup.py new file mode 100644 index 0000000..c9aa3f8 --- /dev/null +++ b/TracRendezVous/setup.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +import os, sys +from setuptools import find_packages, setup +from babel.messages import frontend as babel +from distutils.cmd import Command +from trac.util.dist import get_l10n_js_cmdclass + + +commands = {'compile_catalog': babel.compile_catalog, + 'extract_messages': babel.extract_messages, + 'init_catalog': babel.init_catalog, + 'update_catalog': babel.update_catalog} + +commands.update(get_l10n_js_cmdclass()) + +try: + from epydoc import cli +except ImportError: + print 'epydoc not installed, skipping API documentation target.' +else: + class build_apidoc(Command): + description = 'Builds the api documentation' + user_options = [] + boolean_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + epydoc_conf = os.path.join('docs', 'epydoc.conf') + old_argv = sys.argv[1:] + sys.argv[1:] = [ + '--check', + '-v', + '--config=%s' % epydoc_conf, + '--no-private'] + try: + cli.cli() + except: + pass + finally: + sys.argv[1:] = old_argv + commands['build_apidoc'] = build_apidoc + +setup( + name='TracRendezVous', + version='0.3', + packages=find_packages(), + install_requires = { + #'PIL': ['Imaging>=1.1.6'] + }, + zip_safe=False, + entry_points = """ + [trac.plugins] + tracrendezvous.location.web_ui = tracrendezvous.location.web_ui + tracrendezvous.event.web_ui = tracrendezvous.event.web_ui + tracrendezvous.rendezvous.web_ui = tracrendezvous.rendezvous.web_ui + """, + cmdclass = commands, + message_extractors = {'tracrendezvous': [ + ('**.py', 'python', None), + ('**/templates/**.html', 'genshi', None), + ('**/templates/**.txt', 'genshi', { + 'template_class': 'genshi.template:TextTemplate' + }) + ], + }, + package_data={ + '' : ['templates/*'], + 'tracrendezvous': [ + 'htdocs/css/*.css', + 'htdocs/script/*.js', + 'htdocs/images/*', + 'locale/*/LC_MESSAGES/*.mo', 'htdocs/tracrendezvous/*.js' + ], + 'tracrendezvous.location': ['htdocs/css/*.css','htdocs/script/*.js','htdocs/images/*'], + 'tracrendezvous.rendezvous': ['*.ttf','htdocs/css/*.css','htdocs/script/*.js','htdocs/images/*'], + 'tracrendezvous.event': [ 'htdocs/css/*.css', 'htdocs/script/*.js', 'htdocs/images/*']}, + + author = "Stefan Kögl", + author_email = "skoegl@online.de", + description = "a plugin for meeting dates syndication and event calendar with ical export", + license = "GPL", + keywords = "rendezvous, dates, teaming, syndication, calendar", + url = "http://trac.ctdo.de/dev/", # project home page, if any + #requires=["Imaging (>=1.1.6)"], + test_suite = 'tracrendezvous.test.suite' +) diff --git a/TracRendezVous/tracrendezvous/__init__.py b/TracRendezVous/tracrendezvous/__init__.py new file mode 100644 index 0000000..12460ff --- /dev/null +++ b/TracRendezVous/tracrendezvous/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from trac.core import Component, implements, TracError +from trac.web.chrome import ITemplateProvider + +class RendezVousBase(Component): + + implements(ITemplateProvider) + + # ITemplateProvider methods + # Used to add the plugin's templates and htdocs + def get_templates_dirs(self): + from pkg_resources import resource_filename + return [resource_filename(__name__, 'templates')] + + def get_htdocs_dirs(self): + """Return a list of directories with static resources (such as style + sheets, images, etc.) + + Each item in the list must be a `(prefix, abspath)` tuple. The + `prefix` part defines the path in the URL that requests to these + resources are prefixed with. + + The `abspath` is the absolute path to the directory containing the + resources on the local file system. + """ + from pkg_resources import resource_filename + return [('hw', resource_filename(__name__, 'htdocs'))] + diff --git a/TracRendezVous/tracrendezvous/event/__init__.py b/TracRendezVous/tracrendezvous/event/__init__.py new file mode 100644 index 0000000..5b7ee11 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from tracrendezvous.event.web_ui import * +from tracrendezvous.event.model import * +from tracrendezvous.event.macros import * diff --git a/TracRendezVous/tracrendezvous/event/admin.py b/TracRendezVous/tracrendezvous/event/admin.py new file mode 100644 index 0000000..3fe4ea1 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/admin.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- + +from os.path import join + +from trac.admin import IAdminPanelProvider +from trac.core import * +from trac.web.chrome import add_stylesheet +from tracrendezvous.rendezvous.api import RendezVousSystem +from trac.util.translation import _ + +from model import RendezVousType, TypePermission, RendezVousDate +from ctdotools.utils import validate_id, update_votes_graph, date_cmp + +__all__ = ['ComponentRendezVousGeneral', 'ComponentRendezVousTypes'] + +def get_actions(myactions): + actions = [] + for action in myactions: + if isinstance(action, tuple): + actions.append(action[0]) + else: + actions.append(action) + return actions + +class RendezVousAdminPanel(Component): + + implements(IAdminPanelProvider) + + abstract = True + + # IAdminPanelProvider methods + + def get_admin_panels(self, req): + if 'RENDEZVOUS_ADMIN' in req.perm: + yield ('rendezvous', 'RendezVous System', self._type, self._label[1]) + + def render_admin_panel(self, req, cat, page, rendezvous): + req.perm.require('RENDEZVOUS_ADMIN') + # Trap AssertionErrors and convert them to TracErrors + try: + return self._render_admin_panel(req, cat, page, rendezvous) + except AssertionError, e: + raise TracError(e) + + +class ComponentRendezVousGeneral(RendezVousAdminPanel): + + _type = 'rendezvous' + _label = ('rendezvous', 'General') + + def _render_admin_panel(self, req, cat, page, rendezvous): + add_stylesheet (req, 'hw/css/rendezvous.css') + if req.method == "POST": + if req.args.has_key("show_vote_graph"): + self.config.set("rendezvous", "show_vote_graph", True) + else: + self.config.set("rendezvous", "show_vote_graph", False) + + if req.args.has_key("show_location_map"): + self.config.set("rendezvous", "show_location_map", True) + else: + self.config.set("rendezvous", "show_location_map", False) + + if req.args.has_key("max_description_length"): + tmp = int(req.args["max_description_length"]) + self.config.set("rendezvous", "max_description_length", tmp) + + if req.args.has_key("max_votes_per_date"): + tmp = int(req.args["max_votes_per_date"]) + self.config.set("rendezvous", "max_votes_per_date", tmp) + + if req.args.has_key("max_dates_per_rendezvous"): + tmp = int(req.args["max_dates_per_rendezvous"]) + self.config.set("rendezvous", "max_dates_per_rendezvous", tmp) + + if req.args.has_key("graph_size_x"): + tmp = int(req.args["graph_size_x"]) + self.config.set("rendezvous", "graph_size_x", tmp) + update = True + + update = False + if req.args.has_key("graph_size_y"): + tmp = int(req.args["graph_size_y"]) + self.config.set("rendezvous", "graph_size_y", tmp) + update = True + + if req.args.has_key("default_vote_time_start"): + tmp = req.args["default_vote_time_start"] + self.config.set("rendezvous", "default_vote_time_start", tmp) + + if req.args.has_key("default_vote_time_end"): + tmp = req.args["default_vote_time_end"] + self.config.set("rendezvous", "default_vote_time_end", tmp) + + if req.args.has_key("default_rendezvous_type"): + tmp = req.args["default_rendezvous_type"] + self.config.set("rendezvous", "default_rendezvous_type", tmp) + self.config.save() + if update: + path = join(self.env.path, "htdocs", "rendezvous_graphs") + size = [self.config.getint("rendezvous", "graph_size_x"), + self.config.getint("rendezvous", "graph_size_y")] + dates = RendezVousDate.fetch_all(self.env) + for date in dates: + if date.votes: + update_votes_graph(date.votes, path, size) + data = {"show_vote_graph" : self.config.getbool("rendezvous", "show_vote_graph"), + "show_location_map" : self.config.getbool("rendezvous", "show_location_map"), + "max_description_length" : self.config.getint("rendezvous", "max_description_length"), + "max_votes_per_date" : self.config.getint("rendezvous", "max_votes_per_date"), + "max_dates_per_rendezvous" : self.config.getint("rendezvous", "max_dates_per_rendezvous"), + "graph_size_x" : self.config.getint("rendezvous", "graph_size_x"), + "graph_size_y" : self.config.getint("rendezvous", "graph_size_y"), + "default_vote_time_start" : self.config.get("rendezvous", "default_vote_time_start"), + "default_vote_tTime_end" : self.config.get("rendezvous", "default_vote_time_end")} + return "admin_general.html", data + +class ComponentRendezVousTypes(RendezVousAdminPanel): + + _type = 'types' + _label = ('types', 'Types') + + def _render_admin_panel(self, req, cat, page, rendezvoustype): + + add_stylesheet (req, 'hw/css/rendezvous.css') + data = {} + myPermissions = get_actions(RendezVousSystem._actions) + if req.method == "POST": + rtype = req.args.get("rtype") + permission = req.args.get("permission") + + if rtype and rtype.isupper(): + raise TracError(_('All upper-cased tokens are reserved for ' + 'permission names')) + #if not rtype: + #raise TracError(_('Unknown RendezVousType')) + if permission and permission not in myPermissions: + raise TracError(_('Unknown permission')) + if req.args.get("add") and rtype and permission: + rtype = RendezVousType.fetch_one(self.env, name=rtype) + if rtype.has_permission(permission): + raise TracError(_('permission already granted to RendezVousType')) + tPerm = TypePermission(self.env, rtype.type_id, permission) + self.validate_type_permission(tPerm) + tPerm.commit() + req.redirect(req.href.admin(cat, page)) + elif req.args.has_key("add") and rtype: + realType = RendezVousType.fetch_one(self.env, name=rtype) + if realType: + raise TracError(_('RendezVousType already exists')) + realType = RendezVousType(self.env, 0, rtype) + self.validate_rendezvous_type(realType) + realType.commit() + req.redirect(req.href.admin(cat, page)) + elif req.args.has_key("save") and req.args.has_key("rsel"): + req.perm.require('RENDEZVOUS_ADMIN') + rsel = req.args.get('rsel') + rsel = isinstance(rsel, list) and rsel or [rsel] + for key in rsel: + type_id = int(key) + rtype = RendezVousType.fetch_one(self.env, type_id) + if not rtype: + raise TracError(_('Unknown RendezVousType')) + rtype.delete() + req.redirect(req.href.admin(cat, page)) + elif req.args.has_key("save") and req.args.has_key("sel"): + req.perm.require('RENDEZVOUS_ADMIN') + sel = req.args.get('sel') + sel = isinstance(sel, list) and sel or [sel] + for key in sel: + rtype, permission = key.split(":") + rtype_id = int(rtype) + if permission and permission not in myPermissions: + raise TracError(_('Unknown type permission relation')) + typePermission = TypePermission.fetch_one(self.env, rtype_id, permission) + if typePermission: + typePermission.delete() + req.redirect(req.href.admin(cat, page)) + elif req.args.has_key("save") and req.args.has_key("default"): + default = int(req.args["default"]) + self.config.set("rendezvous", "default_rendezvous_type", default) + self.config.save() + + data.update({"default_rendezvous_type" : self.config.getint("rendezvous", "default_rendezvous_type"), + "rendezVousTypes" : RendezVousType.fetch_all(self.env), + "actions" : myPermissions}) + return "admin_types.html", data + + def validate_rendezvous_type(self, typ): + if type(typ.type_id) != int: + raise TypeError("RendezVousType.validate() wrong type") + if type(typ.name) != unicode: + raise TypeError("RendezVousType.validate() wrong type") + + def validate_type_permission(self, mytype): + if type(mytype.type_id) != int: + raise TypeError("TypePermission.__init__(): expected type int, got '%s'" % type(mytype.type_id)) + if type(mytype.permission) != unicode: + raise TypeError("TypePermission.__init__() expected type 'unicode', got '%s'" % type(mytype.permission)) + validate_id(mytype.type_id) diff --git a/TracRendezVous/tracrendezvous/event/htdocs/css/event.css b/TracRendezVous/tracrendezvous/event/htdocs/css/event.css new file mode 100644 index 0000000..2122a79 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/htdocs/css/event.css @@ -0,0 +1,158 @@ +#main, #content {margin:0 !important;padding:0 !important;left:0;} + +fieldset {-khtml-border-radius: 5px;-webkit-border-radius: 20px;-moz-border-radius: 20px;padding-bottom:10px;margin:0 !important;} +fieldset legend {margin-left:15px;} +span.edit {margin-left:5px;} + +#new_event {width:50em;margin:auto;} +#new_event fieldset {text-align:left;} +#recurrence {padding:0 !important;width:45em;margin:auto;} +#recurrency-freq, #recurrency-timeframe, #recurrency-exceptions {border:1px outset #000 !important;} +#recurrency-exceptions table {border-collapse: collapse;} +#recurrency-exceptions td {vertical-align:top;} +#recurrency-exceptions td.left {border-right:1px solid #000;} +ul#exceptions {padding:5px;list-style-type:none;} +ul#exceptions li {padding:5px;margin:5px;background:#ccc;} +#event-overview {width:35em; margin:auto;} + +table.upcoming +{ +border-spacing:0px; +border-collapse: collapse; +margin:auto; +text-align:center; +color:#000; +font-size:70% !important; +margin-bottom:10px; +} + +table.upcoming :link, table.upcoming a {color:#a00;} +table.upcoming h1, table.upcoming h2 {color:#000 !important;font-size:100%;font-family:serif;} + +div#content h1, div#content h2 {text-align:center;} + +.event-item {margin-bottom:1em; overflow: hidden;} +.event-item h2 {margin:0;padding:0;padding-top:0.5em;} + +.event-intern +{ +padding:0; +margin:0; +background:#e4d6b0; +margin-bottom: -2000px; +padding-bottom: 2000px; +} + +.recurring, .unique +{ +width:2em; +float:left; +margin-bottom: -2000px; +padding-bottom: 2000px; +font-family:monospace; +text-align:center; +font-weight:bold; +} + +.recurring { background:#f00;} +.unique { background:#ccc;} + +td.upcoming, td.upcoming-event +{ +background:#e4d6b0; +padding:5px; +border-top:1px solid #fff; +border-bottom:1px solid #fff; +border-right:1px solid #fff; +min-height:2em; +vertical-align:top; +text-align:center; +} + +td.upcoming-event {background:#ffedbc;} +td.upcoming:hover, td.upcoming-event:hover {background:#ffffee;} +td.upcoming-event table {padding:0;margin:0;} + +table.upcoming thead th, table.upcoming tfoot th { +border-right:1px solid #fff !important; +background:#000; +color:#fff; +font-size:80%; +font-weight: bold; +padding-top:10px; +padding-bottom:10px; +padding-left:5px; +vertical-align: bottom; +} + +table.upcoming tfoot th {border:0 !important;} +table.upcoming thead th.last, table.upcoming thead th:last-child +{ +border-top-right-radius:5px; +-moz-border-radius-topright: 20px; +padding-right:10px; +} + +table.upcoming thead th.first, table.upcoming thead th:first-child +{ +border-top-left-radius:20px; +-khtml-border-radius-topleft: 5px; +-webkit-border-top-left-radius: 20px; +-moz-border-radius-topleft: 20px; +background:#000; +color:#fff; +padding-left:10px; +} + +table.upcoming tfoot th.first, table.upcoming tfoot th:first-child +{ +border-bottom-left-radius:20px; +-khtml-border-radius-bottomleft: 5px; +-webkit-border-bottom-left-radius: 20px; +-moz-border-radius-bottomleft: 20px; +background:#000; +color:#fff; +padding-left:10px; +} + +table.upcoming tfoot th.last, table.upcoming tfoot th:last-child +{ +border-bottom-right-radius:20px; +-khtml-border-radius-bottomright: 5px; +-webkit-border-bottom-right-radius: 20px; +-moz-border-radius-bottomright: 20px; +padding-right:10px; +} + +td.day +{ +font-weight:bold; +font-family:sans-serif; +padding:5px 10px; +margin:0px; +border-right:1px solid #000; +border-bottom:1px solid #000; +min-height:2em; +} + +td.daytext +{ +background:#000; +color:#fff; +font-weight:bold; +font-family:sans-serif; +font-size:80%; +padding:2px 10px 5px 10px; +margin:0px; +border-right:1px solid #fff !important; +border-bottom:1px solid #fff; +min-height:2em; +vertical-align:top; +} + + +#altlinks li a.ical +{ + background-image: url("../images/ical_icon.jpg"); + padding-left: 45px; +} \ No newline at end of file diff --git a/TracRendezVous/tracrendezvous/event/htdocs/images/ical_icon.jpg b/TracRendezVous/tracrendezvous/event/htdocs/images/ical_icon.jpg new file mode 100644 index 0000000..0e944c0 Binary files /dev/null and b/TracRendezVous/tracrendezvous/event/htdocs/images/ical_icon.jpg differ diff --git a/TracRendezVous/tracrendezvous/event/macros.py b/TracRendezVous/tracrendezvous/event/macros.py new file mode 100644 index 0000000..be9d550 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/macros.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- + +import time, calendar +from datetime import datetime, date, timedelta +from cStringIO import StringIO + +from trac.wiki.api import WikiSystem +from trac.wiki.macros import WikiMacroBase +from trac.util import * +from trac.web.chrome import add_stylesheet +from trac.web import Href +from trac.util.datefmt import utc, to_timestamp +from trac.resource import get_resource_url + +from tracrendezvous.event.model import Event +from ctdotools.utils import get_tz + +__all__ = ['RendezVousesCalendarMacro',] + +class EventHeaderMacro(WikiMacroBase): + def expand_macro(self, formatter, name, content): + try: + e_id = int(content) + except ValueError: + return "" + event = Event.fetch_one(self.env, e_id, show_all=True, days=60) + if not event or not event.periodic(): + return "" + + rows = [] + rows.append("""""") + session_tzname, selected_tz = get_tz(formatter.req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + rows.append("%s" % event.rrules_explained) + if hasattr(event, "followups"): + l = [] + for ev in event.followups: + local_time_begin = selected_tz.fromutc(ev.time_begin) + local_time_end = selected_tz.fromutc(ev.time_end) + link_label = "%s %s - %s %s" % (local_time_begin.strftime("%d.%m.%Y %H:%M"), local_time_begin.tzinfo.tzname(None), local_time_end.strftime("%d.%m.%Y %H:%M"), local_time_end.tzinfo.tzname(None)) + link = "events/%s/%s" % (event.e_id, ev.time_begin.strftime("%Y-%m-%d")) + if WikiSystem(self.env).has_page(link): + row = '
  • %s
  • ' % (formatter.href.wiki(link) or formatter.href.event("createpage", event.e_id, ev.time_begin.strftime("%Y-%m-%d")), link_label) + else: + row = '
  • %s
  • ' % (formatter.href.event("createpage", event.e_id, ev.time_begin.strftime("%Y-%m-%d")), link_label) + l.append(row) + rows.append("
      %s
    " % "".join(l)) + return """

    %s

    %s
    """ % (formatter.href.event(event.e_id), event.name, "".join(rows)) + +class RendezVousesCalendarMacro(WikiMacroBase): + """Inserts a small calendar with scheduled RendezVouses with optional locations + constraint + + Examples: + {{{ + [[WikiCalendar]] + [[WikiCalendar(location1,location2,foo_location)]] + }}} + """ + + def expand_macro(self, formatter, name, content): + today = time.localtime() + http_param_year = formatter.req.args.get('year', '') + http_param_month = formatter.req.args.get('month', '') + if content: + args = content.split(',') + else: + args = [] + + if http_param_year == "": + # not clicked on a prev or next button + if len(args) >= 1 and args[0] <> "*": + # year given in macro parameters + year = int(args[0]) + else: + # use current year + year = today.tm_year + else: + # year in http params (clicked by user) overrides everything + year = int(http_param_year) + + if http_param_month == "": + # not clicked on a prev or next button + if len(args) >= 2 and args[1] <> "*": + # month given in macro parameters + month = int(args[1]) + else: + # use current month + month = today.tm_mon + else: + # month in http params (clicked by user) overrides everything + month = int(http_param_month) + + wiki_page_format = "%Y-%m-%d" + if len(args) >= 4: + wiki_page_format = args[3] + + curr_day = None + if year == today.tm_year and month == today.tm_mon: + curr_day = today.tm_mday + + thispageURL = Href(get_resource_url(self.env, formatter.resource, formatter.href)) + # for the prev/next navigation links + prevMonth = month-1 + prevYear = year + nextMonth = month+1 + nextYear = year + # check for year change (KISS version) + if prevMonth == 0: + prevMonth = 12 + prevYear -= 1 + if nextMonth == 13: + nextMonth = 1 + nextYear += 1 + + # 9-tuple for use with time.* functions requiring a struct_time + mydate = [0] * 8 + [-1] # AS: breaks Python 2.4 + + # building the output + buff = [] + buff.append(u'''\ + +
    +\n\n') + for day in calendar.weekheader(2).split(): + buff.append(u'' % day) + buff.append(u'\n\n') + + last_week_prev_month = calendar.monthcalendar(prevYear, prevMonth)[-1]; + first_week_next_month = calendar.monthcalendar(nextYear, nextMonth)[0]; + w = -1 + db = self.env.get_db_cnx() + cursor = db.cursor() + day_list = [] + foo,last_day = calendar.monthrange(year, month) + start_dt = datetime(year, month, 1, tzinfo=utc) + end_dt = datetime(year, month, last_day, 23, 59, tzinfo=utc) + rts = Event.fetch_by_period_dict(self.env, start_dt, end_dt) + session_tzname, selected_tz = get_tz(formatter.req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + def _f(t): + assert type(t) == Event + real_begin = selected_tz.fromutc(t.time_begin) + real_end = selected_tz.fromutc(t.time_end) + return u'
  • %s:
    %s
  • ' % (formatter.href.event(t.e_id), real_begin.strftime("%d.%m.%Y %H:%M"), real_end.strftime("%d.%m.%Y %H:%M"), real_begin.tzinfo.tzname(None), unicode(t.location.name), unicode(t.name)) + for week in calendar.monthcalendar(year, month): + buff.append(u'\n') + w = w+1 + d = -1 + for day in week: + d = d+1 + + # calc date and update CSS classes + mydate[0:3] = [year, month, day] + classes = u'day' + title = u'' + if not day: + classes += u' adjacent_month' + if w == 0: + day = last_week_prev_month[d] + mydate[0:3] = [prevYear, prevMonth, day] + else: + day = first_week_next_month[d] + mydate[0:3] = [nextYear, nextMonth, day] + else: + if day == curr_day: + classes += u' today' + title += u"Heute:" + wiki = time.strftime(wiki_page_format, tuple(mydate)) + actual_date = date(mydate[0], mydate[1], mydate[2]) + if rts and rts.has_key(actual_date): + rt = rts[actual_date] + for t in rt: + if actual_date.day != t.time_end.day: + tomorrow = actual_date + timedelta(1) + rts[tomorrow].append(t) + text = u"".join([_f(t) for t in rt]) + classes += u" active" + title += u" %d Termin(e)" % len(rt) + daylink = unicode(formatter.href.event("by-day", "%d-%d-%d" % tuple(mydate[:3]))) + #text = text.encode("utf8") + buff.append(u'\n' % (classes.strip(), daylink, title, day, text)) + else: + title += u"Noch keine Termine" + daylink = formatter.href.event("new", "%d-%d-%d" % tuple(mydate[:3])) + buff.append(u'\n' % (classes, daylink, title, day)) + buff.append(u'\n') + buff.append(u'\n\n
    +''') + import locale + + encoding = locale.getlocale()[1] + # prev year link + mydate[0:2] = [year-1, month] + mydate_label = time.strftime('%B %Y', tuple(mydate)) + if encoding: + mydate_label = mydate_label.decode(encoding) + + buff.append(u'' % ( + thispageURL(month=month, year=year-1), + mydate_label + )) + # prev month link + mydate[0:2] = [prevYear, prevMonth] + mydate_label = time.strftime('%B %Y', tuple(mydate)) + if encoding: + mydate_label = mydate_label.decode(encoding) + buff.append(u'' % ( + thispageURL(month=prevMonth, year=prevYear), + mydate_label + )) + + # the caption + mydate[0:2] = [year, month] + mydate_label = time.strftime('%B %Y', tuple(mydate)) + if encoding: + mydate_label = mydate_label.decode(encoding) + buff.append(mydate_label) + + + # next month link + mydate[0:2] = [nextYear, nextMonth] + mydate_label = time.strftime('%B %Y', tuple(mydate)) + if encoding: + mydate_label = mydate_label.decode(encoding) + buff.append(u'' % ( + thispageURL(month=nextMonth, year=nextYear), + mydate_label)) + # next year link + mydate[0:2] = [year+1, month] + mydate_label = time.strftime('%B %Y', tuple(mydate)) + if encoding: + mydate_label = mydate_label.decode(encoding) + buff.append(u'' % ( + thispageURL(month=month, year=year+1), + mydate)) + + buff.append(u'
    %s
    %s
      %s
    %s
    \n') + table = u"".join(buff) + return table diff --git a/TracRendezVous/tracrendezvous/event/model.py b/TracRendezVous/tracrendezvous/event/model.py new file mode 100644 index 0000000..82f0677 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/model.py @@ -0,0 +1,1088 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime, timedelta +from collections import defaultdict + +from trac.core import * +from trac.env import IEnvironmentSetupParticipant +from trac.perm import PermissionSystem +from trac.db import Table, Column, Index +from trac.db.util import sql_escape_percent +from trac.util.datefmt import utc, to_timestamp +from trac.util.text import to_unicode +from trac.util.datefmt import get_timezone, utc, format_time, localtz +from trac.util.translation import _ + +from dateutil.rrule import * +from babel.core import Locale +from ctdotools.utils import validate_id, gen_wiki_page +from tracrendezvous.location.model import ItemLocation + +__all__ = ['Event', 'EventRRule', 'EventRDate', 'EventModelProvider'] + + +def rrule_to_ical(rrule): + ttypes = ("YEARLY","MONTHLY","WEEKLY","DAILY") + s = "RRULE:FREQ=%s;INTERVAL=%s%s" % (ttypes[rrule._freq], rrule._interval) + if rrule._until: + s += ";UNTIL=%s" % rrule._until.strftime("%Y%m%dT%H%M%SZ") + elif rrule._count: + s += ";COUNT=%s" % rrule._count + return s + +def unfold(d): + return map(int, d.split()) + +def unfold_weekdays(d): + return map(lambda s:map(int,s.split(",")), d.split()) + +class EventRDate(object): + def __init__(self, env, erd_id=0, e_id=0, erd_exclude=False, + erd_datetime=None): + self.env = env + self.erd_id = erd_id + self.e_id = e_id + self.erd_exclude = erd_exclude + self.erd_datetime = erd_datetime + + @staticmethod + def fetch_one(env, erd_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * " + "FROM event_rdates " + "WHERE erd_id=%s", (erd_id,)) + row = cursor.fetchone() + if not row: + return None + return EventRDate(env, row[0], row[1], bool(row[2]), + datetime.fromtimestamp(row[3], utc)) + + @staticmethod + def fetch_by_event(env, e_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * " + "FROM event_rdates " + "WHERE e_id=%s", (e_id,)) + rows = cursor.fetchall() + res = list() + for row in rows: + res.append(EventRDate(env, row[0], row[1], bool(row[2]), + datetime.fromtimestamp(row[3], utc))) + return res + + def commit(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO event_rdates " + "(e_id, erd_exclude, erd_datetime) VALUES (%s,%s,%s);", + (int(self.e_id), int(self.erd_exclude), + to_timestamp(self.erd_datetime))) + db.commit() + self.erd_id = db.get_last_id(cursor, 'event_rdates') + + def update(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("UPDATE event_rdates SET " + "erd_datetime=%s " + "WHERE erd_id=%s;", + (to_timestamp(self.erd_datetime), int(self.erd_id))) + db.commit() + + @staticmethod + def delete(env, erd_id): + db = env.get_db_cnx() + cursor = db.cursor() + try: + cursor.execute("DELETE FROM event_rdates " \ + "WHERE erd_id=%s", (erd_id,)) + db.commit() + except Exception: + db.rollback() + pass + +class EventWikiPage(object): + def __init__(self, env, ewp_id=0, e_id=0, time_begin=None, wikipage=None): + self.env = env + self.ewp_id = ewp_id + self.e_id = e_id + self.time_begin = time_begin + self.wikipage = wikipage + + @staticmethod + def fetch_by_event_occurrence(env, e_id, time_begin=None): + db = env.get_db_cnx() + cursor = db.cursor() + if not time_begin: + cursor.execute("SELECT * " + "FROM event_wikipages " + "WHERE e_id=%s", (e_id,)) + else: + cursor.execute("SELECT * " + "FROM event_wikipages " + "WHERE e_id=%s and time_begin=%s", (e_id, time_begin)) + rows = cursor.fetchall() + res = list() + for row in rows: + res.append(EventWikiPage(env, *row)) + return res + + def commit(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO event_wikipages " \ + "(e_id, time_begin, wikipage) VALUES (%s,%s,%s);", + (int(self.e_id), to_timestamp(self.time_begin), self.wikipage)) + db.commit() + self.ewp_id = db.get_last_id(cursor, 'event_wikipages') + + def update(self, env): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("UPDATE event_wikipages " \ + "(e_id,timestamp,wikipage) VALUES (%s,%s,%s) where ewp_id=%s;", + (int(self.e_id), to_timestamp(self.time_begin), self.wikipage, int(self.ewp_id))) + db.commit() + + @staticmethod + def delete(env, erd_id): + db = env.get_db_cnx() + cursor = db.cursor() + try: + cursor.execute("DELETE FROM event_wikipages " \ + "WHERE ewp_id=%s", (ewp_id,)) + db.commit() + except Exception: + db.rollback() + pass + + +class EventRRule(object): + freq_enum = (YEARLY, MONTHLY, WEEKLY, DAILY) + day_enum = (MO, TU, WE, TH, FR, SA, SU) + # TODO: porting to babel + day_abr_names = (_("MO"), _("TU"), _("WE"), _("TH"), _("FR"), _("SA"), _("SU")) + day_names = (_("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"), _("Friday"), _("Saturday"), _("Sunday")) + monthday_names = ["%d." % i for i in xrange(1,32)] + ['last',] + ["%d-last" % i for i in xrange(1,32)] + month_names = (_("January"), _("February"), _("March"), _("April"), _("May"), _("June"), _("July"), _("August"), _("September"), _("October"), _("November"), _("December")) + monthday_enum = range(1,32) + range(-1,-33,-1) + weekday_names = (_("1st"), _("2nd"), _("3rd"), _("4th"), _("5th"), _("last"), _("2-last"), _("3-last"), _("4-last"), _("5-last")) + selectkeys = [1, 2, 3, 4, 5, -1, -2, -3, -4, -5, -6] + + def __init__(self, env, err_id=0, e_id=None, exclude=None, freq=None, interval=None, + count=None, until=None, bysetpos=None, + bymonth=None, bymonthday=None, byyearday=None, + byweeknumber=None, byweekday=set(), byweekdayocc=None): + self.env = env + self.err_id = err_id + self.e_id = e_id + self.exclude = exclude + self.freq = freq + self.interval = interval + self.count = count + self.until = until + self.bysetpos = bysetpos + self.bymonth = bymonth + self.bymonthday = bymonthday + self.byyearday = byyearday + self.byweeknumber = byweeknumber + self.byweekday = byweekday + self.byweekdayocc = byweekdayocc + + @staticmethod + def _fetch_data(env, e_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""SELECT * + FROM event_rrules + WHERE e_id=%s""", (e_id,)) + return cursor.fetchall() + + @staticmethod + def fetch_by_event(env, e_id): + '''Returns a list of EventRRule that can be used to display and edit the raw data ''' + rows = EventRRule._fetch_data(env, e_id) + if not rows: + return list() + res = list() + for row in rows: + err_id, e_id, exclude, freq, interval, count, until, bysetpos, bymonth, bymonthday, byyearday, byweeknumber, byweekday, byweekdayocc = row + if byweekday: + try: + byweekday = set(map(int, byweekday.split(" "))) + except AttributeError: + byweekday = set((byweekday, )) + else: + byweekday = set() + res.append(EventRRule(env, + err_id, + e_id, + exclude, + freq, + interval, + count, + until and datetime.fromtimestamp(until, utc) or None, + bysetpos, + bymonth, + bymonthday, + byyearday, + byweeknumber, + byweekday, + byweekdayocc)) + return res + + @staticmethod + def fetch_by_event_rrules(env, e_id, time_begin=None): + ''' returns a rruleset that can be used to actually display dates''' + rows = EventRRule._fetch_data(env, e_id) + if not rows: + return rruleset() + res = rruleset() + for row in rows: + res.rrule(EventRRule.to_rrule(row, time_begin)) + return res + + @staticmethod + def fetch_by_event_full(env, e_id, time_begin=None): + rows = EventRRule._fetch_data(env, e_id) + if not rows: + return list(), rruleset() + lst = list() + rs= rruleset() + for row in rows: + rs.rrule(EventRRule.to_rrule(row, time_begin)) + err_id, e_id, exclude, freq, interval, count, until, bysetpos, bymonth, bymonthday, byyearday, byweeknumber, byweekday, byweekdayocc = row + if byweekday: + try: + byweekday = set(map(int, byweekday.split(" "))) + except AttributeError: + byweekday = set((byweekday, )) + else: + byweekday = set() + lst.append(EventRRule(env, err_id, e_id, exclude, freq, interval, count, until and datetime.fromtimestamp(until, utc) or None, bysetpos, bymonth, bymonthday, byyearday, byweeknumber, byweekday, byweekdayocc)) + return lst, rs + + @staticmethod + def fetch_by_event_ical(env, e_id, time_begin=None): + ''' returns a list of ical formatted strings''' + rows = EventRRule._fetch_data(env, e_id) + lst = list() + for row in rows: + lst.append(EventRRule.to_ical(row)) + return lst + + def commit(self, conn=None): + db = conn and conn or self.env.get_db_cnx() + cursor = db.cursor() + + try: + cursor.execute("INSERT INTO event_rrules (e_id, exclude, freq, interval, count, until, bysetpos, bymonth, bymonthday, byyearday, byweeknumber, byweekday, byweekdayocc) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s);", + (self.e_id, + self.exclude, + self.freq, + self.interval, + self.count, + to_timestamp(self.until), + self.bysetpos, + self.bymonth, + self.bymonthday, + self.byyearday, + self.byweeknumber, + self.byweekday and " ".join(map(str, self.byweekday)) or None, + self.byweekdayocc)) + db.commit() + self.err_id = db.get_last_id(cursor, 'event_rrules') + except Exception, e: + db.rollback() + raise + + @staticmethod + def delete(env, e_id): + db = env.get_db_cnx() + cursor = db.cursor() + try: + cursor.execute("DELETE FROM event_rrules where e_id=%s", (e_id,)) + db.commit() + except Exception, e: + db.rollback() + raise + + def update(self, conn=None): + db = conn and conn or self.env.get_db_cnx() + cursor = db.cursor() + weekdays = self.byweekday and " ".join(map(str, self.byweekday)) or None + cursor.execute("UPDATE event_rrules " \ + "SET exclude=%s, " \ + "freq=%s, " \ + "interval=%s, " \ + "count=%s, " \ + "until=%s, " \ + "bysetpos=%s, " \ + "bymonth=%s, " \ + "bymonthday=%s, " \ + "byyearday=%s, " \ + "byweeknumber=%s, " \ + "byweekday=%s, " \ + "byweekdayocc=%s " \ + "WHERE e_id=%s", (self.exclude, + self.freq, + self.interval, + self.count, + self.until and to_timestamp(self.until) or None, + self.bysetpos, + self.bymonth, + self.bymonthday, + self.byyearday, + self.byweeknumber, + weekdays, + self.byweekdayocc, + self.e_id)) + if not conn: + db.commit() + + @staticmethod + def to_rrule(row, time_begin): + err_id, e_id, exclude, freq, interval, count, until, bysetpos, bymonth, bymonthday, byyearday, byweeknumber, byweekday, byweekdayocc = row + args = {"dtstart" : time_begin, "interval" : interval} + if count!=None: + args["count"] = count + if until: + args["until"] = datetime.fromtimestamp(until, utc) + if bysetpos!=None: + args["bysetpos"] = bysetpos + if bymonth!=None: + args["bymonth"] = bymonth + if bymonthday!=None: + args["bymonthday"] = bymonthday + if byyearday!=None: + args["byyearday"] = byyearday + if byweeknumber!=None: + args["byweekno"] = byweeknumber + if byweekday!=None: + if byweekdayocc != None: + try: + byweekdayocc = byweekdayocc.split() + except Exception: + byweekdayocc = [byweekdayocc,] + else: + byweekdayocc = [] + if byweekday != None: + try: + byweekday = byweekday.split() + except Exception: + byweekday = [byweekday,] + else: + byweekday = [] + args["byweekday"] = map( + lambda x:EventRRule.day_enum[x[0]](x[1] != None and + EventRRule.selectkeys[x[1]] or None), + map(None, *[map(int, byweekday), map(int, byweekdayocc)])) + return rrule(freq, **args) + + @staticmethod + def to_ical(row): + ttypes = ("YEARLY","MONTHLY","WEEKLY","DAILY") + err_id, e_id, exclude, freq, interval, count, until, bysetpos, bymonth, bymonthday, byyearday, byweeknumber, byweekday, byweekdayocc = row + s = ["RRULE:FREQ=%s;INTERVAL=%s" % (ttypes[freq], interval),] + if until: + s.append("UNTIL=%s" % datetime.fromtimestamp(until, utc).strftime("%Y%m%dT%H%M%SZ")) + if count!=None: + s.append("COUNT=%s" % count) + if bysetpos!=None: + s.append("BYSETPOS=%s" % bysetpos) + if bymonth!=None: + s.append("BYMONTH=%s" % bymonth) + if bymonthday!=None: + s.append("MONTHDAY=%s" % bymonthday) + if byyearday!=None: + s.append("COUNT=%s" % byyearday) + if byweeknumber!=None: + s.append("COUNT=%s" % byweeknumber) + if byweekday != None: + if byweekdayocc != None: + try: + byweekdayocc = byweekdayocc.split() + except Exception: + byweekdayocc = [str(byweekdayocc),] + else: + byweekdayocc = [] + if byweekday != None: + try: + byweekday = byweekday.split() + except Exception: + byweekday = [str(byweekday),] + else: + byweekday = [] + #data = list() + #count = 0 + #print "byweekday", byweekday + #print "byweekdayocc", byweekdayocc + #wi = iter(byweekday) + #oi = iter(byweekdayocc) + #while 1: + #d = wi.next() + #print "d", d + #try: + #d = wi.next() + #except StopIteration: + #break + #try: + #o = oi.next() + #except StopIteration: + #o = None + #print "o", o + #data.append((d, o)) + data = map(None, map(int, byweekday), map(int, byweekdayocc)) + s.append("BYDAY=%s" % ",".join( + map(lambda x:"%s%s" % (x[1] != None and EventRRule.selectkeys[x[1]] or '', EventRRule.day_enum[x[0]]), + data))) + return ";".join(s) + + def explain(self): + start = (_(" %d year(s)"), _("monthly"), _("weekly"), _("daily")) + freq = (_("year(s)"), _("month(s)"), _("week(s)"), _("day(s)")) + expl = [] + expl.append("Repeat every %d %s" % (self.interval, freq[self.freq])) + if self.byweekday: + if self.byweekdayocc != None: + try: + byweekdayocc = map(int, self.byweekdayocc.split()) + except Exception: + byweekdayocc = [int(self.byweekdayocc), ] + else: + byweekdayocc = [] + byweekday = self.byweekday + if not byweekday: + byweekday = [] + tmp = map(None, byweekday, byweekdayocc) + tpl = _("on %s") + res = list() + for x0, x1 in tmp: + if x1: + res.append("%s %s" % (unicode(self.weekday_names[x1]), unicode(self.day_names[x0]))) + else: + res.append(unicode(self.day_names[x0])) + #tmp = ", ".join(res) + tmp = tpl % ", ".join( + map(lambda x: x[1] and _("%s %s") % + (unicode(self.weekday_names[x[1]]), unicode(self.day_names[x[0]])) or + unicode(self.day_names[x[0]]), tmp)) + expl.append(tmp) + elif self.bymonthday: + expl.append(_("on day %s") % self.bymonthday) + elif self.bymonth: + expl.append(_("in %s") % self.month_names[self.bymonth]) + elif self.byyearday: + expl.append(_("on day %s") % self.byyearday) + + if self.count and self.count > 0: + expl.append(_("for %d times" % self.count)) + if self.until: + expl.append(_("until %s" % self.until.strftime('%Y-%m-%d'))) + #else: + #raise NotImplementedError("could not provide a sane explanation of event:\n%s" % self.__str__()) + return " ".join(expl) + + def __str__(self): + return "\n".join(("self.e_id %s" % self.e_id, + "self.exclude %s" % self.exclude, + "self.freq %s" % self.freq, + "self.interval %s" % self.interval, + "self.count %s" % self.count, + "self.until %s" % to_timestamp(self.until), + "self.bysetpos %s" % self.bysetpos, + "self.bymonth %s" % self.bymonth, + "self.bymonthday %s" % self.bymonthday, + "self.byyearday %s" % self.byyearday, + "self.byweeknumber %s" % self.byweeknumber, + "self.byweekday %s" % self.byweekday, + "self.byweekdayocc %s" % self.byweekdayocc)) + +class Event(object): + def __init__(self, env, e_id, name, author, time_created, time_modified, time_begin, time_end, location_id, initial_e_id=None, tags=None, attendees=None, is_periodic=False, wikipage=None): + """maps an relation of the 'events' table to a python object + + @type e_id: int + @param e_id: primary key + + @type name: string + @param name: name of that event + + @type author: string + @param author: name of events' creator + + @type time_begin: datetime + @param time_begin: begin of the event + + @type time_end: datetime + @param time_end: end of the event + + @type time_created: datetime + @param time_created: creation timestamp of the event + + @type time_modified: datetime + @param time_modified: timestamp of last modification + + @type location_id: int + @param location_id: primary key of the location the event is taking place + + @type initial_e_id: int + @param initial_e_id: gets the same value as e_id, but only if it's + not the first occurence of an recurring event + + @type tags: unicode + @param tags: space seperated list of tags + + @type attendees: unicode + @param attendees: space seperated list of tags + + @type is_periodic: bool + @param is_periodic: shows if that event is recurring + + @type wikipage: unicode + @param wikipage: the link as plaintext without 'wiki' prefix, e.g: "events/wikipage-of-that-event", or "foo" + """ + self.env = env + self.e_id = e_id + self.name = unicode(name) + self.author = unicode(author) + self.time_created = time_created + self.time_modified = time_modified + self.time_begin = time_begin + self.time_end = time_end + self.location_id = location_id + self.initial_e_id = initial_e_id # reference to another object of the same type. if this is not None, its a follow-up event with a wiki page created + self.tags = tags # space separated list of strings + self.attendees = attendees # space separated list of strings + self.is_periodic = is_periodic + self.location = ItemLocation.fetch_one(env, location_id) + #self.location = 0 + self.wikipage = wikipage + + def periodic(self): + return self.is_periodic or self.initial_e_id + + @staticmethod + def fetch_one(env, event_id, show_next=False, show_all=False, days=365): + """ returns Event with primary key event_id + + @type event_id: int + @param event_id: primary key of Event + + @type show_next: bool + @param show_next: if True and if Event.is periodic==True then + + @type show_all: bool + @param show_all: if True and if Event.is periodic==True then + event gets a new member list 'Event.followups' of followup events between now and now + 1year. + """ + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * " \ + "FROM events " \ + "WHERE e_id=%s", (event_id,)) + row = cursor.fetchone() + if not row: + return None + e_id, name, author, time_created, time_modified, time_begin, time_end, location_id, initial_e_id, tags, attendees, is_periodic, wikipage = row + event = Event(env, e_id, name, author, datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(time_modified, utc), datetime.fromtimestamp(time_begin, utc), datetime.fromtimestamp(time_end, utc), location_id, initial_e_id, tags, attendees, bool(is_periodic), wikipage) + if show_next and is_periodic: + rrules = EventRRule.fetch_by_event_rrules(env, e_id, event.time_begin) + if rrules: + dt = rrules.after(datetime.now(utc), True) + if not dt: + return event + delta = event.time_end - event.time_begin + s = Event(env, + e_id, + name, + author, + event.time_created, + event.time_modified, + dt, + dt + delta, + location_id, + e_id, + tags, + attendees, + False, + wikipage) + return s + if show_all and is_periodic: + event.followups = [] + event.rrules_explained = None + ls, rrules = EventRRule.fetch_by_event_full(env, e_id, event.time_begin) + if rrules: + text = ls[0].explain() + event.rrules_explained = text + n = datetime.now(utc) + e = n + timedelta(days) + followups = rrules.between(n, e, True) + delta = event.time_end - event.time_begin + for i in followups: + dt = datetime(i.year, i.month, i.day, event.time_begin.hour, event.time_begin.minute, tzinfo=utc) + s = Event(env, + e_id, + name, + author, + datetime.fromtimestamp(time_created, utc), + datetime.fromtimestamp(time_modified, utc), + dt, + dt + delta, + location_id, + e_id, + tags, + attendees, + False, + wikipage) + s.rrules_explained = text + event.followups.append(s) + return event + + @staticmethod + def fetch_all(env): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * from events;") + rows = cursor.fetchall() + if not rows: + return [] + res = [] + for row in rows: + e_id, name, author, time_created, time_modified, time_begin, time_end, location_id, initial_e_id, tags, attendees, is_periodic, wikipage = row + time_begin = datetime.fromtimestamp(time_begin, utc) + event = Event(env, e_id, name, author, + datetime.fromtimestamp(time_created, utc), + datetime.fromtimestamp(time_modified, utc), + time_begin, datetime.fromtimestamp(time_end, utc), location_id, initial_e_id, + tags, attendees, is_periodic, wikipage) + if is_periodic: + rrules = EventRRule.fetch_by_event_rrules(env, e_id, event.time_begin) + if rrules: + dt = rrules.after(datetime.now(utc), inc=True) + if not dt: + res.append(event) + continue + delta = event.time_end - event.time_begin + s = Event(env, + e_id, + name, + author, + event.time_created, + event.time_modified, + dt, + dt + delta, + location_id, + e_id, + tags, + attendees, + False, + wikipage) + res.append(s) + else: + res.append(event) + return res + + @staticmethod + def fetch_all_with_rrule(env): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * from events;") + rows = cursor.fetchall() + if not rows: + return [] + res = [] + for row in rows: + e_id, name, author, time_created, time_modified, time_begin, time_end, location_id, initial_e_id, tags, attendees, is_periodic, wikipage = row + time_begin = datetime.fromtimestamp(time_begin, utc) + event = Event(env, e_id, name, author, + datetime.fromtimestamp(time_created, utc), + datetime.fromtimestamp(time_modified, utc), + time_begin, datetime.fromtimestamp(time_end, utc), location_id, initial_e_id, + tags, attendees, is_periodic, wikipage) + res.append(event) + if is_periodic: + try: + event.rrule = EventRRule.fetch_by_event(env, e_id)[0] + except Exception: + pass + return res + + @staticmethod + def fetch_as_ical(env): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * from events;") + rows = cursor.fetchall() + if not rows: + return [] + res = [] + for row in rows: + e_id, name, author, time_created, time_modified, time_begin, time_end, location_id, initial_e_id, tags, attendees, is_periodic, wikipage = row + time_begin = datetime.fromtimestamp(time_begin, utc) + event = Event(env, e_id, name, author, + datetime.fromtimestamp(time_created, utc), + datetime.fromtimestamp(time_modified, utc), + time_begin, datetime.fromtimestamp(time_end, utc), location_id, initial_e_id, + tags, attendees, is_periodic, wikipage) + event.rrules = EventRRule.fetch_by_event_ical(env, int(e_id)) + event.alarms = [ + u""" +BEGIN:VALARM +DESCRIPTION:Gleich beginnt das Event '%s' +ACTION:DISPLAY +TRIGGER;VALUE=DURATION:-PT10M +END:VALARM +BEGIN:VALARM +DESCRIPTION:In 2 Tagen beginnt das Event '%s' +ACTION:DISPLAY +TRIGGER;VALUE=DURATION:-P2D +END:VALARM""" % (event.name, event.name)] + res.append(event) + return res + + @staticmethod + def get_recurrency_data(env, e_id): + return EventRRule.fetch_by_event_data(env, e_id).extend(EventRDate.fetch_by_event(env, e_id)) + + @staticmethod + def _data_fetch_by_period(env, start_dt, end_dt, is_periodic=False, locations=[]): + db = env.get_db_cnx() + cursor = db.cursor() + if is_periodic: + cursor.execute("SELECT * from events where is_periodic=1;") + return cursor.fetchall() + else: + query = "SELECT * from events where is_periodic='0' and (time_begin between %s and %s or time_end between %s and %s)" + if locations: + query += " and location_id in (%s)" % ",".join(map(sql_escape_percent, locations)) + query += ";" + s = to_timestamp(start_dt) + e = to_timestamp(end_dt) + cursor.execute(query, (s, e, s, e)) + return cursor.fetchall() + + @staticmethod + def fetch_by_period_dict(env, start_dt, end_dt, locations=[]): + """returns a dictionary with date as keys and lists of events as values + Some events might be periodic and the actual recurrency ruleset created and processed, + so this method is somewhat cpu intensive. To keep apart singular from recurring events check + Event.is_periodic + """ + rows = Event._data_fetch_by_period(env, start_dt, end_dt, locations=locations) + res = defaultdict(list) + if rows: + for row in rows: + e_id, name, author, time_created, time_modified, time_begin, time_end, location_id, initial_e_id, tags, attendees, is_periodic, wikipage = row + time_begin = datetime.fromtimestamp(time_begin, utc) + res[time_begin.date()].append( + Event(env, e_id, name, author, datetime.fromtimestamp(time_created, utc), + datetime.fromtimestamp(time_modified, utc), + time_begin, datetime.fromtimestamp(time_end, utc), location_id, initial_e_id, + tags, attendees, False, wikipage)) + rows = Event._data_fetch_by_period(env, start_dt, end_dt, is_periodic=True, locations=locations) + if not rows: + return res + for row in rows: + e_id, name, author, time_created, time_modified, time_begin, time_end, location_id, initial_e_id, tags, attendees, is_periodic, wikipage = row + time_begin = datetime.fromtimestamp(time_begin, utc) + time_end = datetime.fromtimestamp(time_end, utc) + delta = time_end - time_begin + rrules = EventRRule.fetch_by_event_rrules(env, e_id, time_begin) + if not rrules: + raise ValueError("missing rruleset") + try: + myrrule = EventRRule.fetch_by_event(env, e_id)[0] + except Exception, e: + myrrule = None + try: + excluding = EventRDate.fetch_by_event(env, e_id) + except Exception,e: + excluding = [] + for i in excluding: + rrules.exdate(i.erd_datetime.replace(hour=time_begin.hour, minute=time_begin.minute)) + followups = rrules.between(start_dt, end_dt, True) + for i in followups: + dt = datetime(i.year, i.month, i.day, time_begin.hour, time_begin.minute, tzinfo=utc) + s = Event(env, + e_id, + name, + author, + datetime.fromtimestamp(time_created, utc), + datetime.fromtimestamp(time_modified, utc), + dt, + dt + delta, + location_id, + e_id, + tags, + attendees, + False, + wikipage) + if rrule: + s.rrule = myrrule + res[i.date()].append(s) + return res + + @staticmethod + def fetch_by_period_list(env, start_dt, end_dt, locations=[]): + """If you need a list of events between datetime a and datetime b, use this method, which is less expensive than Event.fetch_by_period_dict. + """ + rows = Event._data_fetch_by_period(env, start_dt, end_dt, locations=locations) + res = list() + if rows: + for row in rows: + e_id, name, author, time_created, time_modified, time_begin, time_end, location_id, initial_e_id, tags, attendees, is_periodic, wikipage = row + time_begin = datetime.fromtimestamp(time_begin, utc) + res.append(Event(env, e_id, name, author, + datetime.fromtimestamp(time_created, utc), + datetime.fromtimestamp(time_modified, utc), + time_begin, datetime.fromtimestamp(time_end, utc), + location_id, initial_e_id, tags, attendees, False, wikipage)) + rows = Event._data_fetch_by_period(env, start_dt, end_dt, True, locations=locations) + if not rows: + return res + for row in rows: + e_id, name, author, time_created, time_modified, time_begin, time_end, location_id, initial_e_id, tags, attendees, is_periodic, wikipage = row + time_begin = datetime.fromtimestamp(time_begin, utc) + time_end = datetime.fromtimestamp(time_end, utc) + time_created = datetime.fromtimestamp(time_created, utc) + time_modified = datetime.fromtimestamp(time_modified, utc) + delta = time_end - time_begin + rrules = EventRRule.fetch_by_event_rrules(env, e_id, time_begin) + followups = rrules.between(start_dt, end_dt, inc=True) + try: + rrule = EventRRule.fetch_by_event(env, e_id)[0] + except Exception,e: + pass + for i in followups: + dt = datetime(i.year, i.month, i.day, time_begin.hour, time_begin.minute, tzinfo=utc) + s = Event(env, + e_id, + name, + author, + time_created, + time_modified, + dt, + dt + delta, + location_id, + e_id, + tags, + attendees, + False, + wikipage) + if rrule: + s.rrule = rrule + res.append(s) + return res + + def commit(self, conn=None): + db = conn and conn or self.env.get_db_cnx() + t = datetime.now(utc) + cursor = db.cursor() + try: + cursor.execute("INSERT INTO events " \ + "(name,author,time_created,time_modified,time_begin,time_end,location_id,initial_e_id,tags,attendees,is_periodic, wikipage) " \ + "VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s);", + (self.name, + self.author, + to_timestamp(self.time_created), + to_timestamp(self.time_modified), + to_timestamp(self.time_begin), + to_timestamp(self.time_end), + self.location_id, + self.initial_e_id, + self.tags, + self.attendees, + int(self.is_periodic), + self.wikipage)) + db.commit() + self.e_id = db.get_last_id(cursor, 'events') + except Exception, e: + db.rollback() + raise + + @staticmethod + def delete(env, e_id): + db = env.get_db_cnx() + cursor = db.cursor() + try: + cursor.execute("DELETE FROM events WHERE e_id = %s", (e_id,)) + db.commit() + except Exception, e: + db.rollback() + raise + + def update(self, conn=None): + db = conn and conn or self.env.get_db_cnx() + cursor = db.cursor() + try: + cursor.execute("UPDATE events " \ + "SET name =%s, " \ + "author=%s, " \ + "time_created=%s, " \ + "time_modified=%s, " \ + "time_begin=%s, " \ + "time_end=%s, " \ + "initial_e_id=%s, " \ + "location_id=%s, " \ + "tags=%s, " \ + "attendees=%s, " \ + "is_periodic=%s, " \ + "wikipage=%s " \ + "WHERE e_id=%s", (self.name, self.author, to_timestamp(self.time_created), to_timestamp(self.time_modified), + to_timestamp(self.time_begin), to_timestamp(self.time_end), + self.initial_e_id, self.location_id, self.tags, self.attendees, int(self.is_periodic), self.wikipage, self.e_id)) + db.commit() + except Exception, e: + db.rollback() + raise + + def __str__(self): + return "" % (self.e_id, self.name, self.time_begin, self.time_end) + + +class EventModelProvider(Component): + implements(IEnvironmentSetupParticipant) + + SCHEMA = [ + Table('events', key='e_id')[ + Column('e_id', auto_increment=True), + Column('name'), + Column('author'), + Column('time_created', type='int'), + Column('time_modified', type='int'), + Column('time_begin', type='int'), + Column('time_end', type='int'), + Column('location_id', type='int'), + Column('initial_e_id', type='int'), + Column('tags'), + Column('attendees'), + Column('is_periodic', type="int"), + Column('wikipage'), + Index(['name'])], + + Table('event_wikipages', key='ewp_id')[ + Column('ewp_id', auto_increment=True), + Column('e_id'), + Column('wikipage')], + + Table('event_rrules', key='err_id')[ + Column('err_id', auto_increment=True), + Column('e_id', type="int"), + Column('exclude', type="int"), + Column('freq', type="int"), + Column('interval', type="int"), + Column('count', type="int"), + Column('until', type="int"), + Column('bysetpos', type="int"), + Column('bymonth', type="int"), + Column('bymonthday', type="int"), + Column('byyearday', type="int"), + Column('byweeknumber'), + Column('byweekday'), + Column('byweekdayocc')], + + Table('event_rdates')[ + Column('erd_id', auto_increment=True), + Column('e_id', type="int"), + Column('erd_exclude', type="int"), + Column('erd_datetime', type="int")]] + + #TERMINE_DATA = ( + #(u"Offizielles Treffen", + #to_timestamp(datetime.now(utc)), + #to_timestamp(datetime.now(utc)), + #to_timestamp(datetime(2009,5,7,17,0, tzinfo=utc)), + #to_timestamp(datetime(2009,5,7,20,0, tzinfo=utc)), + #1, + #None, + #"foo bar", + #"heinz horst elke peter", + #True), + #(u"Topic Treffen", + #to_timestamp(datetime.now(utc)), + #to_timestamp(datetime.now(utc)), + #to_timestamp(datetime(2009,5,8,17,0, tzinfo=utc)), + #to_timestamp(datetime(2009,5,8,20,0, tzinfo=utc)), + #1, + #None, + #"foo bar", + #"heinz horst elke peter", + #True), + #(u"Zombies Beamer Action", + #to_timestamp(datetime.now(utc)), + #to_timestamp(datetime.now(utc)), + #to_timestamp(datetime(2009,7,2,17,0, tzinfo=utc)), + #to_timestamp(datetime(2009,7,2,23,0, tzinfo=utc)), + #1, + #None, + #"gamez beamer", + #"syn knuddel kalle lucifer schnarchnase", + #False)) + + TERMINE_PERIOD_DATA = ( + (1,0,3,15,30,None), + (2,0,3,30,15,None)) + + def environment_created(self): + + self._create_models(self.env.get_db_cnx()) + + def environment_needs_upgrade(self, db): + """First version - nothing to migrate, but possibly to create. + """ + + cursor = db.cursor() + try: + cursor.execute("select count(*) from events") + cursor.fetchone() + cursor.execute("select count(*) from event_rrules") + cursor.fetchone() + cursor.execute("select count(*) from event_rdates") + cursor.fetchone() + cursor.execute("select count(*) from event_wikipages") + cursor.fetchone() + return False + except: + db.rollback() + return True + + def upgrade_environment(self, db): + """ nothing to do here for now + """ + self._create_models(db) + + def _create_models(self, db): + + """Called when a new Trac environment is created.""" + + db_backend = None + try: + from trac.db import DatabaseManager + db_backend, _ = DatabaseManager(self.env)._get_connector() + except ImportError: + db_backend = self.env.get_db_cnx() + try: + cursor = db.cursor() + for table in self.SCHEMA: + try: + for stmt in db_backend.to_sql(table): + self.env.log.debug(stmt) + cursor.execute(stmt) + except Exception, e: + self.env.log.exception(e) + db.commit() + #cursor.executemany("""INSERT INTO 'events' + # (name, time_created, time_modified, time_begin, time_end, location_id, initial_e_id, tags, attendees, is_periodic) + # VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""", self.TERMINE_DATA) + #cursor.executemany("""INSERT INTO 'event_rrules' + # (e_id,exclude,freq,interval,count,until) + # VALUES(%s,%s,%s,%s,%s,%s)""", self.TERMINE_PERIOD_DATA) + #db.commit() + #gen_wiki_page(self.env, "hotshelf", "events/1", u"= Offizielles Treffen = \nDiese Page kann mit Ideen und Inhalten des Events/Treffs gefüllt werden", "localhost") + #gen_wiki_page(self.env, "hotshelf", "events/2", u"= Topic Treffen = \nDiese Page kann mit Ideen und Inhalten des Events/Treffs gefüllt werden", "localhost") + except Exception, e: + db.rollback() + raise diff --git a/TracRendezVous/tracrendezvous/event/templates/alarm_edit.html b/TracRendezVous/tracrendezvous/event/templates/alarm_edit.html new file mode 100644 index 0000000..0e63e2b --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/templates/alarm_edit.html @@ -0,0 +1,66 @@ + + + + + + + ${event.e_id and 'Edit Event' or 'Add Event'} + + +
    +
    + +
    + ${event.e_id and 'Edit Event' or 'Add Event'} + + + + + + + + + + + +
    + ${mydt.tzinfo.tzname(None)}
    + ${dt2.tzinfo.tzname(None)}
    + edit/search locations
    show recurrency options +
    +
    +
    + Tags +
    + +
    +
    +
    + + + + + + + + +
    +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/event/templates/event_details.html b/TracRendezVous/tracrendezvous/event/templates/event_details.html new file mode 100644 index 0000000..81459a9 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/templates/event_details.html @@ -0,0 +1,39 @@ + + + + + + ${title} + + +
    + + +

    ${title}

    + + + + + + + + + + +
    ${h}
     
    ${selected_tz.fromutc(row[0]).strftime(format)} + + + ${render_event(event, True)} +
    +
    + +

    ${_("No events in this time frame. Create here")} ${_(" a new event")} !

    +
    +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/event/templates/event_display.html b/TracRendezVous/tracrendezvous/event/templates/event_display.html new file mode 100644 index 0000000..433965b --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/templates/event_display.html @@ -0,0 +1,30 @@ + + +

    ${event.name}edit

    + + + + + + + + + + + + + + + + + + + + + + +
    ${with_day and dt.strftime('%Y.%m.%d')} ${dt.strftime('%H:%M')} - ${with_day and dt2.strftime('%Y.%m.%d')} ${dt2.strftime('%H:%M')}
    ${event.rrule and event.rrule.explain() or None}
    ${event.tags and event.tags or None}
    ${event.location.name}
    ${event.location.coordinate_str()}
    wiki page
    +
    + \ No newline at end of file diff --git a/TracRendezVous/tracrendezvous/event/templates/event_edit.html b/TracRendezVous/tracrendezvous/event/templates/event_edit.html new file mode 100644 index 0000000..00bb04b --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/templates/event_edit.html @@ -0,0 +1,66 @@ + + + + + + + ${event.e_id and 'Edit Event' or 'Add Event'} + + +
    +
    + +
    + ${event.e_id and 'Edit Event' or 'Add Event'} + + + + + + + + + + + +
    + ${mydt.tzinfo.tzname(None)}
    + ${dt2.tzinfo.tzname(None)}
    + edit/search locations
    show recurrency options +
    +
    +
    + Tags +
    + +
    +
    +
    + + + + + + + + +
    +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/event/templates/event_list.html b/TracRendezVous/tracrendezvous/event/templates/event_list.html new file mode 100644 index 0000000..2dbbecf --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/templates/event_list.html @@ -0,0 +1,28 @@ + + + + + + ${title} + + +
    +
    +

    ${title}

    +
    +
    C
    I
    R
    C
    U
    L
    A
    R
    +
    S
    I
    N
    G
    U
    L
    A
    R
    +
    +
    + ${render_event(event, True)} +
    +
    +
    +
    + + \ No newline at end of file diff --git a/TracRendezVous/tracrendezvous/event/templates/events.html b/TracRendezVous/tracrendezvous/event/templates/events.html new file mode 100644 index 0000000..8f5f51f --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/templates/events.html @@ -0,0 +1,45 @@ + + + + + + ${title} + + +
    + + +

    ${title}

    +

    ${title2}

    + + + + + + + + + + + +
    ${h}
     
    ${row[0].strftime(format)} + + + ${render_event(event)} + + +
    +
    + +

    ${_("No events in this time frame. Create here")} ${_(" a new event")} !

    +
    +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/event/templates/ical.txt b/TracRendezVous/tracrendezvous/event/templates/ical.txt new file mode 100644 index 0000000..7ad35d7 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/templates/ical.txt @@ -0,0 +1,33 @@ +BEGIN:VCALENDAR +PRODID://TracRendezVous-0.3 by Stefan Kögl //DE +VERSION:2.0 +CALSCALE:GREGORIAN +CAL-ADDRESS:${abs_href.events("upcoming")} +METHOD:PUBLISH +X-WR-CALNAME:${calname} +X-WR-TIMEZONE:UTC +X-WR-CALDESC:${caldesc} +{% for event in events %}\ +BEGIN:VEVENT +UID:${"%s-%d@%s" % (event.time_begin.strftime("%Y%m%dT%H%M%SZ"), event.e_id, abs_href())} +CREATED:${event.time_created.strftime("%Y%m%dT%H%M%SZ")} +DTSTAMP:${stamp} +LAST-MODIFIED:${event.time_modified.strftime("%Y%m%dT%H%M%SZ")} +SUMMARY:${event.name.replace(",", "\,")} +LOCATION:${event.location.name.replace(",", "\,")} +GEO:${event.location.lat};${event.location.lon} +URI:${abs_href.events(event.e_id)} +DESCRIPTION:more information:${abs_href.events(event.e_id)} +CLASS:PUBLIC +DTSTART:${event.time_begin.strftime("%Y%m%dT%H%M%SZ")} +DTEND:${event.time_end.strftime("%Y%m%dT%H%M%SZ")} +{% for rrule in event.rrules %}\ +${rrule} +{% end %}\ +{% for alarm in event.alarms %}\ +${alarm} +{% end %}\ +TRANSP:OPAQUE +END:VEVENT +{% end %}\ +END:VCALENDAR diff --git a/TracRendezVous/tracrendezvous/event/templates/recur_edit.html b/TracRendezVous/tracrendezvous/event/templates/recur_edit.html new file mode 100644 index 0000000..f227c7e --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/templates/recur_edit.html @@ -0,0 +1,141 @@ + + + + + + + ${'Edit Recurrency'} + + +
    +

    Recurrency Options for event '${event.name}'

    +

    ${dt.strftime("%A, %d.%m.%Y %H:%M")} ${dt.tzinfo.tzname(None)} - ${dt2.strftime("%A, %d.%m.%Y %H:%M")} ${dt2.tzinfo.tzname(None)}

    +
    +
    +
    + +
    +
    Recurrency frequency +
    +
    +

    Yearly

    + + + + + + + +
    Repeat every year(s)
    + + +
    + +  in +
    +
    +
    +

    Monthly

    +
    + + + + +
    + +
    +  day of the month
    + +
    +
    +
    +
    +

    Weekly

    +
    + + + + +
    Repeat every week(s)
    +
    +
    +
    +
    +

    Daily

    +
    + + +
    Repeat every day(s)
    +
    +
    +
    +
    +
    Recurrency Timeframe + + + + +
    Repetitions
    +
    +
    Recurrency Exceptions + + + + + + +
      +
    •  ${exception.erd_datetime.strftime('%d.%m.%Y')}
    +
    +
    +
    + +
    +
    +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/event/templates/upcoming_events.rss b/TracRendezVous/tracrendezvous/event/templates/upcoming_events.rss new file mode 100644 index 0000000..fe27a74 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/templates/upcoming_events.rss @@ -0,0 +1,35 @@ + + + + ${project.name}: #${title} + ${abs_href.event("upcoming")} + Die kommenden Termine im und Rund um den 'CTDO' + de-de + + $project.name + ${abs_href.chrome(chrome.logo.src)} + ${abs_href.chrome(chrome.logo.src)} + + TracRendezvous + + + ${http_date(today)} + $event.name + ${abs_href.events(event.srv_id)} + ${abs_href.events(event.srv_id)} + + <ul> + <li><strong>When:</strong> <em>${event.time_begin.strftime("%d.%m.%Y %Z")} - ${event.time_end.strftime("%d.%m.%Y %Z")}</em></li> + <li><strong>Location:</strong> <em>$event.location.name</em></li> + <li><strong>Type:</strong> <em>$event.type_name</em></li> + <li><strong>Tags:</strong> <em>$event.tags</em></li> + <li><strong>Attendees:</strong> <em>$event.attendees</em></li> + </ul> + + Upcoming Events + + + + diff --git a/TracRendezVous/tracrendezvous/event/tests/__init__.py b/TracRendezVous/tracrendezvous/event/tests/__init__.py new file mode 100644 index 0000000..f12a4c3 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/tests/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +import doctest +import unittest + +from tracrendezvous.event.tests import model + +def suite(): + suite = unittest.TestSuite() + suite.addTest(model.suite()) + return suite + +if __name__ == '__main__': + import sys + if '--skip-functional-tests' in sys.argv: + sys.argv.remove('--skip-functional-tests') + INCLUDE_FUNCTIONAL_TESTS = False + unittest.main(defaultTest='suite') diff --git a/TracRendezVous/tracrendezvous/event/tests/model.py b/TracRendezVous/tracrendezvous/event/tests/model.py new file mode 100644 index 0000000..c1a8555 --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/tests/model.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +import unittest +from datetime import datetime, timedelta +from trac.util.datefmt import utc, to_timestamp +from trac.test import EnvironmentStub, Mock +from trac.search.api import * +from tracrendezvous.event.model import Event, EventModelProvider +from tracrendezvous.location.model import ItemLocation, LocationModelProvider + +class EventTestCase(unittest.TestCase): + def setUp(self): + self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'tracrendezvous.*']) + l = EventModelProvider(self.env).environment_created() + l2 = LocationModelProvider(self.env).environment_created() + loc1 = ItemLocation(self.env, 0, u"CTDO, Langer August", "N", 51, 31, 39.4, "E", 7, 27, 53.8, 51.527611, 7.464922) + loc1.commit() + self.created = datetime.now(utc) + self.created = self.created.replace(microsecond=0) + self.time_begin = datetime(2009, 3, 1, 17, 30, tzinfo=utc) + self.time_end = datetime(2009, 3, 1, 22, 30, tzinfo=utc) + loc1 = Event(self.env, 0, "name", self.created, self.created, self.time_begin, self.time_end, 1, initial_e_id=0, tags="a b c", attendees="a b c", is_periodic=True, wikipage="foobar") + loc1.commit() + + def test_1_commit(self): + self.env.get_db_cnx().cursor().execute("select name from events;").fetchall() + + def test_2_fetch_one(self): + event = Event.fetch_one(self.env, 1) + self.assertEqual(1, event.e_id) + self.assertEqual(u"name", event.name) + self.assertEqual(self.created, event.time_created) + self.assertEqual(self.created, event.time_modified) + self.assertEqual(self.time_begin, event.time_begin) + self.assertEqual(self.time_begin, event.time_begin) + self.assertEqual(self.time_end, event.time_end) + self.assertEqual(1, event.location_id) + self.assertEqual(0, event.initial_e_id) + self.assertEqual("a b c", event.tags) + self.assertEqual("a b c", event.attendees) + self.assertEqual(True, event.is_periodic) + self.assertEqual("foobar", event.wikipage) + + def test_3_exists(self): + self.assertEqual(True, Event.exists(self.env, 1, self.time_begin, self.time_end)) + self.assertEqual(False, Event.exists(self.env, 1, datetime(2009,4,1,17,20,tzinfo=utc), datetime(2009,3,1,17,20,tzinfo=utc))) + self.assertEqual(False, Event.exists(self.env, 1, datetime(2009,3,1,17,20,tzinfo=utc), datetime(2009,4,1,17,20,tzinfo=utc))) + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(EventTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/TracRendezVous/tracrendezvous/event/web_ui.py b/TracRendezVous/tracrendezvous/event/web_ui.py new file mode 100644 index 0000000..be918de --- /dev/null +++ b/TracRendezVous/tracrendezvous/event/web_ui.py @@ -0,0 +1,999 @@ +# -*- coding: utf-8 -*- + +import re, math + +from datetime import datetime, timedelta, time +from os import mkdir +from os.path import join +from re import match, sub +from collections import defaultdict +from sqlite3 import IntegrityError +from operator import attrgetter + +from trac.config import * +from trac.mimeview.api import Mimeview, IContentConverter, Context +from trac.core import Component, implements, TracError +from trac.resource import IResourceManager, Resource, get_resource_url +from trac.perm import PermissionError, IPermissionRequestor +from trac.search import ISearchSource, search_to_sql, shorten_result +from trac.util import get_reporter_id, Ranges +from trac.util.text import to_unicode +from trac.util.html import html +from trac.util.datefmt import get_timezone, utc, format_time, localtz +#from trac.util.translation import _, add_domain +from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet, add_warning, add_notice, add_ctxtnav, add_script, add_link, Chrome +from trac.web import IRequestHandler +from trac.wiki import IWikiSyntaxProvider + +from dateutil import rrule +from genshi.builder import tag +from genshi.template import TemplateLoader, NewTextTemplate +from tractags.api import TagSystem, ITagProvider +from tracrendezvous.location.model import ItemLocation +from tracrendezvous.event.model import Event, EventRRule, EventRDate +#from tracrendezvous.event.parse_ical import parse_ical +from ctdotools.utils import gen_wiki_page, validate_id, time_parse, datetime_parse, date_parse, get_tz, local_to_utc + +from trac.util.translation import domain_functions + +add_domain, _, tag_ = domain_functions('tracrendezvous', ('add_domain', '_', 'tag_')) + +import tracrendezvous + +__all__ = ['EventModule','EventTagProvider'] + +class EventTagProvider(Component): + """A tag provider using Events tag field as sources of tags. + """ + implements(ITagProvider) + + # ITagProvider methods + def get_taggable_realm(self): + return 'event' + + def get_tagged_resources(self, req, tags): + split_into_tags = TagSystem(self.env).split_into_tags + db = self.env.get_db_cnx() + cursor = db.cursor() + args = [] + sql = "SELECT * FROM (SELECT e_id, tags, COALESCE(tags, '') as fields FROM events)" + constraints = [] + if tags: + constraints.append( + "(" + ' OR '.join(["fields LIKE %s" for t in tags]) + ")") + args += ['%' + t + '%' for t in tags] + else: + constraints.append("fields != ''") + + if constraints: + sql += " WHERE %s" % " AND ".join(constraints) + sql += " ORDER BY e_id" + self.env.log.debug(sql) + cursor.execute(sql, args) + for row in cursor: + id, ttags = row[0], ' '.join([f for f in row[1:-1] if f]) + event_tags = split_into_tags(ttags) + tags = set([to_unicode(x) for x in tags]) + if (not tags or event_tags.intersection(tags)): + yield Resource('event', id), event_tags + + def get_resource_tags(self, req, resource): + if 'EVENTS_VIEW' not in req.perm(resource): + return + event = Event.fetch_one(self.env, resource.id) + tags = self.__event_tags(event) + return tags + + def set_resource_tags(self, req, resource, tags): + req.perm.require('EVENTS_MODIFY', resource) + split_into_tags = TagSystem(self.env).split_into_tags + event = Event.fetch_one(self.env, resource.id) + all_ = self.__event_tags(event) + tags.difference_update(all_.difference(event.tags)) + event.tags = u' '.join(sorted(map(to_unicode, tags))) + event.update() + + def remove_resource_tags(self, req, resource): + req.perm.require('EVENTS_MODIFY', resource) + event = Event.fetch_one(self.env, resource.id) + event.tags = None + event.update() + + # Private methods + def __event_tags(self, rendezvous): + return TagSystem(self.env).split_into_tags(rendezvous.tags) + + +class EventModule(Component): + + '''The web ui frontend for the event management system''' + + Option("event", "upcoming_name", _(u"Upcoming Events")) + Option("event", "upcoming_desc", _(u"Hier erfährst Du direkt, was in den nächsten 6 Monaten im Chaostreff los ist.")) + Option("event", "wiki_content", _(u"Feel free to fill this outer space with content descibing the actual event.[[BR]]If the event is recurrent, describe the overall topics or similarities here and put the date dependant stuff into the wikipages you find via the links on the right side.")) + implements(IRequestHandler, + IResourceManager, + IWikiSyntaxProvider, + IContentConverter, + IPermissionRequestor, + INavigationContributor, + ISearchSource, + ITemplateProvider) + m1 = re.compile(r'^/event/(\d+)$') + m2 = re.compile(r'^/event/edit/(\d+)$') + m3 = re.compile(r'^/event/recurrency/(\d+)$') + m4 = re.compile(r'^/event/by-day/(\d{4})-(\d{1,2})-(\d{1,2})$') + m5 = re.compile(r'^/event/new/(\d{4})-(\d{1,2})-(\d{1,2})$') + m6 = re.compile(r'^/event/createpage/(\d+)/(\d{4})-(\d{1,2})-(\d{1,2})$') + + def __init__(self, parent=None): + from pkg_resources import resource_filename + locale_dir = resource_filename(tracrendezvous.__name__, 'locale') + add_domain(self.env.path, locale_dir) + + # INavigationContributor methods + def get_active_navigation_item(self, req): + return 'event' + + def get_navigation_items(self, req): + if "EVENTS_VIEW" in req.perm: + yield ('mainnav', 'event', html.A(_('Upcoming Events'), href=req.href.event("upcoming"))) + + def get_permission_actions(self): + '''returns all permissions this component provides''' + return ['EVENTS_ADD', 'EVENTS_DELETE', 'EVENTS_MODIFY', 'EVENTS_VIEW', + ('EVENTS_ADMIN', + ('EVENTS_ADD', 'EVENTS_DELETE', 'EVENTS_MODIFY', 'EVENTS_VIEW'))] + + def match_request(self, req): + + self.env.log.debug("EventModule.match_request %s\n" % req) + + key = req.path_info + simple_paths = ("/event/upcoming", '/event', "/event/new") + if key in simple_paths: + return True + m = self.m1.match(key) + if m: + req.args["e_id"] = int(m.group(1)) + return True + m = self.m2.match(key) + if m: + req.args["e_id"] = int(m.group(1)) + return True + m = self.m3.match(key) + if m: + req.args["e_id"] = int(m.group(1)) + return True + m = self.m4.match(key) + if m: + req.args['arg_date'] = datetime(int(m.group(1)),int(m.group(2)),int(m.group(3)), 17, tzinfo=utc) + return True + m = self.m5.match(key) + if m: + req.args['arg_date'] = datetime(int(m.group(1)),int(m.group(2)),int(m.group(3)), 17, tzinfo=utc) + return True + m = self.m6.match(key) + if m: + req.args["e_id"] = int(m.group(1)) + req.args['arg_date'] = datetime(int(m.group(2)),int(m.group(3)),int(m.group(4)), tzinfo=utc) + return True + return False + + # ITemplateProvider methods + def get_templates_dirs(self): + from pkg_resources import resource_filename + return [resource_filename(__name__, 'templates')] + + def get_htdocs_dirs(self): + """Return a list of directories with static resources (such as style + sheets, images, etc.) + + Each item in the list must be a `(prefix, abspath)` tuple. The + `prefix` part defines the path in the URL that requests to these + resources are prefixed with. + + The `abspath` is the absolute path to the directory containing the + resources on the local file system. + """ + from pkg_resources import resource_filename + return [('hw', resource_filename(__name__, 'htdocs')), + ('parent', resource_filename(tracrendezvous.__name__, 'htdocs'))] + + def process_request(self, req): + + query = req.path_info + if "/event/upcoming" == query: + return self.__display_upcoming_events(req) + elif "/event/by-day" in query: + return self.__display_events_by_day(req) + elif "/event/new" in query: + return self.__process_event(req) + elif "/event/recurrency" in query: + return self.__process_recurrency(req) + elif "/event/edit" in query: + return self.__process_event(req) + elif "/event/createpage" in query: + return self.__create_wiki_page(req) + elif "/event" in query: + return self.__display_events(req) + else: + return self.__display_upcoming_events(req) + + # IContentConverter methods + def get_supported_conversions(self): + #yield ('rss', _('RSS Feed'), 'xml', + #'tracrendezvous.Event', 'application/rss+xml', 8) + yield ('ical', _('Ical Feed'), 'ics', + 'tracrendezvous.Event', 'text/calendar', 8) + + def convert_content(self, req, mimetype, cal, key): + #if key == 'rss': + #return self._export_upcoming_events_rss(req, ticket) + if key == 'ical': + return self.__export_upcoming_events_ical(req, cal) + + # IResourceManager methods + + def get_resource_realms(self): + yield 'event' + + def get_resource_description(self, resource, format=None, context=None, + **kwargs): + if format == 'compact': + return 'Event #%s' % resource.id + from tracrendezvous.event.model import Event + event = Event.fetch_one(self.env, resource.id) + # TODO: really UTC? + return "Event #%d - %s (%s UTC - %s UTC)" % (event.e_id, event.name, event.time_begin, event.time_end) + + # ISearchSource methods + + def get_search_filters(self, req): + if 'EVENTS_VIEW' in req.perm: + yield ('event', _('Events')) + + def get_search_results(self, req, terms, filters): + if not 'event' in filters: + return + db = self.env.get_db_cnx() + sql_query, args = search_to_sql(db, ['e1.name','e1.author'], + terms) + cursor = db.cursor() + cursor.execute("SELECT e1.e_id, e1.name, e1.author, e1.time_modified " + "FROM events e1 " + "WHERE " + sql_query, args) + + event_realm = Resource('event') + for e_id, name, author, ts in cursor: + event = event_realm(id=e_id) + yield (get_resource_url(self.env, event, req.href), + name, + datetime.fromtimestamp(ts, utc), author, + _("Click the link")) + + # IWikiSyntaxProvider methods + def get_wiki_syntax(self): + return [] + + def get_link_resolvers(self): + yield ('event', self._format_event_link) + + # private methods + def __process_recurrency(self, req): + req.perm.require("EVENTS_MODIFY") + e_id = req.args["e_id"] + add_ctxtnav(req, _('Back to') + _('Overview'), req.href.event()) + add_ctxtnav(req, _('Back to') + _('Event') + ' #%s' % e_id, req.href.event('edit', e_id)) + add_stylesheet (req, 'hw/css/ui.all.css') + add_stylesheet (req, 'hw/css/event.css') + add_script (req, 'hw/scripts/jquery-ui-1.6.custom.min.js') + if req.session.has_key("edited"): + #add_notice(req, tag.p(_("Recurrency rule created successfully. Back to "), tag.a(_("Event"), href=req.href.event(event.e_id)))) + del req.session["edited"] + req.session.save() + if req.session.has_key("added"): + #add_notice(req, tag.p(_("Recurrency rule created successfully. Back to "), tag.a(_("Event"), href=req.href.event(event.e_id)))) + del req.session["added"] + req.session.save() + if req.session.has_key("event_added"): + add_notice(req, tag.p(_("Event created successfully.") + _("Back to "), tag.a(_("Event"), href=req.href.event(event.e_id)))) + del req.session["event_added"] + req.session.save() + if req.session.has_key("deleted"): + #add_notice(req, tag.p(_("Recurrency rule deleted successfully. Back to "), tag.a(_("Event"), href=req.href.event(event.e_id)))) + del req.session["deleted"] + req.session.save() + + session_tzname, selected_tz = get_tz(req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + + event = Event.fetch_one(self.env, e_id) + if not event: + raise TracError(_("Event not found")) + try: + period = EventRRule.fetch_by_event(self.env, e_id)[0] + exists = True + except IndexError: + period = EventRRule(self.env, e_id=event.e_id) + exists = False + freq = 0 + if req.method == "POST": + if req.args.has_key("save"): + is_valid = True + is_periodic = req.args.has_key("is_periodic") + if event.is_periodic and not is_periodic: + EventRRule.delete(self.env, event.e_id) + EventRDate.delete(self.env, event.e_id) + event.is_periodic = False + event.update() + req.redirect(req.href.event("recurrency", event.e_id)) + elif not event.is_periodic and is_periodic: + event.is_periodic = True + event.update() + def get_bymonth(req, name, period): + try: + period.bymonth = int(req.args[name]) + if not (0 <= period.bymonth < 12): + raise Exception() + except Exception: + add_warning(req, _("Wrong Value for 'month'.")) + raise + def get_bymonthday(req, name, period): + try: + period.bymonthday = int(req.args[name]) + if not (-33 <= period.bymonthday < 32): + raise Exception() + except Exception, e: + add_warning(req, _("Wrong Value for 'monthday'.")) + raise + def get_byweekday(req, name, period): + try: + period.byweekday = set((int(req.args[name]),)) + if filter(lambda x: not (0 <= x < 7), period.byweekday): + raise Exception() + except Exception, e: + add_warning(req, _("Wrong Value for 'weekday'.")) + raise + def get_byweekdayocc(req, name, period): + byweekdayocc = req.args[name] + if byweekdayocc: + try: + period.byweekdayocc = int(byweekdayocc) + except Exception, e: + add_warning(req, _("Wrong Value for 'weekday occurence'.")) + raise + else: + period.byweekdayocc = None + def get_interval(req, name, period): + try: + period.interval = int(req.args[name]) + if not (0 <= period.interval < 1000): + raise Exception() + except Exception, e: + add_warning(req, _("Wrong Value for interval")) + raise + + # std parameters + count = req.args["count"] + if count: + try: + period.count = int(count) + except Exception, e: + is_valid = False + add_warning(req, _("Wrong Value for count")) + else: + period.count = None + until_date = req.args["until_date"] + if until_date: + try: + until = date_parse(until_date) + period.until = datetime.combine(until, time(12,tzinfo=utc)) + except Exception, e: + add_warning(req, _("Wrong Value for until_date or until_time")) + raise + else: + period.until = None + + # selective parameters + try: + freq = int(req.args["freq"]) + except KeyError: + add_warning(req, _("Please select one of the recurrency types")) + is_valid = False + if freq == 0: + period.freq = rrule.YEARLY + period.byweekday = list() + period.byweekdayocc = None + period.byyearday = None + try: + get_interval(req, "yearinterval", period) + get_bymonthday(req, "monthday-yearly-0", period) + get_bymonth(req, "monthname-yearly-0", period) + except Exception: + is_valid = False + elif freq == 1: + period.freq = rrule.YEARLY + get_interval(req, "yearinterval", period) + period.bymonthday = None + period.byyearday = None + try: + get_byweekdayocc(req, "dayocc-yearly-1", period) + get_byweekday(req, "weekday-yearly-1", period) + get_bymonth(req, "monthname-yearly-1", period) + except Exception: + is_valid = False + elif freq == 2: + period.freq = rrule.YEARLY + try: + get_interval(req, "yearinterval", period) + except Exception: + is_valid = False + period.bymonthday = None + period.bymonth = None + period.byweekday = list() + period.byweekdayocc = None + try: + period.byyearday = int(req.args["yearday"]) + if not (0 <= period.byyearday < 366): + raise Exception() + except Exception, e: + add_warning(req, _("Wrong Value for 'weekday'.")) + is_valid = False + elif freq == 3: + period.freq = rrule.MONTHLY + try: + get_interval(req, "monthinterval", period) + get_bymonthday(req, "monthday-monthly-0", period) + except Exception: + is_valid = False + period.byweekday = None + period.byweekdayocc = None + period.byyearday = None + period.bymonth = None + elif freq == 4: + period.freq = rrule.MONTHLY + period.byyearday = None + period.bymonth = None + period.bymonthday = None + try: + get_interval(req, "monthinterval", period) + get_byweekdayocc(req, "dayocc-monthly-1", period) + get_byweekday(req, "weekday-monthly-1", period) + except Exception: + is_valid = False + elif freq == 5: + period.freq = rrule.WEEKLY + try: + get_interval(req, "weekinterval", period) + except Exception: + is_valid = False + period.byweekday = list() + period.byweekdayocc = None + period.byyearday = None + period.bymonth = None + period.bymonthday = None + try: + weekdays = set(map(int, req.args["weekday-weekly"])) + if any(map(lambda x: not (0 <= x < 7), weekdays)): + is_valid = False + if weekdays != period.byweekday: + period.byweekday = weekdays + except KeyError: + add_warning(req, _("Any days selected.")) + is_valid = False + elif freq == 6: + period.freq = rrule.DAILY + try: + get_interval(req, "dayinterval", period) + except Exception: + is_valid = False + period.byweekday = None + period.byweekdayocc = None + period.byyearday = None + period.bymonth = None + period.bymonthday = None + if is_valid: + if exists: + period.update() + else: + period.commit() + req.redirect(req.href.event("recurrency", e_id)) + elif req.args.has_key("exception-add"): + try: + exception = datetime_parse("%s 12:00" % req.args["exception-name"], selected_tz) + exception = exception.replace(hour=event.time_begin.hour, minute=event.time_begin.minute) + except ValueError: + add_warning(req, _("Wrong recurrency exception date format")) + req.redirect(req.href.event("recurrency", e_id)) + rdate = EventRDate(self.env, 0, event.e_id, True, exception) + rdate.commit() + req.session["added"] = True + req.redirect(req.href.event("recurrency", e_id)) + elif req.args.has_key("exception-edit"): + for k in req.args.iterkeys(): + try: + marker, erd_id = k.split(":", 1) + except Exception,e: + continue + try: + erd_id = int(erd_id) + validate_id(erd_id) + except ValueError,e: + continue + if marker != "exception": + continue + rdate = EventRDate.fetch_one(self.env, erd_id) + try: + exception = datetime_parse("%s 12:00" % req.args["exception-name"], selected_tz) + exception = exception.replace(hour=event.time_begin.hour, minute=event.time_begin.minute) + except KeyError: + add_warning(req, _("You have to specify a valid recurrency exception date for that operation!")) + req.redirect(req.href.event("recurrency", e_id)) + if exception: + rdate.erd_datetime = exception + rdate.update() + req.session["edited"] = True + req.redirect(req.href.event("recurrency", e_id)) + req.redirect(req.href.event("recurrency", e_id)) + elif req.args.has_key("exception-delete"): + for k in req.args.iterkeys(): + try: + marker, erd_id = k.split(":", 1) + except Exception,e: + continue + try: + erd_id = int(erd_id) + validate_id(erd_id) + except ValueError,e: + continue + if marker != "exception": + continue + EventRDate.delete(self.env, erd_id) + req.session["deleted"] = True + req.redirect(req.href.event("recurrency", e_id)) + else: + if period.bymonthday!=None and period.bymonth!=None: + freq = 0 + elif period.byweekday and period.bymonth!=None: + freq = 1 + elif period.byyearday!=None: + freq = 2 + elif period.bymonthday!=None: + freq = 3 + elif period.byweekdayocc!=None and period.byweekday: + freq = 4 + elif period.freq == rrule.WEEKLY: + freq = 5 + elif period.freq == rrule.DAILY: + freq = 6 + exceptions = EventRDate.fetch_by_event(self.env, event.e_id) + return "recur_edit.html", { + "event" : event, + "freq" : freq, + "period" : period, + "session_tzname" : session_tzname, + "selected_tz" : selected_tz, + "exceptions" : exceptions, + "default_location" : self.config.getint("location", "default_location"), + "locations" : ItemLocation.fetch_all(self.env), + "month_names" : EventRRule.month_names, + "day_names" : EventRRule.day_names, + "weekday_names" : EventRRule.weekday_names, + "monthday_names" : EventRRule.monthday_names}, None + + def __process_event(self, req): + '''process add, change and delete rendezvous''' + if 'EVENTS_ADD' not in req.perm and 'EVENTS_MODIFY' not in req.perm and 'EVENTS_DELETE' not in req.perm: + raise PermissionError() + add_stylesheet (req, 'hw/css/event.css') + add_stylesheet (req, 'hw/css/ui.all.css') + add_script (req, 'hw/scripts/jquery-ui-1.6.custom.min.js') + # set information after redirects + if req.session.has_key("edited"): + add_notice(req, tag.p(_("Event edited successfully. Back to "), tag.a(_("Overview"), href=req.href.event()))) + del req.session["edited"] + req.session.save() + if req.session.has_key("added"): + add_notice(req, tag.p(_("Event created successfully. Back to "), tag.a(_("Overview"), href=req.href.event()))) + del req.session["added"] + req.session.save() + if req.session.has_key("deleted"): + add_notice(req, tag.p(_("Event deleted successfully. Back to "), tag.a(_("Overview"), href=req.href.event()))) + del req.session["deleted"] + req.session.save() + myenv = self.env + date_now = datetime.now(utc) + session_tzname, selected_tz = get_tz(req.session.get('tz', self.config.get("trac", "default_timezone") or None)) + + if req.args.has_key("e_id"): + event = Event.fetch_one(myenv, req.args["e_id"]) + if not event: + raise TracError(_("Event not found")) + add_ctxtnav(req, _('Events Overview'), req.href.event()) + add_ctxtnav(req, _('Upcoming Events'), req.href.event("upcoming")) + add_ctxtnav(req, _('Add New Event'), req.href.event("new")) + else: + try: + pi = req.path_info + crap, new_date = pi.split("new/", 1) + year, month, day = re.match("(\d{4})-(\d{1,2})-(\d{1,2})", new_date).groups() + new_date = datetime(int(year), int(month), int(day), 17, tzinfo=utc) + except ValueError: + new_date = date_now + event = Event(myenv, 0, "", req.authname, date_now, date_now, new_date, new_date + timedelta(0,10800), 1) + add_ctxtnav(req, _('Events Overview'), req.href.event()) + add_ctxtnav(req, _('Upcoming Events'), req.href.event("upcoming")) + + if req.method == "POST": + if req.args.has_key("delete"): + req.perm.require("EVENTS_DELETE") + EventRRule.delete(myenv, event.e_id) + EventRDate.delete(myenv, event.e_id) + Event.delete(myenv, event.e_id) + db = myenv.get_db_cnx() + cursor = db.cursor() + # TODO: that is hackish. find out how to use trac arg formatting + # of "select * from foo where bar like 'baz%';" + like = "delete from wiki where name like 'events/%s/%%'" % int(event.e_id) + cursor.execute(like) + db.commit() + req.session["deleted"] = True + req.redirect(req.href.event("new")) + + is_valid = True + event.name = req.args.get("name", None) + try: + event.old_time_begin = event.time_begin + event.time_begin = datetime_parse("%s %s" % (req.args["date_begin"], req.args["time_begin"]), selected_tz) + except Exception: + add_warning(req, _("Wrong format in 'Date begin'.")) + is_valid = False + try: + event.old_time_end = event.time_end + event.time_end = datetime_parse("%s %s" % (req.args["date_end"], req.args["time_end"]), selected_tz) + except Exception, e: + add_warning(req, _("Wrong format in 'Date end'.")) + is_valid = False + try: + event.location_id = int(req.args["location_id"]) + location = ItemLocation.fetch_one(myenv, event.location_id) + except Exception, e: + add_warning(req, _("Could not find location")) + is_valid = False + tags = req.args.get("tags", None) + if tags: + event.tags = sub("[,;.:]", " ", tags) + attendees = req.args.get("attendees", None) + if attendees: + attendees = sub("[,;.:]", " ", attendees) + + if is_valid: + if not req.args.has_key("e_id"): + req.perm.require("EVENTS_ADD") + try: + self.__validate_event(event) + event.commit() + except Exception, e: + add_warning(req, str(e)) + raise + event.wikipage = "events/%d" % event.e_id + event.update() + wikicontent = u" = %s =\n[[EventHeader(%d)]]\n\n%s" % (event.name, event.e_id, self.config.get("event", "wiki_content")) + try: + gen_wiki_page(myenv, req.authname, event.wikipage, wikicontent, req.remote_addr) + except IntegrityError, e: + add_warning(req, _("Wikipage already exists.")) + if req.args.has_key("show_recurrency"): + req.redirect(req.href.event("recurrency", event.e_id)) + req.session["event_added"] = True + req.session.save() + if req.args.has_key("show_recurrency"): + req.session["added"] = True + req.session.save() + req.redirect(req.href.event("edit", event.e_id)) + else: + req.perm.require("EVENTS_MODIFY") + try: + self.__validate_event(event) + except Exception, e: + add_warning(req, str(e)) + raise + else: + event.time_modified = datetime.now(utc) + event.update() + req.session["edited"] = True + req.session.save() + req.redirect(req.href.event("edit", event.e_id)) + return "event_edit.html", {"event" : event, + "session_tzname" : session_tzname, + "selected_tz" : selected_tz, + "default_location" : self.config.getint("location", "default_location"), + "locations" : ItemLocation.fetch_all(myenv)}, None + + def __display_events(self, req): + '''process add,change,delete actions''' + add_stylesheet (req, 'hw/css/event.css') + if "EVENTS_ADD" in req.perm: + add_ctxtnav(req, _('Add New Event'), req.href.event("new")) + add_ctxtnav(req, _('Upcoming Events'), req.href.event("upcoming")) + if req.args.has_key("e_id"): + events = [Event.fetch_one(self.env, req.args["e_id"], show_next=True)] + title = _("Event Details") + else: + events = Event.fetch_all_with_rrule(self.env) + events = sorted(events, key=attrgetter("time_begin")) + title = _("Event Overview") + session_tzname, selected_tz = get_tz(req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + return "event_list.html", {"events" : events, "title" : title, "session_tzname" : session_tzname, "selected_tz" : selected_tz}, None + + def __display_upcoming_events(self, req): + if req.args.get("format") == 'ical': + events = Event.fetch_as_ical(self.env) + Mimeview(self.env).send_converted(req, 'tracrendezvous.Event', events, "ical", _("CTDO: Upcoming Events")) + + add_stylesheet (req, 'hw/css/event.css') + if req.locale is not None: + add_script(req, 'parent/tracrendezvous/%s.js' % req.locale) + if "EVENTS_ADD" in req.perm: + add_ctxtnav(req, _('Add New Event'), req.href.event("new")) + add_ctxtnav(req, _('Events Overview'), req.href.event()) + for conversion in Mimeview(self.env).get_supported_conversions("tracrendezvous.Event"): + conversion_href = req.href.event("upcoming", format=conversion[0]) + add_link(req, 'alternate', conversion_href, conversion[1], conversion[3], conversion[0]) + session_tzname, selected_tz = get_tz(req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + + n = datetime.now(utc) + n = n.replace(hour=0, minute=0, second=0, microsecond=0) + end = n + timedelta(183) + end = end.replace(hour=23, minute=0, second=59, microsecond=999) + table, headers = self.__get_upcoming_table(n, end) + + #ical_file = file("/home/hotshelf/icalout.ics") + #ical = parse_ical(self.env, ical_file) + + return "events.html", {"table" : table, "headers" : headers, "session_tzname" : session_tzname, "format" : "%a, %d.%m.%Y", "selected_tz" : selected_tz, "title" : _("Upcoming Events for"), "title2" : "%s - %s" % (n.strftime('%A, %d.%m.%Y %H:%M'), end.strftime('%A, %d.%m.%Y %H:%M')), "now" : n, "end" : end}, None + + def __display_events_by_day(self, req): + add_stylesheet (req, 'hw/css/event.css') + add_ctxtnav(req, _('Back to Upcoming Events'), req.href.event("upcoming")) + session_tzname, selected_tz = get_tz(req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + # do datetime calc only in utc !!! + n = title = req.args["arg_date"] + n = datetime(n.year, n.month, n.day, tzinfo=utc) + #n = local_to_utc(n, selected_tz) + e = n + timedelta(1) + if "EVENTS_ADD" in req.perm: + add_ctxtnav(req, _('Add New Event'), req.href.event("new", title.strftime("%Y-%m-%d"))) + table, headers = self.__get_day_table(n, e) + return "event_details.html", {"table" : table, "headers" : headers, "format" : "%H:%M", "session_tzname" : session_tzname, "selected_tz" : selected_tz, "title" : _("Details of %s") % (title.strftime('%A, %d.%m.%Y'),), "now" : n, "end" : e}, None + + def __export_upcoming_events_rss(self, req, foo): + now = datetime.now(utc) + now.replace(hour=0,second=0,microsecond=0) + end = now + timedelta(183) + end.replace(hour=23,second=59,microsecond=999) + events = Event.fetch_all(self.env) + data = {"events" : sorted(events, key=attrgetter("time_begin")), "today" : datetime.now(utc)} + output = Chrome(self.env).render_template(req, 'upcoming_events.rss', data, + 'application/rss+xml') + return output, 'application/rss+xml' + + def __export_upcoming_events_ical(self, req, events): + data = {"stamp" : datetime.now(utc).strftime("%Y%m%dT%H%M%SZ"), + "events" : sorted(events, key=attrgetter("time_begin")), + "today" : datetime.now(utc), + "calname" : self.env.config.get("events", "upcoming_name"), + "caldesc" : self.env.config.get("events", "upcoming_desc")} + ch = Chrome(self.env) + ds = ch.get_all_templates_dirs() + data = ch.populate_data(req, data) + templates = TemplateLoader(ds, auto_reload=self.env.config.getbool('trac', 'auto_reload'), variable_lookup='lenient') + template = templates.load("ical.txt", cls=NewTextTemplate) + try: + stream = template.generate(**data) + output = stream.render() + return output, 'text/calendar' + except Exception, e: + # give some hints when hitting a Genshi unicode error + if isinstance(e, UnicodeError): + pos = self._stream_location(stream) + if pos: + location = "'%s', line %s, char %s" % pos + else: + location = _("(unknown template location)") + raise TracError(_("Genshi %(error)s error while rendering " + "template %(location)s", + error=e.__class__.__name__, + location=location)) + raise + + def __validate_event(self, event): + if not event.name: + raise TypeError(_("EventValidationError: Title is empty")) + if event.time_end <= event.time_begin: + raise TypeError(_("EventValidationError: end time before or equal begin time")) + + def __get_upcoming_table(self, timerange_begin, timerange_end): + # ongoing events goes in here referenced by column index + res = Event.fetch_by_period_dict(self.env, timerange_begin, timerange_end) + ongoing = {} # column index -> event + table = [] + timerange_begin = timerange_begin.date() + timerange_end = timerange_end.date() + dt = timedelta(1) + column_count = 0 + def get_next(d): + count = 1 + while 1: + if not d.has_key(count): + return count + count += 1 + while timerange_begin < timerange_end: + row = {0 : timerange_begin} + done = [] + for key,event in ongoing.iteritems(): + if event.time_end.date() < timerange_begin: + done.append(key) + for i in done: + del ongoing[i] + for key in ongoing.iterkeys(): + row[key] = False + events = res[timerange_begin] + for event in events: + dt2 = event.time_end - event.time_begin + dt2_days = dt2.days + 1 + if dt2.seconds > 43200: + dt2_days += 1 + slot = get_next(ongoing) + event.rowspan = dt2_days + ongoing[slot] = event + row[slot] = [event,] + column_count = max(column_count, len(ongoing)) + table.append(row) + timerange_begin += dt + table2 = [] + mbase = [True for x in xrange(column_count+1)] + for row in table: + myrow = mbase[:] + for key,value in row.iteritems(): + myrow[key] = value + table2.append(myrow) + mbase = ["" for x in xrange(column_count+1)] + mbase[0] = _("date") + return table2, mbase + + def __create_wiki_page(self, req): + '''Programatically create a new wiki page tailored to a given occurence of an event + - one date of a recurrence set. + ''' + + req.perm.require("WIKI_CREATE") + dt = req.args["arg_date"] + e_id = req.args["e_id"] + wikipage = "events/%d/%s" % (e_id, dt.strftime("%Y-%m-%d")) + event = Event.fetch_one(self.env, e_id) + wikicontent = u" = %s =\n[[EventHeader(%d)]]\n\n%s" % (event.name, event.e_id, self.env.config.get("event", "wiki_content", "")) + try: + gen_wiki_page(self.env, req.authname, wikipage, wikicontent, req.remote_addr) + except IntegrityError, e: + pass + req.redirect(req.href.wiki(wikipage)) + + def __get_day_table(self, timerange_begin, timerange_end, dt=timedelta(0, 1800)): + ''' Calculates and arranges events during a day period which can be defined + by start datetime 'timerange_begin' and end datetime 'timerange_end'. + The resolution of the "table" prepared for html or other UIs can be + controlled by timedelta 'dt'. + The first column contains the time slots, additional columns contain + destinct locations. + It returns a row list of cells.''' + + class UList(list): + def ___init__(self): + list.__init__(self) + self.rowspan = None + events = Event.fetch_by_period_list(self.env, timerange_begin, timerange_end) + if not events: + return None, None + timerange_begin = min(events, key=lambda x:x.time_begin).time_begin + timerange_end = max(events, key=lambda x:x.time_end).time_end + + # now we calcutate the value of previous "slot" beginning as datetime to beautify + # the slot time beginnings. Try to uncomment the next few lines and reload to see + # what happens to the table without;-) + dts = dt.days * 86400 + dt.seconds + tbs = ((timerange_begin.hour*3600+timerange_begin.minute*60)/dts)*dts + minutes, seconds = divmod(tbs, 60) + hours, minutes = divmod(minutes, 60) + timerange_begin = timerange_begin.replace(hour=hours, minute=minutes) + #timerange_end += dt # we want one slot after the last event ends + + column_count = 1 # column 0 is always timerange_begin + locmap = dict() # location_id -> column index + ongoing = dict() # column index of actual used column -> eventlist + locs = dict() # column index -> location name + slotcount = 0 + table = [] + done = [] + while timerange_begin < timerange_end: + row = {0 : timerange_begin} + for k,e in done: + events.remove(e) + done = [] + slot_end = timerange_begin + dt + for event in events: + if event.time_begin >= timerange_begin and event.time_begin < slot_end: + if not locmap.has_key(event.location_id): + index = column_count + column_count += 1 + locmap[event.location_id] = index + locs[event.location_id] = event.location.name + else: + index = locmap[event.location_id] + if not hasattr(event, "rowspan_begin"): + # event starts in that timeslot + event.rowspan_begin = slotcount + if not row.has_key(index): + row[index] = UList() + if ongoing.has_key(index): + ongoing[index][0]+=1 + refc, event_list = ongoing[index] + else: + event_list = row[index] + ongoing[index] = [1, event_list] + event_list.append(event) + if event.time_end <= slot_end: + # event ends + index = locmap[event.location_id] + done.append((locmap[event.location_id], event)) + event.rowspan_end = slotcount+1 + refc, el = ongoing[index] + if refc<=1: + del ongoing[index] + else: + refc-=1 + ongoing[index] = [refc, el] + if hasattr(event, "rowspan_begin") and event.time_begin < timerange_begin and event.time_end > timerange_begin: + # marking cell as occupied due to longer lasting event than time slot length + index = locmap[event.location_id] + row[index] = False + table.append(row) + timerange_begin += dt + slotcount += 1 + final_table = [] + header = [True for x in xrange(column_count)] + for row in table: + trow = header[:] + for index, item in row.iteritems(): + trow[index] = item + if item and isinstance(item, UList): + rowspan_begin = min(item, key=lambda x:x.rowspan_begin).rowspan_begin + rowspan_end = max(item, key=lambda x:x.rowspan_end).rowspan_end + item.rowspan = rowspan_end - rowspan_begin + final_table.append(trow) + for location_id, index in locmap.iteritems(): + header[index] = locs[location_id] + header[0] = _("Start") + return final_table, header + + def _format_event_link(self, formatter, ns, target, label, fullmatch=None): + link, params, fragment = formatter.split_link(target) + r = Ranges(link) + if len(r) == 1: + num = r.a + validate_id(num) + cursor = formatter.db.cursor() + cursor.execute("SELECT name,time_begin,time_end " + "FROM events WHERE e_id=%s", (num,)) + session_tzname, selected_tz = get_tz(formatter.req.session.get("tz", self.env.config.get("trac", "default_timezone") or None)) + for name, time_begin, time_end in cursor: + time_begin = selected_tz.fromutc(datetime.fromtimestamp(time_begin, utc)) + time_end = selected_tz.fromutc(datetime.fromtimestamp(time_end, utc)) + title = "%s (%s - %s %s)" % (name, + time_begin.strftime('%d.%m.%Y %H:%M'), + time_end.strftime('%d.%m.%Y %H:%M'), time_begin.tzinfo.tzname(None)) + if label == link: + label = title + href = formatter.href.event(num) + return tag.a(label, title=title, href=href) + return tag.a(label, class_='missing event') diff --git a/TracRendezVous/tracrendezvous/htdocs/css/base.css b/TracRendezVous/tracrendezvous/htdocs/css/base.css new file mode 100644 index 0000000..01da07c --- /dev/null +++ b/TracRendezVous/tracrendezvous/htdocs/css/base.css @@ -0,0 +1,5 @@ +p.help { color:#666666; margin-top:1em; margin-right:0.5em; margin-bottom:0.5em; margin-left:0.5em; } +.error{color:#ff0000;} +.rendezvous .main {width:82%;margin-right:1%;padding-right:1%;} +#rendezvous-main {text-align:left;} +#content {text-align:center;} diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_arrows_leftright.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_arrows_leftright.gif new file mode 100644 index 0000000..b26780a Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_arrows_leftright.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_arrows_updown.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_arrows_updown.gif new file mode 100644 index 0000000..69eb077 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_arrows_updown.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_doc.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_doc.gif new file mode 100644 index 0000000..26db434 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_doc.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_minus.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_minus.gif new file mode 100644 index 0000000..6851f39 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_minus.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_plus.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_plus.gif new file mode 100644 index 0000000..74ac5cb Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_plus.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_resize_se.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_resize_se.gif new file mode 100644 index 0000000..251dc16 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_resize_se.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_down.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_down.gif new file mode 100644 index 0000000..29c6c70 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_down.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_left.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_left.gif new file mode 100644 index 0000000..9f95efa Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_left.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_right.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_right.gif new file mode 100644 index 0000000..bc02050 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_right.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_up.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_up.gif new file mode 100644 index 0000000..28169eb Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_up.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/eeeeee_40x100_textures_03_highlight_soft_100.png b/TracRendezVous/tracrendezvous/htdocs/css/images/eeeeee_40x100_textures_03_highlight_soft_100.png new file mode 100644 index 0000000..98f298b Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/eeeeee_40x100_textures_03_highlight_soft_100.png differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_arrows_leftright.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_arrows_leftright.gif new file mode 100644 index 0000000..a4c0dbd Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_arrows_leftright.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_arrows_updown.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_arrows_updown.gif new file mode 100644 index 0000000..b4e5e14 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_arrows_updown.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_close.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_close.gif new file mode 100644 index 0000000..b9df122 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_close.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_doc.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_doc.gif new file mode 100644 index 0000000..452d968 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_doc.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_folder_closed.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_folder_closed.gif new file mode 100644 index 0000000..7fc0432 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_folder_closed.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_folder_open.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_folder_open.gif new file mode 100644 index 0000000..6c1724c Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_folder_open.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_minus.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_minus.gif new file mode 100644 index 0000000..d709e16 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_minus.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_plus.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_plus.gif new file mode 100644 index 0000000..8e285e1 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_plus.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_down.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_down.gif new file mode 100644 index 0000000..f6c8074 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_down.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_left.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_left.gif new file mode 100644 index 0000000..afc50ca Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_left.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_right.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_right.gif new file mode 100644 index 0000000..63eddf6 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_right.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_up.gif b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_up.gif new file mode 100644 index 0000000..5bc65ea Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_up.gif differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/f6f6f6_40x100_textures_02_glass_100.png b/TracRendezVous/tracrendezvous/htdocs/css/images/f6f6f6_40x100_textures_02_glass_100.png new file mode 100644 index 0000000..8443dda Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/f6f6f6_40x100_textures_02_glass_100.png differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/fdf5ce_40x100_textures_02_glass_100.png b/TracRendezVous/tracrendezvous/htdocs/css/images/fdf5ce_40x100_textures_02_glass_100.png new file mode 100644 index 0000000..744f925 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/fdf5ce_40x100_textures_02_glass_100.png differ diff --git a/TracRendezVous/tracrendezvous/htdocs/css/images/ffffff_40x100_textures_02_glass_65.png b/TracRendezVous/tracrendezvous/htdocs/css/images/ffffff_40x100_textures_02_glass_65.png new file mode 100644 index 0000000..2c16183 Binary files /dev/null and b/TracRendezVous/tracrendezvous/htdocs/css/images/ffffff_40x100_textures_02_glass_65.png differ diff --git a/TracRendezVous/tracrendezvous/htdocs/tracrendezvous/de.js b/TracRendezVous/tracrendezvous/htdocs/tracrendezvous/de.js new file mode 100644 index 0000000..50549c1 --- /dev/null +++ b/TracRendezVous/tracrendezvous/htdocs/tracrendezvous/de.js @@ -0,0 +1,2 @@ +// Generated messages javascript file from compiled MO file +babel.Translations.load({"domain":"tracrendezvous-js","locale":"de","messages":{},"plural_expr":"(n != 1)"}).install(); diff --git a/TracRendezVous/tracrendezvous/htdocs/tracrendezvous/de_DE.js b/TracRendezVous/tracrendezvous/htdocs/tracrendezvous/de_DE.js new file mode 100644 index 0000000..d8fa68e --- /dev/null +++ b/TracRendezVous/tracrendezvous/htdocs/tracrendezvous/de_DE.js @@ -0,0 +1,2 @@ +// Generated messages javascript file from compiled MO file +babel.Translations.load({"domain":"tracrendezvous-js","locale":"de_DE","messages":{},"plural_expr":"(n != 1)"}).install(); diff --git a/TracRendezVous/tracrendezvous/locale/de/LC_MESSAGES/messages.po b/TracRendezVous/tracrendezvous/locale/de/LC_MESSAGES/messages.po new file mode 100644 index 0000000..1e480f8 --- /dev/null +++ b/TracRendezVous/tracrendezvous/locale/de/LC_MESSAGES/messages.po @@ -0,0 +1,1291 @@ +# German translations for TracRendezVous. +# Copyright (C) 2010 Stefan Koegl +# This file is distributed under the same license as the TracRendezVous +# project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: TracRendezVous 0.3\n" +"Report-Msgid-Bugs-To: hotshelf@ctdo.de\n" +"POT-Creation-Date: 2010-08-03 04:33+0200\n" +"PO-Revision-Date: 2010-08-03 04:37+0200\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: tracrendezvous/event/admin.py:133 tracrendezvous/rendezvous/admin.py:134 +msgid "All upper-cased tokens are reserved for permission names" +msgstr "" + +#: tracrendezvous/event/admin.py:138 tracrendezvous/rendezvous/admin.py:139 +msgid "Unknown permission" +msgstr "" + +#: tracrendezvous/event/admin.py:142 tracrendezvous/rendezvous/admin.py:143 +msgid "permission already granted to RendezVousType" +msgstr "" + +#: tracrendezvous/event/admin.py:150 tracrendezvous/rendezvous/admin.py:151 +msgid "RendezVousType already exists" +msgstr "" + +#: tracrendezvous/event/admin.py:163 tracrendezvous/rendezvous/admin.py:164 +msgid "Unknown RendezVousType" +msgstr "" + +#: tracrendezvous/event/admin.py:174 tracrendezvous/rendezvous/admin.py:175 +msgid "Unknown type permission relation" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "MO" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "TU" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "WE" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "TH" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "FR" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "SA" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "SU" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Monday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Tuesday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Wednesday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Thursday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Friday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Saturday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Sunday" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "January" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "February" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "March" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "April" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "May" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "June" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "July" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "August" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "September" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "October" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "November" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "December" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "1st" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "2nd" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "3rd" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "4th" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "5th" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "last" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "2-last" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "3-last" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "4-last" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "5-last" +msgstr "" + +#: tracrendezvous/event/model.py:420 +#, python-format +msgid " %d year(s)" +msgstr "" + +#: tracrendezvous/event/model.py:420 +msgid "monthly" +msgstr "" + +#: tracrendezvous/event/model.py:420 +msgid "weekly" +msgstr "" + +#: tracrendezvous/event/model.py:420 +msgid "daily" +msgstr "" + +#: tracrendezvous/event/model.py:421 +#: tracrendezvous/event/templates/recur_edit.html:66 +msgid "year(s)" +msgstr "" + +#: tracrendezvous/event/model.py:421 +#: tracrendezvous/event/templates/recur_edit.html:86 +msgid "month(s)" +msgstr "" + +#: tracrendezvous/event/model.py:421 +#: tracrendezvous/event/templates/recur_edit.html:99 +msgid "week(s)" +msgstr "" + +#: tracrendezvous/event/model.py:421 +#: tracrendezvous/event/templates/recur_edit.html:110 +msgid "day(s)" +msgstr "" + +#: tracrendezvous/event/model.py:439 +#, python-format +msgid "on %s" +msgstr "" + +#: tracrendezvous/event/model.py:447 tracrendezvous/event/model.py:452 +#, python-format +msgid "%s %s" +msgstr "" + +#: tracrendezvous/event/model.py:457 tracrendezvous/event/model.py:461 +#, python-format +msgid "on day %s" +msgstr "" + +#: tracrendezvous/event/model.py:459 +#, python-format +msgid "in %s" +msgstr "" + +#: tracrendezvous/event/web_ui.py:110 tracrendezvous/event/web_ui.py:140 +#: tracrendezvous/event/web_ui.py:611 tracrendezvous/event/web_ui.py:621 +#: tracrendezvous/event/web_ui.py:713 +msgid "Upcoming Events" +msgstr "Anstehende Termine" + +#: tracrendezvous/event/web_ui.py:111 +msgid "" +"Hier erfährst Du direkt, was in den nächsten 6 Monaten im Chaostreff los " +"ist." +msgstr "" + +#: tracrendezvous/event/web_ui.py:112 +msgid "" +"Feel free to fill this outer space with content descibing the actual " +"event.[[BR]]If the event is recurrent, describe the overall topics or " +"similarities here and put the date dependant stuff into the wikipages you" +" find via the links on the right side." +msgstr "" + +#: tracrendezvous/event/web_ui.py:222 +msgid "Ical Feed" +msgstr "" + +#: tracrendezvous/event/web_ui.py:249 +msgid "Events" +msgstr "" + +#: tracrendezvous/event/web_ui.py:268 +msgid "Click the link" +msgstr "" + +#: tracrendezvous/event/web_ui.py:281 +msgid "Back to Overview" +msgstr "" + +#: tracrendezvous/event/web_ui.py:282 +msgid "Back to Event" +msgstr "" + +#: tracrendezvous/event/web_ui.py:295 tracrendezvous/event/web_ui.py:593 +msgid "Event created successfully. Back to " +msgstr "" + +#: tracrendezvous/event/web_ui.py:295 +msgid "Event" +msgstr "" + +#: tracrendezvous/event/web_ui.py:307 tracrendezvous/event/web_ui.py:607 +msgid "Event not found" +msgstr "" + +#: tracrendezvous/event/web_ui.py:334 +msgid "Wrong Value for 'month'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:342 +msgid "Wrong Value for 'monthday'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:350 tracrendezvous/event/web_ui.py:436 +msgid "Wrong Value for 'weekday'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:358 +msgid "Wrong Value for 'weekday occurence'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:368 +msgid "Wrong Value for interval" +msgstr "" + +#: tracrendezvous/event/web_ui.py:378 +msgid "Wrong Value for count" +msgstr "" + +#: tracrendezvous/event/web_ui.py:387 +msgid "Wrong Value for until_date or until_time" +msgstr "" + +#: tracrendezvous/event/web_ui.py:396 +msgid "Please select one of the recurrency types" +msgstr "" + +#: tracrendezvous/event/web_ui.py:478 +msgid "Any days selected." +msgstr "" + +#: tracrendezvous/event/web_ui.py:502 +msgid "Wrong recurrency exception date format" +msgstr "" + +#: tracrendezvous/event/web_ui.py:526 +msgid "You have to specify a valid recurrency exception date for that operation!" +msgstr "" + +#: tracrendezvous/event/web_ui.py:589 +msgid "Event edited successfully. Back to " +msgstr "" + +#: tracrendezvous/event/web_ui.py:589 tracrendezvous/event/web_ui.py:593 +#: tracrendezvous/event/web_ui.py:597 +msgid "Overview" +msgstr "" + +#: tracrendezvous/event/web_ui.py:597 +msgid "Event deleted successfully. Back to " +msgstr "" + +#: tracrendezvous/event/web_ui.py:609 tracrendezvous/event/web_ui.py:712 +#: tracrendezvous/event/web_ui.py:731 tracrendezvous/event/web_ui.py:758 +msgid "Add New Event" +msgstr "" + +#: tracrendezvous/event/web_ui.py:610 tracrendezvous/event/web_ui.py:620 +#: tracrendezvous/event/web_ui.py:732 +msgid "Events Overview" +msgstr "" + +#: tracrendezvous/event/web_ui.py:644 +msgid "Wrong format in 'Date begin'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:650 +msgid "Wrong format in 'Date end'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:656 +msgid "Could not find location" +msgstr "" + +#: tracrendezvous/event/web_ui.py:680 +msgid "Wikipage already exists." +msgstr "" + +#: tracrendezvous/event/web_ui.py:716 +msgid "Event Details" +msgstr "" + +#: tracrendezvous/event/web_ui.py:720 +msgid "Event Overview" +msgstr "" + +#: tracrendezvous/event/web_ui.py:727 +msgid "CTDO: Upcoming Events" +msgstr "CTDO: Anstehende Termine" + +#: tracrendezvous/event/web_ui.py:746 +msgid "Upcoming Events for" +msgstr "Anstehende Termine für" + +#: tracrendezvous/event/web_ui.py:750 +msgid "Back to Upcoming Events" +msgstr "Zurück zu Anstehenden Terminen" + +#: tracrendezvous/event/web_ui.py:760 +#, python-format +msgid "Details of %s" +msgstr "" + +#: tracrendezvous/event/web_ui.py:795 +msgid "(unknown template location)" +msgstr "" + +#: tracrendezvous/event/web_ui.py:796 +#, python-format +msgid "Genshi %(error)s error while rendering template %(location)s" +msgstr "" + +#: tracrendezvous/event/web_ui.py:804 +msgid "EventValidationError: Title is empty" +msgstr "" + +#: tracrendezvous/event/web_ui.py:806 +msgid "EventValidationError: end time before or equal begin time" +msgstr "" + +#: tracrendezvous/event/web_ui.py:854 +msgid "date" +msgstr "" + +#: tracrendezvous/event/web_ui.py:968 +msgid "Start" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:29 +#: tracrendezvous/event/templates/event_edit.html:29 +msgid "Title:" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:30 +#: tracrendezvous/event/templates/event_edit.html:30 +#: tracrendezvous/rendezvous/templates/date.html:67 +msgid "Date begin:" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:33 +#: tracrendezvous/event/templates/event_edit.html:33 +#: tracrendezvous/rendezvous/templates/date.html:69 +msgid "Date end:" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:36 +#: tracrendezvous/event/templates/event_edit.html:36 +msgid "Locations:" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:40 +#: tracrendezvous/event/templates/event_edit.html:40 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:58 +msgid "edit/search locations" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:42 +#: tracrendezvous/event/templates/alarm_edit.html:43 +#: tracrendezvous/event/templates/event_edit.html:42 +#: tracrendezvous/event/templates/event_edit.html:43 +msgid "show recurrency options" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:48 +#: tracrendezvous/event/templates/event_edit.html:48 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:74 +msgid "Tags" +msgstr "" + +#: tracrendezvous/event/templates/event_details.html:34 +#: tracrendezvous/event/templates/events.html:40 +msgid "No events in this time frame. Create here" +msgstr "" + +#: tracrendezvous/event/templates/event_details.html:34 +#: tracrendezvous/event/templates/events.html:40 +msgid " a new event" +msgstr "" + +#: tracrendezvous/event/templates/event_display.html:5 +#: tracrendezvous/rendezvous/templates/rendezvous.html:84 +#: tracrendezvous/rendezvous/templates/rendezvous.html:116 +msgid "edit" +msgstr "" + +#: tracrendezvous/event/templates/event_display.html:25 +msgid "wiki page" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +msgid "C" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "I" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "R" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "U" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "L" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "A" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:19 +msgid "S" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:19 +msgid "N" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:19 +msgid "G" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:1 +msgid "" +"BEGIN:VCALENDAR\n" +"PRODID://TracRendezVous-0.3 by Stefan Kögl //DE\n" +"VERSION:2.0\n" +"CALSCALE:GREGORIAN\n" +"CAL-ADDRESS:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:5 +msgid "" +"METHOD:PUBLISH\n" +"X-WR-CALNAME:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:7 +msgid "" +"X-WR-TIMEZONE:UTC\n" +"X-WR-CALDESC:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:9 +#, python-format +msgid "" +"{% for event in events %}\\\n" +"BEGIN:VEVENT\n" +"UID:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:12 +msgid "CREATED:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:13 +msgid "DTSTAMP:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:14 +msgid "LAST-MODIFIED:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:15 +msgid "SUMMARY:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:16 +msgid "LOCATION:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:17 +msgid "GEO:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:18 +msgid "URI:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:19 +msgid "DESCRIPTION:more information:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:20 +msgid "" +"CLASS:PUBLIC\n" +"DTSTART:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:22 +#, python-format +msgid "{% for rrule in event.rrules %}\\" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:23 +#, python-format +msgid "" +"{% end %}\\\n" +"{% for alarm in event.alarms %}\\" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:25 +#, python-format +msgid "" +"{% end %}\\\n" +"DTEND:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:27 +#, python-format +msgid "" +"TRANSP:OPAQUE\n" +"END:VEVENT\n" +"{% end %}\\\n" +"END:VCALENDAR" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:53 +msgid "Recurrency Options for event '" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:59 +msgid "Repeat this event" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:61 +msgid "Recurrency frequency" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:64 +msgid "Yearly" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:66 +#: tracrendezvous/event/templates/recur_edit.html:84 +#: tracrendezvous/event/templates/recur_edit.html:99 +#: tracrendezvous/event/templates/recur_edit.html:110 +msgid "Repeat every" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:67 +msgid "Repeat on day" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:69 +#: tracrendezvous/event/templates/recur_edit.html:74 +msgid "in" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:72 +#: tracrendezvous/event/templates/recur_edit.html:87 +#: tracrendezvous/event/templates/recur_edit.html:89 +msgid "Repeat on" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:77 +msgid "Repeat on day no." +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:77 +msgid "of the year" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:81 +msgid "Monthly" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:88 +msgid "day of the month" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:96 +msgid "Weekly" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:107 +msgid "Daily" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:116 +msgid "Recurrency Timeframe" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:118 +msgid "Finish after" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:118 +msgid "Repetitions" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:119 +msgid "End on:" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:123 +msgid "Recurrency Exceptions" +msgstr "" + +#: tracrendezvous/location/web_ui.py:79 +msgid "Location edited successfully." +msgstr "" + +#: tracrendezvous/location/web_ui.py:88 +msgid "Location created successfully. Back to " +msgstr "" + +#: tracrendezvous/location/web_ui.py:90 +msgid "Location created successfully." +msgstr "" + +#: tracrendezvous/location/templates/location.html:94 +#: tracrendezvous/rendezvous/templates/location.html:49 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:54 +msgid "Locations" +msgstr "" + +#: tracrendezvous/location/templates/location.html:101 +#: tracrendezvous/rendezvous/templates/location.html:57 +msgid "Known Locations" +msgstr "" + +#: tracrendezvous/location/templates/location.html:103 +#: tracrendezvous/rendezvous/templates/location.html:59 +msgid "Name" +msgstr "" + +#: tracrendezvous/location/templates/location.html:103 +#: tracrendezvous/rendezvous/templates/location.html:59 +#: tracrendezvous/rendezvous/templates/rendezvous.html:37 +msgid "Coordinates" +msgstr "" + +#: tracrendezvous/location/templates/location.html:103 +#: tracrendezvous/rendezvous/templates/admin_types.html:66 +#: tracrendezvous/rendezvous/templates/location.html:59 +msgid "Default" +msgstr "" + +#: tracrendezvous/location/templates/location.html:103 +#: tracrendezvous/rendezvous/templates/location.html:59 +msgid "Delete" +msgstr "" + +#: tracrendezvous/location/templates/location.html:113 +#: tracrendezvous/rendezvous/templates/location.html:69 +msgid "Change or delete existing locations." +msgstr "" + +#: tracrendezvous/location/templates/location.html:122 +#: tracrendezvous/rendezvous/templates/location.html:78 +msgid "New Location" +msgstr "" + +#: tracrendezvous/location/templates/location.html:124 +#: tracrendezvous/rendezvous/templates/location.html:80 +msgid "Name:" +msgstr "" + +#: tracrendezvous/location/templates/location.html:125 +#: tracrendezvous/rendezvous/templates/location.html:81 +msgid "Coordinates:" +msgstr "" + +#: tracrendezvous/location/templates/location.html:132 +#: tracrendezvous/rendezvous/templates/location.html:88 +msgid "Allowed coordinates formats:" +msgstr "" + +#: tracrendezvous/location/templates/location.html:134 +#: tracrendezvous/rendezvous/templates/location.html:90 +msgid "DD format = 'lat,lon', e.g 53.235235,6.235235" +msgstr "" + +#: tracrendezvous/location/templates/location.html:135 +#: tracrendezvous/rendezvous/templates/location.html:91 +msgid "" +"DMS Format = 'N|Wdd°mm'ss\" E|Wdddd°mm'ss\", e.g N51°31'39.40000\" " +"E7°27'53.7200\"" +msgstr "" + +#: tracrendezvous/location/templates/location.html:142 +#: tracrendezvous/rendezvous/templates/location.html:98 +msgid "Location Search" +msgstr "" + +#: tracrendezvous/location/templates/location.html:144 +#: tracrendezvous/rendezvous/templates/location.html:100 +msgid "Location Search:" +msgstr "" + +#: tracrendezvous/location/templates/location.html:148 +#: tracrendezvous/rendezvous/templates/location.html:104 +msgid "e.g \"central station, cityname\"" +msgstr "" + +#: tracrendezvous/location/templates/location.html:150 +#: tracrendezvous/rendezvous/templates/location.html:106 +msgid "Search Results" +msgstr "" + +#: tracrendezvous/location/templates/location.html:157 +#: tracrendezvous/rendezvous/templates/location.html:113 +msgid "km away of" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:206 +msgid "Current state no longer exists" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:208 +msgid "" +"The rendezvous will be prepared for a new voting pass. All comments, " +"dates and votes for that item will be deleted. You have to publish it " +"again to become visible for other users" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:210 +msgid "The rendezvous will be published for voting" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:213 +msgid "to " +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:217 +msgid "The rendezvous will be scheduled (no more voting, but remains visible)" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:219 +msgid "" +"This status may be ignored, since I can handle RendezVous starts " +"automatically, but you may wish to start manually before the scheduled " +"beginning date and time" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:221 +msgid "" +"This status may be ignored, since I can handle expired RendezVouses " +"automatically, but you may wish to stop manually before the scheduled " +"date and time. It's an end status" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:223 +msgid "The rendezvous will not takes place. It's an end status" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:228 +#, python-format +msgid "Next status will be '%s'" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:9 +msgid "RendezVous General Settings" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:12 +msgid "General Settings" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:15 +#: tracrendezvous/rendezvous/templates/overview.html:9 +#: tracrendezvous/rendezvous/templates/rendezvous.html:9 +msgid "RendezVous" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:17 +msgid "max description length:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:22 +msgid "max votes per date:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:27 +msgid "max dates per rendezvous:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:32 +msgid "graph width:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:37 +msgid "graph height:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:42 +msgid "show vote graph:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:47 +msgid "show location map:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_overview.html:17 +#: tracrendezvous/rendezvous/templates/admin_overview.html:20 +msgid "Overview Configuration" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_overview.html:24 +msgid "wrong position - should be" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_overview.html:28 +msgid "help" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:13 +msgid "Manage RendezVousTypes" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:17 +msgid "Add Type:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:20 +msgid "RendezVous Type:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:24 +msgid "Create a new RendezVous Type. Note that" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:25 +#: tracrendezvous/rendezvous/templates/admin_types.html:90 +msgid "RendezVousType" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:25 +msgid "" +"names can't be all upper-case,\n" +" as that is reserved for permission names." +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:36 +msgid "Grant Permission To Type:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:39 +msgid "RendezVousType:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:46 +msgid "Permission:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:53 +msgid "Grant permission to a RendezVousType." +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:66 +msgid "RendezVous Type" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:66 +msgid "Permission" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:66 +#: tracrendezvous/rendezvous/templates/date.html:37 +#: tracrendezvous/rendezvous/templates/vote.html:37 +msgid "delete" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:89 +msgid "Note that" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:90 +msgid "" +"names can't be all upper-case,\n" +" as that is reserved for permission names." +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:17 +msgid "Dates" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:26 +#: tracrendezvous/rendezvous/templates/date.html:86 +msgid "Dates for" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:31 +#: tracrendezvous/rendezvous/templates/date.html:90 +msgid "author" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:32 +#: tracrendezvous/rendezvous/templates/vote.html:33 +msgid "email" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:33 +msgid "date begin" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:34 +msgid "time begin" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:35 +msgid "date end" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:36 +msgid "time end" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:54 +#: tracrendezvous/rendezvous/templates/vote.html:81 +msgid "Change or delete votes for an existing rendezvous date." +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:63 +msgid "New Date" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:66 +#: tracrendezvous/rendezvous/templates/vote.html:91 +msgid "Email:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:71 +msgid "Vote that for me!" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:80 +#: tracrendezvous/rendezvous/templates/vote.html:112 +msgid "Allowed date/time formats:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:82 +msgid "yyyymmdd" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:83 +msgid "yyyy.mm.dd" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:91 +msgid "day" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:92 +msgid "day part" +msgstr "" + +#: tracrendezvous/rendezvous/templates/overview.html:19 +msgid "new" +msgstr "" + +#: tracrendezvous/rendezvous/templates/overview.html:25 +msgid "voting" +msgstr "" + +#: tracrendezvous/rendezvous/templates/overview.html:31 +msgid "canceled" +msgstr "" + +#: tracrendezvous/rendezvous/templates/overview.html:37 +msgid "expired" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:16 +msgid "RendezVous #" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:17 +msgid "Created on" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:17 +#: tracrendezvous/rendezvous/templates/rendezvous.html:116 +#: tracrendezvous/rendezvous/templates/rendezvous.html:126 +msgid "by" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:21 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:46 +msgid "Type" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:25 +msgid "Min. Votes" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:29 +msgid "Tagged with" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:33 +msgid "Location" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:44 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:60 +msgid "Description" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:47 +msgid "Votings" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:50 +msgid "Date proposals ->" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:53 +msgid "scheduled" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:58 +msgid "new date" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:64 +msgid "VoteCount" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:81 +#: tracrendezvous/rendezvous/templates/rendezvous.html:97 +msgid "quick and dirty voting for the date" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:81 +#: tracrendezvous/rendezvous/templates/rendezvous.html:97 +msgid "vote" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:82 +#: tracrendezvous/rendezvous/templates/rendezvous.html:98 +msgid "advanced voting operations" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:82 +#: tracrendezvous/rendezvous/templates/rendezvous.html:98 +msgid "...advanced" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:106 +msgid "Vote Distribution By RendezVousDate" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:114 +msgid "Comments" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:116 +#: tracrendezvous/rendezvous/templates/rendezvous.html:126 +msgid "added on" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:120 +msgid "New Comment" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:123 +msgid "Preview!" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:140 +msgid "Note:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:140 +msgid "See" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:140 +msgid "WikiFormatting" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:140 +msgid "and" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:141 +msgid "TracWiki" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:141 +msgid "for help on editing wiki content." +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:38 +msgid "Title" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:39 +msgid "Author" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:45 +msgid "Email" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:47 +msgid "Minimum votes" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:50 +msgid "Voting deadline" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:65 +msgid "Workflow" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:17 +msgid "Votes" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:25 +msgid "All votes for" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:26 +msgid "Votes for" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:26 +msgid "made by" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:32 +msgid "user" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:34 +msgid "begin" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:35 +msgid "end" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:84 +msgid "Add new vote for" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:87 +msgid "User:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:95 +msgid "Date Begin:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:100 +msgid "Date End:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:110 +msgid "Add a vote to an existing rendezvous date." +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:114 +msgid "time:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:116 +msgid "'hhMM'" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:117 +msgid "'hh:MM'" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:120 +msgid "date:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:122 +msgid "'yyyymmdd'" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:123 +msgid "'yyyy.mm.dd'" +msgstr "" + diff --git a/TracRendezVous/tracrendezvous/locale/de/LC_MESSAGES/tracrendezvous-js.po b/TracRendezVous/tracrendezvous/locale/de/LC_MESSAGES/tracrendezvous-js.po new file mode 100644 index 0000000..6cc997e --- /dev/null +++ b/TracRendezVous/tracrendezvous/locale/de/LC_MESSAGES/tracrendezvous-js.po @@ -0,0 +1,20 @@ +# German translations for TracRendezVous. +# Copyright (C) 2010 Stefan Koegl +# This file is distributed under the same license as the TracRendezVous +# project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: TracRendezVous 0.3\n" +"Report-Msgid-Bugs-To: hotshelf@ctdo.de\n" +"POT-Creation-Date: 2010-08-03 04:59+0200\n" +"PO-Revision-Date: 2010-08-03 04:59+0200\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + diff --git a/TracRendezVous/tracrendezvous/locale/messages-js.pot b/TracRendezVous/tracrendezvous/locale/messages-js.pot new file mode 100644 index 0000000..a202ed6 --- /dev/null +++ b/TracRendezVous/tracrendezvous/locale/messages-js.pot @@ -0,0 +1,20 @@ +# Translations template for TracRendezVous. +# Copyright (C) 2010 Stefan Koegl +# This file is distributed under the same license as the TracRendezVous +# project. +# FIRST AUTHOR , 2010. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TracRendezVous 0.3\n" +"Report-Msgid-Bugs-To: hotshelf@ctdo.de\n" +"POT-Creation-Date: 2010-08-03 04:59+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + diff --git a/TracRendezVous/tracrendezvous/locale/messages.pot b/TracRendezVous/tracrendezvous/locale/messages.pot new file mode 100644 index 0000000..2e993d6 --- /dev/null +++ b/TracRendezVous/tracrendezvous/locale/messages.pot @@ -0,0 +1,1291 @@ +# Translations template for TracRendezVous. +# Copyright (C) 2010 Stefan Koegl +# This file is distributed under the same license as the TracRendezVous +# project. +# FIRST AUTHOR , 2010. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TracRendezVous 0.3\n" +"Report-Msgid-Bugs-To: hotshelf@ctdo.de\n" +"POT-Creation-Date: 2010-08-03 04:33+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: tracrendezvous/event/admin.py:133 tracrendezvous/rendezvous/admin.py:134 +msgid "All upper-cased tokens are reserved for permission names" +msgstr "" + +#: tracrendezvous/event/admin.py:138 tracrendezvous/rendezvous/admin.py:139 +msgid "Unknown permission" +msgstr "" + +#: tracrendezvous/event/admin.py:142 tracrendezvous/rendezvous/admin.py:143 +msgid "permission already granted to RendezVousType" +msgstr "" + +#: tracrendezvous/event/admin.py:150 tracrendezvous/rendezvous/admin.py:151 +msgid "RendezVousType already exists" +msgstr "" + +#: tracrendezvous/event/admin.py:163 tracrendezvous/rendezvous/admin.py:164 +msgid "Unknown RendezVousType" +msgstr "" + +#: tracrendezvous/event/admin.py:174 tracrendezvous/rendezvous/admin.py:175 +msgid "Unknown type permission relation" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "MO" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "TU" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "WE" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "TH" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "FR" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "SA" +msgstr "" + +#: tracrendezvous/event/model.py:166 +msgid "SU" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Monday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Tuesday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Wednesday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Thursday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Friday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Saturday" +msgstr "" + +#: tracrendezvous/event/model.py:167 +msgid "Sunday" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "January" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "February" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "March" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "April" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "May" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "June" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "July" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "August" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "September" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "October" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "November" +msgstr "" + +#: tracrendezvous/event/model.py:169 +msgid "December" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "1st" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "2nd" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "3rd" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "4th" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "5th" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "last" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "2-last" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "3-last" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "4-last" +msgstr "" + +#: tracrendezvous/event/model.py:171 +msgid "5-last" +msgstr "" + +#: tracrendezvous/event/model.py:420 +#, python-format +msgid " %d year(s)" +msgstr "" + +#: tracrendezvous/event/model.py:420 +msgid "monthly" +msgstr "" + +#: tracrendezvous/event/model.py:420 +msgid "weekly" +msgstr "" + +#: tracrendezvous/event/model.py:420 +msgid "daily" +msgstr "" + +#: tracrendezvous/event/model.py:421 +#: tracrendezvous/event/templates/recur_edit.html:66 +msgid "year(s)" +msgstr "" + +#: tracrendezvous/event/model.py:421 +#: tracrendezvous/event/templates/recur_edit.html:86 +msgid "month(s)" +msgstr "" + +#: tracrendezvous/event/model.py:421 +#: tracrendezvous/event/templates/recur_edit.html:99 +msgid "week(s)" +msgstr "" + +#: tracrendezvous/event/model.py:421 +#: tracrendezvous/event/templates/recur_edit.html:110 +msgid "day(s)" +msgstr "" + +#: tracrendezvous/event/model.py:439 +#, python-format +msgid "on %s" +msgstr "" + +#: tracrendezvous/event/model.py:447 tracrendezvous/event/model.py:452 +#, python-format +msgid "%s %s" +msgstr "" + +#: tracrendezvous/event/model.py:457 tracrendezvous/event/model.py:461 +#, python-format +msgid "on day %s" +msgstr "" + +#: tracrendezvous/event/model.py:459 +#, python-format +msgid "in %s" +msgstr "" + +#: tracrendezvous/event/web_ui.py:110 tracrendezvous/event/web_ui.py:140 +#: tracrendezvous/event/web_ui.py:611 tracrendezvous/event/web_ui.py:621 +#: tracrendezvous/event/web_ui.py:713 +msgid "Upcoming Events" +msgstr "" + +#: tracrendezvous/event/web_ui.py:111 +msgid "" +"Hier erfährst Du direkt, was in den nächsten 6 Monaten im Chaostreff los " +"ist." +msgstr "" + +#: tracrendezvous/event/web_ui.py:112 +msgid "" +"Feel free to fill this outer space with content descibing the actual " +"event.[[BR]]If the event is recurrent, describe the overall topics or " +"similarities here and put the date dependant stuff into the wikipages you" +" find via the links on the right side." +msgstr "" + +#: tracrendezvous/event/web_ui.py:222 +msgid "Ical Feed" +msgstr "" + +#: tracrendezvous/event/web_ui.py:249 +msgid "Events" +msgstr "" + +#: tracrendezvous/event/web_ui.py:268 +msgid "Click the link" +msgstr "" + +#: tracrendezvous/event/web_ui.py:281 +msgid "Back to Overview" +msgstr "" + +#: tracrendezvous/event/web_ui.py:282 +msgid "Back to Event" +msgstr "" + +#: tracrendezvous/event/web_ui.py:295 tracrendezvous/event/web_ui.py:593 +msgid "Event created successfully. Back to " +msgstr "" + +#: tracrendezvous/event/web_ui.py:295 +msgid "Event" +msgstr "" + +#: tracrendezvous/event/web_ui.py:307 tracrendezvous/event/web_ui.py:607 +msgid "Event not found" +msgstr "" + +#: tracrendezvous/event/web_ui.py:334 +msgid "Wrong Value for 'month'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:342 +msgid "Wrong Value for 'monthday'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:350 tracrendezvous/event/web_ui.py:436 +msgid "Wrong Value for 'weekday'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:358 +msgid "Wrong Value for 'weekday occurence'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:368 +msgid "Wrong Value for interval" +msgstr "" + +#: tracrendezvous/event/web_ui.py:378 +msgid "Wrong Value for count" +msgstr "" + +#: tracrendezvous/event/web_ui.py:387 +msgid "Wrong Value for until_date or until_time" +msgstr "" + +#: tracrendezvous/event/web_ui.py:396 +msgid "Please select one of the recurrency types" +msgstr "" + +#: tracrendezvous/event/web_ui.py:478 +msgid "Any days selected." +msgstr "" + +#: tracrendezvous/event/web_ui.py:502 +msgid "Wrong recurrency exception date format" +msgstr "" + +#: tracrendezvous/event/web_ui.py:526 +msgid "You have to specify a valid recurrency exception date for that operation!" +msgstr "" + +#: tracrendezvous/event/web_ui.py:589 +msgid "Event edited successfully. Back to " +msgstr "" + +#: tracrendezvous/event/web_ui.py:589 tracrendezvous/event/web_ui.py:593 +#: tracrendezvous/event/web_ui.py:597 +msgid "Overview" +msgstr "" + +#: tracrendezvous/event/web_ui.py:597 +msgid "Event deleted successfully. Back to " +msgstr "" + +#: tracrendezvous/event/web_ui.py:609 tracrendezvous/event/web_ui.py:712 +#: tracrendezvous/event/web_ui.py:731 tracrendezvous/event/web_ui.py:758 +msgid "Add New Event" +msgstr "" + +#: tracrendezvous/event/web_ui.py:610 tracrendezvous/event/web_ui.py:620 +#: tracrendezvous/event/web_ui.py:732 +msgid "Events Overview" +msgstr "" + +#: tracrendezvous/event/web_ui.py:644 +msgid "Wrong format in 'Date begin'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:650 +msgid "Wrong format in 'Date end'." +msgstr "" + +#: tracrendezvous/event/web_ui.py:656 +msgid "Could not find location" +msgstr "" + +#: tracrendezvous/event/web_ui.py:680 +msgid "Wikipage already exists." +msgstr "" + +#: tracrendezvous/event/web_ui.py:716 +msgid "Event Details" +msgstr "" + +#: tracrendezvous/event/web_ui.py:720 +msgid "Event Overview" +msgstr "" + +#: tracrendezvous/event/web_ui.py:727 +msgid "CTDO: Upcoming Events" +msgstr "" + +#: tracrendezvous/event/web_ui.py:746 +msgid "Upcoming Events for" +msgstr "" + +#: tracrendezvous/event/web_ui.py:750 +msgid "Back to Upcoming Events" +msgstr "" + +#: tracrendezvous/event/web_ui.py:760 +#, python-format +msgid "Details of %s" +msgstr "" + +#: tracrendezvous/event/web_ui.py:795 +msgid "(unknown template location)" +msgstr "" + +#: tracrendezvous/event/web_ui.py:796 +#, python-format +msgid "Genshi %(error)s error while rendering template %(location)s" +msgstr "" + +#: tracrendezvous/event/web_ui.py:804 +msgid "EventValidationError: Title is empty" +msgstr "" + +#: tracrendezvous/event/web_ui.py:806 +msgid "EventValidationError: end time before or equal begin time" +msgstr "" + +#: tracrendezvous/event/web_ui.py:854 +msgid "date" +msgstr "" + +#: tracrendezvous/event/web_ui.py:968 +msgid "Start" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:29 +#: tracrendezvous/event/templates/event_edit.html:29 +msgid "Title:" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:30 +#: tracrendezvous/event/templates/event_edit.html:30 +#: tracrendezvous/rendezvous/templates/date.html:67 +msgid "Date begin:" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:33 +#: tracrendezvous/event/templates/event_edit.html:33 +#: tracrendezvous/rendezvous/templates/date.html:69 +msgid "Date end:" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:36 +#: tracrendezvous/event/templates/event_edit.html:36 +msgid "Locations:" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:40 +#: tracrendezvous/event/templates/event_edit.html:40 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:58 +msgid "edit/search locations" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:42 +#: tracrendezvous/event/templates/alarm_edit.html:43 +#: tracrendezvous/event/templates/event_edit.html:42 +#: tracrendezvous/event/templates/event_edit.html:43 +msgid "show recurrency options" +msgstr "" + +#: tracrendezvous/event/templates/alarm_edit.html:48 +#: tracrendezvous/event/templates/event_edit.html:48 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:74 +msgid "Tags" +msgstr "" + +#: tracrendezvous/event/templates/event_details.html:34 +#: tracrendezvous/event/templates/events.html:40 +msgid "No events in this time frame. Create here" +msgstr "" + +#: tracrendezvous/event/templates/event_details.html:34 +#: tracrendezvous/event/templates/events.html:40 +msgid " a new event" +msgstr "" + +#: tracrendezvous/event/templates/event_display.html:5 +#: tracrendezvous/rendezvous/templates/rendezvous.html:84 +#: tracrendezvous/rendezvous/templates/rendezvous.html:116 +msgid "edit" +msgstr "" + +#: tracrendezvous/event/templates/event_display.html:25 +msgid "wiki page" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +msgid "C" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "I" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "R" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "U" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "L" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:18 +#: tracrendezvous/event/templates/event_list.html:19 +msgid "A" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:19 +msgid "S" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:19 +msgid "N" +msgstr "" + +#: tracrendezvous/event/templates/event_list.html:19 +msgid "G" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:1 +msgid "" +"BEGIN:VCALENDAR\n" +"PRODID://TracRendezVous-0.3 by Stefan Kögl //DE\n" +"VERSION:2.0\n" +"CALSCALE:GREGORIAN\n" +"CAL-ADDRESS:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:5 +msgid "" +"METHOD:PUBLISH\n" +"X-WR-CALNAME:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:7 +msgid "" +"X-WR-TIMEZONE:UTC\n" +"X-WR-CALDESC:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:9 +#, python-format +msgid "" +"{% for event in events %}\\\n" +"BEGIN:VEVENT\n" +"UID:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:12 +msgid "CREATED:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:13 +msgid "DTSTAMP:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:14 +msgid "LAST-MODIFIED:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:15 +msgid "SUMMARY:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:16 +msgid "LOCATION:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:17 +msgid "GEO:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:18 +msgid "URI:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:19 +msgid "DESCRIPTION:more information:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:20 +msgid "" +"CLASS:PUBLIC\n" +"DTSTART:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:22 +#, python-format +msgid "{% for rrule in event.rrules %}\\" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:23 +#, python-format +msgid "" +"{% end %}\\\n" +"{% for alarm in event.alarms %}\\" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:25 +#, python-format +msgid "" +"{% end %}\\\n" +"DTEND:" +msgstr "" + +#: tracrendezvous/event/templates/ical.txt:27 +#, python-format +msgid "" +"TRANSP:OPAQUE\n" +"END:VEVENT\n" +"{% end %}\\\n" +"END:VCALENDAR" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:53 +msgid "Recurrency Options for event '" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:59 +msgid "Repeat this event" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:61 +msgid "Recurrency frequency" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:64 +msgid "Yearly" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:66 +#: tracrendezvous/event/templates/recur_edit.html:84 +#: tracrendezvous/event/templates/recur_edit.html:99 +#: tracrendezvous/event/templates/recur_edit.html:110 +msgid "Repeat every" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:67 +msgid "Repeat on day" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:69 +#: tracrendezvous/event/templates/recur_edit.html:74 +msgid "in" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:72 +#: tracrendezvous/event/templates/recur_edit.html:87 +#: tracrendezvous/event/templates/recur_edit.html:89 +msgid "Repeat on" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:77 +msgid "Repeat on day no." +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:77 +msgid "of the year" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:81 +msgid "Monthly" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:88 +msgid "day of the month" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:96 +msgid "Weekly" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:107 +msgid "Daily" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:116 +msgid "Recurrency Timeframe" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:118 +msgid "Finish after" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:118 +msgid "Repetitions" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:119 +msgid "End on:" +msgstr "" + +#: tracrendezvous/event/templates/recur_edit.html:123 +msgid "Recurrency Exceptions" +msgstr "" + +#: tracrendezvous/location/web_ui.py:79 +msgid "Location edited successfully." +msgstr "" + +#: tracrendezvous/location/web_ui.py:88 +msgid "Location created successfully. Back to " +msgstr "" + +#: tracrendezvous/location/web_ui.py:90 +msgid "Location created successfully." +msgstr "" + +#: tracrendezvous/location/templates/location.html:94 +#: tracrendezvous/rendezvous/templates/location.html:49 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:54 +msgid "Locations" +msgstr "" + +#: tracrendezvous/location/templates/location.html:101 +#: tracrendezvous/rendezvous/templates/location.html:57 +msgid "Known Locations" +msgstr "" + +#: tracrendezvous/location/templates/location.html:103 +#: tracrendezvous/rendezvous/templates/location.html:59 +msgid "Name" +msgstr "" + +#: tracrendezvous/location/templates/location.html:103 +#: tracrendezvous/rendezvous/templates/location.html:59 +#: tracrendezvous/rendezvous/templates/rendezvous.html:37 +msgid "Coordinates" +msgstr "" + +#: tracrendezvous/location/templates/location.html:103 +#: tracrendezvous/rendezvous/templates/admin_types.html:66 +#: tracrendezvous/rendezvous/templates/location.html:59 +msgid "Default" +msgstr "" + +#: tracrendezvous/location/templates/location.html:103 +#: tracrendezvous/rendezvous/templates/location.html:59 +msgid "Delete" +msgstr "" + +#: tracrendezvous/location/templates/location.html:113 +#: tracrendezvous/rendezvous/templates/location.html:69 +msgid "Change or delete existing locations." +msgstr "" + +#: tracrendezvous/location/templates/location.html:122 +#: tracrendezvous/rendezvous/templates/location.html:78 +msgid "New Location" +msgstr "" + +#: tracrendezvous/location/templates/location.html:124 +#: tracrendezvous/rendezvous/templates/location.html:80 +msgid "Name:" +msgstr "" + +#: tracrendezvous/location/templates/location.html:125 +#: tracrendezvous/rendezvous/templates/location.html:81 +msgid "Coordinates:" +msgstr "" + +#: tracrendezvous/location/templates/location.html:132 +#: tracrendezvous/rendezvous/templates/location.html:88 +msgid "Allowed coordinates formats:" +msgstr "" + +#: tracrendezvous/location/templates/location.html:134 +#: tracrendezvous/rendezvous/templates/location.html:90 +msgid "DD format = 'lat,lon', e.g 53.235235,6.235235" +msgstr "" + +#: tracrendezvous/location/templates/location.html:135 +#: tracrendezvous/rendezvous/templates/location.html:91 +msgid "" +"DMS Format = 'N|Wdd°mm'ss\" E|Wdddd°mm'ss\", e.g N51°31'39.40000\" " +"E7°27'53.7200\"" +msgstr "" + +#: tracrendezvous/location/templates/location.html:142 +#: tracrendezvous/rendezvous/templates/location.html:98 +msgid "Location Search" +msgstr "" + +#: tracrendezvous/location/templates/location.html:144 +#: tracrendezvous/rendezvous/templates/location.html:100 +msgid "Location Search:" +msgstr "" + +#: tracrendezvous/location/templates/location.html:148 +#: tracrendezvous/rendezvous/templates/location.html:104 +msgid "e.g \"central station, cityname\"" +msgstr "" + +#: tracrendezvous/location/templates/location.html:150 +#: tracrendezvous/rendezvous/templates/location.html:106 +msgid "Search Results" +msgstr "" + +#: tracrendezvous/location/templates/location.html:157 +#: tracrendezvous/rendezvous/templates/location.html:113 +msgid "km away of" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:206 +msgid "Current state no longer exists" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:208 +msgid "" +"The rendezvous will be prepared for a new voting pass. All comments, " +"dates and votes for that item will be deleted. You have to publish it " +"again to become visible for other users" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:210 +msgid "The rendezvous will be published for voting" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:213 +msgid "to " +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:217 +msgid "The rendezvous will be scheduled (no more voting, but remains visible)" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:219 +msgid "" +"This status may be ignored, since I can handle RendezVous starts " +"automatically, but you may wish to start manually before the scheduled " +"beginning date and time" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:221 +msgid "" +"This status may be ignored, since I can handle expired RendezVouses " +"automatically, but you may wish to stop manually before the scheduled " +"date and time. It's an end status" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:223 +msgid "The rendezvous will not takes place. It's an end status" +msgstr "" + +#: tracrendezvous/rendezvous/workflow.py:228 +#, python-format +msgid "Next status will be '%s'" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:9 +msgid "RendezVous General Settings" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:12 +msgid "General Settings" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:15 +#: tracrendezvous/rendezvous/templates/overview.html:9 +#: tracrendezvous/rendezvous/templates/rendezvous.html:9 +msgid "RendezVous" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:17 +msgid "max description length:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:22 +msgid "max votes per date:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:27 +msgid "max dates per rendezvous:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:32 +msgid "graph width:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:37 +msgid "graph height:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:42 +msgid "show vote graph:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_general.html:47 +msgid "show location map:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_overview.html:17 +#: tracrendezvous/rendezvous/templates/admin_overview.html:20 +msgid "Overview Configuration" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_overview.html:24 +msgid "wrong position - should be" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_overview.html:28 +msgid "help" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:13 +msgid "Manage RendezVousTypes" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:17 +msgid "Add Type:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:20 +msgid "RendezVous Type:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:24 +msgid "Create a new RendezVous Type. Note that" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:25 +#: tracrendezvous/rendezvous/templates/admin_types.html:90 +msgid "RendezVousType" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:25 +msgid "" +"names can't be all upper-case,\n" +" as that is reserved for permission names." +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:36 +msgid "Grant Permission To Type:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:39 +msgid "RendezVousType:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:46 +msgid "Permission:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:53 +msgid "Grant permission to a RendezVousType." +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:66 +msgid "RendezVous Type" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:66 +msgid "Permission" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:66 +#: tracrendezvous/rendezvous/templates/date.html:37 +#: tracrendezvous/rendezvous/templates/vote.html:37 +msgid "delete" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:89 +msgid "Note that" +msgstr "" + +#: tracrendezvous/rendezvous/templates/admin_types.html:90 +msgid "" +"names can't be all upper-case,\n" +" as that is reserved for permission names." +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:17 +msgid "Dates" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:26 +#: tracrendezvous/rendezvous/templates/date.html:86 +msgid "Dates for" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:31 +#: tracrendezvous/rendezvous/templates/date.html:90 +msgid "author" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:32 +#: tracrendezvous/rendezvous/templates/vote.html:33 +msgid "email" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:33 +msgid "date begin" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:34 +msgid "time begin" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:35 +msgid "date end" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:36 +msgid "time end" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:54 +#: tracrendezvous/rendezvous/templates/vote.html:81 +msgid "Change or delete votes for an existing rendezvous date." +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:63 +msgid "New Date" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:66 +#: tracrendezvous/rendezvous/templates/vote.html:91 +msgid "Email:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:71 +msgid "Vote that for me!" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:80 +#: tracrendezvous/rendezvous/templates/vote.html:112 +msgid "Allowed date/time formats:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:82 +msgid "yyyymmdd" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:83 +msgid "yyyy.mm.dd" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:91 +msgid "day" +msgstr "" + +#: tracrendezvous/rendezvous/templates/date.html:92 +msgid "day part" +msgstr "" + +#: tracrendezvous/rendezvous/templates/overview.html:19 +msgid "new" +msgstr "" + +#: tracrendezvous/rendezvous/templates/overview.html:25 +msgid "voting" +msgstr "" + +#: tracrendezvous/rendezvous/templates/overview.html:31 +msgid "canceled" +msgstr "" + +#: tracrendezvous/rendezvous/templates/overview.html:37 +msgid "expired" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:16 +msgid "RendezVous #" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:17 +msgid "Created on" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:17 +#: tracrendezvous/rendezvous/templates/rendezvous.html:116 +#: tracrendezvous/rendezvous/templates/rendezvous.html:126 +msgid "by" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:21 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:46 +msgid "Type" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:25 +msgid "Min. Votes" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:29 +msgid "Tagged with" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:33 +msgid "Location" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:44 +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:60 +msgid "Description" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:47 +msgid "Votings" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:50 +msgid "Date proposals ->" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:53 +msgid "scheduled" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:58 +msgid "new date" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:64 +msgid "VoteCount" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:81 +#: tracrendezvous/rendezvous/templates/rendezvous.html:97 +msgid "quick and dirty voting for the date" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:81 +#: tracrendezvous/rendezvous/templates/rendezvous.html:97 +msgid "vote" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:82 +#: tracrendezvous/rendezvous/templates/rendezvous.html:98 +msgid "advanced voting operations" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:82 +#: tracrendezvous/rendezvous/templates/rendezvous.html:98 +msgid "...advanced" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:106 +msgid "Vote Distribution By RendezVousDate" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:114 +msgid "Comments" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:116 +#: tracrendezvous/rendezvous/templates/rendezvous.html:126 +msgid "added on" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:120 +msgid "New Comment" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:123 +msgid "Preview!" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:140 +msgid "Note:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:140 +msgid "See" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:140 +msgid "WikiFormatting" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:140 +msgid "and" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:141 +msgid "TracWiki" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous.html:141 +msgid "for help on editing wiki content." +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:38 +msgid "Title" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:39 +msgid "Author" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:45 +msgid "Email" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:47 +msgid "Minimum votes" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:50 +msgid "Voting deadline" +msgstr "" + +#: tracrendezvous/rendezvous/templates/rendezvous_edit.html:65 +msgid "Workflow" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:17 +msgid "Votes" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:25 +msgid "All votes for" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:26 +msgid "Votes for" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:26 +msgid "made by" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:32 +msgid "user" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:34 +msgid "begin" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:35 +msgid "end" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:84 +msgid "Add new vote for" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:87 +msgid "User:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:95 +msgid "Date Begin:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:100 +msgid "Date End:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:110 +msgid "Add a vote to an existing rendezvous date." +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:114 +msgid "time:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:116 +msgid "'hhMM'" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:117 +msgid "'hh:MM'" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:120 +msgid "date:" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:122 +msgid "'yyyymmdd'" +msgstr "" + +#: tracrendezvous/rendezvous/templates/vote.html:123 +msgid "'yyyy.mm.dd'" +msgstr "" + diff --git a/TracRendezVous/tracrendezvous/location/__init__.py b/TracRendezVous/tracrendezvous/location/__init__.py new file mode 100644 index 0000000..98b6d1f --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from tracrendezvous.location.web_ui import * +from tracrendezvous.location.model import * diff --git a/TracRendezVous/tracrendezvous/location/htdocs/css/location.css b/TracRendezVous/tracrendezvous/location/htdocs/css/location.css new file mode 100644 index 0000000..c165a9b --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/htdocs/css/location.css @@ -0,0 +1,18 @@ + +#main, #content {margin:0 !important;padding:0 !important;left:0;} +/* form.location {margin:auto;width:600px;text-align:center;} */ +#location, #new-location, #location-search {display:inline-block;} +/* #new-location {clear:right;} */ +/* #location {float:left;} */ + +a.location,a.location:visited,a.location:hover +{ +color:#bb0000; +border-bottom-width:1px; +border-bottom-style:dotted; +border-bottom-color:#ff0000; +cursor:pointer; +} + +div.hint {font-family:cursive;background:#aaa;color:#000 !important;} +div.hint h3 {font-size:110%;color:#000;padding:1em;} diff --git a/TracRendezVous/tracrendezvous/location/htdocs/css/map.css b/TracRendezVous/tracrendezvous/location/htdocs/css/map.css new file mode 100644 index 0000000..28b6d52 --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/htdocs/css/map.css @@ -0,0 +1,43 @@ +html,body { + background-color: #000000; + height: 100%; + width: 100%; + margin: 1%; padding: 0; + font-family: Verdana, Arial; + font-size: 1em; + overflow: hidden; + color: #ffffff; +} + +a { + color: #ffff00; + text-decoration: none; +} + +a:hover { + color: #ffff00; + text-decoration: underline; +} + +#header { + font-family: Verdana, Arial; + font-size: 1em; + overflow: hidden; + color: #ffffff; +} + +#map { + height: 86%; + width: 96%; + padding: 0; margin: 0; +} + +#content { + font-size: 1em; +} + +#osm { + font-size: 0.7em; + font-style: italic; + margin-bottom: 20px; +} \ No newline at end of file diff --git a/TracRendezVous/tracrendezvous/location/htdocs/script/tom.js b/TracRendezVous/tracrendezvous/location/htdocs/script/tom.js new file mode 100644 index 0000000..1fc3275 --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/htdocs/script/tom.js @@ -0,0 +1,63 @@ +function jumpTo(lon, lat, zoom) { + var x = Lon2Merc(lon); + var y = Lat2Merc(lat); + map.setCenter(new OpenLayers.LonLat(x, y), zoom); + return false; +} + +function Lon2Merc(lon) { + return 20037508.34 * lon / 180; +} + +function Lat2Merc(lat) { + var PI = 3.14159265358979323846; + lat = Math.log(Math.tan( (90 + lat) * PI / 360)) / (PI / 180); + return 20037508.34 * lat / 180; +} + +function addMarker(layer, lon, lat, popupContentHTML) { + + var ll = new OpenLayers.LonLat(Lon2Merc(lon), Lat2Merc(lat)); + var feature = new OpenLayers.Feature(layer, ll); + feature.closeBox = true; + feature.popupClass = OpenLayers.Class(OpenLayers.Popup.FramedCloud, {minSize: new OpenLayers.Size(300, 180) } ); + feature.data.popupContentHTML = popupContentHTML; + feature.data.overflow = "hidden"; + + var marker = new OpenLayers.Marker(ll); + marker.feature = feature; + + var markerClick = function(evt) { + if (this.popup == null) { + this.popup = this.createPopup(this.closeBox); + map.addPopup(this.popup); + this.popup.show(); + } else { + this.popup.toggle(); + } + OpenLayers.Event.stop(evt); + }; + marker.events.register("mousedown", feature, markerClick); + + layer.addMarker(marker); + map.addPopup(feature.createPopup(feature.closeBox)); +} + +function getCycleTileURL(bounds) { + var res = this.map.getResolution(); + var x = Math.round((bounds.left - this.maxExtent.left) / (res * this.tileSize.w)); + var y = Math.round((this.maxExtent.top - bounds.top) / (res * this.tileSize.h)); + var z = this.map.getZoom(); + var limit = Math.pow(2, z); + + if (y < 0 || y >= limit) + { + return null; + } + else + { + x = ((x % limit) + limit) % limit; + + return this.url + z + "/" + x + "/" + y + "." + this.type; + } +} \ No newline at end of file diff --git a/TracRendezVous/tracrendezvous/location/loc_xml.py b/TracRendezVous/tracrendezvous/location/loc_xml.py new file mode 100644 index 0000000..5429067 --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/loc_xml.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +import httplib +from trac.util.text import unicode_urlencode +from genshi import escape + +try: + import xml.etree.ElementTree as ET +except: + try: + import elementtree.ElementTree as ET + except: + import celementtree.ElementTree as ET + +def search_location(query): + conn = httplib.HTTPConnection("gazetteer.openstreetmap.org") + conn.request(u"GET", u"/namefinder/search.xml?%s" % unicode_urlencode({u'find': query})) + xmlData = conn.getresponse() + tree = ET.parse(xmlData) + return tree.findall("named") diff --git a/TracRendezVous/tracrendezvous/location/model.py b/TracRendezVous/tracrendezvous/location/model.py new file mode 100644 index 0000000..ded2b99 --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/model.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- + +from trac.core import * +from trac.env import IEnvironmentSetupParticipant +from trac.db import Table, Column, Index +from trac.search.api import search_to_sql + +from ctdotools.utils import validate_id + +__all__ = ['LocationModelProvider', 'ItemLocation'] + +class ItemLocation(object): + + def __init__(self, env, location_id=0, name=None, lat_side=None, lat_deg=None, lat_min=None, lat_sec=None, lon_side=None, lon_deg=None, lon_min=None, lon_sec=None, lat=None, lon=None): + self.env = env + self.location_id = location_id + self.name = name + self.lat_side = lat_side + self.lat_deg = lat_deg + self.lat_min = lat_min + self.lat_sec = lat_sec + self.lon_side = lon_side + self.lon_deg = lon_deg + self.lon_min = lon_min + self.lon_sec = lon_sec + self.lat = lat + self.lon = lon + + @staticmethod + def fetch_all(env): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * FROM item_location") + rows = cursor.fetchall() + if not rows: + return [] + res = [] + for row in rows: + res.append(ItemLocation(env, *row)) + return res + + @staticmethod + def fetch_one(env, location_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * FROM item_location WHERE location_id=%s", + (location_id,)) + row = cursor.fetchone() + if not row: + return None + return ItemLocation(env, *row) + + @staticmethod + def fetch_by_name(env, name): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * FROM item_location WHERE name=%s", + (name,)) + row = cursor.fetchone() + if not row: + return None + return ItemLocation(env, *row) + + @staticmethod + def search_one(env, name): + db = env.get_db_cnx() + cursor = db.cursor() + sql, params = search_to_sql(db, ["name",], [name,]) + cursor.execute("SELECT * FROM item_location WHERE " + sql, params) + row = cursor.fetchone() + if not row: + return None + return ItemLocation(env, *row) + + @staticmethod + def exists(env, name): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""SELECT * + FROM 'item_location' + WHERE name=%s""", (name,)) + row = cursor.fetchone() + return row != None + + def commit(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""INSERT INTO item_location + (name,lat_side,lat_deg,lat_min,lat_sec,lon_side,lon_deg,lon_min,lon_sec,lat,lon) + VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""", (self.name, self.lat_side, self.lat_deg, self.lat_min, self.lat_sec, self.lon_side, self.lon_deg, self.lon_min, self.lon_sec, self.lat, self.lon)) + db.commit() + self.location_id = db.get_last_id(cursor, 'item_location') + + def update(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""UPDATE item_location + SET name =%s, + lat_side=%s, + lat_deg=%s, + lat_min=%s, + lat_sec=%s, + lon_side=%s, + lon_deg=%s, + lon_min=%s, + lon_sec=%s, + lat=%s, + lon=%s + WHERE location_id=%s""", (self.name, self.lat_side, self.lat_deg, self.lat_min, self.lat_sec, self.lon_side, self.lon_deg, self.lon_min, self.lon_sec, self.lat, self.lon, self.location_id)) + db.commit() + + @staticmethod + def delete(env, location_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("DELETE FROM item_location WHERE location_id=%s", (location_id,)) + db.commit() + + def coordinate_str(self): + if self.lat == None: + return u"" + return u"%s%d°%d'%.5f\" %s%d°%d'%.5f\""%(self.lat_side, self.lat_deg, self.lat_min, self.lat_sec, self.lon_side, self.lon_deg, self.lon_min, self.lon_sec) + + def __str__(self): + return "" % (self.location_id, self.name) + +class LocationModelProvider(Component): + implements(IEnvironmentSetupParticipant) + + def environment_created(self): + + self._create_models(self.env.get_db_cnx()) + + def environment_needs_upgrade(self, db): + """First version - nothing to migrate, but possibly to create. + """ + + cursor = db.cursor() + try: + cursor.execute("select count(*) from item_location;") + cursor.fetchone() + return False + except: + db.rollback() + return True + + def upgrade_environment(self, db): + """ nothing to do here for now + """ + self._create_models(db) + + def _create_models(self, db): + + """Called when a new Trac environment is created.""" + + db_backend = None + try: + from trac.db import DatabaseManager + db_backend, _ = DatabaseManager(self.env)._get_connector() + except ImportError: + db_backend = self.env.get_db_cnx() + + cursor = db.cursor() + t = Table('item_location', key='location_id')[ + Column('location_id', auto_increment=True), + Column('name'), + Column('lat_side'), + Column('lat_deg', type='int'), + Column('lat_min', type='int'), + Column('lat_sec', type='real'), + Column('lon_side'), + Column('lon_deg', type='int'), + Column('lon_min', type='int'), + Column('lon_sec', type='real'), + Column('lat', type='real'), + Column('lon', type='real'), + Index(['name'])] + for stmt in db_backend.to_sql(t): + self.env.log.debug(stmt) + try: + cursor.execute(stmt) + db.commit() + except Exception, e: + self.env.log.warning(str(e)) + db.rollback() + + #LOCATION_DATA = ( + #(u"CTDO, Langer August", "N", 51, 31, 39.4, "E", 7, 27, 53.8, 51.527611, 7.464922), + #(u"WILA, Langer August", "N", 51, 31, 39.4, "E", 7, 27, 53.8, 51.527611, 7.464922)) + #cursor.executemany("""INSERT INTO 'item_location' + #(name,lat_side,lat_deg,lat_min,lat_sec,lon_side,lon_deg,lon_min,lon_sec, lat, lon) + #VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""", LOCATION_DATA) + #db.commit() diff --git a/TracRendezVous/tracrendezvous/location/templates/location.html b/TracRendezVous/tracrendezvous/location/templates/location.html new file mode 100644 index 0000000..3d9c4d5 --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/templates/location.html @@ -0,0 +1,169 @@ + + + + + + Locations + + +
    +
    + +
    + Known Locations + + + + + + + + + + +
    NameCoordinatesDefaultDelete
    +

    Change or delete existing locations.

    +
    + + +
    +
    +
    +
    +
    + New Location + + + +
    +
    + + +
    +
    +

    Allowed coordinates formats:

    +
      +
    • DD format = 'lat,lon', e.g 53.235235,6.235235
    • +
    • DMS Format = 'N|Wdd°mm'ss" E|Wdddd°mm'ss", e.g N51°31'39.40000" E7°27'53.7200"
    • +
    +
    +
    +
    +
    + +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/location/tests/__init__.py b/TracRendezVous/tracrendezvous/location/tests/__init__.py new file mode 100644 index 0000000..b93b16e --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/tests/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import doctest +import unittest +from tracrendezvous.location.tests import model + +def suite(): + suite = unittest.TestSuite() + suite.addTest(model.suite()) + return suite + +if __name__ == '__main__': + import sys + if '--skip-functional-tests' in sys.argv: + sys.argv.remove('--skip-functional-tests') + INCLUDE_FUNCTIONAL_TESTS = False + unittest.main(defaultTest='suite') diff --git a/TracRendezVous/tracrendezvous/location/tests/model.py b/TracRendezVous/tracrendezvous/location/tests/model.py new file mode 100644 index 0000000..23c209e --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/tests/model.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +import os.path +import shutil +import tempfile +import unittest + +from trac.test import EnvironmentStub, Mock +from trac.search.api import * + +from tracrendezvous.location.model import ItemLocation, LocationModelProvider + +class ItemLocationTestCase(unittest.TestCase): + def setUp(self): + #self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'tracrendezvous.location.*']) + l = LocationModelProvider(self.env).environment_created() + loc1 = ItemLocation(self.env, 0, u"CTDO, Langer August", "N", 51, 31, 39.4, "E", 7, 27, 53.8, 51.527611, 7.464922) + loc1.commit() + loc2 = ItemLocation(self.env, 0, u"WILA, Langer August", "N", 51, 31, 39.4, "E", 7, 27, 53.8, 51.527611, 7.464922) + loc2.commit() + + def test_1_commit(self): + self.env.get_db_cnx().cursor().execute("select name from item_location;").fetchall() + + def test_2_fetch_one(self): + loc1 = ItemLocation.fetch_one(self.env, 1) + self.assertEqual(1, loc1.location_id) + self.assertEqual(u"CTDO, Langer August", loc1.name) + self.assertEqual(u"N", loc1.lat_side) + self.assertEqual(51, loc1.lat_deg) + self.assertEqual(31, loc1.lat_min) + self.assertEqual(39.4, loc1.lat_sec) + self.assertEqual("E", loc1.lon_side) + self.assertEqual(7, loc1.lon_deg) + self.assertEqual(27, loc1.lon_min) + self.assertEqual(53.8, loc1.lon_sec) + self.assertEqual(51.527611, loc1.lat) + self.assertEqual(7.464922, loc1.lon) + + def test_3_search_one(self): + loc2 = ItemLocation.search_one(self.env, u"WILA") + self.assertEqual(2, loc2.location_id) + self.assertEqual(u"WILA, Langer August", loc2.name) + self.assertEqual(u"N", loc2.lat_side) + self.assertEqual(51, loc2.lat_deg) + self.assertEqual(31, loc2.lat_min) + self.assertEqual(39.4, loc2.lat_sec) + self.assertEqual("E", loc2.lon_side) + self.assertEqual(7, loc2.lon_deg) + self.assertEqual(27, loc2.lon_min) + self.assertEqual(53.8, loc2.lon_sec) + self.assertEqual(51.527611, loc2.lat) + self.assertEqual(7.464922, loc2.lon) + + def test_4_fetch_all(self): + locs = ItemLocation.fetch_all(self.env) + self.assertEqual(1, locs[0].location_id) + self.assertEqual(u"CTDO, Langer August", locs[0].name) + self.assertEqual(u"N", locs[0].lat_side) + self.assertEqual(51, locs[0].lat_deg) + self.assertEqual(31, locs[0].lat_min) + self.assertEqual(39.4, locs[0].lat_sec) + self.assertEqual("E", locs[0].lon_side) + self.assertEqual(7, locs[0].lon_deg) + self.assertEqual(27, locs[0].lon_min) + self.assertEqual(53.8, locs[0].lon_sec) + self.assertEqual(51.527611, locs[0].lat) + self.assertEqual(7.464922, locs[0].lon) + self.assertEqual(2, locs[1].location_id) + self.assertEqual(u"WILA, Langer August", locs[1].name) + self.assertEqual(u"N", locs[1].lat_side) + self.assertEqual(51, locs[1].lat_deg) + self.assertEqual(31, locs[1].lat_min) + self.assertEqual(39.4, locs[1].lat_sec) + self.assertEqual("E", locs[1].lon_side) + self.assertEqual(7, locs[1].lon_deg) + self.assertEqual(27, locs[1].lon_min) + self.assertEqual(53.8, locs[1].lon_sec) + self.assertEqual(51.527611, locs[1].lat) + self.assertEqual(7.464922, locs[1].lon) + + def test_5_exists(self): + self.assertEqual(True, ItemLocation.exists(self.env, u"WILA, Langer August")) + self.assertEqual(False, ItemLocation.exists(self.env, u"WILA")) + + def test_6_update(self): + loc = ItemLocation.fetch_one(self.env, 1) + loc.name = "foo" + loc.lat_side = "Q" + loc.lat_deg = 1 + loc.lat_min = 1 + loc.lat_sec = 1 + loc.lon_side = "Q" + loc.lon_deg = 1 + loc.lon_min = 1 + loc.lon_sec = 1 + loc.update() + self.assertEqual(loc.lat_side , "Q") + self.assertEqual(loc.lat_deg , 1) + self.assertEqual(loc.lat_min , 1) + self.assertEqual(loc.lat_sec , 1) + self.assertEqual(loc.lon_side , "Q") + self.assertEqual(loc.lon_deg , 1) + self.assertEqual(loc.lon_min , 1) + self.assertEqual(loc.lon_sec , 1) + + def test_7_delete(self): + ItemLocation.delete(self.env, 1) + self.assertEqual(None, ItemLocation.fetch_one(self.env, 1)) + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ItemLocationTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/TracRendezVous/tracrendezvous/location/utils.py b/TracRendezVous/tracrendezvous/location/utils.py new file mode 100644 index 0000000..bc0971a --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/utils.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +from re import compile as re_compile +from re import match +from decimal import Decimal, Context, getcontext +from datetime import date, time, datetime, timedelta +from os.path import join, dirname +from os.path import exists as path_exists +from os import mkdir +from sys import maxint + +from trac.util import Ranges +from trac.wiki import WikiPage, WikiSystem +from trac.util.datefmt import utc, to_timestamp, localtz, format_time, get_timezone, timezone +from PIL import Image, ImageDraw, ImageFont + +__all__ = ["validate_dd_coordinates", "validate_dms_coordinates", "convert_dms_2_dd", "convert_dd_2_dms"] + +class ValidationError(ValueError): + def __str__(self): + return "ValidationError: value out of bounds!" + + +def validate_dms_coordinates(value): + mytest=re_compile(u"(N|S)(\d{1,2})°(\d{1,2})'(\d{1,2}\.\d{1,5})\" (E|W)(\d{1,3})°(\d{1,2})'(\d{1,2}\.\d{1,5})\"$") + m = mytest.match(value) + if not m: + raise ValueError(u"validate_rendezvous(): coordinates have wrong format:") + groups = m.groups() + if 0 > groups[1] > 90.0: + raise ValueError(u"validate_rendezvous(): lat not valid:") + if 0 > groups[5] > 180.0: + raise ValueError(u"validate_rendezvous(): lon not valid:") + return groups + + +def validate_dd_coordinates(value): + mytest=re_compile(u"(-?\d{1,3}\.\d{1,8}),(-?\d{1,3}\.\d{1,8})$") + m = mytest.match(value) + if not m: + raise ValueError(u"validate_rendezvous(): coordinates have wrong format:") + lat, lon = m.groups() + if 0 > lat > 90.0: + raise ValueError(u"validate_rendezvous(): lat not valid:") + if 0 > lon > 180.0: + raise ValueError(u"validate_rendezvous(): lon not valid:") + return lat, lon + + +def convert_dms_2_dd(ns,a,b,c,ew,d,e,f): + getcontext().prec = 20 + r1 = Decimal(a) + Decimal(b) / 60 + Decimal(str(c))/3600 + r2 = Decimal(d) + Decimal(e) / 60 + Decimal(str(f))/3600 + if ns == u"S": + r1=-r1 + if ew == u"W": + r2=-r2 + a = round(r1,6) + b = round(r2,6) + return a, b + + +def convert_dd_2_dms(lat, lon): + def convert(value, pos, neg): + getcontext().prec = 32 + dValue = Decimal(value) + tValue = dValue.as_tuple() + valueDir = tValue[0] and neg or pos + # extracting full degrees, keep in mind we want an int + degValue = Decimal((0, tValue[1][:tValue[2]], 0)) + # extracting minutes as int + tMinValueRemainder = (Decimal((0, tValue[1][tValue[2]:], tValue[2])) * 60).as_tuple() + minValue = Decimal((0, tMinValueRemainder[1][:tMinValueRemainder[2]], 0)) + # extracting sec, we want the remaining seconds as float + secValueRemainder = Decimal((0, tMinValueRemainder[1][tMinValueRemainder[2]:], tMinValueRemainder[2])) + secValueRemainder = secValueRemainder * 60 + return valueDir, int(degValue), int(minValue), float(secValueRemainder) + return convert(lat, "N", "S") + convert(lon, "E", "W") diff --git a/TracRendezVous/tracrendezvous/location/web_ui.py b/TracRendezVous/tracrendezvous/location/web_ui.py new file mode 100644 index 0000000..4a4ac6d --- /dev/null +++ b/TracRendezVous/tracrendezvous/location/web_ui.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime, timedelta +from os import mkdir +from os.path import join +from re import match, sub +from collections import defaultdict +from sqlite3 import IntegrityError +from operator import attrgetter +from trac.config import * +from trac.core import Component, implements, TracError +from trac.perm import PermissionError, IPermissionRequestor +from trac.resource import Resource, get_resource_url, get_resource_name +from trac.util import get_reporter_id +from trac.util.text import to_unicode +from trac.util.datefmt import utc, get_timezone, localtz, timezone +from trac.util.translation import _ +from trac.util.html import html +from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet, add_warning, add_notice, add_ctxtnav, add_script +from trac.web import IRequestHandler +from genshi.builder import tag + +from tracrendezvous.location.loc_xml import * +from tracrendezvous.location.model import * +from tracrendezvous.location.utils import * + +__all__ = ['LocationModule',] + +class LocationModule(Component): + + '''The web ui frontend for the location management system''' + + implements(INavigationContributor, + IRequestHandler, + IPermissionRequestor, + ITemplateProvider) + + # INavigationContributor methods + def get_active_navigation_item(self, req): + return 'location' + + def get_navigation_items(self, req): + if "LOCATION_VIEW" in req.perm: + yield ('mainnav', 'location', html.A('Locations', href=req.href.location())) + + # IPermissionRequestor methods + def get_permission_actions(self): + '''returns all permissions this component provides''' + return ['LOCATION_VIEW', 'LOCATION_ADD', 'LOCATION_DELETE', 'LOCATION_MODIFY', + ('LOCATION_ADMIN', ('LOCATION_VIEW', 'LOCATION_ADD', 'LOCATION_DELETE', 'LOCATION_MODIFY'))] + + def match_request(self, req): + self.env.log.debug("LocationModule.match_request %r\n" % req.__dict__) + res = req.path_info == "/location" + self.env.log.debug("LocationModule.match_request res = %r\n" % res) + return res + + def process_request(self, req): + self.env.log.debug("LocationModule.process_request %r\n" % req.__dict__) + '''process add,change,delete actions''' + req.perm.require("LOCATION_VIEW") + #add_stylesheet(req, 'hw/css/base.css') + add_stylesheet (req, 'hw/css/location.css') + #add_stylesheet (req, 'hw/css/map.css') + add_script(req, 'http://www.openlayers.org/api/OpenLayers.js') + add_script(req, 'http://www.openstreetmap.org/openlayers/OpenStreetMap.js') + add_script(req, 'hw/script/tom.js') + data = {"results": []} + if req.args.has_key("from") and req.args.has_key("id"): + req.session["from"] = req.args["from"] + req.session["id"] = req.args["id"] + req.session.save() + if req.session.has_key("edited"): + try: + from_resource = req.session.get("from") + resource_id = int(req.session.get("id")) + resource = Resource(from_resource, id=resource_id) + link = get_resource_url(self.env, resource, req.href) + add_notice(req, tag.p("Location edited successfully. Back to " , tag.a(get_resource_description(self.env, resource, "summary"), href=link))) + del req.session["from"] + del req.session["id"] + except Exception, e: + add_notice(req, _("Location edited successfully.")) + del req.session["edited"] + req.session.save() + if req.session.has_key("added"): + try: + from_resource = req.args.get("from") + resource_id = int(req.args.get("id")) + resource = Resource(from_resource, id=resource_id) + link = get_resource_url(from_resource, resource_id) + add_notice(req, tag.p(_("Location created successfully. Back to ") % resource, tag.a(resource, href=link))) + except Exception: + add_notice(req, _("Location created successfully.")) + del req.session["added"] + req.session.save() + if req.method == "POST": + if req.args.has_key("location_search"): + query = unicode(req.args["location_search"]) + results = search_location(query) + data["location_results"] = results + if req.args.has_key("savelocations"): + req.perm.require("LOCATION_MODIFY") + deleted = [] + locations = {} + changed = {} + if req.args.has_key("default"): + default_location = self.config.getint("rendezvous", "default_location") + default = int(req.args["default"]) + if default_location != default: + default_location = default + self.config.set("rendezvous", "default_location", default) + self.config.save() + for key in req.args: + kind = location_id = None + try: + kind, location_id = key.split(":", 1) + except ValueError: + continue + location_id = int(location_id) + if location_id in deleted: + continue + if not locations.has_key(location_id): + location = ItemLocation.fetch_one(self.env, location_id) + if not location: + add_warning(req, "Could not find ItemLocation with location_id '%d'" % location_id) + continue + locations[location_id] = location + + if kind == "delete": + req.perm.require("LOCATION_DELETE") + locations[location_id].delete() + deleted.append(location_id) + del locations[location_id] + elif kind == "name": + name = req.args[key] + if not name: + add_warning(req, "location name must be specified for ItemLocation '%d'" % location_id) + continue + if name != locations[location_id].name: + locations[location_id].name = name + changed[location_id] = True + elif kind == "location": + coordinates = req.args[key] + if coordinates: + try: + lat, lon = validate_dd_coordinates(coordinates) + lat_side, lat_deg, lat_min, lat_sec, lon_side, lon_deg, lon_min, lon_sec = convert_dd_2_dms(lat, lon) + except ValueError: + try: + lat_side, lat_deg, lat_min, lat_sec, lon_side, lon_deg, lon_min, lon_sec = validate_dms_coordinates(coordinates) + lat, lon = convert_dms_2_dd(lat_side, lat_deg, lat_min, lat_sec, lon_side, lon_deg, lon_min, lon_sec) + except ValueError: + add_warning(req, "coordinates have wrong format") + continue + if lat != location.lat: + location.lat = lat + changed[location_id] = True + if lon != location.lon: + location.lon = lon + changed[location_id] = True + if lat_side != location.lat_side: + location.lat_side = lat_side + changed[location_id] = True + if lat_deg != location.lat_deg: + location.lat_deg = lat_deg + changed[location_id] = True + if lat_min != location.lat_min: + location.lat_min = lat_min + changed[location_id] = True + if lat_sec != location.lat_sec: + location.lat_sec= lat_sec + changed[location_id] = True + if lon_side != location.lon_side: + location.lon_side = lon_side + changed[location_id] = True + if lon_deg != location.lon_deg: + location.lon_deg = lon_deg + changed[location_id] = True + if lon_min != location.lon_min: + location.lon_min = lon_min + changed[location_id] = True + if lon_sec != location.lon_sec: + location.lon_sec = lon_sec + changed[location_id] = True + done=True + for dvi in changed: + if dvi not in deleted: + try: + locations[dvi].update() + except Exception, err: + add_warning(req, str(err)) + done=False + continue + if done: + req.session["edited"] = True + req.session.save() + req.redirect(req.href.location()) + if req.args.has_key("addlocation") and req.args.has_key("location_name"): + req.perm.require("LOCATION_ADD") + rl = ItemLocation(self.env, name=req.args["location_name"]) + is_valid = True + if not rl.name: + add_warning(req, "Coordinate name is empty") + is_valid = False + if req.args.has_key("coordinates"): + coordinates = unicode(req.args["coordinates"]) + if coordinates: + try: + rl.lat, rl.lon = validate_dd_coordinates(coordinates) + rl.lat_side, rl.lat_deg, rl.lat_min, rl.lat_sec, rl.lon_side, rl.lon_deg, rl.lon_min, rl.lon_sec = convert_dd_2_dms(rl.lat, rl.lon) + except ValueError: + try: + rl.lat_side, rl.lat_deg, rl.lat_min, rl.lat_sec, rl.lon_side, rl.lon_deg, rl.lon_min, rl.lon_sec = validate_dms_coordinates(coordinates) + rl.lat, rl.lon = convert_dms_2_dd(rl.lat_side, rl.lat_deg, rl.lat_min, rl.lat_sec, rl.lon_side, rl.lon_deg, rl.lon_min, rl.lon_sec) + except ValueError: + add_warning(req, "coordinates have wrong format") + is_valid = False + if is_valid: + rl.commit() + req.session["added"] = True + req.session.save() + req.redirect(req.href.location()) + data.update({"results" : None, + "default_location" : self.config.getint("rendezvous", "default_location"), + "locations" : ItemLocation.fetch_all(self.env)}) + return 'location.html', data, None + + # ITemplateProvider methods + def get_templates_dirs(self): + from pkg_resources import resource_filename + return [resource_filename(__name__, 'templates')] + + def get_htdocs_dirs(self): + from pkg_resources import resource_filename + return [('hw', resource_filename(__name__, 'htdocs'))] diff --git a/TracRendezVous/tracrendezvous/rendezvous/__init__.py b/TracRendezVous/tracrendezvous/rendezvous/__init__.py new file mode 100644 index 0000000..ca354bd --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +import workflow +import macros +import api diff --git a/TracRendezVous/tracrendezvous/rendezvous/admin.py b/TracRendezVous/tracrendezvous/rendezvous/admin.py new file mode 100644 index 0000000..fff9d78 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/admin.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + +from os.path import join + +from trac.admin import IAdminPanelProvider +from trac.core import * +from trac.web.chrome import add_stylesheet +from api import RendezVousSystem +from trac.util.translation import _ + +from model import RendezVousType, TypePermission, RendezVousDate +from ctdotools.utils import validate_id +from tracrendezvous.rendezvous.utils import * + +__all__ = ['ComponentRendezVousGeneral', 'ComponentRendezVousTypes'] + +def get_actions(myactions): + actions = [] + for action in myactions: + if isinstance(action, tuple): + actions.append(action[0]) + else: + actions.append(action) + return actions + +class RendezVousAdminPanel(Component): + + implements(IAdminPanelProvider) + + abstract = True + + # IAdminPanelProvider methods + + def get_admin_panels(self, req): + if 'RENDEZVOUS_ADMIN' in req.perm: + yield ('rendezvous', 'RendezVous System', self._type, self._label[1]) + + def render_admin_panel(self, req, cat, page, rendezvous): + req.perm.require('RENDEZVOUS_ADMIN') + # Trap AssertionErrors and convert them to TracErrors + try: + return self._render_admin_panel(req, cat, page, rendezvous) + except AssertionError, e: + raise TracError(e) + + +class ComponentRendezVousGeneral(RendezVousAdminPanel): + + _type = 'rendezvous' + _label = ('rendezvous', 'General') + + def _render_admin_panel(self, req, cat, page, rendezvous): + add_stylesheet (req, 'hw/css/rendezvous.css') + if req.method == "POST": + if req.args.has_key("show_vote_graph"): + self.config.set("rendezvous", "show_vote_graph", True) + else: + self.config.set("rendezvous", "show_vote_graph", False) + + if req.args.has_key("show_location_map"): + self.config.set("rendezvous", "show_location_map", True) + else: + self.config.set("rendezvous", "show_location_map", False) + + if req.args.has_key("max_description_length"): + tmp = int(req.args["max_description_length"]) + self.config.set("rendezvous", "max_description_length", tmp) + + if req.args.has_key("max_votes_per_date"): + tmp = int(req.args["max_votes_per_date"]) + self.config.set("rendezvous", "max_votes_per_date", tmp) + + if req.args.has_key("max_dates_per_rendezvous"): + tmp = int(req.args["max_dates_per_rendezvous"]) + self.config.set("rendezvous", "max_dates_per_rendezvous", tmp) + + if req.args.has_key("graph_size_x"): + tmp = int(req.args["graph_size_x"]) + self.config.set("rendezvous", "graph_size_x", tmp) + update = True + + update = False + if req.args.has_key("graph_size_y"): + tmp = int(req.args["graph_size_y"]) + self.config.set("rendezvous", "graph_size_y", tmp) + update = True + + if req.args.has_key("default_vote_time_start"): + tmp = req.args["default_vote_time_start"] + self.config.set("rendezvous", "default_vote_time_start", tmp) + + if req.args.has_key("default_vote_time_end"): + tmp = req.args["default_vote_time_end"] + self.config.set("rendezvous", "default_vote_time_end", tmp) + + if req.args.has_key("default_rendezvous_type"): + tmp = req.args["default_rendezvous_type"] + self.config.set("rendezvous", "default_rendezvous_type", tmp) + self.config.save() + if update: + path = join(self.env.path, "htdocs", "rendezvous_graphs") + size = [self.config.getint("rendezvous", "graph_size_x"), + self.config.getint("rendezvous", "graph_size_y")] + dates = RendezVousDate.fetch_all(self.env) + for date in dates: + if date.votes: + update_votes_graph(date.votes, path, size) + data = {"show_vote_graph" : self.config.getbool("rendezvous", "show_vote_graph"), + "show_location_map" : self.config.getbool("rendezvous", "show_location_map"), + "max_description_length" : self.config.getint("rendezvous", "max_description_length"), + "max_votes_per_date" : self.config.getint("rendezvous", "max_votes_per_date"), + "max_dates_per_rendezvous" : self.config.getint("rendezvous", "max_dates_per_rendezvous"), + "graph_size_x" : self.config.getint("rendezvous", "graph_size_x"), + "graph_size_y" : self.config.getint("rendezvous", "graph_size_y"), + "default_vote_time_start" : self.config.get("rendezvous", "default_vote_time_start"), + "default_vote_tTime_end" : self.config.get("rendezvous", "default_vote_time_end")} + return "admin_general.html", data + +class ComponentRendezVousTypes(RendezVousAdminPanel): + + _type = 'types' + _label = ('types', 'Types') + + def _render_admin_panel(self, req, cat, page, rendezvoustype): + + add_stylesheet (req, 'hw/css/rendezvous.css') + data = {} + myPermissions = get_actions(RendezVousSystem._actions) + if req.method == "POST": + rtype = req.args.get("rtype") + permission = req.args.get("permission") + + if rtype and rtype.isupper(): + raise TracError(_('All upper-cased tokens are reserved for ' + 'permission names')) + #if not rtype: + #raise TracError(_('Unknown RendezVousType')) + if permission and permission not in myPermissions: + raise TracError(_('Unknown permission')) + if req.args.get("add") and rtype and permission: + rtype = RendezVousType.fetch_one(self.env, name=rtype) + if rtype.has_permission(permission): + raise TracError(_('permission already granted to RendezVousType')) + tPerm = TypePermission(self.env, rtype.type_id, permission) + self.validate_type_permission(tPerm) + tPerm.commit() + req.redirect(req.href.admin(cat, page)) + elif req.args.has_key("add") and rtype: + realType = RendezVousType.fetch_one(self.env, name=rtype) + if realType: + raise TracError(_('RendezVousType already exists')) + realType = RendezVousType(self.env, 0, rtype) + self.validate_rendezvous_type(realType) + realType.commit() + req.redirect(req.href.admin(cat, page)) + elif req.args.has_key("save") and req.args.has_key("rsel"): + req.perm.require('RENDEZVOUS_ADMIN') + rsel = req.args.get('rsel') + rsel = isinstance(rsel, list) and rsel or [rsel] + for key in rsel: + type_id = int(key) + rtype = RendezVousType.fetch_one(self.env, type_id) + if not rtype: + raise TracError(_('Unknown RendezVousType')) + rtype.delete() + req.redirect(req.href.admin(cat, page)) + elif req.args.has_key("save") and req.args.has_key("sel"): + req.perm.require('RENDEZVOUS_ADMIN') + sel = req.args.get('sel') + sel = isinstance(sel, list) and sel or [sel] + for key in sel: + rtype, permission = key.split(":") + rtype_id = int(rtype) + if permission and permission not in myPermissions: + raise TracError(_('Unknown type permission relation')) + typePermission = TypePermission.fetch_one(self.env, rtype_id, permission) + if typePermission: + typePermission.delete() + req.redirect(req.href.admin(cat, page)) + elif req.args.has_key("save") and req.args.has_key("default"): + default = int(req.args["default"]) + self.config.set("rendezvous", "default_rendezvous_type", default) + self.config.save() + + data.update({"default_rendezvous_type" : self.config.getint("rendezvous", "default_rendezvous_type"), + "rendezVousTypes" : RendezVousType.fetch_all(self.env), + "actions" : myPermissions}) + return "admin_types.html", data + + def validate_rendezvous_type(self, typ): + if type(typ.type_id) != int: + raise TypeError("RendezVousType.validate() wrong type") + if type(typ.name) != unicode: + raise TypeError("RendezVousType.validate() wrong type") + + def validate_type_permission(self, mytype): + if type(mytype.type_id) != int: + raise TypeError("TypePermission.__init__(): expected type int, got '%s'" % type(mytype.type_id)) + if type(mytype.permission) != unicode: + raise TypeError("TypePermission.__init__() expected type 'unicode', got '%s'" % type(mytype.permission)) + validate_id(mytype.type_id) diff --git a/TracRendezVous/tracrendezvous/rendezvous/api.py b/TracRendezVous/tracrendezvous/rendezvous/api.py new file mode 100644 index 0000000..7dd9505 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/api.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- + +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Library General Public +# License version 2 as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Library General Public License for more details. +# +# You should have received a copy of the GNU Library General Public License +# along with this library; see the file COPYING.LIB. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. +# +# --- +# Copyright (C) 2008, hotshelf +# + +''' the api of the rendezvous system''' + +from trac.resource import IResourceManager +from trac.config import ExtensionOption +from trac.core import Interface, Component, implements +from trac.perm import IPermissionRequestor +from trac.util import Ranges +from trac.util.datefmt import get_timezone, utc, format_time, localtz +from trac.wiki import IWikiSyntaxProvider +from genshi.builder import tag +from ctdotools.utils import validate_id, gen_wiki_page +from tracrendezvous.location.model import ItemLocation +from tracrendezvous.event.model import Event +from model import * +from datetime import datetime + +__all__ = ['IRendezVousActionController', 'RendezVousSystem'] + +class IRendezVousActionController(Interface): + """Extension point interface for components willing to participate + in the rendezvous workflow. + + This is mainly about controlling the changes to the rendezvous ''status'', + though not restricted to it. + """ + + def get_rendezvous_actions(req, rendezvous): + """Return an iterable of `(weight, action)` tuples corresponding to + the actions that are contributed by this component. + That list may vary given the current state of the rendezvous and the + actual request parameter. + + `action` is a key used to identify that particular action. + (note that 'history' and 'diff' are reserved and should not be used + by plugins) + + The actions will be presented on the page in descending order of the + integer weight. The first action in the list is used as the default + action. + + When in doubt, use a weight of 0.""" + + def get_all_status(): + """Returns an iterable of all the possible values for the ''status'' + field this action controller knows about. + + This will be used to populate the query options and the like. + It is assumed that the initial status of a rendezvous is 'new' and + the terminal status of a rendezvous is 'closed'. + """ + + def render_rendezvous_action_control(req, rendezvous, action): + """Return a tuple in the form of `(label, control, hint)` + + `label` is a short text that will be used when listing the action, + `control` is the markup for the action control and `hint` should + explain what will happen if this action is taken. + + This method will only be called if the controller claimed to handle + the given `action` in the call to `get_rendezvous_actions`. + + Note that the radio button for the action has an `id` of + `"action_%s" % action`. Any `id`s used in `control` need to be made + unique. The method used in the default ITicketActionController is to + use `"action_%s_something" % action`. + """ + + def change_rendezvous_workflow(req, rendezvous, action): + """Change workflow + """ + + def current_rendezvous(): + """Return an iterable of current, active rendezvouses.""" + + def past_rendezvous(): + """Return an iterable of past rendezvouses.""" + +class RendezVousSystem(Component): + """base mathods of the rendezvous system""" + + implements(IPermissionRequestor, + IResourceManager, + IWikiSyntaxProvider) + + workflow_controller = ExtensionOption('rendezvous', 'workflow', + IRendezVousActionController, 'RendezVousWorkflow', + """Ordered list of workflow controllers to use for rendezvous actions.""") + + _actions = ['RENDEZVOUS_ADD', 'RENDEZVOUS_DELETE', 'RENDEZVOUS_MODIFY', + 'RENDEZVOUS_VIEW', 'RENDEZVOUS_COMMENT_ADD', 'RENDEZVOUS_COMMENT_VIEW', + 'RENDEZVOUS_DATE_ADD', 'RENDEZVOUS_DATE_DELETE', 'RENDEZVOUS_DATE_MODIFY', + 'RENDEZVOUS_DATE_VIEW', 'RENDEZVOUS_VOTE_ADD', 'RENDEZVOUS_VOTE_DELETE', + 'RENDEZVOUS_VOTE_MODIFY', 'RENDEZVOUS_VOTE_VIEW', 'RENDEZVOUS_VOTE_GRAPH_VIEW', + 'RENDEZVOUS_VOTE_VIEW_OTHERS', 'RENDEZVOUS_LOCATION_VIEW', 'RENDEZVOUS_LOCATION_ADD', + 'RENDEZVOUS_LOCATION_DELETE', 'RENDEZVOUS_LOCATION_MODIFY', + ('RENDEZVOUS_ADMIN', + ('RENDEZVOUS_ADD', 'RENDEZVOUS_DELETE', 'RENDEZVOUS_MODIFY', 'RENDEZVOUS_VIEW', + 'RENDEZVOUS_VIEW', 'RENDEZVOUS_COMMENT_ADD', 'RENDEZVOUS_DATE_ADD', + 'RENDEZVOUS_DATE_DELETE', 'RENDEZVOUS_DATE_MODIFY', 'RENDEZVOUS_DATE_VIEW', + 'RENDEZVOUS_VOTE_ADD', 'RENDEZVOUS_VOTE_DELETE', 'RENDEZVOUS_VOTE_MODIFY', + 'RENDEZVOUS_VOTE_GRAPH_VIEW', 'RENDEZVOUS_VOTE_VIEW_OTHERS', + 'RENDEZVOUS_LOCATION_ADD', 'RENDEZVOUS_LOCATION_DELETE', 'RENDEZVOUS_LOCATION_MODIFY'))] + + # workflow stuff + def get_available_actions(self, req, rendezvous): + """Returns a sorted list of available actions""" + # The list should not have duplicates. + actions = {} + + weighted_actions = self.workflow_controller.get_rendezvous_actions(req, rendezvous) + for weight, action in weighted_actions: + if action in actions: + actions[action] = max(actions[action], weight) + else: + actions[action] = weight + all_weighted_actions = [(weight, action) for action, weight in + actions.items()] + return [x[1] for x in sorted(all_weighted_actions, reverse=True)] + + def get_all_status(self): + """Returns a sorted list of all the states all of the action + controllers know about.""" + valid_states = set() + valid_states.update(self.workflow_controller.get_all_status()) + return sorted(valid_states) + + # IWikiSyntaxProvider methods + def get_wiki_syntax(self): + return [] + + def get_link_resolvers(self): + yield ('rendezvous', self._format_link) + + def _format_link(self, formatter, ns, target, label, fullmatch=None): + link, params, fragment = formatter.split_link(target) + r = Ranges(link) + if len(r) == 1: + num = r.a + rendezvous = formatter.resource("rendezvous", num) + validate_id(num) + cursor = formatter.db.cursor() + cursor.execute("SELECT name,status " + "FROM rendezvous WHERE rendezvous_id=%s", (num,)) + for name, status in cursor: + title = "rendezvous #%d: %s (%s)" % (num, name, status) + if label == link: + label = title + href = formatter.href.rendezvous(num) + return tag.a(label, title=title, href=href) + return tag.a(label, class_='missing rendezvous') + + + # IPermissionRequestor methods + def get_permission_actions(self): + '''returns all permissions this component provides''' + return self._actions + + # IResourceManager methods + + def get_resource_realms(self): + yield 'rendezvous' + + def get_resource_description(self, resource, format=None, context=None, + **kwargs): + if format == 'compact': + return 'RendezVous #%s' % resource.id + elif format == 'summary': + from tracrendezvous.model import RendezVous + rendezvous = RendezVous.fetch_one(self.env, resource.id) + return "RendezVous #%d - %s (%s)" % (rendezvous.rendezvous_id, rendezvous.name, rendezvous.status) diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/css/rendezvous.css b/TracRendezVous/tracrendezvous/rendezvous/htdocs/css/rendezvous.css new file mode 100644 index 0000000..1d2a214 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/htdocs/css/rendezvous.css @@ -0,0 +1,264 @@ +p.help +{ +color:#666666; +margin-top:1em; +margin-right:0.5em; +margin-bottom:0.5em; +margin-left:0.5em; +} + +.error{color:#ff0000;} + +div.overview-set +{ +display:inline-block; +margin:5px; +min-width:20em; +border:1px solid #000; +-moz-border-radius:17px; +-webkit-border-radius:17px; +} + +a.voting{color:black;} + +li a.overview +{ +font-size:14pt; +font-weight:bold; +border-bottom-style:none; +color:#000; +font-size:14pt; +-moz-border-radius:17px; +-webkit-border-radius:17px; +padding:5px; +} + +li a.overview:hover{color:#ffffaa;background-color:#333300;border-bottom-style:none;} +ul.overview-set{list-style-type:none;} + +a.location,a.location:visited,a.location:hover +{ +color:#bb0000; +border-bottom-width:1px; +border-bottom-style:dotted; +border-bottom-color:#ff0000; +cursor:pointer; +} + +.rendezvous .main +{ +width:82%; +margin-right:1%; +padding-right:1%; +} + +#rendezvous-main +{ + text-align:left; +} + +#rendezvous-details.table +{ +border-top:1px solid Gray; +border-bottom:1px solid Gray; +margin-bottom:10px; +padding:10px; +} + +table.rendezvous +{ +clear:both; +border-top:1px solid #dd9; +border-collapse:separate; +table-layout:fixed; +width:100%; +} + +td.rendezvous-voted +{ +height:30px; +margin:10px; +background:green; +color:black; +} + +td.rendezvous-notvoted +{ +height:30px; +margin:10px; +background:red; +color:black; +} + +td.rendezvous-notelected +{ +margin:10px; +background:gray; +color:black; +} + +div.rendezvous-item +{ +border:1px outset #996; +background:#ffd; +margin:10px; +width:300px; +} + +.rendezvous-header +{ +color:#005500; +position:relative; +top:10px; +right:10px; +float:right; +} + +.rendezvous-header:link,.rendezvous-header:visited +{ +text-decoration:none; +border-bottom-style:none; +font-size:22pt; +font-family:serif; +} + +.rendezvous-stats +{ +border-top:1px double #000; +} + +#rendezvous-details +{ +min-width:400px; +margin-bottom:1em; +margin-top:1em; +padding:.5em 1em; +padding-left:10px; +padding-right:10px; +padding-top:10px; +} + +.rendezvous-wizard th +{ + text-align:left; +} + +table.properties th{color:#666633;text-align:left;width:20%;} +div.description{border:1px outset #996;background:#dfd;padding-left:10px;padding-right:10px;} +div.newrendezvous{border:1px outset #996;background:#ffffdd;width:200px;padding:20px;} +div.graph{border:1px outset #996;background:#ffd;padding:10px;} +.scheduled{color:#ff0000;font-weight:bold;text-decoration:blink;} +.comment{border-bottom:1px solid #000000;margin-bottom:10px;padding-top:1em;} + +comment h1 +{ +font-style:italic; +font-size:12pt; +font-weight:bold; +padding:10px; +margin-bottom:0.7em; +} + +.comment h2 +{ +font-style:italic; +font-size:10pt; +font-weight:bold; +padding:10px; +margin-bottom:0.7em; +} + +comment h3 +{ +font-style:italic; +font-size:8pt; +font-weight:bold; +padding:10px; +margin-bottom:0.2em; +margin-bottom:0.5em; +} + +.comment-header +{ +color:#005500; +position:relative; +top:10px; +right:10px; +float:right; +} + +#content {text-align:center;} + +#new_event +{ + margin:auto; +} + +#new_event fieldset {text-align:left;} + +.event-item +{ +border-bottom:1px solid #ccc; +border-left:1px solid #ccc; +border-right:1px solid #ccc; +margin:0px; +padding:10px 5px; +} + +.event-item.first +{ +border-top:1px solid #ccc; +} + +.event-item h2 {margin-top:0;} + +table.upcoming +{ +border:1px solid #000 !important; +border-spacing:0px; +border-collapse: collapse; +margin:auto; +text-align:left; +} + + + +tr.upcoming-even +{ +margin:0px; +padding:0px; +background:#e4d6b0; +} + +tr.upcoming +{ +padding:0px; +background:#ffedbc; +} + +td.upcoming +{ +min-width:20em; +padding:5px; +border-bottom:1px solid #000; +border-right:1px solid #000; +height:10em; +} + +td.upcoming:hover, td.upcoming-even:hover +{ + background:#ffffee; +} + +td.day +{ +font-size:200%; +font-weight:bold; +font-family:sans-serif; +padding:5px 10px; +margin:0px; +border-right:1px solid #000; +border-bottom:1px solid #000; +min-height:5em; +} + diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/canceled.png b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/canceled.png new file mode 100644 index 0000000..4afd793 Binary files /dev/null and b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/canceled.png differ diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/expired.png b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/expired.png new file mode 100644 index 0000000..1dee266 Binary files /dev/null and b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/expired.png differ diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/new.png b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/new.png new file mode 100644 index 0000000..bd3bd89 Binary files /dev/null and b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/new.png differ diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/running.png b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/running.png new file mode 100644 index 0000000..1a2e1d1 Binary files /dev/null and b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/running.png differ diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/selected.png b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/selected.png new file mode 100644 index 0000000..882bec6 Binary files /dev/null and b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/selected.png differ diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/voting.png b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/voting.png new file mode 100644 index 0000000..8f49486 Binary files /dev/null and b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/voting.png differ diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/script/tom.js b/TracRendezVous/tracrendezvous/rendezvous/htdocs/script/tom.js new file mode 100644 index 0000000..1fc3275 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/htdocs/script/tom.js @@ -0,0 +1,63 @@ +function jumpTo(lon, lat, zoom) { + var x = Lon2Merc(lon); + var y = Lat2Merc(lat); + map.setCenter(new OpenLayers.LonLat(x, y), zoom); + return false; +} + +function Lon2Merc(lon) { + return 20037508.34 * lon / 180; +} + +function Lat2Merc(lat) { + var PI = 3.14159265358979323846; + lat = Math.log(Math.tan( (90 + lat) * PI / 360)) / (PI / 180); + return 20037508.34 * lat / 180; +} + +function addMarker(layer, lon, lat, popupContentHTML) { + + var ll = new OpenLayers.LonLat(Lon2Merc(lon), Lat2Merc(lat)); + var feature = new OpenLayers.Feature(layer, ll); + feature.closeBox = true; + feature.popupClass = OpenLayers.Class(OpenLayers.Popup.FramedCloud, {minSize: new OpenLayers.Size(300, 180) } ); + feature.data.popupContentHTML = popupContentHTML; + feature.data.overflow = "hidden"; + + var marker = new OpenLayers.Marker(ll); + marker.feature = feature; + + var markerClick = function(evt) { + if (this.popup == null) { + this.popup = this.createPopup(this.closeBox); + map.addPopup(this.popup); + this.popup.show(); + } else { + this.popup.toggle(); + } + OpenLayers.Event.stop(evt); + }; + marker.events.register("mousedown", feature, markerClick); + + layer.addMarker(marker); + map.addPopup(feature.createPopup(feature.closeBox)); +} + +function getCycleTileURL(bounds) { + var res = this.map.getResolution(); + var x = Math.round((bounds.left - this.maxExtent.left) / (res * this.tileSize.w)); + var y = Math.round((this.maxExtent.top - bounds.top) / (res * this.tileSize.h)); + var z = this.map.getZoom(); + var limit = Math.pow(2, z); + + if (y < 0 || y >= limit) + { + return null; + } + else + { + x = ((x % limit) + limit) % limit; + + return this.url + z + "/" + x + "/" + y + "." + this.type; + } +} \ No newline at end of file diff --git a/TracRendezVous/tracrendezvous/rendezvous/init.py b/TracRendezVous/tracrendezvous/rendezvous/init.py new file mode 100644 index 0000000..bda2649 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/init.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from admin import * +from api import * +from macros import * +from model import * +from rendezvous_tag import * +from web_ui import * +from workflow import * diff --git a/TracRendezVous/tracrendezvous/rendezvous/luxisr.ttf b/TracRendezVous/tracrendezvous/rendezvous/luxisr.ttf new file mode 100644 index 0000000..c47fd20 Binary files /dev/null and b/TracRendezVous/tracrendezvous/rendezvous/luxisr.ttf differ diff --git a/TracRendezVous/tracrendezvous/rendezvous/macros.py b/TracRendezVous/tracrendezvous/rendezvous/macros.py new file mode 100644 index 0000000..c2903b5 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/macros.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +from genshi.builder import tag +from trac.util.translation import _ +from trac.wiki.macros import WikiMacroBase +from tracrendezvous.rendezvous import api +from tracrendezvous.rendezvous import model + +__all__ = ['ExpiredRendezVousesMacro', 'CanceledRendezVousesMacro', 'ScheduledRendezVousesMacro'] + +class ExpiredRendezVousesMacro(WikiMacroBase): + + """Renders an overview of canceled or expired !RendezVouses""" + + revision = "$Rev: 186 $" + + def expand_macro(self, formatter, name, content): + if 'RENDEZVOUS_VIEW' not in formatter.perm: + return "" + uperm = model.RendezVousTypePermissionSystem(self.env) + controller = api.RendezVousSystem(self.env).workflow_controller + ls = controller.expired_rendezvouses() + lsr = [tag.li(tag.a("%s: (%s)" % (i.name, i.status), href=formatter.href.rendezvous(i.rendezvous_id))) + for i in ls + if 'RENDEZVOUS_ADMIN' in formatter.perm or uperm.check_user_type_permissions(formatter.req.authname, i.type_id)] + if lsr: + return tag.div([tag.ul(lsr)]) + return None + +class CanceledRendezVousesMacro(WikiMacroBase): + + """Renders an overview of canceled or expired !RendezVouses""" + + revision = "$Rev: 186 $" + + def expand_macro(self, formatter, name, content): + if 'RENDEZVOUS_VIEW' not in formatter.perm: + return "" + uperm = model.RendezVousTypePermissionSystem(self.env) + controller = api.RendezVousSystem(self.env).workflow_controller + ls = controller.canceled_rendezvouses() + lsr = [tag.li(tag.a("%s: (%s)" % (i.name, i.status), href=formatter.href.rendezvous(i.rendezvous_id))) + for i in ls + if 'RENDEZVOUS_ADMIN' in formatter.perm or uperm.check_user_type_permissions(formatter.req.authname, i.type_id)] + if lsr: + return tag.div([tag.ul(lsr)]) + return None + + +class CurrentRendezVousesMacro(WikiMacroBase): + + """Renders an overview of current !RendezVouses""" + + revision = "$Rev:$" + + def expand_macro(self, formatter, name, content): + if 'RENDEZVOUS_VIEW' not in formatter.perm: + return "" + uperm = model.RendezVousTypePermissionSystem(self.env) + controller = api.RendezVousSystem(self.env).workflow_controller + ls = controller.voting_rendezvouses() + lsr = [tag.li(tag.a("%s: (%s)" % (i.name, i.status), href=formatter.href.rendezvous(i.rendezvous_id))) + for i in ls + if 'RENDEZVOUS_ADMIN' in formatter.perm or uperm.check_user_type_permissions(formatter.req.authname, i.type_id)] + if lsr: + return tag.div([tag.ul(lsr)]) + return None + + +class ScheduledRendezVousesMacro(WikiMacroBase): + + """Renders an overview of scheduled !RendezVouses""" + + revision = "$Rev: 186 $" + + def expand_macro(self, formatter, name, content): + if 'RENDEZVOUS_VIEW' not in formatter.perm: + return "" + uperm = model.RendezVousTypePermissionSystem(self.env) + controller = api.RendezVousSystem(self.env).workflow_controller + rendezvouses = controller.scheduled_rendezvouses(check=True) + lsr = [] + for i in rendezvouses: + if i.elected and ('RENDEZVOUS_ADMIN' in formatter.perm or uperm.check_user_type_permissions(formatter.req.authname, i.type_id)): + lsr.append(tag.li(tag.a("%s: %s - %s" % (i.name, i.get_date(i.elected).time_begin.strftime('%Y.%m.%d %H:%M'), i.get_date(i.elected).time_end.strftime('%Y.%m.%d %H:%M')), href=formatter.href.rendezvous(i.rendezvous_id)))) + if lsr: + return tag.div([tag.ul(lsr, style="list-style-image:url(%s)" % formatter.href("/chrome/hw/images/selected.png"))]) + return None diff --git a/TracRendezVous/tracrendezvous/rendezvous/model.py b/TracRendezVous/tracrendezvous/rendezvous/model.py new file mode 100644 index 0000000..ca23e0d --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/model.py @@ -0,0 +1,850 @@ +# -*- coding: utf-8 -*- + +from trac.core import * +from trac.perm import PermissionSystem +from trac.db import Table, Column, Index +from trac.db.util import sql_escape_percent +from datetime import datetime, timedelta +from trac.util.datefmt import utc, to_timestamp +from trac.util.text import to_unicode +from trac.env import IEnvironmentSetupParticipant +from ctdotools.utils import validate_id, gen_wiki_page +from dateutil.rrule import * +from collections import defaultdict +from tracrendezvous.location.model import ItemLocation as RendezVousLocation +from tracrendezvous.rendezvous import api + +__all__ = ['RendezVous', 'RendezVousComment', 'RendezVousType', 'RendezVousDate', + 'RendezVousVote', 'TypePermission', 'RendezVousModelProvider', + 'RendezVousTypePermissionSystem'] + +class RendezVousVote(object): + + def __init__(self, env, vote_id, date_id, user, email, time_created, time_begin, time_end): + self.env = env + self.vote_id = vote_id + self.date_id = date_id + self.user = to_unicode(user) + self.email = to_unicode(email) + self.time_created = time_created + self.time_begin = time_begin + self.time_end = time_end + + @staticmethod + def fetch_one(env, vote_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""SELECT * + FROM rendezvous_vote + WHERE vote_id=%s""", (vote_id,)) + row = cursor.fetchone() + if not row: + return None + vote_id, date_id, user, email, time_created, time_begin, time_end = row + return RendezVousVote(env, vote_id, date_id, user, email, datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(time_begin, utc), datetime.fromtimestamp(time_end, utc)) + + @staticmethod + def exists(env, date_id, user, time_begin, time_end): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""SELECT COUNT(*) FROM rendezvous_vote + WHERE date_id=%s + and user=%s + and time_begin=%s + and time_end=%s""", (date_id, user, to_timestamp(time_begin), to_timestamp(time_end))) + row = cursor.fetchone() + return row[0] > 0 + + @staticmethod + def fetch_by_date(env, date_id, userName=None): + db = env.get_db_cnx() + cursor = db.cursor() + if userName == None: + cursor.execute("""SELECT * FROM rendezvous_vote + WHERE date_id=%s + ORDER BY user""", (date_id,)) + else: + cursor.execute("""SELECT * FROM rendezvous_vote + WHERE date_id=%s and user=%s + ORDER BY user;""", (date_id, userName)) + res = [] + for row in cursor: + vote_id, date_id, user, email, time_created, time_begin, time_end = row + res.append(RendezVousVote(env, vote_id, date_id, user, email, datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(time_begin, utc), datetime.fromtimestamp(time_end, utc))) + return res + + def commit(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""INSERT INTO rendezvous_vote + (date_id, user, email, time_created, time_begin, time_end) + VALUES(%s, %s, %s, %s, %s, %s)""", ( + self.date_id, + self.user, + self.email, + to_timestamp(self.time_created), + to_timestamp(self.time_begin), + to_timestamp(self.time_end))) + db.commit() + self.vote_id = db.get_last_id(cursor, 'rendezvous_vote') + + @staticmethod + def delete(env, vote_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""DELETE FROM rendezvous_vote + WHERE vote_id =%s;""", (vote_id,)) + db.commit() + + @staticmethod + def delete_by_date(env, date_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""DELETE FROM rendezvous_vote + WHERE date_id =%s;""", (date_id,)) + db.commit() + + def update(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""UPDATE rendezvous_vote + SET user=%s, + email=%s, + time_created=%s, + time_begin=%s, + time_end=%s + WHERE vote_id=%s;""", (self.user, + self.email, + to_timestamp(self.time_created), + to_timestamp(self.time_begin), + to_timestamp(self.time_end), + self.vote_id)) + db.commit() + + def __str__(self): + return " 0: + cursor.execute("""SELECT * + FROM rendezvous_date + WHERE date_id=%s""", (date_id,) ) + row = cursor.fetchone() + if not row: + return None + date_id, rendezvous_id, author, email, time_created, time_begin, time_end, elected = row + return RendezVousDate(env, date_id, rendezvous_id, author, email, datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(time_begin, utc), datetime.fromtimestamp(time_end, utc), elected, fetch_votes) + + @staticmethod + def exists(env, ts_begin, ts_end): + db = env.get_db_cnx() + cursor = db.cursor() + + cursor.execute("SELECT date_id " + "FROM rendezvous_date " + "WHERE time_begin=%s AND time_end = %s", + (to_timestamp(ts_begin), + to_timestamp(ts_end))) + rows = cursor.fetchall() + return bool(rows) + + @staticmethod + def fetch_by_rendezvous(env, rendezvous_id, fetch_votes=True): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""SELECT * + FROM rendezvous_date + WHERE rendezvous_id=%s;""", (rendezvous_id,) ) + rows = cursor.fetchall() + res = [] + for row in rows: + date_id, rendezvous_id, author, email, time_created, time_begin, time_end, elected= row + res.append(RendezVousDate(env, date_id, rendezvous_id, author, email, datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(time_begin, utc), datetime.fromtimestamp(time_end, utc), elected, fetch_votes)) + return res + + @staticmethod + def fetch_all(env, fetch_votes=True): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * FROM rendezvous_date;") + rows = cursor.fetchall() + res = [] + for row in rows: + date_id, rendezvous_id, author, email, time_created, time_begin, time_end, elected = row + res.append(RendezVousDate(env, date_id, rendezvous_id, author, email, datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(time_begin, utc), datetime.fromtimestamp(time_end, utc), elected, fetch_votes)) + return res + + def get_vote_count(self, authname=None): + + db = self.env.get_db_cnx() + cursor = db.cursor() + if authname: + cursor.execute("""SELECT COUNT(rendezvous_date.date_id) from rendezvous_date INNER JOIN rendezvous_vote ON + rendezvous_date.date_id=rendezvous_vote.date_id where rendezvous_date.date_id=%s and rendezvous_vote.user=%s;""", (self.date_id, authname)) + else: + cursor.execute("SELECT COUNT(rendezvous_date.date_id) from rendezvous_date INNER JOIN rendezvous_vote " + "ON rendezvous_date.date_id=rendezvous_vote.date_id " + "where rendezvous_date.date_id=%s;", (self.date_id,)) + row = cursor.fetchone() + if row: + return row[0] + return 0 + + def commit(self, conn=None): + db = conn and conn or self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""INSERT INTO rendezvous_date + (rendezvous_id, author, email, time_created, time_begin, time_end, elected) + VALUES(%s,%s,%s,%s,%s,%s,%s)""", ( + self.rendezvous_id, + self.author, + self.email, + to_timestamp(self.time_created), + to_timestamp(self.time_begin), + to_timestamp(self.time_end), + self.elected)) + db.commit() + self.date_id = db.get_last_id(cursor, 'rendezvous_date') + + @staticmethod + def delete(env, date_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("DELETE FROM rendezvous_date WHERE date_id=%s", (date_id,)) + db.commit() + + @staticmethod + def delete_by_rendezvous(env, rendezvous_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""DELETE FROM rendezvous_date + WHERE rendezvous_id=%s;""", (rendezvous_id,) ) + db.commit() + + def update(self, conn=None): + db = conn and conn or self.env.get_db_cnx() + cursor = db.cursor() + try: + cursor.execute("""UPDATE rendezvous_date + SET rendezvous_id=%s, + author=%s, + email=%s, + time_created=%s, + time_begin=%s, + time_end=%s, + elected=%s + WHERE date_id=%s""", (self.rendezvous_id, + self.author, + self.email, + to_timestamp(self.time_created), + to_timestamp(self.time_begin), + to_timestamp(self.time_end), + self.elected, + self.date_id)) + if not conn: + db.commit() + except Exception: + pass + + def __str__(self): + return "" % (self.date_id, self.rendezvous_id, self.author, self.email, str(self.time_created), str(self.time_begin), str(self.time_end), self.elected) + +class RendezVousComment(object): + + def __init__(self, env, comment_id, rendezvous_id, author, comment, time_created): + self.env = env + self.comment_id = comment_id + self.rendezvous_id = rendezvous_id + self.author = to_unicode(author) + self.comment = to_unicode(comment) + self.time_created = time_created + + @staticmethod + def fetch_one(env, comment_id): + db = env.get_db_cnx() + cursor = db.cursor() + if int(comment_id) > 0: + cursor.execute("SELECT * " + "FROM rendezvous_comment " + "WHERE comment_id=%s", (comment_id,) ) + row = cursor.fetchone() + if not row: + return None + comment_id, rendezvous_id, author, comment, time_created = row + return RendezVousComment(env, comment_id, rendezvous_id, author, comment, datetime.fromtimestamp(time_created, utc)) + + @staticmethod + def fetch_by_rendezvous(env, rendezvous_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""SELECT * + FROM rendezvous_comment + WHERE rendezvous_id=%s;""", (rendezvous_id,) ) + rows = cursor.fetchall() + res = [] + for row in rows: + comment_id, rendezvous_id, author, comment, time_created = row + res.append(RendezVousComment(env, comment_id, rendezvous_id, author, comment, datetime.fromtimestamp(time_created, utc))) + return res + + @staticmethod + def fetch_all(env): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * FROM rendezvous_comment;") + rows = cursor.fetchall() + res = [] + for row in rows: + comment_id, rendezvous_id, author, comment, time_created = row + res.append(RendezVousComment(env, comment_id, rendezvous_id, author, comment, datetime.fromtimestamp(time_created, utc))) + return res + + def commit(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""INSERT INTO rendezvous_comment + (rendezvous_id, author, comment, time_created) + VALUES(%s,%s,%s,%s)""", ( + self.rendezvous_id, + self.author, + self.comment, + to_timestamp(self.time_created))) + db.commit() + self.comment_id = db.get_last_id(cursor, 'rendezvous_comment') + + @staticmethod + def delete(env, comment_id): + db = env.get_db_cnx() + RendezVousVote.delete_by_date(env, comment_id) + cursor = db.cursor() + cursor.execute("DELETE FROM rendezvous_comment WHERE comment_id=%s", (comment_id,)) + db.commit() + + @staticmethod + def delete_by_rendezvous(env, rendezvous_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("DELETE FROM rendezvous_comment WHERE rendezvous_id=%s", (rendezvous_id,)) + db.commit() + + def update(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + try: + cursor.execute("""UPDATE rendezvous_comment + SET rendezvous_id=%s, + author=%s, + comment=%s, + time_created=%s + WHERE comment_id=%s""", (self.rendezvous_id, + self.author, + self.comment, + to_timestamp(self.time_created), + self.comment_id)) + db.commit() + except Exception: + pass + + def __str__(self): + return "" % (self.comment_id, self.rendezvous_id, self.author, self.comment, str(self.time_created)) + + +class RendezVousType(object): + + def __init__(self, env, type_id, name): + self.env = env + self.type_id = type_id + self.name = to_unicode(name) + self.typePermissions = TypePermission.fetch(env, type_id) + + @staticmethod + def fetch_one(env, type_id=None, name=None): + db = env.get_db_cnx() + cursor = db.cursor() + if type_id and type_id > 0: + validate_id(int(type_id)) + cursor.execute("""SELECT * + FROM rendezvous_type + WHERE type_id=%s""", (type_id,)) + else: + cursor.execute("""SELECT * + FROM rendezvous_type + WHERE name=%s""", (name,)) + row = cursor.fetchone() + if row: + return RendezVousType(env, row[0], row[1]) + return None + + @staticmethod + def fetch_all(env): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT * FROM rendezvous_type") + rows = cursor.fetchall() + if not rows: + return [] + res = [] + for row in rows: + res.append(RendezVousType(env, row[0], row[1])) + return res + + def commit(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""INSERT INTO rendezvous_type + (name) + VALUES(%s)""", (self.name,)) + db.commit() + self.type_id = db.get_last_id(cursor, 'rendezvous_type') + + def has_permission(self, permission): + for i in self.typePermissions: + if i.permission == permission: + return True + return False + + def delete(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + TypePermission.delete_by_type(self.env, self.type_id) + cursor.execute("DELETE FROM rendezvous_type WHERE type_id=%s", (self.type_id,)) + db.commit() + + def update(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""UPDATE rendezvous_time + SET name=%s + WHERE type_id=%s""", (self.name, self.type_id)) + db.commit() + +class TypePermission(object): + + def __init__(self, env, type_id, permission): + self.env = env + self.type_id = type_id + self.permission = permission + + @staticmethod + def fetch(env, type_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""SELECT * + FROM rendezvous_type_to_permission + WHERE type_id=%s""", (type_id,) ) + rows = cursor.fetchall() + if not rows: + return [] + res = [] + for row in rows: + res.append(TypePermission(env, row[0], row[1])) + return res + + @staticmethod + def fetch_one(env, type_id, permission): + db = env.get_db_cnx() + cursor = db.cursor() + if int(type_id) > 0: + cursor.execute("""SELECT * + FROM rendezvous_type_to_permission + WHERE type_id=%s AND permission=%s""", (type_id, permission)) + row = cursor.fetchone() + if row: + return TypePermission(env, row[0], row[1]) + return None + + @staticmethod + def delete_by_type(env, type_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("DELETE FROM rendezvous_type_to_permission WHERE type_id=%s", (type_id,)) + db.commit() + + def commit(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""INSERT INTO rendezvous_type_to_permission + (type_id, permission) + VALUES(%s,%s)""", (self.type_id, self.permission)) + db.commit() + + def delete(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("DELETE FROM rendezvous_type_to_permission WHERE type_id=%s AND permission=%s", (self.type_id, self.permission)) + db.commit() + + def __str__(self): + return "" % (self.type_id, self.permission) + + +class RendezVous(object): + def __init__(self, env, fetch_dates, rendezvous_id, name, author, email, description, time_created, schedule_deadline, min_votes, type_id, status, location_id, is_date_fixed, tags): + self.env = env + self.rendezvous_id = rendezvous_id + self.name = to_unicode(name) + self.author = to_unicode(author) + self.email = to_unicode(email) + self.description = to_unicode(description) + self.time_created = time_created + self.schedule_deadline = schedule_deadline + self.min_votes = min_votes + self.type_id = type_id + t = RendezVousType.fetch_one(self.env, type_id) + self.type_name = t and t.name or None + self.status = to_unicode(status) + self.location_id = location_id + self.is_date_fixed = is_date_fixed + self.dates = [] + self.tags = tags + self.elected = 0 + if fetch_dates: + self.dates = RendezVousDate.fetch_by_rendezvous(env, self.rendezvous_id) + for i in self.dates: + if i.elected: + self.elected = i.date_id + + @staticmethod + def fetch_one(env, rid=None, name=None, fetch_dates=False): + db = env.get_db_cnx() + cursor = db.cursor() + rendezvous_id=0 + if rid: + rendezvous_id = int(rid) + validate_id(rendezvous_id) + cursor.execute("""SELECT * + FROM rendezvous + WHERE rendezvous_id=%s""", (rendezvous_id,)) + if name: + myname = unicode(name) + cursor.execute("""SELECT * + FROM rendezvous + WHERE name=%s""", name) + + row = cursor.fetchone() + if not row: + return None + + rendezvous_id, name, author, email, description, time_created, schedule_deadline, min_votes, type_id, status, location_id, is_date_fixed, tags = row + return RendezVous(env, fetch_dates, rendezvous_id, name, author, email, description, + datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(schedule_deadline, utc), min_votes, type_id, status, location_id, is_date_fixed, tags) + + @staticmethod + def _fetch_some(env, fetch_dates, query, *args): + db = env.get_db_cnx() + cursor = db.cursor() + if args: + cursor.execute(query, args) + else: + cursor.execute(query) + rows = cursor.fetchall() + if not rows: + return [] + res = [] + for row in rows: + rendezvous_id, name, author, email, description, time_created, schedule_deadline, min_votes, type_id, status, location_id, is_date_fixed, tags = row + res.append( + RendezVous(env, fetch_dates, rendezvous_id, name, author, + email, description, datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(schedule_deadline, utc), + min_votes, type_id, status, location_id, is_date_fixed, tags)) + return res + + def get_date(self, date_id): + for i in self.dates: + if i.date_id == date_id: + return i + raise ValueError("RendezVousDate not found in RendezVous") + + @staticmethod + def fetch_all(env, fetch_dates=False, sort=None): + if not sort: + return RendezVous._fetch_some(env, fetch_dates, "SELECT * FROM rendezvous;") + return RendezVous._fetch_some(env, fetch_dates, "SELECT * FROM rendezvous ORDER BY name") + + @staticmethod + def my_rendezvous(env, name): + return RendezVous._fetch_some(env, False, "SELECT * FROM rendezvous where author = %s;", name) + + @staticmethod + def exists(env, rendezvous_id=0): + db = env.get_db_cnx() + cursor = db.cursor() + if int(rendezvous_id) <= 0: + return False + cursor.execute("""SELECT * + FROM 'rendezvous' + WHERE rendezvous_id=%s""", (rendezvous_id,)) + row = cursor.fetchone() + return row != None + + def has_voted(self, authname=None): + for date in self.dates: + for vote in date.votes: + if vote.user == authname: + return True + return False + + def has_votes(self): + for date in self.dates: + if date.votes: + return True + return False + + def commit(self): + db = self.env.get_db_cnx() + t = datetime.now(utc) + cursor = db.cursor() + cursor.execute( "INSERT INTO rendezvous " + "(name,author,email,description,time_created,schedule_deadline,min_votes,type_id,status,location_id,is_date_fixed,tags) " + "VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + (self.name, self.author, self.email, self.description, to_timestamp(t), + to_timestamp(self.schedule_deadline), self.min_votes, self.type_id, + self.status, self.location_id, self.is_date_fixed, self.tags)) + db.commit() + self.rendezvous_id = db.get_last_id(cursor, 'rendezvous') + + @staticmethod + def delete(env, rendezvous_id): + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("DELETE FROM rendezvous WHERE rendezvous_id = %s", (rendezvous_id,)) + db.commit() + + def update(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("UPDATE rendezvous " \ + "SET name =%s, " \ + "author=%s, " \ + "email=%s, " \ + "description=%s, " \ + "time_created=%s, " \ + "schedule_deadline=%s, " \ + "min_votes=%s, " \ + "type_id=%s, " \ + "status=%s, " \ + "location_id=%s, " \ + "is_date_fixed=%s, " \ + "tags=%s " \ + "WHERE rendezvous_id=%s", (self.name, self.author, self.email, + self.description, to_timestamp(self.time_created), to_timestamp(self.schedule_deadline), + self.min_votes, self.type_id, self.status, self.location_id, self.is_date_fixed, self.tags, self.rendezvous_id)) + db.commit() + + def __str__(self): + return "" % (self.rendezvous_id, self.name, self.author, self.email, str(self.time_created)) + + +class RendezVousModelProvider(Component): + implements(IEnvironmentSetupParticipant) + + SCHEMA = [ + + # rendezvous system + Table('rendezvous', key='rendezvous_id')[ + Column('rendezvous_id', auto_increment=True), + Column('name'), + Column('author'), + Column('email'), + Column('description'), + Column('time_created', type='int'), + Column('schedule_deadline', type='int'), + Column('min_votes', type='int'), + Column('type_id', type='int'), + Column('status'), + Column('location_id', type='int'), + Column('is_date_fixed', type='int'), + Column('tags'), + Index(['name']), + Index(['status']) ], + + Table('rendezvous_comment', key='comment_id')[ + Column('comment_id', auto_increment=True), + Column('rendezvous_id', type='int'), + Column('author'), + Column('comment'), + Column('time_created', type='int')], + + # an user's spare time frame + Table('rendezvous_date', key='id')[ + Column('date_id', auto_increment=True), + Column('rendezvous_id', type='int'), + Column('author'), + Column('email'), + Column('time_created', type='int'), + Column('time_begin', type='int'), + Column('time_end', type='int'), + Column('elected', type='int')], + + + Table('rendezvous_type', key=["type_id"])[ + Column("type_id", auto_increment=True), + Column("name"), + Index(["name"])], + + Table('rendezvous_type_to_permission', key=["type_id", "permission"])[ + Column("type_id", type="int"), + Column("permission")], + + # user's votings for a date with date and length of time frame + Table('rendezvous_vote', key=['vote_id'])[ + Column('vote_id', auto_increment=True), + Column('date_id', type='int'), + Column('user'), + Column('email'), + Column('time_created', type='int'), + Column('time_begin', type='int'), + Column('time_end', type='int'), + Index(['time_begin']), + Index(['time_end'])]] + + RendezVousDateTrigger = "CREATE TRIGGER fkd_date_rendezvous_id " \ + "BEFORE DELETE ON rendezvous " \ + "FOR EACH ROW BEGIN " \ + "DELETE from rendezvous_date WHERE rendezvous_id = OLD.rendezvous_id; " \ + "END;" + + RendezVousCommentTrigger = "CREATE TRIGGER fkd_comment_rendezvous_id " \ + "BEFORE DELETE ON rendezvous " \ + "FOR EACH ROW BEGIN " \ + "DELETE from rendezvous_comment WHERE rendezvous_id = OLD.rendezvous_id; " \ + "END;" + + RendezVousVoteTrigger = "CREATE TRIGGER fkd_vote_date_id " \ + "BEFORE DELETE ON rendezvous_date " \ + "FOR EACH ROW BEGIN " \ + "DELETE from rendezvous_vote WHERE date_id = OLD.date_id; " \ + "END;" + + TYPE_DATA = ( + (u'public',), + (u'admin',), + (u'Offizieller Treff',), + (u'Topic Treff',)) + + + LOCATION_DATA = ( + (u"CTDO, Langer August", "N", 51, 31, 39.4, "E", 7, 27, 53.8, 51.527611, 7.464922), + (u"WILA, Langer August", "N", 51, 31, 39.4, "E", 7, 27, 53.8, 51.527611, 7.464922)) + + TYPE_PERMISSIONS_DATA = ( + (1, u'RENDEZVOUS_VIEW'), + (2, u'RENDEZVOUS_ADMIN')) + + def environment_created(self): + + if not "rendezvous" in self.config.sections(): + data = {"graph_size_x" : 1024, + "graph_size_y" : 300, + "max_dates_per_rendezvous" : 99, + "max_description_length" : 1024, + "max_votes_per_date" : 99, + "show_location_map" : True, + "show_vote_graph" : True} + for k, v in data.iteritems(): + self.config.set("rendezvous", k, v) + self.config.save() + self._create_models(self.env.get_db_cnx()) + + def environment_needs_upgrade(self, db): + """First version - nothing to migrate, but possibly to create. + """ + + cursor = db.cursor() + try: + cursor.execute("select count(*) from rendezvous") + cursor.fetchone() + cursor.execute("select count(*) from rendezvous_date") + cursor.fetchone() + cursor.execute("select count(*) from rendezvous_comment") + cursor.fetchone() + cursor.execute("select count(*) from rendezvous_type") + cursor.fetchone() + cursor.execute("select count(*) from rendezvous_type_to_permission") + cursor.fetchone() + cursor.execute("select count(*) from rendezvous_vote") + cursor.fetchone() + return False + except: + db.rollback() + return True + + def upgrade_environment(self, db): + """ nothing to do here for now + """ + self._create_models(db) + + def _create_models(self, db): + + """Called when a new Trac environment is created.""" + + db_backend = None + try: + from trac.db import DatabaseManager + db_backend, _ = DatabaseManager(self.env)._get_connector() + except ImportError: + db_backend = self.env.get_db_cnx() + + cursor = db.cursor() + for table in self.SCHEMA: + for stmt in db_backend.to_sql(table): + self.env.log.debug(stmt) + try: + cursor.execute(stmt) + db.commit() + except Exception, e: + self.env.log.warning(str(e)) + db.rollback() + cursor.execute(self.RendezVousCommentTrigger) + cursor.execute(self.RendezVousDateTrigger) + cursor.execute(self.RendezVousVoteTrigger) + db.commit() + #try: + cursor.executemany("""INSERT INTO 'rendezvous_type' + (name) + VALUES(%s)""", self.TYPE_DATA) + db.commit() + +class RendezVousTypePermissionSystem(Component): + + def check_user_type_permissions(self, user, type_id=None, name=None): + ps = PermissionSystem(self.env).get_user_permissions(user) + if 'RENDEZVOUS_ADMIN' in ps: + return True + t = RendezVousType.fetch_one(self.env, type_id=type_id, name=name) + if not t: + return False + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("""SELECT permission + FROM rendezvous_type_to_permission + WHERE type_id=%s""", (t.type_id,)) + rows = cursor.fetchall() + for p in rows: + if not ps.has_key(p[0]): + return False + return True diff --git a/TracRendezVous/tracrendezvous/rendezvous/notification.py b/TracRendezVous/tracrendezvous/rendezvous/notification.py new file mode 100644 index 0000000..5706028 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/notification.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +from trac import __version__ +from trac.core import * +from trac.config import * +from trac.notification import NotifyEmail +from trac.util import md5 +from trac.util.datefmt import to_timestamp +from trac.util.text import CRLF, wrap, to_unicode, obfuscate_email_address + +from genshi.template.text import TextTemplate + +from tracrendezvous.model import RendezVous, RendezVousLocation + +class RendezVousNotificationSystem(Component): + + always_notify_workflow_change = BoolOption('rendezvous', 'always_notify_workflow_changes', + 'true', + """Always send notifications to all authors, voters in the rendezvous on workflow changes""") + + ticket_subject_template = Option('rendezvous', 'ticket_subject_template', + '$prefix #$rendezvous.rendezvous_id: $summary', + """A Genshi text template snippet used to get the notification subject. + (since 0.3)""") + +class RendezVousSchedulingNotifyEmail(NotifyEmail): + """Notification for a scheduled rendezvous. + """ + + template_name = "rendezvous_notify_email.txt" + rendezvous = None + modtime = 0 + from_email = 'trac+rendezvous@localhost' + COLS = 75 + ical_fmt = "%Y%m%dT%H%M00Z" + + def __init__(self, env): + NotifyEmail.__init__(self, env) + + def notify(self, editor, rendezvous, is_new=False, modtime=None): + self.editor = editor + self.rendezvous = rendezvous + self.modtime = modtime + self.is_new = is_new + + changes_body = '' + changes_descr = '' + self.rendezvous.link = self.env.abs_href.rendezvous(rendezvous.rendezvous_id) + self.rendezvous_description = wrap( + self.rendezvous.description, self.COLS, + initial_indent=' ', subsequent_indent=' ', linesep=CRLF) + + subject = self.format_subj("RendezVous evolved to %s") + if not is_new: + subject = 'Re: ' + subject + self.data.update({ + 'rendezvous_props': self.format_props(), + 'rendezvous_body_hdr': self.format_hdr(), + 'subject': subject, + 'rendezvous': rendezvous, + 'changes_body': changes_body, + 'changes_descr': changes_descr + }) + NotifyEmail.notify(self, rendezvous.rendezvous_id, subject) + + def format_props(self): + rendezvous = self.rendezvous + text = "" + text+= "%s: %s\n" % ("Name".center(12), rendezvous.name) + text+= "%s: %s\n" % ("Author".center(12), rendezvous.author) + text+= "%s: %s\n" % ("Created on".center(12), rendezvous.time_created) + location = RendezVousLocation.fetch_one(self.env, rendezvous.location_id) + text+= "%s: %s\n" % ("Location".center(12), location and location.name or "") + text+= "%s: %s\n" % ("Coordinates".center(12), location and location.coordinate_str() or "") + return text + + def get_recipients(self, rendezvous_id): + + def parse_email(txt): + return filter(lambda x: '@' in x, txt.replace(',', ' ').split()) + + r = RendezVous.fetch_one(self.env, rendezvous_id, fetch_dates=True) + recipients = [] + if r: + tmp = r.email + if tmp: + emails = parse_email(tmp) + recipients.extend(emails) + tmp = r.dates[r.elected].email + if tmp: + emails = parse_email(tmp) + recipients.extend(emails) + return recipients, [] + + def format_hdr(self): + return '#%s: %s' % (self.rendezvous.rendezvous_id, wrap(self.rendezvous.description, + self.COLS, linesep=CRLF)) + + def format_subj(self, summary): + template = self.config.get('rendezvous','rendezvous_subject_template') + template = TextTemplate(template.encode('utf8')) + + prefix = self.config.get('notification', 'smtp_subject_prefix') + if prefix == '__default__': + prefix = '[%s]' % self.config.get('project', 'name') + + data = { + 'prefix': prefix, + 'summary': summary, + 'rendezvous': self.rendezvous, + 'env': self.env, + } + + return template.generate(**data).render('text', encoding=None).strip() + + def get_message_id(self, rcpt, modtime=None): + """Generate a predictable, but sufficiently unique message ID.""" + s = '%s.%08d.%d.%s' % (self.config.get('project', 'url'), + int(self.rendezvous.rendezvous_id), to_timestamp(modtime), + rcpt.encode('ascii', 'ignore')) + dig = md5(s).hexdigest() + host = self.from_email[self.from_email.find('@') + 1:] + msgid = '<%03d.%s@%s>' % (len(s), dig, host) + return msgid + + def send(self, torcpts, ccrcpts): + dest = self.editor or 'anonymous' + hdrs = {} + hdrs['Message-ID'] = self.get_message_id(dest, self.modtime) + hdrs['X-Trac-Ticket-ID'] = str(self.rendezvous.rendezvous_id) + hdrs['X-Trac-Ticket-URL'] = self.rendezvous.link + if not self.is_new: + msgid = self.get_message_id(dest) + hdrs['In-Reply-To'] = msgid + hdrs['References'] = msgid + NotifyEmail.send(self, torcpts, ccrcpts, hdrs) \ No newline at end of file diff --git a/TracRendezVous/tracrendezvous/rendezvous/rendezvous_tag.py b/TracRendezVous/tracrendezvous/rendezvous/rendezvous_tag.py new file mode 100644 index 0000000..e99c0d7 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/rendezvous_tag.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +import re +from trac.core import * +from tractags.api import TagSystem, ITagProvider +from trac.util.text import to_unicode +from trac.util.compat import set, sorted +from trac.config import * +from trac.resource import Resource + +from model import RendezVous + +__all__ = ['RendezVousTagProvider',] + +class RendezVousTagProvider(Component): + """A tag provider using rendezvous tag field as sources of tags. + """ + implements(ITagProvider) + + ignore_closed_rendezvous = BoolOption('tags', 'ignore_closed_rendezvous', True, + 'Do not collect tags from closed rendezvous.') + + # ITagProvider methods + def get_taggable_realm(self): + return 'rendezvous' + + def get_tagged_resources(self, req, tags): + if 'RENDEZVOUS_VIEW' not in req.perm: + return + + split_into_tags = TagSystem(self.env).split_into_tags + db = self.env.get_db_cnx() + cursor = db.cursor() + args = [] + ignore = '' + if self.ignore_closed_rendezvous: + ignore = " WHERE status != 'closed'" + sql = "SELECT * FROM (SELECT rendezvous_id, tags, COALESCE(tags, '') as fields FROM rendezvous%s)" % ignore + constraints = [] + if tags: + constraints.append( + "(" + ' OR '.join(["fields LIKE %s" for t in tags]) + ")") + args += ['%' + t + '%' for t in tags] + else: + constraints.append("fields != ''") + + if constraints: + sql += " WHERE " + " AND ".join(constraints) + sql += " ORDER BY rendezvous_id" + self.env.log.debug(sql) + cursor.execute(sql, args) + for row in cursor: + id, ttags = row[0], ' '.join([f for f in row[1:-1] if f]) + rendezvous_tags = split_into_tags(ttags) + tags = set([to_unicode(x) for x in tags]) + if (not tags or rendezvous_tags.intersection(tags)): + yield Resource('rendezvous', id), rendezvous_tags + + def get_resource_tags(self, req, resource): + if 'RENDEZVOUS_VIEW' not in req.perm(resource): + return + rendezvous = RendezVous.fetch_one(self.env, resource.id) + tags = self._rendezvous_tags(rendezvous) + return tags + + def set_resource_tags(self, req, resource, tags): + #req.perm.require('RENDEZVOUS_MODIFY', resource) + split_into_tags = TagSystem(self.env).split_into_tags + rendezvous = RendezVous.fetch_one(self.env, resource.id) + all = self._rendezvous_tags(rendezvous) + tags.difference_update(all.difference(rendezvous.tags)) + rendezvous.tags = u' '.join(sorted(map(to_unicode, tags))) + rendezvous.update() + + def remove_resource_tags(self, req, resource): + req.perm.require('RENDEZVOUS_MODIFY', resource) + rendezvous = RendezVous.fetch_one(self.env, resource.id) + rendezvous.tags = None + rendezvous.update() + + # Private methods + def _rendezvous_tags(self, rendezvous): + return TagSystem(self.env).split_into_tags(rendezvous.tags) diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/admin_general.html b/TracRendezVous/tracrendezvous/rendezvous/templates/admin_general.html new file mode 100644 index 0000000..a49e5ec --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/admin_general.html @@ -0,0 +1,57 @@ + + + + + RendezVous General Settings + + +

    General Settings

    +
    +
    + RendezVous +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/admin_overview.html b/TracRendezVous/tracrendezvous/rendezvous/templates/admin_overview.html new file mode 100644 index 0000000..b381394 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/admin_overview.html @@ -0,0 +1,38 @@ + + + + + $label_plural + + + + +

    Overview Configuration

    +
    +
    + Overview Configuration +
    +
    + ${l[0]} + wrong position - should be ${l[3]}! + +
    +
    +

    + help +

    +
    + +
    +
    +
    + + + diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/admin_types.html b/TracRendezVous/tracrendezvous/rendezvous/templates/admin_types.html new file mode 100644 index 0000000..0305d52 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/admin_types.html @@ -0,0 +1,95 @@ + + + + + $label_plural + + + +

    Manage RendezVousTypes

    + +
    +
    + Add Type: + + + + + +
    +

    + Create a new RendezVous Type. Note that RendezVousType names can't be all upper-case, + as that is reserved for permission names. +

    +
    + +
    +
    +
    + +
    +
    + Grant Permission To Type: + + + + + + + + + +
    +
    +
    +

    + Grant permission to a RendezVousType. +

    +
    + +
    +
    +
    +
    + +
    + + + + + + + + + + + + +
    RendezVous TypeDefaultPermissiondelete
    ${rtype.name} + +
    + + +
    +
    +
    +
    + +
    +
    +

    + Note that RendezVousType names can't be all upper-case, + as that is reserved for permission names. +

    + + + diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/date.html b/TracRendezVous/tracrendezvous/rendezvous/templates/date.html new file mode 100644 index 0000000..e947da9 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/date.html @@ -0,0 +1,106 @@ + + + + + + Dates + + +
    +
    + + +
    +
    + Dates for ${rendezvous.name} + + + + + + + + + + + + + + + + + + + + + + + + +
    authoremaildate begintime begindate endtime enddelete
    ${date.author} ${dt.tzinfo.tzname(None)} ${dt2.tzinfo.tzname(None)}
    +
    +

    Change or delete votes for an existing rendezvous date.

    +
    + + +
    +
    +
    + +
    + New Date + + + + + +
    + ${dt.tzinfo.tzname(None)}
    + ${dt2.tzinfo.tzname(None)}
    +
    +
    + + +
    +
    +
    +

    Allowed date/time formats:

    +
      +
    • yyyymmdd
    • +
    • yyyy.mm.dd
    • +
    + +

    Dates for ${rendezvous.name}

    + + + + + + + + + + + + + + + +
    authordayday part
    ${date.author}${date.time_begin.strftime('%d.%m.%Y %H:%M')}${date.time_end.strftime('%d.%m.%Y %H:%M')}
    +
    +
    +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/location.html b/TracRendezVous/tracrendezvous/rendezvous/templates/location.html new file mode 100644 index 0000000..6896a7b --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/location.html @@ -0,0 +1,126 @@ + + + + + + Locations + + +
    +
    +
    + +
    + Known Locations + + + + + + + + + + +
    NameCoordinatesDefaultDelete
    +

    Change or delete existing locations.

    +
    + + +
    +
    +
    +
    +
    + New Location + + + +
    +
    + + +
    +

    +

    Allowed coordinates formats:

    +
      +
    • DD format = 'lat,lon', e.g 53.235235,6.235235
    • +
    • DMS Format = 'N|Wdd°mm'ss" E|Wdddd°mm'ss", e.g N51°31'39.40000" E7°27'53.7200"
    • +
    +

    +
    +
    +
    + +
    +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/overview.html b/TracRendezVous/tracrendezvous/rendezvous/templates/overview.html new file mode 100644 index 0000000..600d507 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/overview.html @@ -0,0 +1,57 @@ + + + + + RendezVous + + + +
    + + + +
    + + + +

    new

    +
    +
    + + + +

    voting

    +
    +
    + + + +

    canceled

    +
    +
    + + + +

    expired

    +
    + + + + +
    + + diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous.html b/TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous.html new file mode 100644 index 0000000..831c6dc --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous.html @@ -0,0 +1,149 @@ + + + + + RendezVous + + + +
    +
    +
    +

    RendezVous #${rendezvous.rendezvous_id} (${rendezvous.status})

    + Created on ${rendezvous.time_created.strftime('%d.%m.%Y %H:%M')} by ${rendezvous.author} +

    ${rendezvous.name}

    + + + + + + + + + + + + + + + + + + + + + + +
    Type${rendezvous.type_name}
    Min. Votes${rendezvous.min_votes}
    Tagged with${rendezvous.tags and rendezvous.tags or None}
    Location${location.name}
    Coordinates${location.coordinate_str()}
    +
    +
    +

    Description

    + ${wiki_to_html(context, rendezvous.description, escape_newlines=preserve_newlines)} +
    +

    Votings

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Date proposals -> +
    scheduled
    + ${dt_begin.strftime('%d.%m.%Y %H:%M')} ${dt_begin.tzinfo.tzname(None)} - ${dt_end.strftime('%d.%m.%Y %H:%M')} ${dt_end.tzinfo.tzname(None)} +
    + new date +
    VoteCount${date.get_vote_count()}
    ${row[0]} + + + + + vote   + ...advanced + + edit + + +
    + vote   + ...advanced +
    + +
    +

    Vote Distribution By RendezVousDate

    +
    + +
    +
    +
    + + +

    Comments

    +
    + added on ${comment.time_created.strftime('%d.%m.%Y %H:%M')} by ${comment.author} edit + ${wiki_to_html(context, comment.comment, escape_newlines=preserve_newlines)} +
    +
    +

    New Comment

    +
    + +

    Preview!

    +
    +
    + added on ${preview_comment.time_created.strftime('%d.%m.%Y %H:%M')} by ${preview_comment.author} + ${wiki_to_html(context, preview_comment.comment, escape_newlines=preserve_newlines)} +
    +
    +
    +
    + + + +
    + + + +
    + Note: See WikiFormatting and + TracWiki for help on editing wiki content. +
    +
    +
    +
    +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous_edit.html b/TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous_edit.html new file mode 100644 index 0000000..9e36bb2 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous_edit.html @@ -0,0 +1,93 @@ + + + + + + + ${title} + + +
    +
    +
    + +
    + ${title} + + + + + + + + + + + + + + + +
    + + + ${rendezvous.author} + +
    + ${dt.tzinfo.tzname(None)}
    + edit/search locations
    +
    + +
    + ${_("Workflow")} +
    + + + $controls + $hint +
    +
    +
    + Tags +
    + +
    +
    +
    + + + + + + + + +
    +
    +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous_notify_email.txt b/TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous_notify_email.txt new file mode 100644 index 0000000..f260bca --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous_notify_email.txt @@ -0,0 +1,2 @@ +$rendezvous_body_hdr + diff --git a/TracRendezVous/tracrendezvous/rendezvous/templates/vote.html b/TracRendezVous/tracrendezvous/rendezvous/templates/vote.html new file mode 100644 index 0000000..0cd25b6 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/templates/vote.html @@ -0,0 +1,129 @@ + + + + + + Votes + + +
    +
    +
    +
    + + All votes for ${rdate.time_begin.strftime('%x')} + Votes for ${rdate.time_begin.strftime('%x')} made by $authname + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    useremailbeginenddelete
    ${vote.user} + ${dt.tzinfo.tzname(None)} + ${dt2.tzinfo.tzname(None)}
    ${vote.user}${vote.mail}${dt.strftime('%d.%m.%Y')} + ${dt.strftime('%H:%M')} ${dt.tzinfo.tzname(None)}${dt2.strftime('%d.%m.%Y')} + ${dt2.strftime('%H:%M')} ${dt2.tzinfo.tzname(None)}
    + ${dt.tzinfo.tzname(None)} + ${dt2.tzinfo.tzname(None)}
    ${vote.user}${vote.time_begin.strftime('%d.%m.%Y')}${dt.strftime('%H:%M')} ${dt.tzinfo.tzname(None)}${dt2.strftime('%d.%m.%Y')}${dt2.strftime('%H:%M')} ${dt2.tzinfo.tzname(None)}
    +
    +
    + + +
    +
    +

    Change or delete votes for an existing rendezvous date.

    +
    +
    + Add new vote for ${rdate.time_begin.strftime("%x")}: + + + + + + + + + + + + + + + + + +
    + ${dt.tzinfo.tzname(None)}
    + ${dt.tzinfo.tzname(None)}
    +
    + +
    +
    +
    +

    Add a vote to an existing rendezvous date.

    +
    +

    Allowed date/time formats:

    +
      +
    • time: +
        +
      • 'hhMM'
      • +
      • 'hh:MM'
      • +
      +
    • +
    • date: +
        +
      • 'yyyymmdd'
      • +
      • 'yyyy.mm.dd'
      • +
      +
    • +
    +
    + + diff --git a/TracRendezVous/tracrendezvous/rendezvous/utils.py b/TracRendezVous/tracrendezvous/rendezvous/utils.py new file mode 100644 index 0000000..45f5970 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/utils.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +from re import compile as re_compile +from re import match +from decimal import Decimal, Context, getcontext +from datetime import date, time, datetime, timedelta +from os.path import join, dirname +from os.path import exists as path_exists +from os import mkdir +from sys import maxint + +from trac.util import Ranges +from trac.wiki import WikiPage, WikiSystem +from trac.util.datefmt import utc, to_timestamp, localtz, format_time, get_timezone, timezone +from PIL import Image, ImageDraw, ImageFont + +__all__ = ["check_date_collision", "check_vote_collision","update_votes_graph"] + +class ValidationError(ValueError): + def __str__(self): + return "ValidationError: value out of bounds!" + + +class VoteCollisionError(ValueError): + def __str__(self): + return "VoteCollisionError: that vote collides with another one from you!" + + +def check_vote_collision(newvote, votes): + for vote in votes: + if newvote.vote_id == vote.vote_id: + continue + if vote.time_begin < newvote.time_begin < vote.time_end: + raise VoteCollisionError + if vote.time_begin < newvote.time_end < vote.time_end: + raise VoteCollisionError + if newvote.time_begin < vote.time_begin < newvote.time_end: + raise VoteCollisionError + if newvote.time_begin < vote.time_end < newvote.time_end: + raise VoteCollisionError + +def check_date_collision(newdate, dates): + for date in dates: + if newdate.date_id == date.date_id: + continue + if date.time_begin < newdate.time_begin < date.time_end: + raise VoteCollisionError + if date.time_begin < newdate.time_end < date.time_end: + raise VoteCollisionError + if newdate.time_begin < date.time_begin < newdate.time_end: + raise VoteCollisionError + if newdate.time_begin < date.time_end < newdate.time_end: + raise VoteCollisionError + +def colorGen(steps): + r = 255 + g = 0 + b = 0 + rsteps=512/steps + for ry in xrange(255): + g+=rsteps + g = max(255,g) + yield r,g,b + for yg in xrange(255, 0, -1): + r-=rsteps + r = min(0,r) + yield r,g,b + + +def create_graph(gname, votes, path, real_mindate, maxdate, size, selected_tz): + """ I wanna be a better gantt diagram - oh yeah ... + Render an diagram image of all votes users made. The + x-axis is the timeline beginning with the hour of the first vote as origin and + the hour after the last vote. + """ + + import math + def trange(start, end, vhours, deltaT, width): + pixels = 0 + units = pixelPerHour + scaledDeltaTime = timedelta(0,3600,0) + scaleFactor = 1 + if vhours > 10: + scaleFactor = math.ceil(vhours / 10.0) + scaledDeltaTime *= int(scaleFactor) + units *= scaleFactor + e = end.replace(minute=0,second=0, microsecond=0)+timedelta(0,3600) + while start <= e: + yield pixels, (format_time(start, '%d', tzinfo=selected_tz), format_time(start, '%H:%M', tzinfo=selected_tz)) + start += scaledDeltaTime + pixels += units + + deltaT = maxdate - real_mindate + mindate = real_mindate.replace(minute=0, second=0, microsecond=0) + steps = len(votes) + if steps <= 0: + return + hours = int(deltaT.days * 24.0 + deltaT.seconds / 3600.0) + vhours = hours + 2 + font = None + fontpath = join(dirname(__file__), "luxisr.ttf") + xBase = 0 + pixelPerHour = (size[0]-xBase) / vhours + hOffset = mindate.hour + halfHour = pixelPerHour / 2 + user2Row = {} + rowCount=0 + for d in xrange(steps): + user=votes[d].user + if not user2Row.has_key(user): + user2Row[user] = rowCount + rowCount+=1 + fontSize = 22 + deltaHeightPerVote = fontSize*2 + size[1] = (rowCount + 2) * deltaHeightPerVote + chart = Image.new("RGBA", size, (230, 230, 230, 255)) + chartDraw = ImageDraw.Draw(chart) + yBase = size[1]-fontSize*2 + font = ImageFont.truetype(fontpath, fontSize) + timefont = ImageFont.truetype(fontpath, 13) + del rowCount + for d in xrange(steps): + user=votes[d].user + n = user2Row[user] + i = votes[d].time_begin + j = votes[d].time_end + tmp=i-mindate + tmp=tmp.days * 24.0 + tmp.seconds / 3600.0 + x1 = tmp * pixelPerHour + xBase + tmp=j - mindate + tmp=tmp.days * 24 + tmp.seconds / 3600.0 + x2 = tmp * pixelPerHour + xBase + y1 = yBase - (n + 1) * deltaHeightPerVote + y2 = y1 + deltaHeightPerVote + chartDraw.rectangle((x1, y1, x2, y2), fill=(161, 235, 255)) + chartDraw.line((x1, y1, x1, yBase), fill=(150, 150, 150)) + chartDraw.line((x2, y1, x2, yBase), fill=(150, 150, 150)) + chartDraw.line((xBase, y1, size[0], y1), fill=(150, 150, 150)) + chartDraw.line((xBase, y2, size[0], y2), fill=(150, 150, 150)) + chartDraw.text((xBase, y1 + deltaHeightPerVote/4), votes[d].user, font=font, fill=(0,0,0)) + chartDraw.line((0, yBase, size[0], yBase), fill = (0, 0, 0, 255), width=2) + fontBaseLine = size[1] - 30 + #chartDraw.text((xBase, fontSize/2), gname, font=font, fill=(0, 0, 250)) + for xMajor, ts in trange(mindate, maxdate, vhours, deltaT, size[0] - xBase): + chartDraw.text((xMajor + xBase, fontBaseLine), ts[0], font=timefont, fill=(0, 0, 0)) + chartDraw.text((xMajor + xBase, fontBaseLine + 15), ts[1], font=timefont, fill=(0, 0, 0)) + chartDraw.line((xMajor + xBase, size[1] - fontSize, xMajor + xBase, yBase), fill=(0, 0, 0, 255), width=2) + chart.save(join(path, "date%d.png" % votes[0].date_id), "PNG") + + + +def update_votes_graph(gname, dvotes, path, size, selected_tz): + if dvotes: + if not path_exists(path): + mkdir(path, 0755) + votes = sorted(dvotes, date_cmp) + matchCount, mindate, maxdate = date_stats(votes) + create_graph(gname, votes, path, mindate, maxdate, size, selected_tz) + + +def date_cmp(a,b): + if a.time_begin < b.time_begin: + return -1 + elif a.time_begin > b.time_begin: + return 1 + else: + return 0 + +def date_stats(votes): + """ I'm expecting a list of votes sorted by time_begin. + I'm returning a tuple of 3 values + * a list with match counts + * the minimum date in the votes + * the maximum date in the votes + """ + if len(votes) == 0: + return None, None, None + maxdate = datetime(1, 1, 1, tzinfo=utc) + mindate = votes[0].time_begin + ic=0 + matchCount = [0 for i in xrange(len(votes))] + for vote in votes: + if vote.time_end > maxdate: + maxdate = vote.time_end + jc=0 + for voteB in votes: + if voteB.time_begin < vote.time_end: + matchCount[ic] += 1 + matchCount[jc] += 1 + jc+=1 + ic+=1 + return matchCount, mindate, maxdate + + +def sortVotesPerUser(votes, users): + myvotes = [False for i in users] + for i in votes: + myvotes[users[i.user]] = i + + +def sortedUsers(users): + myusers = ["" for i in users] + for i in users: + myusers[users[i]] = i diff --git a/TracRendezVous/tracrendezvous/rendezvous/web_ui.py b/TracRendezVous/tracrendezvous/rendezvous/web_ui.py new file mode 100644 index 0000000..63b46f6 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/web_ui.py @@ -0,0 +1,1076 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime, timedelta +from os import mkdir +from os.path import join +from re import match, sub +from collections import defaultdict +from sqlite3 import IntegrityError +from operator import attrgetter +from trac.config import * +from trac.core import Component, implements, TracError +from trac.perm import PermissionError +from trac.util import get_reporter_id +from trac.util.text import to_unicode +from trac.util.datefmt import utc, get_timezone, localtz, timezone +from trac.util.html import html +from trac.util.translation import _ +from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet, add_warning, add_notice, add_ctxtnav, add_script +from trac.web import IRequestHandler +from genshi.builder import tag + +from tracrendezvous.location.loc_xml import * +from tracrendezvous.rendezvous import api +from tracrendezvous.rendezvous.model import * +from tracrendezvous.location.utils import * +from tracrendezvous.location.model import ItemLocation +from ctdotools.utils import * +from tracrendezvous.rendezvous.utils import * + +__all__ = ['RendezVousModule',] + +class RendezVousModule(Component): + + '''The web ui frontend for the rendezvous system''' + + implements(INavigationContributor, + IRequestHandler, + ITemplateProvider) + + IntOption("rendezvous", "default_location", 1, doc=u"used for rendezvous' location") + Option("rendezvous", "default_status", u"open") + IntOption("rendezvous", "default_rendezvous_type", 1, doc=u"default RendezVous type") + BoolOption("rendezvous", "show_location_map", True, doc="if set to True displays an openmaps iframe in the rendezvous properties") + BoolOption("rendezvous", "show_vote_graph", False, doc=u"if set to True displays a vote graph for every date") + + # INavigationContributor methods + def get_active_navigation_item(self, req): + return 'rendezvous' + + def get_navigation_items(self, req): + if "RENDEZVOUS_VIEW" in req.perm: + yield ('mainnav', 'rendezvous', html.A('RendezVous', href=req.href.rendezvouses())) + + def match_request(self, req): + + key = req.path_info + if key == u"/rendezvouses": + return True + + m = match(r'/rendezvous/(\d+)$', key) + if m: + req.args['rendezvous_id'] = int(m.group(1)) + return True + + m = match(r'/quickvote/(\d+)$', key) + if m: + req.args['date_id'] = int(m.group(1)) + return True + + m = match(r'/vote/(\d+)$', key) + if m: + req.args['date_id'] = int(m.group(1)) + return True + + m = match(r'/rendezvous/(\d+)/comment/(\d+)$', key) + if m: + req.args['rendezvous_id'] = int(m.group(1)) + req.args['comment_id'] = int(m.group(2)) + return True + + if key == u"/newrendezvous": + return True + + m = match(r"/location(/(\d+))?$", key) + if m: + if m.groups(1) != None: + req.args['rendezvous_id'] = int(m.group(2)) + return True + + m = match(r'/editrendezvous/(\d+)$', key) + if m: + req.args["rendezvous_id"] = int(m.group(1)) + return True + + m = match(r'/date/(\d+)$', key) + if m: + req.args["rendezvous_id"] = int(m.group(1)) + return True + return False + + def process_request(self, req): + + query = req.path_info + if "/rendezvouses" in query: + return self._display_overview(req) + elif "/rendezvous" in query: + return self._display_rendezvous(req) + elif "/quickvote" in query: + return self._process_quickvote(req) + elif "/vote" in query: + return self._process_vote(req) + elif "/date" in query: + return self._process_date(req) + elif "/comment" in query: + return self._process_rendezvous(req) + elif "/newrendezvous" in query: + return self._process_rendezvous(req) + elif "/location" in query: + return self._process_location(req) + elif "/editrendezvous" in query: + return self._process_rendezvous(req) + raise TracError("unknown request") + + # ITemplateProvider methods + # Used to add the plugin's templates and htdocs + def get_templates_dirs(self): + from pkg_resources import resource_filename + return [resource_filename(__name__, 'templates')] + + def get_htdocs_dirs(self): + """Return a list of directories with static resources (such as style + sheets, images, etc.) + + Each item in the list must be a `(prefix, abspath)` tuple. The + `prefix` part defines the path in the URL that requests to these + resources are prefixed with. + + The `abspath` is the absolute path to the directory containing the + resources on the local file system. + """ + from pkg_resources import resource_filename + return [('hw', resource_filename(__name__, 'htdocs'))] + + def _process_comment(self, req): + + '''display an overview of active rendezvous or details about one if requested''' + req.perm.require("RENDEZVOUS_COMMENT_VIEW") + add_stylesheet (req, 'hw/css/rendezvous.css') + comment_id = int(req.args["comment_id"]) + comment = RendezVousComment.fetch_one(self.env, comment_id) + data = {"comment" : comment} + add_ctxtnav(req, "Back to RendezVous # '%s'" % comment.rendezvous_id, req.href.rendezvous(comment.rendezvous_id)) + if not comment: + raise ValueError("RendezVousComment not found") + if req.method == "POST" and \ + req.args.has_key("comment"): + req.perm.require('RENDEZVOUS_COMMENT_ADD') + comment.comment = to_unicode(req.args["comment"]) + if not req.args.has_key("preview"): + comment.update() + req.redirect(req.href.comment(comment_id)) + else: + data["preview"] = True + return 'comment.html', data, None + + def _process_date(self, req): + + '''process add,change,delete actions for dates''' + req.perm.require("RENDEZVOUS_DATE_VIEW") + add_stylesheet (req, 'hw/css/rendezvous.css') + add_stylesheet (req, 'hw/css/ui.all.css') + add_script (req, 'hw/scripts/jquery-ui-1.6.custom.min.js') + rendezvous_id = int(req.args["rendezvous_id"]) + validate_id(rendezvous_id) + now = datetime.now(utc) + rd = RendezVousDate(self.env, 0, rendezvous_id, req.authname, req.args.get("email", None), datetime.now(utc), now, now + timedelta(seconds=7200), False, False) + session_tzname, selected_tz = get_tz(req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + if req.session.has_key("notice"): + add_notice(req, req.session["notice"]) + del req.session["notice"] + req.session.save() + data = {'settings': {'session': req.session, 'session_id': req.session.sid}, + 'localtz': localtz, + "new_date" : rd, + "selected_tz" : selected_tz, + "session_tzname" : session_tzname} + del now + ren = RendezVous.fetch_one(self.env, rendezvous_id, fetch_dates=True) + if not ren: + add_warning(req, tag.span("RendezVous not found. You can create a new RendezVous ", tag.a("here", href=req.href.newrendezvous()))) + return 'rendezvous.html', data, None + data["rendezvous"] = ren + data["dates"] = RendezVousDate.fetch_by_rendezvous(self.env, rendezvous_id) + updated_dates = set() + add_ctxtnav(req, 'Back to Overview', req.href.rendezvouses()) + add_ctxtnav(req, 'Back to RendezVous #%d'% rendezvous_id, req.href.rendezvous(rendezvous_id)) + add_ctxtnav(req, 'Edit RendezVous #%d'% rendezvous_id, req.href.editrendezvous(rendezvous_id)) + if req.method == "POST": + # perhaps we can permit certain changes e.g time_*. + # Tell me your point of view on that subject as ticket + if ren.elected: + add_warning(req, "RendezVous is already scheduled. No changes allowed!") + return 'date.html', data, None + if req.args.has_key("newdate") and \ + req.args.has_key("date_begin") and \ + req.args.has_key("time_begin") and \ + req.args.has_key("date_end") and \ + req.args.has_key("time_end"): + req.perm.require('RENDEZVOUS_DATE_ADD') + is_valid = True + try: + rd.time_begin = datetime_parse("%s %s" % (req.args["date_begin"], req.args["time_begin"]), selected_tz) + except Exception, e: + add_warning(req, str(e)) + is_valid = False + try: + rd.time_end = datetime_parse("%s %s" % (req.args["date_end"], req.args["time_end"]) , selected_tz) + except Exception, e: + add_warning(req, str(e)) + is_valid = False + try: + self._validate_date(rd) + except Exception, e: + add_warning(req, str(e)) + is_valid = False + if RendezVousDate.exists(self.env, rd.time_begin, rd.time_end): + add_warning(req, "RendezVousDate already exists") + is_valid = False + try: + check_date_collision(rd, data["dates"]) + except Exception, e: + add_warning(req, "RendezVousDate collides with other dates") + is_valid = False + if is_valid: + rd.commit() + if req.args.has_key("autovoting"): + self._do_quickvote(req, rd.date_id) + req.session["notice"] = "new RendezVousDate saved successfully" + req.redirect(req.href.date(rendezvous_id)) + elif req.args.has_key("savedates"): + req.perm.require("RENDEZVOUS_DATE_MODIFY") + dates = {} + for i in ren.dates: + i.changes = {} + dates[i.date_id] = i + deleted = set() + changed = set() + deleted_dates = req.args.get("delete", []) + if isinstance(deleted_dates, basestring): + deleted_dates = [deleted_dates,] + for date_id in deleted_dates: + did = int(date_id) + RendezVousDate.delete(self.env, did) + try: + del dates[did] + deleted.add(did) + except Exception: + pass + del deleted_dates + for key in req.args: + kind = date_id = None + try: + kind, date_id = key.split(":", 1) + except ValueError: + continue + date_id = int(date_id) + try: + validate_id(date_id) + except Exception, e: + add_warning(str(e)) + continue + if date_id in deleted: + continue + if not dates.has_key(date_id): + add_warning(req, "could not find RendezVousDate with date_id '%d" % date_id) + continue + vd = dates[date_id] + if vd.author != req.authname: + req.perm.require("RENDEZVOUS_ADMIN") + if kind == "email": + email = unicode(req.args[key]) + if email != vd.email: + vd.email = email + changed.add(date_id) + elif kind == "time_begin": + try: + tmp = time_parse(req.args[key]) + tmp = local_to_utc(vd.time_begin.replace(hour=tmp.hour, minute=tmp.minute), selected_tz) + except Exception, e: + add_warning(req, str(e)) + continue + else: + if vd.time_begin != tmp: + ch = vd.changes.get("time_begin", dict()) + if ch: + ch["new"].update({"old" : vd.time_begin.time(), "new" : tmp}) + else: + ch.update({"old" : vd.time_begin.time(), "new" : tmp}) + vd.time_begin = tmp + changed.add(date_id) + elif kind == "time_end": + try: + tmp = time_parse(req.args[key]) + tmp = local_to_utc(vd.time_end.replace(hour=tmp.hour, minute=tmp.minute), selected_tz) + except ValueError, e: + add_warning(req, str(e)) + continue + else: + if vd.time_end != tmp: + ch = vd.changes.get("time_end", dict()) + if ch: + ch.update({"old" : vd.time_end, "new" : tmp}) + else: + ch["new"] = tmp + vd.time_end = tmp + changed.add(date_id) + elif kind == "date_begin": + try: + tmp = date_parse(req.args[key]) + tmp = vd.time_begin.replace(year=tmp.year, month=tmp.month, day=tmp.day) + except Exception, e: + add_warning(req, str(e)) + continue + else: + if vd.time_begin != tmp: + ch = vd.changes.get("time_begin", dict()) + if ch: + ch["new"] = tmp + else: + ch.update({"old" : vd.time_begin, "new" : tmp}) + vd.time_begin = tmp + changed.add(date_id) + elif kind == "date_end": + try: + tmp = date_parse(req.args[key]) + tmp = vd.time_end.replace(year=tmp.year, month=tmp.month, day=tmp.day) + except Exception, e: + add_warning(req, str(e)) + continue + else: + if vd.time_end != tmp: + ch = vd.changes.get("time_end", dict()) + if ch: + ch["new"] = tmp + else: + ch.update({"old" : vd.time_end, "new" : tmp}) + vd.time_end = tmp + changed.add(date_id) + path = join(self.env.path, "htdocs", "rendezvous_graphs") + sizex = self.config.getint("rendezvous", "graph_size_x") + sizey = self.config.getint("rendezvous", "graph_size_y") + for date_id in changed: + if date_id in deleted: + continue + try: + d = dates[date_id] + check_date_collision(d, dates.values()) + except Exception, e: + add_warning(req, "RendezVousDate collides with other dates") + continue + try: + d = dates[date_id] + self._validate_date(d) + if RendezVousDate.exists(self.env, dates[date_id].time_begin, dates[date_id].time_end): + raise TracError("RendezVousDate already exists") + d.update() + real_tz = selected_tz.fromutc(d.time_begin).tzinfo + update_votes_graph("", d.votes, path, [sizex, sizey], real_tz) + except Exception, err: + add_warning(req, str(err)) + continue + req.session["notice"] = "RendezVousDates updated successfully." + req.redirect(req.href.date(rendezvous_id)) + data["dates"] = RendezVousDate.fetch_by_rendezvous(self.env, rendezvous_id) + return 'date.html', data, None + + def _process_location(self, req): + '''process add,change,delete actions''' + add_stylesheet (req, 'hw/css/rendezvous.css') + data = {"results": []} + default_location = self.config.getint("rendezvous", "default_location") + try: + rendezvous_id = req.args.get("rendezvous_id", None) + except Exception: + req.redirect(req.href.rendezvouses()) + if rendezvous_id: + add_ctxtnav(req, "Back to RendezVous #%s" % rendezvous_id, req.href.rendezvous(rendezvous_id)) + else: + add_ctxtnav(req, "New RendezVous", req.href.newrendezvous()) + if req.method == "GET": + # edit existing RendezVous + req.perm.require("RENDEZVOUS_LOCATION_VIEW") + data.update({}) + elif req.method == "POST": + if req.args.has_key("location_search"): + query = unicode(req.args["location_search"]) + results = search_location(query) + data["location_results"] = results + if req.args.has_key("savelocations"): + req.perm.require("RENDEZVOUS_LOCATION_MODIFY") + deleted = [] + locations = {} + changed = {} + if req.args.has_key("default"): + default = int(req.args["default"]) + if default_location != default: + default_location = default + self.config.set("rendezvous", "default_location", default) + self.config.save() + for key in req.args: + kind = location_id = None + try: + kind, location_id = key.split(":", 1) + except ValueError: + continue + location_id = int(location_id) + if location_id in deleted: + continue + if not locations.has_key(location_id): + location = ItemLocation.fetch_one(self.env, location_id) + if not location: + add_warning(req, "could not find location with location id '%d'" % location_id) + continue + locations[location_id] = location + + if kind == "delete": + req.perm.require("RENDEZVOUS_LOCATION_DELETE") + locations[location_id].delete() + deleted.append(location_id) + del locations[location_id] + elif kind == "name": + name = req.args[key] + if not name: + add_warning(req, "location name must be specified for location '%d'" % location_id) + continue + if name != locations[location_id].name: + locations[location_id].name = name + changed[location_id] = True + elif kind == "location": + coordinates = req.args[key] + if coordinates: + try: + lat, lon = validate_dd_coordinates(coordinates) + lat_side, lat_deg, lat_min, lat_sec, lon_side, lon_deg, lon_min, lon_sec = convert_dd_2_dms(lat, lon) + except ValueError: + try: + lat_side, lat_deg, lat_min, lat_sec, lon_side, lon_deg, lon_min, lon_sec = validate_dms_coordinates(coordinates) + lat, lon = convert_dms_2_dd(lat_side, lat_deg, lat_min, lat_sec, lon_side, lon_deg, lon_min, lon_sec) + except ValueError: + add_warning(req, "coordinates have wrong format") + continue + if lat != location.lat: + location.lat = lat + changed[location_id] = True + if lon != location.lon: + location.lon = lon + changed[location_id] = True + if lat_side != location.lat_side: + location.lat_side = lat_side + changed[location_id] = True + if lat_deg != location.lat_deg: + location.lat_deg = lat_deg + changed[location_id] = True + if lat_min != location.lat_min: + location.lat_min = lat_min + changed[location_id] = True + if lat_sec != location.lat_sec: + location.lat_sec= lat_sec + changed[location_id] = True + if lon_side != location.lon_side: + location.lon_side = lon_side + changed[location_id] = True + if lon_deg != location.lon_deg: + location.lon_deg = lon_deg + changed[location_id] = True + if lon_min != location.lon_min: + location.lon_min = lon_min + changed[location_id] = True + if lon_sec != location.lon_sec: + location.lon_sec = lon_sec + changed[location_id] = True + for dvi in changed: + if dvi not in deleted: + try: + locations[dvi].update() + except Exception, err: + add_warning(req, str(err)) + continue + + if req.args.has_key("addlocation") and \ + req.args.has_key("location_name"): + req.perm.require("RENDEZVOUS_LOCATION_ADD") + rl = ItemLocation(self.env, name=req.args["location_name"]) + is_valid = True + if not rl.name: + add_warning(req, "Coordinate name is empty") + is_valid = False + if req.args.has_key("coordinates"): + coordinates = unicode(req.args["coordinates"]) + if coordinates: + try: + rl.lat, rl.lon = validate_dd_coordinates(coordinates) + rl.lat_side, rl.lat_deg, rl.lat_min, rl.lat_sec, rl.lon_side, rl.lon_deg, rl.lon_min, rl.lon_sec = convert_dd_2_dms(rl.lat, rl.lon) + except ValueError: + try: + rl.lat_side, rl.lat_deg, rl.lat_min, rl.lat_sec, rl.lon_side, rl.lon_deg, rl.lon_min, rl.lon_sec = validate_dms_coordinates(coordinates) + rl.lat, rl.lon = convert_dms_2_dd(rl.lat_side, rl.lat_deg, rl.lat_min, rl.lat_sec, rl.lon_side, rl.lon_deg, rl.lon_min, rl.lon_sec) + except ValueError: + add_warning(req, "coordinates have wrong format") + is_valid = False + if is_valid: + rl.commit() + data.update({"results" : None, + "default_location" : default_location, + "rendezvous_id" : rendezvous_id, + "locations" : ItemLocation.fetch_all(self.env)}) + return 'location.html', data, None + + def _process_rendezvous(self, req): + + '''process add, change and delete rendezvous''' + add_ctxtnav(req, 'Back to Overview', req.href.rendezvouses()) + data = {"results": None} + add_stylesheet (req, 'hw/css/rendezvous.css') + add_stylesheet (req, 'hw/css/ui.all.css') + add_script (req, 'hw/scripts/jquery-ui-1.6.custom.min.js') + rtype = self.config.getint("rendezvous", "default_rendezvous_type") + rendezvous = None + rendezvous_id = int(req.args.get("rendezvous_id", 0)) + if req.session.has_key("notice"): + add_notice(req, req.session["notice"]) + del req.session["notice"] + req.session.save() + if rendezvous_id != 0: + rendezvous = RendezVous.fetch_one(self.env, rendezvous_id, fetch_dates=True) + if not rendezvous: + raise TracError("rendezvous not found") + add_ctxtnav(req, 'Back to RendezVous # %s' % rendezvous_id, req.href.rendezvous(rendezvous_id)) + add_ctxtnav(req, 'Edit dates for RendezVous # %s' % rendezvous_id, req.href.date(rendezvous_id)) + add_ctxtnav(req, 'Edit votes for RendezVous # %s' % rendezvous_id, req.href.vote(rendezvous_id)) + data["title"] = "RendezVous #%d" % rendezvous.rendezvous_id + else: + rendezvous = RendezVous(self.env, False, 0, u"", u"", u"", u"", + datetime.now(utc), datetime.now(utc) + timedelta(1), 0, rtype, "new", + self.config.getint("rendezvous", "default_location"), False, u"") + data["title"] = "New RendezVous" + actions = api.RendezVousSystem(self.env).get_available_actions( + req, rendezvous) + + new_status = req.args.get("action", None) + + if req.method == "GET": + req.perm.require("RENDEZVOUS_MODIFY") + + if req.method == "POST": + is_valid = True + if req.args.has_key("add"): + req.perm.require("RENDEZVOUS_ADD") + if req.args.has_key("delete") and rendezvous: + req.perm.require("RENDEZVOUS_DELETE") + RendezVous.delete(self.env, rendezvous_id) + req.redirect(req.href.rendezvouses()) + elif req.args.has_key("edit"): + req.perm.require("RENDEZVOUS_MODIFY") + if req.authname != rendezvous.author: + req.perm.require("RENDEZVOUS_ADMIN") + if new_status: + for controller in self._get_action_controllers(req, rendezvous, new_status): + controller.change_rendezvous_workflow(req, rendezvous, new_status) + new_status = None + + type_name = req.args.get("type_name", False) + location_text = req.args.get("location", False) + + name = req.args.get("name", False) + email = req.args.get("email", False) + description = req.args.get("description", False) + tags = req.args.get("tags", False) + try: + rendezvous.schedule_deadline = datetime_parse("%s %s" % (req.args["schedule_deadline_date"], req.args["schedule_deadline_time"]), tzinfo=utc) + except Exception, e: + is_valid = False + add_warning(req, "schedule_deadline has wrong format/type" + str(e)) + + if name: + rendezvous.name = name + + if email: + rendezvous.email = email + + if description: + rendezvous.description = description + + if tags: + rendezvous.tags = sub("[,;.:]", " ", tags) + + if type_name: + rt = RendezVousType.fetch_one(self.env, name=type_name) + if rt: + rendezvous.type_id = rt.type_id + + if location_text: + try: + name,coords = location_text.split(" :", 1) + loc = ItemLocation.search_one(self.env, name=name) + if loc: + rendezvous.location_id = loc.location_id + except ValueError: + pass + + try: + self._validate_rendezvous(rendezvous) + except Exception, err: + add_warning(req, str(err)) + else: + if req.args.has_key("add") and is_valid: + rendezvous.author = req.args.get("author", req.authname) + rendezvous.commit() + req.redirect(req.href.date(rendezvous.rendezvous_id)) + add_notice(req, "Added rendezvous!") + if req.args.has_key("edit") and is_valid: + rendezvous.update() + + action_controls = [] + sorted_actions = api.RendezVousSystem(self.env).get_available_actions(req, + rendezvous) + if not new_status: + new_status = sorted_actions[0] + for saction in sorted_actions: + first_label = None + hints = [] + widgets = [] + for controller in self._get_action_controllers(req, rendezvous, + saction): + label, widget, hint = controller.render_rendezvous_action_control( + req, rendezvous, saction) + if not first_label: + first_label = label + widgets.append(widget) + hints.append(hint) + action_controls.append((saction, first_label, tag(widgets), hints)) + session_tzname, selected_tz = get_tz(req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + data.update({"action_controls" : action_controls, + "cstatus" : new_status, + "types" : RendezVousType.fetch_all(self.env), + 'settings': {'session': req.session, 'session_id': req.session.sid}, + "selected_tz" : selected_tz, + "session_tzname" : session_tzname, + "default_rendezvous_type" : self.config.getint("rendezvous", "default_rendezvous_type"), + "default_location" : self.config.getint("rendezvous", "default_location"), + "locations" : ItemLocation.fetch_all(self.env), + "rendezvous": rendezvous}) + return 'rendezvous_edit.html', data, None + + def _process_quickvote(self, req, redirecting=True): + date_id = int(req.args.get("date_id")) + a,b = self._do_quickvote(req, date_id) + if a: + req.redirect(req.href("rendezvous", b)) + + def _process_vote(self, req): + '''display and process create,view,change and delete actions to vote''' + add_stylesheet (req, 'hw/css/rendezvous.css') + add_stylesheet (req, 'hw/css/ui.all.css') + add_script (req, 'hw/scripts/jquery-ui-1.6.custom.min.js') + session_tzname, selected_tz = get_tz(req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + data = {'settings': {'session': req.session, 'session_id': req.session.sid}, + "session_tzname" : session_tzname, + "selected_tz" : selected_tz} + date_id = int(req.args.get("date_id")) + validate_id(date_id) + rdate = RendezVousDate.fetch_one(self.env, date_id) + if not rdate: + raise TracError("RendezVousDate not found") + if req.session.has_key("notice"): + add_notice(req, req.session["notice"]) + del req.session["notice"] + req.session.save() + update_graph=False + if req.method == "POST": + if req.args.has_key("add"): + user = to_unicode(req.args.get("user")) + email = to_unicode(req.args.get("email")) + req.perm.require("RENDEZVOUS_VOTE_ADD") + dv = RendezVousVote(self.env, 1, date_id, user, email, datetime.now(utc), rdate.time_begin, rdate.time_end) + try: + rd.time_begin = datetime_parse("%s %s" % (req.args["date_begin"], req.args["time_begin"]), selected_tz) + except Exception, e: + add_warning(req, str(e)) + is_valid = False + try: + rd.time_end = datetime_parse("%s %s" % (req.args["date_end"], req.args["time_end"]) , selected_tz) + except Exception, e: + add_warning(req, str(e)) + is_valid = False + if not RendezVousVote.exists(self.env, dv.date_id, dv.user, dv.time_begin, dv.time_end): + try: + self._validate_vote(dv) + check_vote_collision(dv, RendezVousVote.fetch_by_date(self.env, date_id, user)) + except Exception, err: + add_warning(req, str(err)) + else: + dv.commit() + req.session["notice"] = "Vote added successfully" + update_graph = True + else: + add_warning(req, "vote already exists") + + elif req.args.has_key("savevotes"): + req.perm.require("RENDEZVOUS_VOTE_MODIFY") + votes = {} + tmp = RendezVousVote.fetch_by_date(self.env, date_id) + for i in tmp: + votes[i.vote_id] = i + del tmp + deleted = set() + changed = set() + deleted_votes = req.args.get("delete", []) + if isinstance(deleted_votes, basestring): + deleted_votes = [deleted_votes,] + for vote_id in deleted_votes: + RendezVousVote.delete(self.env, int(vote_id)) + try: + del votes[vote_id] + except Exception: + pass + del deleted_votes + for key in req.args: + kind = vote_id = None + try: + kind, vote_id = key.split(":", 1) + except: + continue + vote_id = int(vote_id) + try: + validate_id(vote_id) + except Exception, e: + add_warning(str(e)) + continue + if vote_id in deleted: + continue + if not votes.has_key(vote_id): + add_warning(req, "could not find RendezVousVote") + continue + vd = votes[vote_id] + if vd.user != req.authname: + req.perm.require("RENDEZVOUS_ADMIN") + if kind == "delete": + vd.delete() + deleted.add(vote_id) + del votes[vote_id] + update_graph = True + elif kind == "email": + email = req.args[key] + if email != vd.email: + vd.email = email + changed.add(vote_id) + elif kind == "time_begin": + try: + tmp = time_parse(req.args[key]) + except ValueError, e: + add_warning(req, str(e)) + continue + else: + if vd.time_begin != tmp: + vd.time_begin = vd.time_begin.replace(hour=tmp.hour, minute=tmp.minute) + changed.add(vote_id) + elif kind == "time_end": + try: + tmp = time_parse(req.args[key]) + except ValueError, e: + add_warning(req, str(e)) + continue + else: + if vd.time_end != tmp: + vd.time_end = vd.time_end.replace(hour=tmp.hour, minute=tmp.minute) + changed.add(vote_id) + elif kind == "date_begin": + try: + tmp = date_parse(req.args[key]) + except ValueError, e: + add_warning(req, str(e)) + continue + else: + if vd.time_begin != tmp: + vd.time_begin = vd.time_begin.replace(year=tmp.year, month=tmp.month, day=tmp.day) + changed.add(vote_id) + elif kind == "date_end": + try: + tmp = date_parse(req.args[key]) + except ValueError, e: + add_warning(req, str(e)) + continue + else: + if vd.time_end != tmp: + vd.time_end = vd.time_end.replace(year=tmp.year, month=tmp.month, day=tmp.day) + changed.add(vote_id) + for dvi in changed: + if dvi not in deleted: + try: + vd = votes[dvi] + self._validate_vote(vd) + check_vote_collision(vd, RendezVousVote.fetch_by_date(self.env, vd.date_id, vd.user)) + vd.update() + except Exception, err: + add_warning(req, str(err)) + continue + votes = None + if "RENDEZVOUS_VOTE_VIEW_OTHERS" not in req.perm: + votes = RendezVousVote.fetch_by_date(self.env, date_id, req.authname) + else: + votes = RendezVousVote.fetch_by_date(self.env, date_id) + data["rdate"] = rdate + data["votes"] = votes + add_ctxtnav(req, 'Back to Overview', req.href.rendezvouses()) + add_ctxtnav(req, 'Back to RendezVous # %d' % rdate.rendezvous_id, href=req.href.rendezvous(rdate.rendezvous_id)) + if update_graph: + path = join(self.env.path, "htdocs", "rendezvous_graphs") + size = [self.config.getint("rendezvous", "graph_size_x"), + self.config.getint("rendezvous", "graph_size_y")] + update_votes_graph("%s - %s" % (rdate.time_begin.strftime('%Y.%m.%d %H:%M'), + rdate.time_end.strftime('%Y.%m.%d %H:%M')), votes, path, size, get_timezone(req.session.get('tz'))) + if req.session.has_key("notice"): + req.redirect(req.href.vote(dv.vote_id)) + return 'vote.html', data, None + + def _display_rendezvous(self, req): + + '''display an overview of active rendezvous or details about one if requested''' + req.perm.require("RENDEZVOUS_VIEW") + add_stylesheet(req, 'hw/css/rendezvous.css') + uperm = RendezVousTypePermissionSystem(self.env) + r_list = RendezVous.fetch_all(self.env, True) + api.RendezVousSystem(self.env).workflow_controller.process_expired(r_list) + session_tzname, selected_tz = get_tz(req.session.get('tz', self.env.config.get("trac", "default_timezone") or None)) + data = {"show_vote_graph" : self.config.getbool("rendezvous", "show_vote_graph"), + "show_location_map" : self.config.getbool("rendezvous", "show_location_map"), + "voted" : False, + 'settings': {'session': req.session, 'session_id': req.session.sid}, + "session_tzname" : session_tzname, + "selected_tz" : selected_tz} + rendezvous_id = int(req.args["rendezvous_id"]) + rendezvous = RendezVous.fetch_one(self.env, rendezvous_id, fetch_dates=True) + if not rendezvous: + add_warning(req, tag.span("RendezVous not found. You can create a new RendezVous ", tag.a("here", href=req.href.newrendezvous()))) + return 'rendezvous.html', data, None + if not uperm.check_user_type_permissions(req.authname, rendezvous.type_id): + raise PermissionError() + add_ctxtnav(req, 'Back to Overview', req.href.rendezvouses()) + if rendezvous.author == req.authname or 'RENDEZVOUS_ADMIN' in req.perm: + add_ctxtnav(req, 'Edit RendezVous #%d'% rendezvous_id, req.href.editrendezvous(rendezvous_id)) + if 'RENDEZVOUS_ADD' in req.perm: + add_ctxtnav(req, 'New RendezVous', req.href.newrendezvous()) + table = self._prepare_rendezvous_table(rendezvous) + # Can be None. handle in genshi + location = ItemLocation.fetch_one(self.env, rendezvous.location_id) + + if req.method == "POST" and \ + req.args.has_key("comment"): + req.perm.require('RENDEZVOUS_COMMENT_ADD') + comment = to_unicode(req.args["comment"]) + # handle adding totally new comment + if not req.args.has_key("comment_id"): + # new comment + c = RendezVousComment(self.env, 0, rendezvous_id, req.authname, comment, datetime.now(utc)) + if not req.args.has_key("preview"): + c.commit() + req.redirect(req.href.rendezvous(rendezvous_id)) + # only previewing new comment + data["preview_comment"] = c + else: + # editing + comment_id = int(req.args["comment_id"]) + validate_id(comment_id) + if req.args.has_key("delete"): + RendezVousComment.delete(self.env, comment_id) + req.redirect(req.href.rendezvous(rendezvous_id)) + c = RendezVousComment.fetch_one(self.env, comment_id) + c.comment = comment + if not req.args.has_key("preview"): + c.update() + # saving edited comment + req.redirect(req.href.rendezvous(rendezvous_id)) + data["preview_comment"] = c + else: + # preparing editing + if req.args.has_key("comment_id"): + comment_id = int(req.args["comment_id"]) + data["preview_comment"] = RendezVousComment.fetch_one(self.env, comment_id) + if "RENDEZVOUS_COMMENT_VIEW" in req.perm: + data["comments"] = RendezVousComment.fetch_by_rendezvous(self.env, rendezvous_id) + data.update({"location": location, + "rendezvous" : rendezvous, + "voted" : rendezvous.has_voted(req.authname), + "has_votes" : rendezvous.has_votes(), + "table" : table}) + path = join(self.env.path, "htdocs", "rendezvous_graphs") + size = [self.config.getint("rendezvous", "graph_size_x"), + self.config.getint("rendezvous", "graph_size_y")] + dates = RendezVousDate.fetch_all(self.env) + for date in dates: + if date.votes: + update_votes_graph("", date.votes, path, size, get_timezone(req.session.get('tz'))) + #noty = RendezVousSchedulingNotifyEmail(self.env) + #noty.notify("hotshelf", rendezvous) + return 'rendezvous.html', data, None + + def _display_overview(self, req): + + '''displays an overview of active rendezvous or details about one if requested''' + req.perm.require("RENDEZVOUS_VIEW") + add_stylesheet(req, 'hw/css/rendezvous.css') + uperm = RendezVousTypePermissionSystem(self.env) + r_list = RendezVous.fetch_all(self.env, True, sort="name") + ctl = api.RendezVousSystem(self.env).workflow_controller + ctl.process_expired(r_list) + if 'RENDEZVOUS_ADD' in req.perm: + add_ctxtnav(req, 'New RendezVous', req.href.newrendezvous()) + all_rendezvouses = defaultdict(list) + if 'RENDEZVOUS_ADMIN' in req.perm: + for i in r_list: + all_rendezvouses[i.status].append((i.rendezvous_id, i.name)) + else: + for i in r_list: + if uperm.check_user_type_permissions(req.authname, i.type_id): + all_rendezvouses[i.status].append((i.rendezvous_id, "%s (%s)" % (i.name, i.status))) + data = {"all_rendezvouses" : all_rendezvouses, + "icons" : ctl.get_icons(), + "show_vote_graph" : self.config.getbool("rendezvous", "show_vote_graph"), + "show_location_map" : self.config.getbool("rendezvous", "show_location_map"), + "voted" : False} + return 'overview.html', data, None + + def _do_quickvote(self, req, date_id): + validate_id(date_id) + rdate = RendezVousDate.fetch_one(self.env, date_id) + user = req.authname + if not rdate: + raise TracError("RendezVousDate not found") + if not RendezVousVote.exists(self.env, date_id, user, rdate.time_begin, rdate.time_end): + dv = RendezVousVote(self.env, 0, date_id, user, "", datetime.now(utc), rdate.time_begin, rdate.time_end) + self._validate_vote(dv) + check_vote_collision(dv, RendezVousVote.fetch_by_date(self.env, date_id, user)) + dv.commit() + return True, rdate.rendezvous_id + return False, 0 + + def _get_action_controllers(self, req, rendezvous, action): + """Generator yielding the controllers handling the given `action`""" + controller = api.RendezVousSystem(self.env).workflow_controller + actions = [a for w,a in + controller.get_rendezvous_actions(req, rendezvous)] + if action in actions: + yield controller + + def _prepare_rendezvous_table(self, rendezvous): + users = {} + table = [] + user_pos=0 + def _s(l, r): + if l.time_begin < r.time_begin: + return -1 + elif l.time_begin > r.time_begin: + return 1 + return 0 + dates = sorted(rendezvous.dates, _s) + rendezvous.dates = dates + for mydate in dates: + for vote in mydate.votes: + if not users.has_key(vote.user): + users[vote.user] = user_pos + user_pos += 1 + rowCount = len(users) + colCount = len(dates)+1 + row = [[False, 0, 0] for i in xrange(colCount)] + table = [row[:] for i in xrange(rowCount)] + # now we want to switch to an row = user, column = date table + # count == 0 -> column0 = username + dateCount = 1 + for i in users: + table[users[i]][0] = i + for mydate in dates: + for i in xrange(user_pos): + table[i][dateCount][1] = mydate.date_id + votes = mydate.votes + if votes: + for vote in votes: + table[users[vote.user]][dateCount] = [True, mydate.date_id, vote.vote_id] + dateCount+=1 + return table + + def _validate_date(self, mydate): + if not isinstance(mydate.date_id, int): + raise TypeError("validate_date() - date_id has wrong type") + + if not isinstance(mydate.rendezvous_id, int): + raise TypeError("validate_date() - rendezvous_id has wrong type") + + if not isinstance(mydate.author, unicode): + raise TypeError("validate_date() - author has wrong type") + + if not isinstance(mydate.email, unicode): + raise TypeError("validate_date() - email has wrong type") + + if not isinstance(mydate.time_created, datetime): + raise TypeError("validate_date() - time_created has wrong type") + + if not isinstance(mydate.time_begin, datetime): + raise TypeError("validate_date() - time_begin has wrong type") + + if not isinstance(mydate.time_end, datetime): + raise TypeError("validate_date() - time_end has wrong type") + + validate_id(mydate.date_id) + validate_id(mydate.rendezvous_id) + + def _validate_vote(self, vote): + if not isinstance(vote.vote_id, int): + raise TypeError("validate_date_vote() vote_id has wrong type") + validate_id(vote.vote_id) + + if not isinstance(vote.date_id, int): + raise TypeError("validate_date_vote() date_id has wrong type") + validate_id(vote.date_id) + + if not isinstance(vote.user, unicode): + raise TypeError("validate_date_vote() user has wrong type") + + if not isinstance(vote.email, unicode): + raise TypeError("validate_date_vote() email has wrong type") + + if not isinstance(vote.time_created, datetime): + raise TypeError("validate_date_vote() time_created has wrong type") + + if not isinstance(vote.time_begin, datetime): + raise TypeError("validate_date_vote() time_begin has wrong type") + + if not isinstance(vote.time_end, datetime): + raise TypeError("validate_date_vote() time_end has wrong type") + + if vote.time_begin > vote.time_end: + raise ValueError("end datetime before start datetime") + + def _validate_rendezvous(self, rendezvous): + if not isinstance(rendezvous.rendezvous_id, int): + raise TypeError("RendezvousValidationError: vote_id has wrong type") + + if not isinstance(rendezvous.author, unicode): + raise TypeError("RendezvousValidationError: user has wrong type") + + if not isinstance(rendezvous.name, unicode): + raise TypeError("RendezvousValidationError: name has wrong type") + + if not rendezvous.name: + raise TypeError("RendezvousValidationError: Title is empty") + + if not isinstance(rendezvous.email, unicode): + raise TypeError("RendezvousValidationError: email has wrong type") + + if not isinstance(rendezvous.time_created, datetime): + raise TypeError("RendezvousValidationError: time_created has wrong type") + + if not isinstance(rendezvous.description, unicode): + raise TypeError("RendezvousValidationError: description has wrong type") + + if len(rendezvous.description) > self.config.getint("rendezvous", "max_description_length"): + raise ValueError("RendezvousValidationError: description is too long") + + if not isinstance(rendezvous.status, unicode): + raise TypeError("RendezvousValidationError: status has wrong type") + + if not isinstance(rendezvous.type_id, int): + raise TypeError("RendezvousValidationError: type_id has wrong type") + + if not isinstance(rendezvous.location_id, int): + raise TypeError("RendezvousValidationError: location_id has wrong type") diff --git a/TracRendezVous/tracrendezvous/rendezvous/workflow.py b/TracRendezVous/tracrendezvous/rendezvous/workflow.py new file mode 100644 index 0000000..4c23360 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/workflow.py @@ -0,0 +1,385 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime, timedelta +from trac.core import Component, implements +#from trac.perm import PermissionSystem +from trac.env import IEnvironmentSetupParticipant +from trac.config import Configuration +from trac.util.datefmt import utc +from trac.util.compat import set +from trac.util.translation import _ +from genshi.builder import tag + +from model import RendezVousDate, RendezVous, RendezVousType, RendezVousLocation +from tracrendezvous.location.model import ItemLocation +from tracrendezvous.event.model import Event +from api import RendezVousSystem, IRendezVousActionController +from ctdotools.utils import gen_wiki_page +# -- Utilities for the RendezVous workflow + +__all__ = ['RendezVousWorkflow',] + +def parse_workflow_config(rawactions): + + """Given a list of options from [rendezvous-workflow]""" + + actions = {} + for option, value in rawactions: + parts = option.split('.') + action = parts[0] + if action not in actions: + actions[action] = {} + if len(parts) == 1: + # Base name, of the syntax: old,states,here -> newstate + try: + oldstates, newstate = [x.strip() for x in value.split('->')] + except ValueError: + raise Exception('Bad option "%s"' % (option, )) # 500, no _ + actions[action]['newstate'] = newstate + actions[action]['oldstates'] = oldstates + else: + action, attribute = option.split('.') + actions[action][attribute] = value + # Fill in the defaults for every action, and normalize them to the desired + # types + for action, attributes in actions.items(): + # Default the 'name' attribute to the name used in the ini file + if 'name' not in attributes: + attributes['name'] = action + # If not specified, an action is not the default. + if 'default' not in attributes: + attributes['default'] = 0 + else: + attributes['default'] = int(attributes['default']) + if 'icon' not in attributes: + attributes['icon'] = "" + else: + attributes['icon'] = unicode(attributes['icon']) + # If operations are not specified, that means no operations + if 'operations' not in attributes: + attributes['operations'] = [] + else: + attributes['operations'] = [a.strip() for a in + attributes['operations'].split(',')] + # If no permissions are specified, then no permissions are needed + if 'permissions' not in attributes: + attributes['permissions'] = [] + else: + attributes['permissions'] = [a.strip() for a in + attributes['permissions'].split(',')] + # Normalize the oldstates + attributes['oldstates'] = [x.strip() for x in + attributes['oldstates'].split(',')] + return actions + +def get_workflow_config(config): + """Usually passed self.config, this will return the parsed rendezvous-workflow + section. + """ + raw_actions = list(config.options('rendezvous-workflow')) + actions = parse_workflow_config(raw_actions) + return actions + +def load_workflow_config_snippet(config, filename): + """Loads the rendezvous-workflow section from the given file (expected to be in + the 'workflows' tree) into the provided config. + """ + from pkg_resources import resource_filename + filename = resource_filename(__name__, + 'workflows/%s' % filename) + new_config = Configuration(filename) + for name, value in new_config.options('rendezvous-workflow'): + config.set('rendezvous-workflow', name, value) + + +class RendezVousWorkflow(Component): + def __init__(self, *args, **kwargs): + Component.__init__(self, *args, **kwargs) + self.actions = get_workflow_config(self.config) + if not '_reset' in self.actions: + # Special action that gets enabled if the current status no longer + # exists, as no other action can then change its state. (#5307) + self.actions['_reset'] = { + 'default': 0, + 'name': 'reset', + 'newstate': 'new', + 'icon': 'new.png', + 'oldstates': [], # Will not be invoked unless needed + 'operations': ['reset_workflow'], + 'permissions': []} + self.log.debug('Workflow actions at initialization: %s\n' % + str(self.actions)) + + implements(IRendezVousActionController, IEnvironmentSetupParticipant) + + # IEnvironmentSetupParticipant methods + + def environment_created(self): + """When an environment is created, we provide the basic-workflow, + unless a rendezvous-workflow section already exists. + """ + if not 'rendezvous-workflow' in self.config.sections(): + load_workflow_config_snippet(self.config, 'rendezvous_workflow.ini') + self.config.save() + self.actions = get_workflow_config(self.config) + + def environment_needs_upgrade(self, db): + """The environment needs an upgrade if there is no [rendezvous-workflow] + section in the config. + """ + return not list(self.config.options('rendezvous-workflow')) + + def upgrade_environment(self, db): + """Insert a [rendezvous-workflow] section using the original-workflow""" + load_workflow_config_snippet(self.config, 'rendezvous_workflow.ini') + self.config.save() + self.actions = get_workflow_config(self.config) + + def get_rendezvous_actions(self, req, rendezvous): + """Returns a list of (weight, action) tuples that are valid for this + request and this rendezvous.""" + # Get the list of actions that can be performed + + # Determine the current status of this rendezvous. If this rendezvous is in + # the process of being modified, we need to base our information on the + # pre-modified state so that we don't try to do two (or more!) steps at + # once and get really confused. + status = rendezvous.status + allowed_actions = [] + for action_name, action_info in self.actions.items(): + oldstates = action_info['oldstates'] + if oldstates == ['*'] or status in oldstates: + # This action is valid in this state. Check permissions. + allowed = 0 + required_perms = action_info['permissions'] + if required_perms: + for permission in required_perms: + if permission in req.perm: + allowed = 1 + break + else: + allowed = 1 + if allowed: + allowed_actions.append((action_info['default'], + action_name)) + if not (status in ['new', 'closed'] or \ + status in RendezVousSystem(self.env).get_all_status()): + # State no longer exists - add a 'reset' action if admin. + allowed_actions.append((0, '_reset')) + return allowed_actions + + def get_all_status(self): + """Return a list of all states described by the configuration. + """ + all_status = set() + for action_name, action_info in self.actions.items(): + all_status.update(action_info['oldstates']) + all_status.add(action_info['newstate']) + all_status.discard('*') + return all_status + + def get_status_position(self): + """Return a list of all states described by the configuration. + """ + def pred(a,b): + if a[1] < b[1]: + return -1 + elif a[1] > b[1]: + return 1 + return 0 + all_status = list() + for action_name, action_info in self.actions.items(): + if action_info.has_key("position"): + all_status.append((action_info['newstate'], int(action_info['position']), bool(int(action_info["show"])))) + return sorted(all_status, pred) + + def render_rendezvous_action_control(self, req, rendezvous, action): + + self.log.debug('render_ticket_action_control: action "%s"' % action) + this_action = self.actions[action] + status = this_action['newstate'] + operations = this_action['operations'] + + control = [] # default to nothing + hints = [] + if 'reset_workflow' in operations: + control.append(tag("from invalid state ")) + hints.append(_("Current state no longer exists")) + if 'revote_rendezvous' in operations: + hints.append(_("The rendezvous will be prepared for a new voting pass. All comments, dates and votes for that item will be deleted. You have to publish it again to become visible for other users")) + if 'publish_rendezvous' in operations: + hints.append(_("The rendezvous will be published for voting")) + if 'schedule_rendezvous' in operations: + dates = RendezVousDate.fetch_by_rendezvous(self.env, rendezvous.rendezvous_id) + control.append(tag([_("to "), tag.select( + [tag.option("%s" % x.time_begin.strftime('%Y.%m.%d %H:%M'), value=x.date_id) + for x in dates], + id="elected", name="elected")])) + hints.append(_("The rendezvous will be scheduled (no more voting, but remains visible)")) + if 'start_rendezvous' in operations: + hints.append(_("This status may be ignored, since I can handle RendezVous starts automatically, but you may wish to start manually before the scheduled beginning date and time")) + if 'expire_rendezvous' in operations: + hints.append(_("This status may be ignored, since I can handle expired RendezVouses automatically, but you may wish to stop manually before the scheduled date and time. It's an end status")) + if 'cancel_rendezvous' in operations: + hints.append(_("The rendezvous will not takes place. It's an end status")) + if 'leave_status' in operations: + control.append('as %s ' % rendezvous.status) + else: + if status != '*': + hints.append(_("Next status will be '%s'") % status) + return (this_action['name'], tag(*control), '. '.join(hints)) + + def _change_rendezvous(self, operation, rendezvous, args, req): + if operation == 'reset_workflow': + rendezvous.status = 'new' + + elif operation == 'revote_rendezvous': + #RendezVousDate.delete_by_rendezvous(self.env, rendezvous.rendezvous_id) + rendezvous.elected = 0 + rendezvous.update() + + elif operation == 'schedule_rendezvous': + if args.has_key("elected"): + date_id = int(args["elected"]) + # TODO: making all this into a transaction based commit + try: + now = datetime.now(utc) + date = rendezvous.get_date(date_id) + date.elected = True + date.update() + rendezvous.elected = date_id + dt = RendezVousDate.fetch_one(self.env, rendezvous.elected) + location = RendezVousLocation.fetch_one(self.env, rendezvous.location_id) + event = Event(self.env, 0, rendezvous.name, req.authname, now, now, dt.time_begin, dt.time_end, rendezvous.location_id, tags=rendezvous.tags, attendees=" ".join([vote.user for vote in dt.votes])) + event.commit() + event.wikipage = "events/%d" % event.e_id + event.update() + try: + gen_wiki_page(self.env, req.authname, event.wikipage, rendezvous.description and u" = %s =\n[events:%d]\n\n%s" % (event.name, event.srv_id, rendezvous.description) or u" = %s =\n[events:%d]\n\n%s" % (event.name, event.e_id, u"Diese Page kann mit Ideen und Inhalten des Events/Treffs gefüllt werden"), req.remote_addr) + except Exception: + pass + RendezVous.delete(self.env, rendezvous.rendezvous_id) + req.redirect(req.href.event("edit", event.e_id)) + except ValueError, e: + self.env.log.warning("%s with date_id '%d'" % (str(e), date_id)) + + def change_rendezvous_workflow(self, req, rendezvous, action): + this_action = self.actions[action] + + # Status changes + status = this_action['newstate'] + if status != '*': + rendezvous.status = status + + for operation in this_action['operations']: + self._change_rendezvous(operation, rendezvous, req.args, req) + + def process_expired(self, r_list): + """we have to check if scheduled rendezvouses are passed, and change their status to closed""" + for rendezvous in r_list: + if rendezvous.elected: + n = datetime.now(utc) + d = rendezvous.get_date(rendezvous.elected) + if d.time_end < datetime.now(utc): + self.env.log.debug("RendezVous #%d expired!" % rendezvous.rendezvous_id) + rendezvous.status = 'expired' + rendezvous.update() + if d.time_begin < n < d.time_end: + rendezvous.status = 'running' + rendezvous.update() + + def scheduled_rendezvouses(self, r_list=None, check=False): + """Called without r_list argument fetches the result from db. + If check=True, the intermediate result will be checked for expired (past) elected date + and finally cleaned.""" + icon = self.actions["schedule"]["icon"] + if not r_list: + res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'scheduled') + if check: + self.process_expired(res) + for r in res: + if r.status == 'scheduled': + r.icon = icon + yield r + else: + for r in res: + r.icon = icon + yield r + else: + if check: + self.process_expired(r_list) + for i in r_list: + if i.status == 'scheduled': + i.icon = icon + yield i + + def canceled_rendezvouses(self, r_list=None): + icon = self.actions["cancel"]["icon"] + if not r_list: + res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'canceled') + for r in res: + r.icon = icon + yield r + else: + for i in r_list: + if i.status == "canceled": + i.icon = icon + yield i + + def voting_rendezvouses(self, r_list=None): + icon = self.actions["publish"]["icon"] + if not r_list: + res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'voting') + for r in res: + r.icon = icon + yield r + else: + for i in r_list: + if i.status == "voting": + i.icon = icon + yield i + + def running_rendezvouses(self, r_list=None): + icon = self.actions["start"]["icon"] + if not r_list: + res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'running') + for r in res: + r.icon = icon + yield r + else: + for i in r_list: + if i.status == "running": + i.icon = icon + yield i + + def expired_rendezvouses(self, r_list=None): + icon = self.actions["expire"]["icon"] + if not r_list: + res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'expired') + for r in res: + r.icon = icon + yield r + else: + for i in r_list: + if i.status == "expired": + i.icon = icon + yield i + + def get_icon(self, status): + for i in self.actions.values(): + if i["newstate"] == status: + return i.get("icon", None) + return None + + def get_icons(self): + return dict([(i["newstate"], i.get("icon", None)) + for i in self.actions.values()]) + + #def check_for_timer_event(self): + + #for rendezvous in current_rendezvous(): + #timers = RendezVousTimers().fetch_by_rendezvous(rendezvous.rendezvous_id) + #now = datetime.now(utc) + #for timer in timers: + #if timer.time < now: + #_change_rendevous(timer.operation, rendezvous,...) \ No newline at end of file diff --git a/TracRendezVous/tracrendezvous/rendezvous/workflows/rendezvous_workflow.ini b/TracRendezVous/tracrendezvous/rendezvous/workflows/rendezvous_workflow.ini new file mode 100644 index 0000000..ca97854 --- /dev/null +++ b/TracRendezVous/tracrendezvous/rendezvous/workflows/rendezvous_workflow.ini @@ -0,0 +1,50 @@ +[rendezvous-workflow] +; rendezvous_workflow.ini +; + +leave = * -> * +leave.operations = leave_status +leave.default = 1 +leave.icon = leave.png + +publish = new,revoting -> voting +publish.permissions = RENDEZVOUS_MODIFY +publish.operations = publish_rendezvous +publish.icon = voting.png +publish.position = 2 +publish.show = 1 + +schedule = voting -> scheduled +schedule.permissions = RENDEZVOUS_MODIFY +schedule.operations = schedule_rendezvous +schedule.icon = scheduled.png +schedule.position = 1 +schedule.show = 1 + +start = scheduled -> running +start.permissions = RENDEZVOUS_MODIFY +start.operations = start_rendezvous +start.icon = running.png +start.position = 0 +start.show = 1 + +revote = scheduled,canceled,expired -> revoting +revote.permissions = RENDEZVOUS_MODIFY,RENDEZVOUS_DATE_DELETE,RENDEZVOUS_VOTE_DELETE +revote.operations = revote_rendezvous +revote.icon = revoting.png +revote.position = 3 +revote.show = 1 + +cancel = running,voting,scheduled -> canceled +cancel.permissions = RENDEZVOUS_MODIFY +cancel.operations = cancel_rendezvous +cancel.icon = canceled.png +cancel.position = 5 +cancel.show = 1 + +expire = running,voting,scheduled -> expired +expire.permissions = RENDEZVOUS_MODIFY +expire.operations = expire_rendezvous +expire.icon = expired.png +expire.position = 4 +expire.show = 1