From 042b5ffd984674690e281579eca767a391cf49da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 31 Mar 2012 17:45:24 +0200 Subject: [PATCH] initial commit --- TracRendezVous/AUTHORS | 1 + TracRendezVous/CHANGELOG | 64 + TracRendezVous/LICENSE | 340 +++++ TracRendezVous/__init__.py | 0 TracRendezVous/babel.ini | 8 + TracRendezVous/docs/epydoc.conf | 19 + TracRendezVous/setup.cfg | 47 + TracRendezVous/setup.py | 92 ++ TracRendezVous/tracrendezvous/__init__.py | 28 + .../tracrendezvous/event/__init__.py | 5 + TracRendezVous/tracrendezvous/event/admin.py | 200 +++ .../tracrendezvous/event/htdocs/css/event.css | 158 ++ .../event/htdocs/images/ical_icon.jpg | Bin 0 -> 979 bytes TracRendezVous/tracrendezvous/event/macros.py | 267 ++++ TracRendezVous/tracrendezvous/event/model.py | 1088 ++++++++++++++ .../event/templates/alarm_edit.html | 66 + .../event/templates/event_details.html | 39 + .../event/templates/event_display.html | 30 + .../event/templates/event_edit.html | 66 + .../event/templates/event_list.html | 28 + .../event/templates/events.html | 45 + .../tracrendezvous/event/templates/ical.txt | 33 + .../event/templates/recur_edit.html | 141 ++ .../event/templates/upcoming_events.rss | 35 + .../tracrendezvous/event/tests/__init__.py | 17 + .../tracrendezvous/event/tests/model.py | 54 + TracRendezVous/tracrendezvous/event/web_ui.py | 999 +++++++++++++ .../tracrendezvous/htdocs/css/base.css | 5 + .../222222_11x11_icon_arrows_leftright.gif | Bin 0 -> 58 bytes .../222222_11x11_icon_arrows_updown.gif | Bin 0 -> 56 bytes .../css/images/222222_11x11_icon_doc.gif | Bin 0 -> 64 bytes .../css/images/222222_11x11_icon_minus.gif | Bin 0 -> 56 bytes .../css/images/222222_11x11_icon_plus.gif | Bin 0 -> 61 bytes .../images/222222_11x11_icon_resize_se.gif | Bin 0 -> 61 bytes .../css/images/222222_7x7_arrow_down.gif | Bin 0 -> 52 bytes .../css/images/222222_7x7_arrow_left.gif | Bin 0 -> 53 bytes .../css/images/222222_7x7_arrow_right.gif | Bin 0 -> 53 bytes .../htdocs/css/images/222222_7x7_arrow_up.gif | Bin 0 -> 52 bytes ..._40x100_textures_03_highlight_soft_100.png | Bin 0 -> 201 bytes .../ef8c08_11x11_icon_arrows_leftright.gif | Bin 0 -> 58 bytes .../ef8c08_11x11_icon_arrows_updown.gif | Bin 0 -> 56 bytes .../css/images/ef8c08_11x11_icon_close.gif | Bin 0 -> 62 bytes .../css/images/ef8c08_11x11_icon_doc.gif | Bin 0 -> 64 bytes .../ef8c08_11x11_icon_folder_closed.gif | Bin 0 -> 61 bytes .../images/ef8c08_11x11_icon_folder_open.gif | Bin 0 -> 61 bytes .../css/images/ef8c08_11x11_icon_minus.gif | Bin 0 -> 56 bytes .../css/images/ef8c08_11x11_icon_plus.gif | Bin 0 -> 61 bytes .../css/images/ef8c08_7x7_arrow_down.gif | Bin 0 -> 52 bytes .../css/images/ef8c08_7x7_arrow_left.gif | Bin 0 -> 53 bytes .../css/images/ef8c08_7x7_arrow_right.gif | Bin 0 -> 53 bytes .../htdocs/css/images/ef8c08_7x7_arrow_up.gif | Bin 0 -> 52 bytes .../f6f6f6_40x100_textures_02_glass_100.png | Bin 0 -> 210 bytes .../fdf5ce_40x100_textures_02_glass_100.png | Bin 0 -> 235 bytes .../ffffff_40x100_textures_02_glass_65.png | Bin 0 -> 207 bytes .../htdocs/tracrendezvous/de.js | 2 + .../htdocs/tracrendezvous/de_DE.js | 2 + .../locale/de/LC_MESSAGES/messages.po | 1291 +++++++++++++++++ .../de/LC_MESSAGES/tracrendezvous-js.po | 20 + .../tracrendezvous/locale/messages-js.pot | 20 + .../tracrendezvous/locale/messages.pot | 1291 +++++++++++++++++ .../tracrendezvous/location/__init__.py | 4 + .../location/htdocs/css/location.css | 18 + .../location/htdocs/css/map.css | 43 + .../location/htdocs/script/tom.js | 63 + .../tracrendezvous/location/loc_xml.py | 20 + .../tracrendezvous/location/model.py | 193 +++ .../location/templates/location.html | 169 +++ .../tracrendezvous/location/tests/__init__.py | 16 + .../tracrendezvous/location/tests/model.py | 117 ++ .../tracrendezvous/location/utils.py | 78 + .../tracrendezvous/location/web_ui.py | 236 +++ .../tracrendezvous/rendezvous/__init__.py | 4 + .../tracrendezvous/rendezvous/admin.py | 201 +++ .../tracrendezvous/rendezvous/api.py | 191 +++ .../rendezvous/htdocs/css/rendezvous.css | 264 ++++ .../rendezvous/htdocs/images/canceled.png | Bin 0 -> 3425 bytes .../rendezvous/htdocs/images/expired.png | Bin 0 -> 4240 bytes .../rendezvous/htdocs/images/new.png | Bin 0 -> 6127 bytes .../rendezvous/htdocs/images/running.png | Bin 0 -> 44112 bytes .../rendezvous/htdocs/images/selected.png | Bin 0 -> 752 bytes .../rendezvous/htdocs/images/voting.png | Bin 0 -> 8108 bytes .../rendezvous/htdocs/script/tom.js | 63 + .../tracrendezvous/rendezvous/init.py | 9 + .../tracrendezvous/rendezvous/luxisr.ttf | Bin 0 -> 67548 bytes .../tracrendezvous/rendezvous/macros.py | 88 ++ .../tracrendezvous/rendezvous/model.py | 850 +++++++++++ .../tracrendezvous/rendezvous/notification.py | 136 ++ .../rendezvous/rendezvous_tag.py | 83 ++ .../rendezvous/templates/admin_general.html | 57 + .../rendezvous/templates/admin_overview.html | 38 + .../rendezvous/templates/admin_types.html | 95 ++ .../rendezvous/templates/date.html | 106 ++ .../rendezvous/templates/location.html | 126 ++ .../rendezvous/templates/overview.html | 57 + .../rendezvous/templates/rendezvous.html | 149 ++ .../rendezvous/templates/rendezvous_edit.html | 93 ++ .../templates/rendezvous_notify_email.txt | 2 + .../rendezvous/templates/vote.html | 129 ++ .../tracrendezvous/rendezvous/utils.py | 204 +++ .../tracrendezvous/rendezvous/web_ui.py | 1076 ++++++++++++++ .../tracrendezvous/rendezvous/workflow.py | 385 +++++ .../workflows/rendezvous_workflow.ini | 50 + 102 files changed, 11914 insertions(+) create mode 100644 TracRendezVous/AUTHORS create mode 100644 TracRendezVous/CHANGELOG create mode 100644 TracRendezVous/LICENSE create mode 100644 TracRendezVous/__init__.py create mode 100644 TracRendezVous/babel.ini create mode 100644 TracRendezVous/docs/epydoc.conf create mode 100644 TracRendezVous/setup.cfg create mode 100644 TracRendezVous/setup.py create mode 100644 TracRendezVous/tracrendezvous/__init__.py create mode 100644 TracRendezVous/tracrendezvous/event/__init__.py create mode 100644 TracRendezVous/tracrendezvous/event/admin.py create mode 100644 TracRendezVous/tracrendezvous/event/htdocs/css/event.css create mode 100644 TracRendezVous/tracrendezvous/event/htdocs/images/ical_icon.jpg create mode 100644 TracRendezVous/tracrendezvous/event/macros.py create mode 100644 TracRendezVous/tracrendezvous/event/model.py create mode 100644 TracRendezVous/tracrendezvous/event/templates/alarm_edit.html create mode 100644 TracRendezVous/tracrendezvous/event/templates/event_details.html create mode 100644 TracRendezVous/tracrendezvous/event/templates/event_display.html create mode 100644 TracRendezVous/tracrendezvous/event/templates/event_edit.html create mode 100644 TracRendezVous/tracrendezvous/event/templates/event_list.html create mode 100644 TracRendezVous/tracrendezvous/event/templates/events.html create mode 100644 TracRendezVous/tracrendezvous/event/templates/ical.txt create mode 100644 TracRendezVous/tracrendezvous/event/templates/recur_edit.html create mode 100644 TracRendezVous/tracrendezvous/event/templates/upcoming_events.rss create mode 100644 TracRendezVous/tracrendezvous/event/tests/__init__.py create mode 100644 TracRendezVous/tracrendezvous/event/tests/model.py create mode 100644 TracRendezVous/tracrendezvous/event/web_ui.py create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/base.css create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_arrows_leftright.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_arrows_updown.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_doc.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_minus.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_plus.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_11x11_icon_resize_se.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_down.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_left.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_right.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/222222_7x7_arrow_up.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/eeeeee_40x100_textures_03_highlight_soft_100.png create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_arrows_leftright.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_arrows_updown.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_close.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_doc.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_folder_closed.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_folder_open.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_minus.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_11x11_icon_plus.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_down.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_left.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_right.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ef8c08_7x7_arrow_up.gif create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/f6f6f6_40x100_textures_02_glass_100.png create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/fdf5ce_40x100_textures_02_glass_100.png create mode 100644 TracRendezVous/tracrendezvous/htdocs/css/images/ffffff_40x100_textures_02_glass_65.png create mode 100644 TracRendezVous/tracrendezvous/htdocs/tracrendezvous/de.js create mode 100644 TracRendezVous/tracrendezvous/htdocs/tracrendezvous/de_DE.js create mode 100644 TracRendezVous/tracrendezvous/locale/de/LC_MESSAGES/messages.po create mode 100644 TracRendezVous/tracrendezvous/locale/de/LC_MESSAGES/tracrendezvous-js.po create mode 100644 TracRendezVous/tracrendezvous/locale/messages-js.pot create mode 100644 TracRendezVous/tracrendezvous/locale/messages.pot create mode 100644 TracRendezVous/tracrendezvous/location/__init__.py create mode 100644 TracRendezVous/tracrendezvous/location/htdocs/css/location.css create mode 100644 TracRendezVous/tracrendezvous/location/htdocs/css/map.css create mode 100644 TracRendezVous/tracrendezvous/location/htdocs/script/tom.js create mode 100644 TracRendezVous/tracrendezvous/location/loc_xml.py create mode 100644 TracRendezVous/tracrendezvous/location/model.py create mode 100644 TracRendezVous/tracrendezvous/location/templates/location.html create mode 100644 TracRendezVous/tracrendezvous/location/tests/__init__.py create mode 100644 TracRendezVous/tracrendezvous/location/tests/model.py create mode 100644 TracRendezVous/tracrendezvous/location/utils.py create mode 100644 TracRendezVous/tracrendezvous/location/web_ui.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/__init__.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/admin.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/api.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/htdocs/css/rendezvous.css create mode 100644 TracRendezVous/tracrendezvous/rendezvous/htdocs/images/canceled.png create mode 100644 TracRendezVous/tracrendezvous/rendezvous/htdocs/images/expired.png create mode 100644 TracRendezVous/tracrendezvous/rendezvous/htdocs/images/new.png create mode 100644 TracRendezVous/tracrendezvous/rendezvous/htdocs/images/running.png create mode 100644 TracRendezVous/tracrendezvous/rendezvous/htdocs/images/selected.png create mode 100644 TracRendezVous/tracrendezvous/rendezvous/htdocs/images/voting.png create mode 100644 TracRendezVous/tracrendezvous/rendezvous/htdocs/script/tom.js create mode 100644 TracRendezVous/tracrendezvous/rendezvous/init.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/luxisr.ttf create mode 100644 TracRendezVous/tracrendezvous/rendezvous/macros.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/model.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/notification.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/rendezvous_tag.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/admin_general.html create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/admin_overview.html create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/admin_types.html create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/date.html create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/location.html create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/overview.html create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous.html create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous_edit.html create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/rendezvous_notify_email.txt create mode 100644 TracRendezVous/tracrendezvous/rendezvous/templates/vote.html create mode 100644 TracRendezVous/tracrendezvous/rendezvous/utils.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/web_ui.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/workflow.py create mode 100644 TracRendezVous/tracrendezvous/rendezvous/workflows/rendezvous_workflow.ini 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 0000000000000000000000000000000000000000..0e944c005930961eebca9bf2e3880c824418aca3 GIT binary patch literal 979 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<ECr+Na zbot8FYu9hwy!G(W<0ns_J%91?)yGetzkL1n{m0K=Ab&A3Fhjfr_ZgbM1cClyVqsxs zVF&q(k*OSrnFU!`6%E;h90S=C3x$=88aYIqCNA7~kW<+>=!0ld(M2vX6_bamA3Tw{OW@q{Ng*;R)$_fU z%~QSJvwdFbyP2|k7k50}sd3wU{zKW5Z!d36`7t}{(wF%i=j|4j2sl1HxiQv!l8E48 z!J{elhk_sOd~`);+1}JG^Pa>URj4?<`1`4M2W2K++Y=sCb7{}~BftLMSAMD>s!i#vjPg;hZI9>LIY5DA9by7R$hZ)6e zDSRb(bcf4Fp>@l3dv-jS_-mF|P-a}-udl!N3x!6l)A|~3wk!5q+4ZaXd+uMb$z1)a zPUZ5NbOVm`iHG0xvos2Ccy6~!V_p8C@L#FNkH~7-+P(3XpRIP_-R$%4RL(g~zHbtq p#}r&Le@l6}ZOy#W)b_m1xp&`xvR&1svF`CTE{#PEAZ-8tCIAQzh#UX_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b26780a3b519071fe2341991b85621d6f44e8df4 GIT binary patch literal 58 zcmZ?wbhEHbJ3nK#qBZCeD5P)PDm;`(JSDt3vcVM!oUzf(Z L4L{e5FjxZsbBGUF literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..69eb0770ae9de5f12144e945fd5786d47e551a16 GIT binary patch literal 56 zcmZ?wbhEHbJ3nK#qBZCeD5P)PDnD~48S8`=Dh|gJf?!<;Q IcSISi0ZAea&;S4c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..26db4340c1a9ba73ff8a8e024fd4bb2cf35d1733 GIT binary patch literal 64 zcmZ?wbhEHbJ3nK#qBZCeD5P)PDn8bVf*|{&CxYcyVS6H~O Q^mBUcDfUSV%0w8f0nR=VBLDyZ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6851f39811a8ae8a532c8641b76039bf74b384b1 GIT binary patch literal 56 zcmZ?wbhEHbJ3nK#qBZCeD5P)PDnD~48SDxnOU)Wg^baT_a IWeg0~08dH{{Qv*} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..74ac5cb910d74824b3c098481a2b1f35acd52cbb GIT binary patch literal 61 zcmZ?wbhEHbJ3nK#qBZCeD5P)PDm_&N|IcIOba3*;Dp@h|E Oc+UPXO`IaaU=0AQbP*H) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..251dc1628e0fe7820d7ae0d494a2e4708d6cfada GIT binary patch literal 61 zcmZ?wbhEHbJ3nK#qBZCeD5P)PDm_&N|8&+4pWhyB+b-dux Np18-A3j!G!tO2bd5heft literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..29c6c706dd9961f80220744c417ca2dc6a6145ba GIT binary patch literal 52 zcmZ?wbhEHbWM^P!XkcJaQBq<2_gC>J3nK#qBZCeD5P)PDn7DiTIZg|_J3nK#qBZCeD5P)PDn0R`a(J3nK#qBZCeD5P)PDn0R~=Dpx#sdt;KfY$yYR FH2@=@3;_TD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..28169eb91959d1b21e2e1c99bd68e433f59febfc GIT binary patch literal 52 zcmZ?wbhEHbWM^P!XkcJaQBq<2_gC>J3nK#qBZCeD5P)PDn7DiT8yIVOC3{xSVq&lc E0162TQ2+n{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..98f298b90503d4b1eebad416af56c3c6f4c911b1 GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F!3HG1q!d*FsR^Dgjv*P1Z*OcAY%t(qaZGrz zKQ4V43%mG&gzZhU!!F_*kar~9f;xh6t zj!b3M<_;5EqZ1LiVN-&s-Gxpd7r$n>>@SA<1xic%w4dz&x{blp)z4*}Q$iB}p;<-K literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a4c0dbd8111c3399dde87fe12a6baecac9aa3848 GIT binary patch literal 58 zcmZ?wbhEHbEeGDlV+%}GFSru+>Q|J literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..452d96853e19c15b926de124bd2e44e6fcab35a3 GIT binary patch literal 64 zcmZ?wbhEHbF4y?Q|yx#l!-7{0{{l45{82|tP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8e285e10d244aefc8b7e68474f74729e6aa4ab26 GIT binary patch literal 61 zcmZ?wbhEHb*|k zKkKpv9svc{-Hp``4(?~LJlLFa@bA9(`CRXA@cx+UFpDI3>q_xkGo9J1?&U^&V?S`N zu5EIidT{MKGo9k5Ei1j)pT!scV!7PE, 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 0000000000000000000000000000000000000000..4afd793ca5cdb1f34d953941c5eb0770a08a3640 GIT binary patch literal 3425 zcmdT{hcg^(6X(<*FG57`EqaX-CDAWhhTj>ARoEkqEJ(?#zs zdKUy2?Yn>DduDcbc4ud2cXofjnJ4z8k?wseRw^PQqWeHSEmMLuCWtE~IYFP=e9VB2LPC17vtVBdKOF*sXW`4N`s6cl{-H@=3Je)JVUioDBCl?l$-%`KgH`p1I zpK{eI&D%*C^)pMo6eNvPwtsMyTDY&cX#k1|j+JEejVvMAuonuU4AN$=0q!dbG9@v` z|DL_9p_3%0paj;BQ4zPvwxQH@%EzI_2**G9ZePo)7PAfx@PC?RRaIrJ7v1+{m}ZC; zrk3Fg&hMbvKAg#AW#n+O8;+lq|LKAzZ#1K5YLSw*5?g~=48%orzih)ALK0o=_#!Tg zv5=H4bf}Hn^4B-4Q9KH;C9!s!>SD;8lAW_{59Vw#D)qaq{$uo910)Tzr(CF;L~hrY zDv*vqlrjhzu?s{f5?zbupF@jC;{7#Zw&xn5#b9QS**b@lvz_rVsrXGZ5J&(Wd~W$T zBBrb?fUbA%D4zo0TV57$vl`8nI59Dy*DbZa@Y&Nk7MgXwKg*{Z&kQarnEw-6z!O9f0oD-WvISR~vnkMO?Bx7kDGtV-%Cgog{ zMz+|jtgOn4u~+~dRiqvexO4VV4V{#l`oQG%>-g!tHzkm{4|V}E;cP!oBIr68H5d^{ zq;sK8g0qdSr6thD#(rp)!C0jWPC_x!mnM|NsF6;rE(jD#gNq$$Ki>FOY27TMt*u?D zrs%n8aGLdm{^aCjrp>py)p=5c%z}0hs1rs4S^})#_NFS=YH^&QBV0X#R!4j9O?hV)w4D^CSJ<>$rV;Zm&BnH8pil>) zR|n0F!^7Co_l!KhT3ve*IiC9cnKUQPBUv(hQDjJy-u%SX*5R>R85Dq)S3r==7U znwUDf06!%xE>-&@Q(q%=+9wM?ZmjNrOXNWLBo~efi~d^?lc(%0^3HNf^O@m;H4dqT z0!X&B_paGqKSK0_W@?@zGcs@pN9L%7_P}N{kUVdM@Zk~F zx!_%q@C&c4VbquujsMP=fF)s9PJ@}FVq)_0APJi<7Ke*%&9A_W@pfdzaOdL z>(_3B-!4pw>k6m1Xz2{TIsoTwYl&dtC~nrT>SzOw@^2fPhyt05UlOsfm~ z7b$1RCoY~qfR+q&s=^J{g2DWEb<~00o33U^(0Mu#KytNKVs04(N z)!(wms67b$@r-L9IJ=d;J%v#_g^Y}>uA-u1KmrH?A#$I%TAe=Bf>!5-C zwMUbdLb4~k-?%Nbm`qJg{XjH(?U)GLVaU&BF__?)y7obJN^>fCgj!+p29*XYj#xu)VcoHV#C>a4}Z#a!xwmWMMuE6-6%$TkU)P&LoGSeA8lPSxH=oe zzVW2hK)~68T~T`zA%CrR55U|U6e{%P=$ zwIHOKONt2R8(iUhbX^s6p@sfi3H5_mEf%?6eIq38c*FiX%chc=$+Yh3PcEdlf}?}a z|9%B?z64T^df-bF`Fq+_pohV?>&5bQRbSx@LZUqiLZV9E=e*cirw%`KK1AfL&X&jY zWe}Cn`T)>(u@9MQ8=Ewc=yTg%MU|)K@PG{eS?=aRCbDOzzN7fdFO`i?!yn5h;hRqc zrub{2`~dvA}DeG^8UUUwZ7HMA)4M65ijz6s25 z5DI#`Vt&NIw6jy{G386eVw;SMsPzqN*H?f^MWv_Ob$iFRaz}v`#-^(lxm~h-GQP=4 z55+8QNEvt`Y-C&*&uZ<$PAw5b}59+)RZ=}sT}sf?|S<}B?M`FYt7gj3MgX*)P1 zqjvbZGot7)i<&1h9%~77DBBICbl)|{%sZt^HWg08!M{e}`kEhmJT~+A*i0W7TrB&n z4F|`v@VMyojj#bjmU?|Wo?0N^1T9r=yqqbL@Z$*8*}8LT;HA%-zl_rR6X3TISzI?h z$dUZB5N9#M&FcE*w$u~EF4@uBRrs*d`Q%3xiTzFaQH-ehp4CQ@R^j!#0k_|C!^X-g zY39@9{x>LNt^7l)1&gJd>XGlKk{tqWQNNbu{9SQ(aa&KmK9&HHDOZ{czcVpk)-%Wd zN=e^paxEs`WlJ$NWqU7P@H-%8903aY?d=e~v9EreAbfSk_D?Y9UDWaKlT$O$^ecbq z5ZqKrnP&jxH~s44?JcfTJVA7XxJreh<%E5u z-!tt0!bwj4LMjySx1Fy5-a_uL8WiZEw#3-pyv8C`@}j(J;1LOI%u%8s@mz(t=m*nR zB}mfM*~Hp1f_lpkDo<{57-Qh|X5-w%5=iwKW&$eiUR@qpw7j=T7<0 zy+WFauV-zq+nLkQ1L>H13#zrxLdxA~7;p)WKl2&ut~D=r{tB2v>VH2yjrve3^32F* zk7tIlXZTSy%X^bpBl5UZ*V87=@d=}5hVC59>Gwj}qAHPGwZN3Owy5=)456M{$${C> z?P=F~wu2^yPYZuK)p1NlsL#EnvXA^vQM6=X*fPfW&k1cpiQu5;dFdv#EYYA)_jeNX(R5^}+8kATaJh*DTc7rB~b!3t~*VW!E;d zZgektQ@XG1_fdQ~(`0O{pRE{HoEy6)cZtlQfZWP2{E5N-sgj9kerA_=Yt9hs|zQjwx{Id5~FeSz&ZDr;Hrg; zxW=l~YK^v{%LA(Ddyxj;ut4E=aqcrkQzf5To2r1R;(LxIy06eEk~sKK=TH-uAg=&z zCiwz2Bk{#g+<4C!z2LbR8Ja{lXAEpbqc)Gs^U*k|)n+0l>wIZv@v46toE6+{{4`6vc+<}KEM+WRMSKZoz15Xh&Kt34NZANM;kw(s@frZhlqgMMp{)GcK`hk@wC6w literal 0 HcmV?d00001 diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/expired.png b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/expired.png new file mode 100644 index 0000000000000000000000000000000000000000..1dee2663c277007d1ed69f396051cf5cc9992fb8 GIT binary patch literal 4240 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 muAd`EjvP6128pcjzv=G?(UTCX7S(e zpXa%AC(hhC@y>hZoN!GIB|h0D!^)svxWDoxWu1qiZ~q@tAo<{tQZ=lR*>JAtcl}3###kj;o3FZL0gt52iRc z+K535GoVQ+6Df~;I~TP@DYa1X6bE$yXsXCt9pUJ-*_aM|>@=D$Jg*!`mFd3`<3eFG zY0?fl7D7;5=Q~u<{ZY&DwbYW=F$#6$b<9*bOb^wL<5j)o&+3t4Uv*(t!_FA697hUL zM$<$qKFx9Nq`*_M{?wYiv~`tH`)1hB0Sw~aw`HoL6=r~HmkodOV^e38`0*C{I_*r? zR>g-C*Zg)NR$Xml{PjTRXyuZf*RY?IkQ7SiO9vP^ef5)rgBe{P*G@PGD>$ zVA{XSF74x9ov{B>>R{7uU)e#{<7u(Hain|2U<<;E(La(PBw8}>VSjn;GUNT}RBxq+q+eYWd;-kd$)$yQ+ z&cLRA{o{&-yXWKuqT`kz@>lM5DKhE5x#Id>8`TM&3*@f%algM*3!9m(qI+T9*nwrk)x%xlWHh8@o>zIZ0In-i$A zB!24N>>`<>$MR{P>-ph)zZtW~aD#RBgsu0%+EM{!!De}5e^=@$(x&xl@=>yDWP5Kb z{bp!E#95SEu6+a6rCm^3S6(BT$j|p(4v}BhD06<)GR1N>mD0$knI9=S&1WO4QnFq- z21_^P2vDnt)nM^m8^BSyEUkBXdb%1o?%?rtldWpg^g9s2Og?vaVfW-8{^d0H=g%Xu zPKCLt%+M_rAAM#>5u(Kz?h!5|TIk4qR63VC!_M-mEFNyCt&Gs!vEsTqX*}38tPfmu zF=X%6(?c*98}qJI=l|8MrXbQKw@28|cdwgygd63>KRO^KR1s)ZTfzFs$bkQ1LGu~(4aRl-h+r=(U3y#M zXUK06SP7A)#xiIK-ch$m_#O_;dv99y*FX`2%>tjEaG(L;wzW__G2K%Z4Y{2@5JdY2 zDLz5*4Z*p7tU>Z%)h5B+IfXH|n(@rG@`ox=fqz@TSiz|oJHdj2Nhzia69{i(5~<_GTEwOd?%giNo?HF z*@TSNq{Ljp5?^#y+a1IAxPrp&Znp3T2Of`qk9f{;Ad7H?T#<>%5ZfdiKPkF8k2;Z+ zal)LFuV>*JMR~Qgz>h7oJ82^pA3svsq&DSDuvc2xOU)k4O87+No@B0JDwb_1XcR^? zU2>9Q(yzL0uaCF`=%I*|Qhh(TTcFXRdk#fcnv)s~^+@mkF#fAEK;j?fTRo#NNpfQu zCftW@ub#2xPvqAQuZ&(ZjIoEfQSkm2TEk2m>VABB&p?bj>bA?NrJIb;dOk8RX~Ii^ zL?Y>7m8`JDyP>dNVs0mW4VF&1T2*>;ax0!fA7z;N)07V!+Malena1CV*xsOF_W~f0+dl z7(_5RJtfBuWTeJpBE}8sF`g|Z#SC0uH;2_3rNgCS`}&}Qbr-)S%;o3me_I)!OkNOZ zB^SM)+~#&PGSXSNlmoO70F;voC-O!Sgkl(YJuG? zFD+A2BavXu*Dy+m{u??#`|jCB&%aAaJOI(;TWuRknwZk)LMalfU54{ybxTlCTf0caULJQKBpb@ zT?R7)F5D*yp&CQpImZ7hyB?k!-y4na+?&)$wU}RJFkRVRjMGX)+I?u$_pH! zlX0_uWo>QEKpM%%f5cD^q~N~6`STsO_5QFzXXftf$+5Iw;ExjSB1NShKB+bOTwutT z`zuXjRXmE8SFt_v9_p{vSjpxW=GT(l*J|Q+s*qs(v@-$OJRX!H?5(z;j<_pGf~ZuX z6!9~h%-~zmpIHABLQW7~Ug-{Hs?o>qX$6q6LJZxR1FXcqy-S&f`M6j=8f~cR;as^< z`Z$`cEtsU?BHLpc6*EjsOivxl9l>cse~}WON%loNk7lEEtDML9ReVZ<2R-uTsj z9WgZ?vIm2LJO=**TL>2N$NVpwzUEd#>0~u*knla@i^!02mXL4C>(?@~4nmyQy5{i9 zcg{!?HFaO-lB%b`I*|qwUVJ}VV~7R+Up!u~!)eclM|#R(&-UZza-0eONprK^xcEvC z;~}_efstKq3i(Bic+u^6$-O)IBixNYu4aDU&z0DTtlHYGQZrj?n2wU@1BE>T}OV zj?;S@gR+D^FQjC!2&8c@g&zr18eFSe~FKePabGmT7KIm1%%|BIxw-avbKK^(9$uatkEB_97PIZ zf63I%fLs?y5fB*4=8b9AHQ2wJ zgWQFpWs6wK3rzN@zMhsm160~y^IBS3cE0uRJ`B-p|3$K@Ol)du3Ic%u(TZkoy7ZK_ zE#L+J*(iecP-1+dIVG)pPnSL5bM+bjU-yAM^A}xq_i?*_j}K%X z*crnGW4ootI*N`&Ca^XF06Q;-;=5(Uc&ydhl{Z;t9sv{<%;1!&D;@Tfs4kEQ6oEFbP++4 zzv{WE*urtvlqW}ULz(y*;$iV}8@G|k>8UB_HnRs^O73W2Bo7hA&B;X>IEF1&y%;nD zXKQg}&lGcnn0z(p1)yl}CdU#3hn)N>3ew30iY@_=NW#uJ=P67oVZO zMI-II?m;~k zdG&U_YL99XXyyDB*@09Xho<^`68uDB88NBzlwk_FNPFHSs_iu@n!=u6&T+wNtg?lA z5Zl)(m7?K8VAXf+I9BwaVh-2FXk< zbM{*D-&mu-u?Fby;qFYz5r(rva9cDWyS1FS2x37yN87I6UR)3XWeHb>ocYrs;y6!%Zn??m}I5`{@DM&CS$O0iesG)bP`1JweS9v=m~|q+;a}D$IV3 zz@}fn##UCOcSe+1aue`b0PpEBBO=5==z@0poJS+Vr}OL8QE76jt9&Xe+x)KzUXW`-(M5mM9#=){E(b&q%I>v`Pc57WLmx%CrqyCXnz z%kSei>}TNTYtA6TOG4*gOT>8IKYlJ^Z{ulLSAVD$StN@SAZ*W?`koZ}^^hwwi#0C7 zWu+C*fp-uC&9mO#ASG$hh@709;ArGh)|%_wk8;jv!`URYMZd>^c6Dt6Y?RxqrbbO$(LSTU6!6$#!W{+<8}J)gM)~)KNQ)!CS(pm3+e%dzz2xC(|+K~*byf% zNUq&c%xI)6fH6}=8zM*jgQuq+VKTZa7!?z<{+CT@Y-WbSji#KZ97IctouMH=^|PFh zgL($9LHZvxM~7MC#F|O9u^|vsHXHmV_WtTnAyq93UExe8>iPMvyEQHi%uG#8d}(c! z8iiqUdL`1U*Sna*Lnormz>7Zn*NYD(SqTE1BCMF^NhS_stcMN8`E7TB9Lwb=a4dS%)q@oaHgn~3i5=u+Ia~1OkM~Wgpt)_C>&tTGU)A>?CL|2 zE{tqPRz``wcRHjrKv}1Xx0~~$E3#usIY4%ueY?|3w^l!!I}9{*z7($rN>xf(J96Kt zZ#>2R(&27%m+SOnVoP$zwZaM5w6NxSzv}KY!sF?vrLE|w(Hpq&zOJ$YJ2$5};Nhk^7BHq6$r4%k1tYu9UGJP* zCaLk*)x#*0iRdt^G@+|}J$#4o7eswEo2F>Ld}6$zl2v8E$L~-tmMQJoWHZ!iU70C@ zKJYHk!|43~Gla>k8ote?&o$x967Qt3)yRqnvA;MLH1m}+B$vnRNK1{`8r4}*g~h&c zPc^q?k%>~Opp5RGdkDgK9y{<;uV1Il*@m?qUAMSj5TRCQ;B8bBJ=o@n__ej3o>o8H z)6S~}BBnq3n(}|}YRXfRvcE@-y!S^*oAZ#(kS62cy;vptdI(huQ zee_{}Gww7#Qq{m*kdmosF#Y4q;K7$Pd_k&@(<uT1hTxY zFJE2trTkUOs{eE`{%YlBM4_LpxeWf&O$t??Q#E(w&UVofSr@dl8`J6tbEuCJ&C;li z3GT8}%;U=)w|ul7m8eoLSDuQ1JP=AFCI!u}+pg~mH_+sdgQlL}nFl36zyCz1t!agf zX()3($CWCR!Dp!p7hsv9u4O3UM7|vhG!)+c*(kA!R?H)zFcObZ8P`={mUA4Q%rC#^ zye;15+8|UycMo zDsg@5e}$rFvy2ChGnVUCgbLX4@-}q9?;7Sq%j1|li|!k(25Z|J y5(b`z{3gk=nLfT4#*TR-VlDN~j>EkCPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXD$ z6&DF0Bf((+000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}001BWNklq$L4y>8jE6p}1#BrG9g7Jy8M1xxfFuqD{AhJXl` zuwuo6JrN9HvoJWYY;1(Xp~03c=$5+Gl6rXkhC7|1GCvlXS(TM1vnuPHbKkqK-8Z7* zoI6)#e);{r@Apl_cq%^V8r%c_6?x9<^1&a=o5gJ;?1GhP>qoS!oJ-1hTach88Qf_=Rleu^pNEO2VL>TZ}PS4a3HU1 z0G}V@&s?>vK24yp?wUPr(AehQURSS!ufe`vOVAkNAq@F^TL6*+0G#=~zON7Umgorz z>NXA_Z7A?O;NJNO%IV|qgrBsBOj}sDr`a=p5}v}k`*)>ldDWmnYaGJ9_q4`=&RyKp z=e!|+YyuS5(Q>=$`x^TBrq8#IoDSWq$87)%@rHSWHQB~<^A=_JNq9t?-t$#499N>= zuL?AB1TGgGr~@?C@jUt#o;yH;GkzxZw}-At&hu@{g5M^{t$S}n-){pvuEMBU*DBwZ zA{yc)_pKFeAg}A5#mDU#Unehc6NcllfyY&V#t`c{=amNjPL442c@#OXL%on83TfSg zxv2&JxICgE9^Ja@yskyMt}*jCfg|Uc*8$;84ZUqD;j7%&#|0cio?*y-U8N!QSV3dR zA?yQ(@clX2>>tS_0=eh#94dro_V7(4_@D<4ECf@M2y|Zn2%0p#! zQ_$Sh&>3Qo4Ux~sk@dw}+_$STrXCw;&;|mR0*Suw^?^uuFPsS)F_1`otxu_S(NlfT zuqh~RVy&#>32h6kuL7vo0mO9;nX3ejT)^m(=b>vx1LbvH@Yn_wvuPsDy7zn4Adw^g zn;KJ(6*MSA(2xlZ>s(!jHT=BG((M9<+^fr)%RPj&fg;*OAcrarZ1KK$z#gDu&DS{) zw8p;l88x|JziD<+u96wLZn+?uYrN5higi=a*fxc@Ex;JUa142WrC!ITZA55|L)dqT z#QWIixxmq9?WPWvjwmfX5Ip)6)w-TlA1H1@^&kWEhgz87|JnqkyY_QPn(wBvI>eY6 zO63hzLPHFo*w5v}9oGfS)U&P=+gg`SyGr_SXi~yDARk^7{A?GSLW%pKb%3MTB47-$ zPEz2|_u2&op=quj~?ZgtC+-^z?h?)`zSRG*vlb+oeLDY<9*Jd$T^`7m$^Z`fiy5xc*5>`a6Q1$^_&VJ@lXt8xqDKzb^ zrM7<~TvxpzaP$F2u0gX6nH);r4B6jMmc;;t(+8j;A;0L6({09Mh8j7!6zjG?Bj@>2 z`=?K{1dP<73qO~^_fkLCr&x1Aqf2>(?^9RTp-d^^)Y^z1i?+{-8)~^= zpyJ4-WpeK;w#MP-?tPJ_3RAC?>50bxV>TXIO3Iaf_~f>IEpI( z49Yx!GV8*V}jndHaT9_I?tzj-Xn3Zx%7k>2pWB0;Q&De z0HOkr&>hOQkDdyjFJ?WO&9YZ;@f>Z z1H=GBVrVi7XL=gBUh%eog9q#vI${)kfS=mW@LBr6qYF5Cfuz^~FotmSDX*f4ye|^p zml+DRPY`l>b(K(L;Z-GmHgy=00T@FSQ4Sz>xww6xAu=h77;qpFhu{P#SCPNj$I()P zIv~VTN&CPe+!r}OMmLeoT*cG}7^=tM5gnd!3N%Cp8;~QXGypJk2XN$qMsB9tHeyag zg5G*fhQnRbdruMYKu`Q{BjDIHQwg#s2HxFR@=MdWZ68NjhFebZ;5#X!Qn7T$Y* zL-hdA$n~^h2Qnty>jd;uIs3U3MHdy*1!O957#w(1J#h_mP*u6P{C!{%lg;pJG4SXE zjvULDH#B&1jGUC5iZ$g_Zr~Mn$!^MfkAcQIsr%~!2M^q@TuQueA4S3&_n$>~#Q)X? zjv@mXhGZv*H4mV>%EZ@mfP>bwZgLJ?6?t`63^+oC9Im10IgCXHc;t8*eFB+FrKNcO z9K{$rIH@(3-hrSFK$OAfF^_{10rD>Sr4G3b<+Du4X^sLNs+{^1Y)W>k#QORG9kb&5 z44a~d2^iuP1Nj+WT``y(Dm6Fja+@uCa?|>U7_2h*+;jXNVoe>iDzQN-0ur&S3phBTh+|aUPz#oNg3W!we4U`ED3eOdrOvv% zjBqWN9hPbr4M#mz(-03i1r}v+b%BTIkjHIwFW`>&o9ZxbWT1Sy4r4C@mX9rcOReK2O25@*2@4eL;wP@!zLo{Q{D*%7(BAW%P2kBDdo2LWJP>gkSK8NQ4{*e+;gm%?4lW-+ zeMLk)o|G??D!4Bn5EasC!r}hL_9j%0ee{%nPbzXhbGz2F9&1#hU5Ou3RGg8xflL|p z<<~b@CIF`PbrP(P``x}+)X>%|U0xdpO2*0N%#-DrXPJtGoT5iYC!Tyx0vRnLGTP;B zbSY95c^)UnIzdl~2&jc%Mj65SAsw?hp^Q0_#gw9mEAKVsVJto1UuIJCUCARD5Ci0O zv#0uU+jFcm9~ed*?@KF`{-NBl;IIQJ_aNmyieewnK6{Mkp4-KZ8zY>Y6gWJT*xep?a~A6DTpQ@G76*d?%GPx+?)MN6mr-8`Z%Dz0P{J3GtYzS0t^}Ow0#zBPKHe? zo)OCL6u=IET>yI^a{D!ofChnCBHe1E&rnNQt&M^u3cbmt-R}!9UjR4(85d{5_ z=>?`^7|u9N#V&l7qX7!vpj&$S#lH%Vo{|Z#yvy+52*9mV;}ivrobia10URm7h+X60 z{Z4%_iFP2wKBPQAQJmn$jhp!Nr;qT7PmJ-x3ng}U1%xo(gb>aBiGZ1sHg-Cl0$Kwn zCybX~VrV@E^A5B=#Odkt_}R}E_{mS^`13!%j|UGvz;byXT0er;XI80Q1|U*+o4V}q zHu^wg$AZNnq&z`c-a=WPpeXjN=Tv~%KZI??+}a^>UX>A0rRJ!rQ!JMspsMbJ`OGSj zOAj=X5NHJ8x+u|?wWcs`V7kQ7Dl5RkkZL$*?MMQCi?0$ zn&$z43R+iC>H^E<11y*Kp!Fkr{+#z8lhu)BA$B3e0m||Q%JK+O?trMUz_EH?@qLN! z33Ci28pX`4?blQ-$68l45IUB_Tn-qQ78sY7-%Gb=NA-9T4q9hyxVG4eagR#%Di!6QBO{E?#({fRqA42oM=h!rx#+ zMFHHC6yPW;BG`*@cqp*9Cvo@g7{B?OdwBcpXYqp{T;O}(dmm?K@1m-l@Sa(V>mu+x zWJhcx1YH>cjRQz|6aVsG{!J*g3#FX(T7bwrZ$q~^;7DhrXzc{K0`n3?GyJ=M_ZN8U ztsg^c_y1=dnO;sH4WJ2e1OMV*dRXNXX9$3&t!2OBE-sY+QIO5QjEU8Sxtd!f_afu?sLMwX! z!@s5$Fm|m_9-%01;^5!~eCbQK@P#i-FdjH{eCzpfLr}E{ftd zs%i(!Cn!r}9SI?7u<#XN{6MlN*lfY0gw`W0mj!<1SMK1QcP=cTP+)DQJPiZlMRCg> z#3M*qKnQ8U+kr*{+N+Aer>PpN%-x&T6I7MPXmlG&J+gky;S3)tN~0&s+)C?SLc4AD|TK1js&EG4t6;IR62B3_|-v}U`eW9;oQMxz&@ z^$c1&AP&6=)d3p*^BZroEN`Q#cENm7Kev%l(K2Y9jP@IFT+51)OAV#U`WdPUD2iLq zdg=YGn5>%nD~emFs;Tve%35!@`wR6&&ffIqPI2mM-11V~*P0kBW3kY9_>l3z2TQ#B z?i_Evd5U-6{Q!%F(KC&gj11A7?5)s4&uXEbn{wM%^(_YihaXHcR-1A64^b4) zU}xtA{Effy5`Ohprfyat(jq{k-{-gp>>+jQ8>7 zFH3yk3sZdi+c)v8Z@rD#>}RN|53JWwbO1-piVriVj1@<;i?Td{QWF$K0ix0Zm}ot3 z%u;1WP02Zn)CeWjuR<_hd+iAS<9|Ghs=Bn6J_2hq=4ng-?4l@6pw!d?h69cEp18GN zRc;7q71%?pYOhbJ3Ci*YX0vy|JPKk)Ikf;lcg;p_=hhTORn+n;#2%h`<~c|?g%D*8 zqCT*jeWIw9os!Kih16b0w7g`qu5SGqLI7385MqK`x1Pt_Z@*_{bdtz8$^nfrh^8pZ z8&*b32w~2rGi-c{GNnk{hJ=*_VHB#AY=750kJfV0Va0L-PAXwa2h&{tKTN>%UmHi`jW#?ajM6gA~%UKmOSIDil*7>%C8 zOE0~Ozx#J@;O5N|MbTKY1~6b16nTel)rw154sAt7QERRNq-NoyG;XaB0>AMadwA`& zyZD{oImTOW{RGS9&!M$b{uh?+kv(yZR3Ll;Dfhr^XbvF+c6ZAai_N&jaVxH^oMGK& zhSmx~0LRCSTeoiGt+zgeQZ8n&NKh1RkEb>XVHaADAf?&s>9kmpo0PI3GK#_NU2{pC zpD!SUgi>S6kNcTpRRA8T_9Ys_a_cvV%Q?_EvF}Z$-gH{D_Qg;b8a{VJ-%uw=HbBUMl4YEq zS0Iwmx`YrW5ym8QR0&IXTqnzDZS)6`z|KxvPJO-MUtN0>8lmpK4LH!s?+D=o2YY{q zJa41f)1qKYXBzM0=FI{(ZWQ?97kBaPZ{NZn{n1;vxOm&fr-}eL(BoO83MErN`-a}bRNLg3~BU)hLE2h|$4;nFxjaEKq zW@t@y{Des0_HDvH`bW3$t#9q%Ti=>uKL06{@>33HL2M%y43DMTj#@DcKtOB4cjQv^Lq4K5#emA$wkOKK&HaH@>_#9t_vd+4dGG2&hvl z$-%6vfDj5_{puk;^{G$cKmDge{Oo6MV7ZjgTA;sxHg?G%Rei9xQLJH=<)kT=iTw7E zk~)Nxw=kW)ir@b2SMZt7Od(|nDI4#iaVs0JZ~>^HfRP6rsFkiS#KyuG)>B=FU* z?qh%dE`IlSOUz~xO8J%Eiv&eQ9qzxOWQ-*tYWLhIAhL%Mo|29!XtLa}uUyEkr-l#` zckWE^`@esLqpF^gSZ8f6N7#Wt|tKoKg*z{xf%Z#s2dM;huxNn4`ZRzQ+>-4ZluCwB*!WZT%5?LVe!G zIv>7)*G1blnV%-EW8ysKTxczE`?kP0zxe|G>wlf%``?#XEQDR7c8Y!8B|1{ov*&40 zPsSA1q^3p@(3pB2#!c++e-i)TAH0gYcgHQjFhbP)Z>Ky%&}f3%d4Q4HKtCSfyZXkv zXn@3JB$*Um!skCv*x7j%|M5Q(E-uJsC6VQimmR_I7-$r&oPPI=Mlm;@MpbbXG{XBg z_ug0#PLbTaS>WX42tWJTJuok=@GK)Sji#{A0fsq@jiPY(*4!T_K|=_jC}_o6YwArL zAPwznl=6h~2W0>n zMFb@LStS%nw=E*_*~n>Cq2*yMmmGcWIBjX}-~D`>dN#meE`J~5bs9!a-Op2P(1dd8 zpP@d`=mLWLthe|c`ktaQ^2}N^EQgdDL?xzE!Z*Hg15*AnzVjVmK39>tkFI8*G6Ec_ z{%t8Qk=FGzx-7F)+XV+2Cg?5Y38vGR@r`f1g1dK3;Mc`3eD9*MVz{;pV%%`bUF_IM zj$3dIh!B}WUlcB;!8TVDTKeWz~4;a27mSQcsz`KjSN@0qxU;Km>_snKL8;`5eO^)F(`ugi%Kf!GF zDg59E3oMtauJ`C4J+O z0SkBV+@W*ek+Re}4k4!wV;DsCK!lC*ZLB#b=w>qk<^rGj%oO+Uzl{I+KNT$&+PjB& zM=*Rf)2va<4PZLU)E!2j@<<7+qbr!%uBZDwV_n?2Q{oT)-~d&1=4DYSq$|QpOHDN% zkh7wVrOB;>8KWW0nz^`3JDH(TLaFrE+>W}`WB4Egc)BtI9dJyrzu$OXZjYRm9Rh~d zyb=r_LVftMX%q_8YwGse0i;`JM+NTg3XDcmoS%=;?v>?(h6-w3QuLH;84o^X?8~VC z?1qL6v%o0uDtzFemXT=IH#{$0Y3yyI%fgu`}$I!yS%6rkTe;Nq37u9 z8mie2tu?e3kg`Bo0)PMS-@u0-eiFa<#SB&D3LYJBRFMP{Uyc>hwRc$+;nWcT&$Z5E zBHh+^>0K4=K8oTtKKHph`1N1kwW*2*FdUs?-24VGLKnZ`{iqK#IMZ$9FlgtHz5$5F zN_PCK5qQDo`vrdEH+J#fd!OJx|MMBDiak+V29_NK6o5<|NLXvW?fUyPkY^cjJFBBn z?nTjF7a<6@Z%Z5=?&JOUr!KC+Z5pkfrP#_^1xN;@ZeMBy2n~q+_JJ7M@wRWuWKkz8OeQ;+Ohk)D^p$6`Uu~nuzOtWk zxtfwnom5qmiH4dcb#m&gcVDR!37nkl@`DF{=LN;9-fz^?kg*D6U%s?%gJ$S?de%HQ zUIw>W+L^1MxW;qqpLzA!x^eV z2rz31Ay5<&J3A$P{p-)+Km3OW(E1!&`@m5pfFsrQS4ZL+$-Y#%y{Co*4O5Tc za`G~M>$eW;9B-eJVkT5o!w0v$j5sem6fC@2o5&>GLftgsL!d&NE%yWeZp*4-B|v)~38 zoPsDAD3<0`YCtM`34kult11D~20ZvUPEYm4=lW#3v;p7|PEMwta;H|D$Cq25vSHTs zUWIAO{hkH~8AoL6cbY$=lpj#_V@t$0ZjABfn-c~~21W!d)hb=;cvY8OQwY1x(g#t$ z=b3r##()b!)5xfKZg=2Z;)9d|MM1Su;Ts%&Mqc1)@BmMP97x;0O(D1&&x6@i()*cA z4VCNI58oh*rQr^Xb8s(snEN*OV>~bcr8I;PBxRvrdTER=eBl-PlRvqS_`G9C zeT3-kTQP}^?CWQ|^0yp4WxWb+ECCasa)RU(F#tL^DEMn%yUD-vJ7=_9R>Z9BX(*4l8G=6jOefo>=Z$D0y6MdGa4J9kR`$A7#BrDpagWe99!QyE1= zoq@DArj}Du;eFY|$d2}67SNh%kX8z8JPlXW2ewMLVipQ06);x7r~;G*)DqB3fGdD! z04)HT1Ec_20&{yZc*g*SRscPdZdRCU42dX>NHoUr@p#4JZnCG^igUSSXsx(S|1}D| zu~?}`xv=L&$lB}mipkhyOw}N4*InqNqf!&?Xke;=83Wn@Fb8or`_NPXWW(%*1`0A> z=c;!VJEP80HZod)sIIMGlPpry_wT$UrP|h=t?FLftR^Arwr2p&yWP$`da=gy1x zr#FzitmzfzN&x z>O0?g7H_?EDVW)k27)|qL&WPfWQ&>#!NTW$wT87?D*%dJPPkIUrh#A`v~Pi_wT78l z;JN2ywY$5=%jKm#EK9V9w~!^_M$B9aKnmjxz#d2=B&#WwV0SRh+Ongh*eQmpGVAOt zWTQ;zQtM&~OqRfO4(u%fF$2T`DCR&h1IjaC!8#`<001BWNkl2mKP-#G0`D6)bZ*Ywm0irP8 zoB&`b%67dJ1?liEe#2~@xw32E=v~DwzGgjoc4@=b17+vgTQ9U?mfCtr9G`hD2p9tIZs0f<6$&^v-UXZ`!gWwTAB#2eu7uPAyx1*XLI260ybG<*t*7DY#LCJR$QJ#8LAmL-IThhUo4gef&=^CAJ&Z=T z@#QZcw|dn$Ydsc3a~!#eZE$VjIQqpMj0m^L5w4hfK*RffZI!#PvCXz(A-E!GgR2VeYg#E{X3|>IBdyCz=G_@^cwqowdI`)ff!R4Qch?!PxBxEaz)S&W0yy0T z9=!+XmjPA)AFfu3=gM>1lQzA;#yC7|SLCMR8g8G`6ln@P+O6en@t3cLY6HfBuTwlu zUYUul;;jx2GQGl%n zq#39wPBw;u`E^&6Xzv}&#=x*1$EDVVW}a9NL+GFY&7P&PV4f6OOJXLWl-Wox;7+~; z@5!kH432fcB{GImxGzI`Ri#?0G@xsK!eu;K4_-G3EoMzz%DPf)<9vnw%xCtOfBL7l zbX7e7^Cbf_2~-5A0$568B$N`U%JgK+l%ud>SFhW9s&B@1*n@uxArA23i_hWZmrEPR zXxuGBOSMx;9bj1AzLlmID*hB$v;(YB(BQNOLN0pZlpBbk-GOwVVPCCsHm(!1#H+7j zcH_nky!-CFV1@)_VJzZhVS97HV&=cm(XtfJ5uGO_xTQD4QIsw4tU{0aQD{Vh>m_Fc-#Bg>2}`py3C{6rlqI z?}(Y07&DVLYXB$(?aPfdNWAY6l%h-Kq5`JM#Mnk0*nQL(H{*i2ER-Ta1ZfRY>3LrS z-wAH2upNL~U=wvFpKqYLP3q_&Heh})wf-&GD`C`G$v89rn zuvy_GW+r~_Ihs!<6Mb<3ZSTnk4Qb;8Ms2YynMqog-#>uVQ4!9L^3&QRj59+Knah4& zF_Y3-e5lmqjQQvcC{Kaib6|M^RA=6G2~_96Vh+p}z?lXif>!6g^?=gtEmP5?(sMs?Fs++=1NfYwA%T9XD) zjYi_MEO#yzi=!DZnFI5s0SvV?d(3R)u_}P&+;HRP61XUU`N({|$tet*F~;T7H(aQe zQD)&Tw(&75ALI-$?LIe2DdJ4)vNG|Q!pbQJeZ5H<3au!$E+$%wLMdVZg%BVi%x3zY zpKmBS0C%9aI=iM$LZILoW7TjJw$Vv=9zJ+jX25ynY>kWBbJW(eWGTr@#T3jeXLs+O zoWB45GnJuh*(!Ik{fYG07y`=?Pzjr8GEPtX+sY88p>0I`Y?Bd)b}*To;FF&m*^2LW z)`?$>(N2Z)Gmb=yj*58@jg`x+HT0kT8T5C*3x4znm`oV2yh7qDUjas={yL5}8_C|` zZ2=c6We%y~k{!sHG{;w8EiQ#P5Qd@>3$sy|BVeI{r8XHqEZc>2{-KN?#00gbjRgl~ zEl%wfF*<6ph0O( z6_Mk@sA5J1@KS5>NbB(_aBv1ptXHAV0DTUqb3k1H`T|gwKs5tq74S#{Q=2_?Rsauo zfYSv)r~P9)b7L8ed=r%@iZOO~MN(P%yncADnhZ1nXyQtSJSwa9G+MJnnayQ@ z0@|!EX@C_oRm@_3csMyfKi|IuCJO@$dSQ|av;tUL&=>*p3b+tJF#%{7-~~Wqm;TI5 z)TU55+E=Tcx?0Pg1{`P@@6&mqo`J*mc^ueWGM5UNDC1=)6)ceD5QPAQVlI}}A}1o! zT0?|gXMNfA0Nom14$z5>0f+!Ray^OWjCB26LnF6nJMDu7QEdQz?q3<-*;&x9m)e zCivaXOKt;(Qf5exnMNkO;9)!eKL!}gP`~$kP~ZC=oXf#yXN2#4kJbJARQ$t#I5
    +14kBGmKGPv>vFRSC-pE1*!oOdHpK$)?6Y+gx}-F3{ZXWvm`Z2M888 z;3x~ z4~^TIn8v}u#B0%lm29HsYwUTywIZ@$AzD4v@-zQh0dcEbiLG_+JPdF6G%2%>qz(_| z{AWL#&dnO>g}JaafWbBxURAcmzl|v^7ly`SZKxsFmnQCJwCgKE`6NeCY2$H_qb#+} zBJ*0X)?U71rn%O#0>*w9uSgXt73S|r#SEb}Nf+a>@<)lnO7`}Mo7C?XST|X?-Qba$ zXw;KM)i4#onPCeK%6gJY2q=IQ5!+X&+qa8{z|q|7msl9)VnRSU0)#1&gWd(I1E4wv z7E@dQG);#iIs@U9sW^vPX0GMJK3;!)x1P4wtUg7HpG-D^va^qq>9X8*FP&hJzITtS z?|v5?&^G|U@4N&0=}#eFd@&m>QnL{2h46#y=J1jWPRmzcEzaM1Yf9z-OB;Y3DU;za zv$YWlhL4qK4rP2({8rNL(QKg9?|Tep)+T`GXWTR~4(~7&FTThajRY?iE}-wvAvM8E zlM5qiPh&OK(plxqp%>zN9#*j_0CVPIY0_wQWdqt4RM-LxD`OceZPt-BaLR`79#=rQ zY-AAk+o`v-flN1^#*G_epDL|EGNh>5YvIQ&1T~yA4V1&E0i%PMTrfz1G{D8-p-X3$ z79`kfTEyD6pddgj0aY3LZ@e_Wv(zq#9lP%V9T{rHY}Y_L>dINxf#=L-?}Y_Iks77} z`z*rJOHRf(Of2cp^}L!G)1H;!fsfw~w)(gK7AQ;5?k?!Y4Tx7>fw+6OF09Q2kCi+0 zyvy1!)t)D*h>Srn8euk_?w*~VR&$_On6oj~z(@i`35XqlkBrA&RlsuJ#{M)QcJo^C za9_&pIvG@Lf8`Y62cm2*g z5FTjcb{|W1#%ryv?c zPmy#4{6SLE=+#%T_~8$Uyfyc-(RC|x!~%x{OG@_uqmXiGrg?J9Q>)`K=5m$Z>~~@F zlpEW?E!Y6>SOcZyyPf=@ST7?m6&x?>!ahIU7Av{hD{-fwhm!0_we9Q{m+D;|%4W+` z^?sx{Is$uu6%>H5^&ToqNsbgSvT3=61sZ|8PQpk8QCB=r4rJ6n%PkMZ_V5k83b+13q!f5I1Mb~}`pdtB{$KwK^0$8*bn|AGQA!auB?K$q z@N))%0}guTnR0%5x_b%8Spys+YmkZ&fKxyn0@WN?+yWLifyK!s^0gA1>y=@>!tTwQYC~GlrQJJ%%(oqU&AbGTWeW z^X37{l3G*qVl@bUgR>mKXo6)e-cmh!WYP_TXQ`(Rot>>%5h;s;(-y(X=Ef>x#Armt z!2y7YAL9Wf>901i(d)WGI|L%;C`#Otp|_b&w!eYzzkJH`G!S+E}~Kvp*SW?}Y_ z2whbiq|;IK4s-E zUa_|aSjB6pDI&9>Sk&4Gq5!yHrjmIT79#DL>$V5hLh=fpn7!b5%9076d35(K^eSkO zu!n$R88V^AxFuJjFP60a+0L%fSj`F~lGf7@D@ro#5w^M>e)}8hT$8F=383~pKUoLf zDO=%z1~5U zG;d8sqwfrNcbsKE>x(lyfnX1qB0r|@-K%dhbq2U2=_WVbuiV*j)FV5()@IPTOIN^B zm;{Fl1)Px?7I!IaQqC9<#{=l8zIX-(9<~NFc6XcLUsy@G>dMz;{p3Cb45@ zXN=KkHQlz$(}-1j$Ai`82(^Gx(PxHpNh;JV#ks5!(D*ko`LLhPKcCdp4+TCS~SJO&V#4kX#8qHFy~NH zv2)~~1{?ch3XWZ^Q_jvPhiXJmtg7#QF&@{T@5QMKw%LgblXbXcV73I#6>zG7Qwf~y z0v8itz7Nm`CgX50X!HdV^A4YxpIl&PM@9ucKEbRV;B5!ATfuML&c11Y!xfRIHc6@I zMD-L{^aaQIKtxO?t<7}3e5~t_r7g{00M*DVdXiqc9i=QL+tUZGKEzNH>{k+&7flsU@E|I1ax#uss}!3e z*_shDgaUd0{Tei4drlru=%$YuX$u^!am99@$x@y>H*V;uvD$TI3>8<)A*i8x>Sy;` z(c7%5R%TIV*uw9h)#G5;MiD}|41Ff8c}Iv!X+0}U1-t8L>w@2cvTv#XhGWSW6nJzs z4Q=IHnCH_DmRwB>0I+AI#^~}srOO?P*nck@JyUC0yN9qDhPndw6>tpTv;rP5@IV3& z3*c-YxSRmX+x-UF;DsJWR}mE$kHc2os}`v@{KR?|VIbS&UHjFdSWVGu^@>Wld-nS| znJ6i5qpNl!TqN080If_9n?fj+$#THbaF0?kldy+ec$N)gWj;@Xp>-X(0St|F&Lg~T z^e!;!)_?rkYgoMT2D1eUc)@Tqfp4nsC#0x|@T5PA{%}cOTqC?>9iKZsE|cbU+z_Hz z;G4S)jrZS2SFMgOyLF1G^E7-b-IhfW5y&={+&MYH#1!>awg`-*iK)5dks5ZKeCj5y zf!<(v=KgaJa0nsT^^THK>K&p=84J0xDWa^K`;)F~Ft3LbgMP?@g)yv%Otrboa`(=R zYx9AsB49IAql}oJD9iABK5$4j0rK4?a0uYI0&XyHPXK!(VDh%M-M!o5Z7YB zz4pD2kIWCePoMm@V;VyEH5#q9_z7x?bF~zOo(3Yx525knXv-#!MdZ;t z+U_#}AR!o9V|;u}r4Se?rAp)B3fr?uddVA30lS!n^kN$6G0OuLbTq@y13O6sDFh@l z#zab+F40SF0=(tY_DWkzAya2fdopemQi8SNWkJKzLB5f&dQ7f>v#s|!L3p^#Dh=#t z0~|+eypF>H_;4S1^av2|0OIu?&weO{JY-0d@ig4jwu~)GwJg^WdMcH&7yf+eed>3) z!(-*x=#IYVYXBPIC>LkZErG&;hPFeCbj;If*J_x1YR5NP)sbF46O+4yFb7gfNGTf- z8IMuE^wNsfY~8ydIE&>U_d_$o)I6x^S6?SfhG_Q5PG10bCg*T|l0SECKSw_AL za*q{X(^Q6{VnM_ukuFVtv8pu96Dymhu92Hzp7MeQP^iHq%=#=+I}py^dptH1$)kz` zCEXe?t$|4e>{g~t{=p2`I|Ft;1jg?I#W_Ilcbp}$MvSp5#(`*DqZu9&YdsLA+cF$4 z-@!HN4uv24Iof9& zr)Ej46U;w9H_4D%uR*l1UsxJ?cLuP8Kyi3jh$h{ey@&PZ+7nhYsEG3o&9&UXAnwMF z3K9TIA}uXY2;*UBUpaRjM_!XQ?bH!oY{TAb2p*3WGbo5JM!E~Q{VX*wt;~?K{W-9I z0qmRsqx(Q{&+hS$6Cx@G$aEe^td=Xd^uhdHYtIg-DJZ{?FBV@2fMDeq>m0yn_H|l(jΠqp7TyVZj9Q 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 literal 0 HcmV?d00001 diff --git a/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/selected.png b/TracRendezVous/tracrendezvous/rendezvous/htdocs/images/selected.png new file mode 100644 index 0000000000000000000000000000000000000000..882bec6c9f8464b9b9d2629a3b8c9a801f41a859 GIT binary patch literal 752 zcmVKbqBJq$IrvA`&?=LJ1^^-U@u{A!tbS zBEA`lNI21+1o2^rJ;Ve}!ih=wB8CX+n34}}=$JWK`{!a8belFD;=g%%kPBn!-Q5lx zxWDr|zw^20-tXauh;Ws+w(@LzoVQvz*}M=9OCzo~CKhD=N5?Ome=r zM`Co8=lOJ?y$OZkFbNSP79%<`!jr58;hNRTtDYVjvjRi}zn_r0+}R;wvGC}!Jd=t0 zc00YnAU;JwX=&lEE`Zm|dU{Av@DC1hPeiD!wNKgS&PRxO5a~v;zvyl*RxJCz{v>;@K9YJ9g&)!=Y4at%&4*Y z`hwd=BXw0(R2LUhU0ON)3d zNL&}-;NSv~N`c)@m)R`4dd>mbcd}}`yF>ui*7$BTidAoM-2j&hB^Dz#J4;6{#Q}XX iEidPm#X?&?_~1HtM*cE!mAaVfx6w)yG9`p}6)5 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 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 0000000000000000000000000000000000000000..c47fd20beac01131ebfe7f6b927344056487c77a GIT binary patch literal 67548 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 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 + + + +
    + + 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