initial commit
|
@ -0,0 +1 @@
|
||||||
|
Stefan Kögl
|
|
@ -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
|
|
@ -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.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 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.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 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.
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
||||||
|
)
|
|
@ -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'))]
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from tracrendezvous.event.web_ui import *
|
||||||
|
from tracrendezvous.event.model import *
|
||||||
|
from tracrendezvous.event.macros import *
|
|
@ -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)
|
|
@ -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;
|
||||||
|
}
|
After Width: | Height: | Size: 979 B |
|
@ -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("""<style type="text/css">
|
||||||
|
div.eventheader {text-align:center; min-width:5em; min-height:20em; float:right; clear:both; background:#ffedbc;}
|
||||||
|
a.existing_occurence, a.existing-occurence:visited {color:#00f;}
|
||||||
|
</style>""")
|
||||||
|
session_tzname, selected_tz = get_tz(formatter.req.session.get('tz', self.env.config.get("trac", "default_timezone") or None))
|
||||||
|
rows.append("<tr><td>%s</td></tr>" % 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 = '<li><a class="existing-occurence" href="%s">%s</a></li>' % (formatter.href.wiki(link) or formatter.href.event("createpage", event.e_id, ev.time_begin.strftime("%Y-%m-%d")), link_label)
|
||||||
|
else:
|
||||||
|
row = '<li><a href="%s">%s</a></li>' % (formatter.href.event("createpage", event.e_id, ev.time_begin.strftime("%Y-%m-%d")), link_label)
|
||||||
|
l.append(row)
|
||||||
|
rows.append("<tr><td><ul>%s</ul></td></tr>" % "".join(l))
|
||||||
|
return """<div class="eventheader"><h2><a href="%s">%s</a></h2><table class="eventheader"><tbody>%s<tr><td></td></tr></tbody></table></div>""" % (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'''\
|
||||||
|
<style type="text/css">
|
||||||
|
<!--
|
||||||
|
div#wiki-calendar-block {margin:auto; display:inline-block; border:2px solid #000; padding:0; -khtml-border-radius: 17px;-moz-border-radius: 17px;}
|
||||||
|
table.wiki-calendar {margin:0px}
|
||||||
|
table.wiki-calendar caption {font-size: 120%; white-space: nowrap;}
|
||||||
|
table.wiki-calendar caption a {display: inline; margin: 0; border: 0; padding: 0; background-color: transparent; color: #b00; text-decoration: none;}
|
||||||
|
table.wiki-calendar caption a.prev {padding-right: 5px;}
|
||||||
|
table.wiki-calendar caption a.next {padding-left: 5px;}
|
||||||
|
table.wiki-calendar caption a:hover {background-color: #eee;background:transparent;border:0;}
|
||||||
|
table.wiki-calendar th {border: none; border-bottom: 2px solid #000; text-align: center; font-weight: bold;}
|
||||||
|
table.wiki-calendar td {border: none; text-align: center;padding:10px 10px;wrap:nowrap;}
|
||||||
|
table.wiki-calendar td.day {min-width: 2em; height: 100%; margin: 0; border: 2px solid #eee; background-color: #fff; color: #888; text-decoration: none; -khtml-border-radius: 17px;-moz-border-radius: 17px;}
|
||||||
|
table.wiki-calendar td.active {border-color: #eee; background:#0f0; color: #000;}
|
||||||
|
table.wiki-calendar td.active:hover {border-color: #eee; background:#afa; color: #000;}
|
||||||
|
table.wiki-calendar td.today {border-color: #b77 !important;}
|
||||||
|
table.wiki-calendar ul, table.wiki-calendar li {margin:0;padding:0;}
|
||||||
|
table.wiki-calendar td.adjacent_month {background-color: #333;}
|
||||||
|
table.wiki-calendar td.adjacent_month:hover {background-color: #333;color:#888}
|
||||||
|
table.wiki-calendar td.collision {background:#800;}
|
||||||
|
table.wiki-calendar a.byday {min-width:100px; border:1px solid #eee;padding:0px 10px; margin:0; font-weight:bold; -khtml-border-radius: 17px;-moz-border-radius: 17px;}
|
||||||
|
table.wiki-calendar a.byday:hover {background:#ccc;}
|
||||||
|
table.wiki-calendar td.adjacent_month a.byday {color: #ddd !important;}
|
||||||
|
table.wiki-calendar td.adjacent_month a.byday:hover {color: #000 !important;}
|
||||||
|
table.wiki-calendar :link,table.wiki-calendar :visited {color: #000 !important;font-size:1.2em;wrap:nowrap;}
|
||||||
|
|
||||||
|
//-->
|
||||||
|
</style>
|
||||||
|
<div id="wiki-calendar-block">
|
||||||
|
<table class="wiki-calendar"><caption>
|
||||||
|
''')
|
||||||
|
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'<a class="prev" href="%s" title="%s"><<</a>' % (
|
||||||
|
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'<a class="prev" href="%s" title="%s"><</a>' % (
|
||||||
|
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'<a class="next" href="%s" title="%s">></a>' % (
|
||||||
|
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'<a class="next" href="%s" title="%s">>></a>' % (
|
||||||
|
thispageURL(month=month, year=year+1),
|
||||||
|
mydate))
|
||||||
|
|
||||||
|
buff.append(u'</caption>\n<thead>\n<tr>')
|
||||||
|
for day in calendar.weekheader(2).split():
|
||||||
|
buff.append(u'<th scope="col">%s</th>' % day)
|
||||||
|
buff.append(u'</tr>\n</thead>\n<tbody>')
|
||||||
|
|
||||||
|
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'<li><a href="%s" title="%s - %s %s">%s:<br/>%s</a></li>' % (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<tr>')
|
||||||
|
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<td class="%s"><a class="byday" href="%s" title="%s">%s</a><br/><ul>%s</ul></td>' % (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<td class="%s"><a class="byday" href="%s" title="%s">%s</a></td>' % (classes, daylink, title, day))
|
||||||
|
buff.append(u'\n</tr>')
|
||||||
|
buff.append(u'\n</tbody>\n</table></div>\n')
|
||||||
|
table = u"".join(buff)
|
||||||
|
return table
|
|
@ -0,0 +1,66 @@
|
||||||
|
<!DOCTYPE htm
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript" src="${chrome.htdocs_location}js/wikitoolbar.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
/* <![CDATA[ */
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$("#name").focus();
|
||||||
|
$("#date_begin").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
$("#date_end").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
$("#until_date").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
});
|
||||||
|
/* ]]> */
|
||||||
|
</script>
|
||||||
|
<title>${event.e_id and 'Edit Event' or 'Add Event'}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<form name="new_event" id='new_event' uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<input py:if="event.e_id != 0" type="hidden" name="event_id" value="${event.e_id}"/>
|
||||||
|
<fieldset id="properties">
|
||||||
|
<legend>${event.e_id and 'Edit Event' or 'Add Event'}</legend>
|
||||||
|
<table class="event-wizard" py:with="mydt = selected_tz.fromutc(event.time_begin);dt2 = selected_tz.fromutc(event.time_end)">
|
||||||
|
<tr><th><label for="name">Title:</label></th><td><input id="name" type="text" size="60" maxlength="200" name="name" value="${event.name}"/></td></tr>
|
||||||
|
<tr><th><label for="time_begin">Date begin:</label></th><td><input id="date_begin" type="text" size="10" maxlength="10" name="date_begin" value="${mydt.strftime('%d.%m.%Y')}"/>
|
||||||
|
<input id="time_begin" type="text" size="5" maxlength="5" name="time_begin" value="${mydt.strftime('%H:%M')}"/> ${mydt.tzinfo.tzname(None)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr><th><label for="time_begin">Date end:</label></th><td><input id="date_end" type="text" size="10" maxlength="10" name="date_end" value="${dt2.strftime('%d.%m.%Y')}"/>
|
||||||
|
<input id="time_end" type="text" size="5" maxlength="5" name="time_end" value="${dt2.strftime('%H:%M')}"/> ${dt2.tzinfo.tzname(None)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr><th><label for="location">Locations:</label></th>
|
||||||
|
<td><select id="location" name="location_id" size="1">
|
||||||
|
<option py:for="location in locations" selected="${location.location_id == event.location_id and 'checked' or None}" value="${location.location_id}">${location.name} :${location.coordinate_str()}</option>
|
||||||
|
</select>
|
||||||
|
<a py:if="'LOCATION_MODIFY' in perm" href="${href.location(from_='event', id=event.e_id)}">edit/search locations</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr><td></td><td py:choose="" test=""><a py:when="event.e_id" class="buttonlike" href="${href.event('recurrency', event.e_id)}">show recurrency options</a>
|
||||||
|
<input py:otherwise="" type="checkbox" id="show_recurrency" name="show_recurrency"/><label for="show_recurrency">show recurrency options</label></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend >Tags</legend>
|
||||||
|
<div>
|
||||||
|
<input id="tags" type="text" size="70" maxlength="70" name="tags" value="${event.tags}"/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" value="Reset"/>
|
||||||
|
<py:choose test="">
|
||||||
|
<py:when test="event.event_id != 0">
|
||||||
|
<input type="submit" name="edit" value="${event.e_id == 0 and 'add' or 'edit'}"/>
|
||||||
|
<input py:if="'EVENTS_DELETE' in perm and event.e_id != 0" type="submit" name="delete" value="delete"/>
|
||||||
|
</py:when>
|
||||||
|
<input py:otherwise="" type="submit" name="add" value="add"/>
|
||||||
|
</py:choose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE html
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<xi:include href="event_display.html" />
|
||||||
|
<head>
|
||||||
|
<title>${title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content" class="event">
|
||||||
|
<py:choose test="">
|
||||||
|
<py:when test="table">
|
||||||
|
<h1>${title}</h1>
|
||||||
|
<table class="upcoming">
|
||||||
|
<thead><tr><th py:for="h in headers">${h}</th></tr></thead>
|
||||||
|
<tfoot><tr><th py:for="h in headers"> </th></tr></tfoot>
|
||||||
|
<tbody>
|
||||||
|
<tr py:for="ix,row in enumerate(table)" class="upcoming">
|
||||||
|
<td class="daytext">${selected_tz.fromutc(row[0]).strftime(format)}</td>
|
||||||
|
<py:for each="eventlist in row[1:]">
|
||||||
|
<td py:if="eventlist != False" class="${isinstance(eventlist, list) and 'upcoming-event' or 'upcoming'}" rowspan="${eventlist.rowspan and eventlist.rowspan or None}">
|
||||||
|
<py:if test="eventlist != True">
|
||||||
|
<py:for each="event in eventlist">
|
||||||
|
${render_event(event, True)}
|
||||||
|
</py:for></py:if></td></py:for>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</py:when>
|
||||||
|
<py:otherwise>
|
||||||
|
<h2>${_("No events in this time frame. Create here")} <a href="${href.event('new', now.strftime('%Y-%m-%d'))}">${_(" a new event")}</a> !</h2>
|
||||||
|
</py:otherwise>
|
||||||
|
</py:choose>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<py:def function="render_event(event, with_day=False)">
|
||||||
|
<h2>${event.name}<span py:if="'EVENTS_MODIFY' in perm" class="edit"><a style="color:#f00;" href="${href.event('edit', event.e_id)}">edit</a></span></h2>
|
||||||
|
<table py:with="dt = selected_tz.fromutc(event.time_begin);dt2 = selected_tz.fromutc(event.time_end)">
|
||||||
|
<tr>
|
||||||
|
<td >${with_day and dt.strftime('%Y.%m.%d')} ${dt.strftime('%H:%M')} - ${with_day and dt2.strftime('%Y.%m.%d')} ${dt2.strftime('%H:%M')}</td>
|
||||||
|
<!-- <td >${dt.strftime('%H:%M')} ${dt.tzinfo.tzname(None)} - ${dt2.strftime('%H:%M')} ${dt2.tzinfo.tzname(None)}</td> -->
|
||||||
|
</tr>
|
||||||
|
<tr py:if="event.periodic()">
|
||||||
|
<td headers="h_tags">${event.rrule and event.rrule.explain() or None}</td>
|
||||||
|
</tr>
|
||||||
|
<tr py:if="event.tags">
|
||||||
|
<td headers="h_tags">${event.tags and event.tags or None}</td>
|
||||||
|
</tr>
|
||||||
|
<py:if test="event.location and event.location.lat != None">
|
||||||
|
<tr>
|
||||||
|
<td headers="h_location">${event.location.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr py:if="event.location">
|
||||||
|
<td headers="h_coordinates"><a href="${'http://www.openstreetmap.org/index.html?mlat=%s&mlon=%s&zoom=15&layers=B00TTT' % (event.location.lat, event.location.lon)}">${event.location.coordinate_str()}</a></td>
|
||||||
|
</tr>
|
||||||
|
</py:if>
|
||||||
|
<tr>
|
||||||
|
<td headers="wiki_link"><a href="${href.wiki(event.wikipage)}">wiki page</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</py:def>
|
||||||
|
</html>
|
|
@ -0,0 +1,66 @@
|
||||||
|
<!DOCTYPE htm
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript" src="${chrome.htdocs_location}js/wikitoolbar.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
/* <![CDATA[ */
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$("#name").focus();
|
||||||
|
$("#date_begin").datepicker({"dateFormat" : "dd.mm.yy", altField: '#date_end'});
|
||||||
|
$("#date_end").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
$("#until_date").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
});
|
||||||
|
/* ]]> */
|
||||||
|
</script>
|
||||||
|
<title>${event.e_id and 'Edit Event' or 'Add Event'}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<form name="new_event" id='new_event' uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<input py:if="event.e_id != 0" type="hidden" name="event_id" value="${event.e_id}"/>
|
||||||
|
<fieldset id="properties">
|
||||||
|
<legend>${event.e_id and 'Edit Event' or 'Add Event'}</legend>
|
||||||
|
<table class="event-wizard" py:with="mydt = selected_tz.fromutc(event.time_begin);dt2 = selected_tz.fromutc(event.time_end)">
|
||||||
|
<tr><th><label for="name">Title:</label></th><td><input id="name" type="text" size="60" maxlength="200" name="name" value="${event.name}"/></td></tr>
|
||||||
|
<tr><th><label for="time_begin">Date begin:</label></th><td><input id="date_begin" type="text" size="10" maxlength="10" name="date_begin" value="${mydt.strftime('%d.%m.%Y')}"/>
|
||||||
|
<input id="time_begin" type="text" size="5" maxlength="5" name="time_begin" value="${mydt.strftime('%H:%M')}"/> ${mydt.tzinfo.tzname(None)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr><th><label for="time_begin">Date end:</label></th><td><input id="date_end" type="text" size="10" maxlength="10" name="date_end" value="${dt2.strftime('%d.%m.%Y')}"/>
|
||||||
|
<input id="time_end" type="text" size="5" maxlength="5" name="time_end" value="${dt2.strftime('%H:%M')}"/> ${dt2.tzinfo.tzname(None)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr><th><label for="location">Locations:</label></th>
|
||||||
|
<td><select id="location" name="location_id" size="1">
|
||||||
|
<option py:for="location in locations" selected="${location.location_id == event.location_id and 'checked' or None}" value="${location.location_id}">${location.name} :${location.coordinate_str()}</option>
|
||||||
|
</select>
|
||||||
|
<a py:if="'LOCATION_MODIFY' in perm" href="${href.location(from_='event', id=event.e_id)}">edit/search locations</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr><td></td><td py:choose="" test=""><a py:when="event.e_id" class="buttonlike" href="${href.event('recurrency', event.e_id)}">show recurrency options</a>
|
||||||
|
<input py:otherwise="" type="checkbox" id="show_recurrency" name="show_recurrency"/><label for="show_recurrency">show recurrency options</label></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend >Tags</legend>
|
||||||
|
<div>
|
||||||
|
<input id="tags" type="text" size="70" maxlength="70" name="tags" value="${event.tags}"/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" value="Reset"/>
|
||||||
|
<py:choose test="">
|
||||||
|
<py:when test="event.event_id != 0">
|
||||||
|
<input type="submit" name="edit" value="${event.e_id == 0 and 'add' or 'edit'}"/>
|
||||||
|
<input py:if="'EVENTS_DELETE' in perm and event.e_id != 0" type="submit" name="delete" value="delete"/>
|
||||||
|
</py:when>
|
||||||
|
<input py:otherwise="" type="submit" name="add" value="add"/>
|
||||||
|
</py:choose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<!DOCTYPE html
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<xi:include href="event_display.html" />
|
||||||
|
<head>
|
||||||
|
<title>${title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content" class="event">
|
||||||
|
<div id="event-overview">
|
||||||
|
<h1>${title}</h1>
|
||||||
|
<div py:for="ix,event in enumerate(events)"
|
||||||
|
id="event:${event.e_id}" class="event-item">
|
||||||
|
<py:choose test=""><div py:when="event.periodic() or event.initial_e_id==True" class="recurring">C<br/>I<br/>R<br/>C<br/>U<br/>L<br/>A<br/>R</div>
|
||||||
|
<div py:otherwise="" class="unique">S<br/>I<br/>N<br/>G<br/>U<br/>L<br/>A<br/>R</div>
|
||||||
|
</py:choose>
|
||||||
|
<div class="event-intern">
|
||||||
|
${render_event(event, True)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<!DOCTYPE html
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
i18n:domain="tracrendezvous">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<xi:include href="event_display.html" />
|
||||||
|
<head>
|
||||||
|
<title>${title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<py:choose test="">
|
||||||
|
<py:when test="table">
|
||||||
|
<h1>${title}</h1>
|
||||||
|
<h2>${title2}</h2>
|
||||||
|
<table class="upcoming">
|
||||||
|
<thead><tr><th py:for="h in headers">${h}</th></tr></thead>
|
||||||
|
<tfoot><tr><th py:for="h in headers"> </th></tr></tfoot>
|
||||||
|
<tbody>
|
||||||
|
<tr py:for="ix,row in enumerate(table)" class="upcoming">
|
||||||
|
<td class="daytext"><a href="${href.event('by-day', row[0].strftime('%Y-%m-%d'))}">${row[0].strftime(format)}</a></td>
|
||||||
|
<py:for each="eventlist in row[1:]">
|
||||||
|
<td py:if="eventlist != False" class="${isinstance(eventlist, list) and 'upcoming-event' or 'upcoming'}" rowspan="${eventlist.rowspan and eventlist.rowspan or None}">
|
||||||
|
<py:if test="eventlist != True">
|
||||||
|
<py:for each="event in eventlist">
|
||||||
|
${render_event(event)}
|
||||||
|
</py:for>
|
||||||
|
</py:if>
|
||||||
|
</td>
|
||||||
|
</py:for>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</py:when>
|
||||||
|
<py:otherwise>
|
||||||
|
<h2>${_("No events in this time frame. Create here")} <a href="${href.event('new', now.strftime('%Y-%m-%d'))}">${_(" a new event")}</a> !</h2>
|
||||||
|
</py:otherwise>
|
||||||
|
</py:choose>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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
|
|
@ -0,0 +1,141 @@
|
||||||
|
<!DOCTYPE htm
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript" src="${chrome.htdocs_location}js/wikitoolbar.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
/* <![CDATA[ */
|
||||||
|
function toggleStatus() {
|
||||||
|
if ($('#is_periodic').is(':checked')) {
|
||||||
|
$('#recurrency-freq :input').removeAttr('disabled');
|
||||||
|
$('#recurrency-exceptions :input').removeAttr('disabled');
|
||||||
|
$('#recurrency-timeframe :input').removeAttr('disabled');
|
||||||
|
} else {
|
||||||
|
$('#recurrency-freq :input').attr('disabled', true);
|
||||||
|
$('#recurrency-exceptions :input').attr('disabled', true);
|
||||||
|
$('#recurrency-timeframe :input').attr('disabled', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$("#until_date").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
var ix = 2;
|
||||||
|
switch(${freq}){
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
ix = 0;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
ix = 1;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
ix = 2;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
ix = 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$("#accordion").accordion({header: "h3"}).accordion('activate', ix);
|
||||||
|
$("#exception_name").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
toggleStatus();
|
||||||
|
});
|
||||||
|
/* ]]> */
|
||||||
|
</script>
|
||||||
|
<title>${'Edit Recurrency'}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<h1>Recurrency Options for event '${event.name}'</h1>
|
||||||
|
<h2 py:with="dt = selected_tz.fromutc(event.time_begin);
|
||||||
|
dt2 = selected_tz.fromutc(event.time_end)">${dt.strftime("%A, %d.%m.%Y %H:%M")} ${dt.tzinfo.tzname(None)} - ${dt2.strftime("%A, %d.%m.%Y %H:%M")} ${dt2.tzinfo.tzname(None)}</h2>
|
||||||
|
<form name="recurrence" id='recurrence' uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<fieldset>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="checkbox" id="is_periodic" name="is_periodic" value="${event.is_periodic}" checked="${event.is_periodic and 'checked' or None}" onchange="toggleStatus()"/><label for="is_periodic"> Repeat this event</label>
|
||||||
|
</div>
|
||||||
|
<fieldset id="recurrency-freq"><legend>Recurrency frequency</legend>
|
||||||
|
<div id="accordion">
|
||||||
|
<div>
|
||||||
|
<h3><a href="#">Yearly</a></h3>
|
||||||
|
<table>
|
||||||
|
<tr><td>Repeat every <input type="text" size="3" maxlength="3" name="yearinterval" value="${period.interval}"/> year(s)</td></tr>
|
||||||
|
<tr><td><input type="radio" name="freq" value="0" checked="${freq == 0 and 'checked' or None}"/><label for="monthday-yearly-0">Repeat on day</label>
|
||||||
|
<input type="text" size="3" maxlength="3" id="monthday-yearly-0" name="monthday-yearly-0" value="${period.bymonthday}"/>
|
||||||
|
<label for="monthname-yearly-0">in</label>
|
||||||
|
<select name="monthname-yearly-0"><option py:for="ix, x in enumerate(month_names)" value="${ix}" selected="${period.bymonth == ix and 'selected' or None}">${x}</option></select></td>
|
||||||
|
</tr>
|
||||||
|
<tr><td><input type="radio" name="freq" value="1" checked="${freq == 1 and 'checked' or None}"/><label for="dayocc-yearly-1">Repeat on</label>
|
||||||
|
<select id="dayocc-yearly-1" name="dayocc-yearly-1"><option py:for="ix, x in enumerate(weekday_names)" value="${ix}" selected="${period.byweekdayocc == ix and 'selected' or None}">${x}</option></select>
|
||||||
|
<select name="weekday-yearly-1"><option py:for="ix, x in enumerate(day_names)" value="${ix}" selected="${period.byweekday == ix and 'selected' or None}">${x}</option></select> in
|
||||||
|
<select name="monthname-yearly-1"><option py:for="ix, x in enumerate(month_names)" value="${ix}" selected="${period.bymonth == ix and 'selected' or None}">${x} </option></select></td>
|
||||||
|
</tr>
|
||||||
|
<tr><td><input type="radio" name="freq" value="2" checked="${freq == 2 and 'checked' or None}"/><label for="yearday">Repeat on day no.</label><input type="text" size="3" maxlength="3" id="yearday" name="yearday" value="${period.byyearday}"/><label for="yearday">of the year</label></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3><a href="#">Monthly</a></h3>
|
||||||
|
<div>
|
||||||
|
<table class="event-wizard">
|
||||||
|
<tr><td><label for="monthinterval">Repeat every</label>
|
||||||
|
<input type="text" size="3" maxlength="3" id="monthinterval" name="monthinterval" value="${period.interval}"/>
|
||||||
|
<label for="monthinterval">month(s)</label></td></tr>
|
||||||
|
<tr><td><input type="radio" name="freq" value="3" checked="${freq == 3 and 'checked' or None}"/><label for="monthday-monthly-0">Repeat on</label>
|
||||||
|
<select id="monthday-monthly-0" name="monthday-monthly-0"><option py:for="ix, x in enumerate(monthday_names)" value="${ix}">${x}</option></select> day of the month</td></tr>
|
||||||
|
<tr><td><input type="radio" name="freq" value="4" checked="${freq == 4 and 'checked' or None}"/><label for="dayocc-monthly-1">Repeat on</label>
|
||||||
|
<select id="dayocc-monthly-1" name="dayocc-monthly-1"><option py:for="ix, x in enumerate(weekday_names)" value="${ix}" selected="${period.byweekdayocc == ix and 'selected' or None}">${x}</option></select>
|
||||||
|
<select name="weekday-monthly-1"><option py:for="ix, x in enumerate(day_names)" value="${ix}" selected="${ix in period.byweekday and 'selected' or None}">${x}</option></select></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3><a href="#">Weekly</a></h3>
|
||||||
|
<div>
|
||||||
|
<table class="event-wizard">
|
||||||
|
<tr><td><input type="radio" name="freq" value="5" checked="${freq == 5 and 'checked' or None}"/>Repeat every <input type="text" size="3" maxlength="3" name="weekinterval" value="${period.interval}"/> week(s)</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<py:for each="ix, x in enumerate(day_names)"><input type="checkbox" py:with="myid='weekday:%d' % ix" id="{$myid}" name="weekday-weekly" value="${ix}" checked="${ix in period.byweekday and 'selected' or None}"/><label for="{$myid}">${x}</label></py:for></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3><a href="#">Daily</a></h3>
|
||||||
|
<div>
|
||||||
|
<table class="event-wizard">
|
||||||
|
<tr><td><input type="radio" name="freq" value="6" checked="${freq == 6 and 'checked' or None}"/> Repeat every <input type="text" size="3" maxlength="3" name="dayinterval" value="${period.interval}"/> day(s)</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset id="recurrency-timeframe"><legend>Recurrency Timeframe</legend>
|
||||||
|
<table class="event-wizard">
|
||||||
|
<tr><td><label for="repeatframe2">Finish after </label><input id="count" type="text" size="3" maxlength="3" name="count" value="${period.count}"/> Repetitions</td></tr>
|
||||||
|
<tr><td><label for="repeatframe3">End on:</label><input id="until_date" type="text" size="10" maxlength="10" name="until_date" value="${period.until and period.until.strftime('%d.%m.%Y') or None}"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset id="recurrency-exceptions"><legend>Recurrency Exceptions</legend>
|
||||||
|
<table class="event-wizard">
|
||||||
|
<tr><td class="left"><input type="text" id="exception_name" name="exception-name" size="10" value="${exception_name}"/></td>
|
||||||
|
<td rowspan="4"><ul id="exceptions">
|
||||||
|
<li py:for="exception in exceptions"><input type="checkbox" name="exception:${exception.erd_id}"/> ${exception.erd_datetime.strftime('%d.%m.%Y')}</li></ul>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td class="left"><input type="submit" name="exception-add" value="add"/></td></tr>
|
||||||
|
<tr><td class="left"><input type="submit" name="exception-edit" value="edit"/></td></tr>
|
||||||
|
<tr><td class="left"><input type="submit" name="exception-delete" value="del"/></td></tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="submit" name="save" value="save"/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<rss version="2.0" xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<channel>
|
||||||
|
<title>${project.name}: #${title}</title>
|
||||||
|
<link>${abs_href.event("upcoming")}</link>
|
||||||
|
<description>Die kommenden Termine im und Rund um den 'CTDO'</description>
|
||||||
|
<language>de-de</language>
|
||||||
|
<image py:if="chrome.logo.src">
|
||||||
|
<title>$project.name</title>
|
||||||
|
<url>${abs_href.chrome(chrome.logo.src)}</url>
|
||||||
|
<link>${abs_href.chrome(chrome.logo.src)}</link>
|
||||||
|
</image>
|
||||||
|
<generator>TracRendezvous</generator>
|
||||||
|
|
||||||
|
<item py:for="key,value in events">
|
||||||
|
<pubDate>${http_date(today)}</pubDate>
|
||||||
|
<title>$event.name</title>
|
||||||
|
<link>${abs_href.events(event.srv_id)}</link>
|
||||||
|
<guid isPermaLink="false">${abs_href.events(event.srv_id)}</guid>
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li><strong>When:</strong> <em>${event.time_begin.strftime("%d.%m.%Y %Z")} - ${event.time_end.strftime("%d.%m.%Y %Z")}</em></li>
|
||||||
|
<py:if test="event.location"><li><strong>Location:</strong> <em>$event.location.name</em></li></py:if>
|
||||||
|
<py:if test="event.type_name"><li><strong>Type:</strong> <em>$event.type_name</em></li></py:if>
|
||||||
|
<py:if test="event.tags"><li><strong>Tags:</strong> <em>$event.tags</em></li></py:if>
|
||||||
|
<py:if test="event.attendees"><li><strong>Attendees:</strong> <em>$event.attendees</em></li></py:if>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
<category>Upcoming Events</category>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</channel>
|
||||||
|
</rss>
|
|
@ -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')
|
|
@ -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')
|
|
@ -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')
|
|
@ -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;}
|
After Width: | Height: | Size: 58 B |
After Width: | Height: | Size: 56 B |
After Width: | Height: | Size: 64 B |
After Width: | Height: | Size: 56 B |
After Width: | Height: | Size: 61 B |
After Width: | Height: | Size: 61 B |
After Width: | Height: | Size: 52 B |
After Width: | Height: | Size: 53 B |
After Width: | Height: | Size: 53 B |
After Width: | Height: | Size: 52 B |
After Width: | Height: | Size: 201 B |
After Width: | Height: | Size: 58 B |
After Width: | Height: | Size: 56 B |
After Width: | Height: | Size: 62 B |
After Width: | Height: | Size: 64 B |
After Width: | Height: | Size: 61 B |
After Width: | Height: | Size: 61 B |
After Width: | Height: | Size: 56 B |
After Width: | Height: | Size: 61 B |
After Width: | Height: | Size: 52 B |
After Width: | Height: | Size: 53 B |
After Width: | Height: | Size: 53 B |
After Width: | Height: | Size: 52 B |
After Width: | Height: | Size: 210 B |
After Width: | Height: | Size: 235 B |
After Width: | Height: | Size: 207 B |
|
@ -0,0 +1,2 @@
|
||||||
|
// Generated messages javascript file from compiled MO file
|
||||||
|
babel.Translations.load({"domain":"tracrendezvous-js","locale":"de","messages":{},"plural_expr":"(n != 1)"}).install();
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Generated messages javascript file from compiled MO file
|
||||||
|
babel.Translations.load({"domain":"tracrendezvous-js","locale":"de_DE","messages":{},"plural_expr":"(n != 1)"}).install();
|
|
@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: de <LL@li.org>\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"
|
||||||
|
|
|
@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\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"
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from tracrendezvous.location.web_ui import *
|
||||||
|
from tracrendezvous.location.model import *
|
|
@ -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;}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
|
@ -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 "<ItemLocation: %d %s>" % (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()
|
|
@ -0,0 +1,169 @@
|
||||||
|
<!DOCTYPE htm
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// <![CDATA[
|
||||||
|
function setLocation()
|
||||||
|
{
|
||||||
|
for (i = 0; i < document.mylocation.location_sel.length; ++i)
|
||||||
|
if (document.mylocation.location_sel.options[i].selected == true)
|
||||||
|
{
|
||||||
|
document.mylocation.location_name.value = document.mylocation.location_sel.options[i].value;
|
||||||
|
document.mylocation.coordinates.value = document.getElementById("nr_coord_" + i.toString()).value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createIframe(lat, lon)
|
||||||
|
{
|
||||||
|
var iframe;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
iframe = document.getElementById("mapframe");
|
||||||
|
iframe.parentNode.removeChild(iframe);
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.createElement && (iframe =
|
||||||
|
document.createElement("iframe")))
|
||||||
|
{
|
||||||
|
var blonstart= lon - 0.00928;
|
||||||
|
var blonend= lon + 0.011276;
|
||||||
|
var blatstart = lat - 0.0033;
|
||||||
|
var bladend = lat + 0.0109;
|
||||||
|
iframe.name = iframe.id = "mapframe";
|
||||||
|
iframe.width = "100%";
|
||||||
|
iframe.height = "600px";
|
||||||
|
iframe.src = "http://www.openstreetmap.org/export/embed.html?bbox=" + blonstart.toString() + "," + blatstart.toString() + "," + blonend.toString() + "," + bladend.toString() + "&marker=" + lat + "," + lon + "&zoom=16";
|
||||||
|
document.getElementById("location-results").appendChild(iframe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var map;
|
||||||
|
var layer_mapnik;
|
||||||
|
var layer_tah;
|
||||||
|
var layer_markers;
|
||||||
|
|
||||||
|
function drawmap() {
|
||||||
|
// Popup und Popuptext mit evtl. Grafik
|
||||||
|
var popuptext="<font color=\"black\"><b>Thomas Heiles<br>Straße 123<br>54290 Trier</b><p><img src=\"test.jpg\" width=\"180\" height=\"113\"></p></font>";
|
||||||
|
|
||||||
|
OpenLayers.Lang.setCode('de');
|
||||||
|
|
||||||
|
// Position und Zoomstufe der Karte
|
||||||
|
var lon = 6.641389;
|
||||||
|
var lat = 49.756667;
|
||||||
|
var zoom = 7;
|
||||||
|
|
||||||
|
map = new OpenLayers.Map('map', {
|
||||||
|
projection: new OpenLayers.Projection("EPSG:900913"),
|
||||||
|
displayProjection: new OpenLayers.Projection("EPSG:4326"),
|
||||||
|
controls: [
|
||||||
|
new OpenLayers.Control.MouseDefaults(),
|
||||||
|
new OpenLayers.Control.LayerSwitcher(),
|
||||||
|
new OpenLayers.Control.PanZoomBar()],
|
||||||
|
maxExtent:
|
||||||
|
new OpenLayers.Bounds(-20037508.34,-20037508.34,
|
||||||
|
20037508.34, 20037508.34),
|
||||||
|
numZoomLevels: 18,
|
||||||
|
maxResolution: 156543,
|
||||||
|
units: 'meters'
|
||||||
|
});
|
||||||
|
|
||||||
|
layer_mapnik = new OpenLayers.Layer.OSM.Mapnik("Mapnik");
|
||||||
|
layer_markers = new OpenLayers.Layer.Markers("Address", { projection: new OpenLayers.Projection("EPSG:4326"),
|
||||||
|
visibility: true, displayInLayerSwitcher: false });
|
||||||
|
|
||||||
|
map.addLayers([layer_mapnik, layer_markers]);
|
||||||
|
jumpTo(lon, lat, zoom);
|
||||||
|
|
||||||
|
// Position des Markers
|
||||||
|
addMarker(layer_markers, 6.641389, 49.756667, popuptext);
|
||||||
|
}
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$("#nr_location_search").get(0).focus();
|
||||||
|
drawmap();
|
||||||
|
});
|
||||||
|
// ]]>
|
||||||
|
</script>
|
||||||
|
<title>Locations</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<form name="mylocation" class="location" uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<input type="hidden" value="${rendezvous_id}"/>
|
||||||
|
<fieldset id="location">
|
||||||
|
<legend>Known Locations</legend>
|
||||||
|
<table class="listing">
|
||||||
|
<thead><tr><th>Name</th><th>Coordinates</th><th py:if="'LOCATION_MODIFY' in perm">Default</th><th py:if="'LOCATION_DELETE' in perm">Delete</th></tr></thead>
|
||||||
|
<tr py:for="location in locations">
|
||||||
|
<td><input name="name:${location.location_id}" type="text" size="30" maxlength="100" value="${location.name}"/></td>
|
||||||
|
<td><input name="location:${location.location_id}" type="text" size="32" maxlength="32" value="${location.coordinate_str()}"/></td>
|
||||||
|
<py:if test="'LOCATION_MODIFY' in perm">
|
||||||
|
<td><input type="radio" name="default" value="${location.location_id}" checked="${default_location == location.location_id and 'checked' or None}"/></td>
|
||||||
|
<td><input name="delete:${location.location_id}" type="checkbox" /></td>
|
||||||
|
</py:if>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p class="help">Change or delete existing locations.</p>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" name="reset"/>
|
||||||
|
<input type="submit" name="savelocations" value="Save Changes"/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<form name="newlocation" class="location" uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<fieldset id="new-location">
|
||||||
|
<legend>New Location</legend>
|
||||||
|
<table class="rendezvous-wizard">
|
||||||
|
<tr><th><label for="nr_location_text">Name:</label></th><td><input id="nr_location_text" type="text" size="24" maxlength="25" name="location_name" value=""/></td></tr>
|
||||||
|
<tr><th><label for="nr_location_text">Coordinates:</label></th><td><input id="nr_coordinates_text" type="text" size="32" maxlength="32" name="coordinates" value=""/></td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" name="reset"/>
|
||||||
|
<input type="submit" name="addlocation" value="Add Location"/>
|
||||||
|
</div>
|
||||||
|
<div class="hint">
|
||||||
|
<h3>Allowed coordinates formats:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>DD format = 'lat,lon', e.g 53.235235,6.235235</li>
|
||||||
|
<li>DMS Format = 'N|Wdd°mm'ss" E|Wdddd°mm'ss", e.g N51°31'39.40000" E7°27'53.7200"</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<form name="search_location" class="location" uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<fieldset id="location-search">
|
||||||
|
<legend>Location Search</legend>
|
||||||
|
<div>
|
||||||
|
<label for="nr_location_search">Location Search:</label>
|
||||||
|
<input id="nr_location_search" type="text" size="40" maxlength="40" name="location_search"/>
|
||||||
|
<input type="submit" name="search" value="Search"/>
|
||||||
|
</div>
|
||||||
|
<p class="help">e.g "central station, cityname"</p>
|
||||||
|
<div py:if="location_results" id="location-results">
|
||||||
|
<h3>Search Results</h3>
|
||||||
|
<table>
|
||||||
|
<tr py:for="rx, result in enumerate(location_results)" >
|
||||||
|
<td>${result.attrib["info"]} <a class="location" onclick="createIframe(${result.attrib['lat']},${result.attrib['lon']});">${result.attrib["name"]}</a> ${result.attrib["is_in"]}
|
||||||
|
<py:with vars="nears = result.find('nearestplaces')">
|
||||||
|
<py:if test="nears">,
|
||||||
|
<py:with vars="places = nears.findall('named')">
|
||||||
|
<py:for each="place in places">${place.attrib["distance"]} km away of <b>${place.attrib["name"]}</b></py:for>
|
||||||
|
</py:with>
|
||||||
|
</py:if>
|
||||||
|
</py:with>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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')
|
|
@ -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')
|
|
@ -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")
|
|
@ -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'))]
|
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import workflow
|
||||||
|
import macros
|
||||||
|
import api
|
|
@ -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)
|
|
@ -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 <skoegl@online.de>
|
||||||
|
#
|
||||||
|
|
||||||
|
''' 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)
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 752 B |
After Width: | Height: | Size: 7.9 KiB |
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 *
|
|
@ -0,0 +1,88 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from genshi.builder import tag
|
||||||
|
from trac.util.translation import _
|
||||||
|
from trac.wiki.macros import WikiMacroBase
|
||||||
|
from tracrendezvous.rendezvous import api
|
||||||
|
from tracrendezvous.rendezvous import model
|
||||||
|
|
||||||
|
__all__ = ['ExpiredRendezVousesMacro', 'CanceledRendezVousesMacro', 'ScheduledRendezVousesMacro']
|
||||||
|
|
||||||
|
class ExpiredRendezVousesMacro(WikiMacroBase):
|
||||||
|
|
||||||
|
"""Renders an overview of canceled or expired !RendezVouses"""
|
||||||
|
|
||||||
|
revision = "$Rev: 186 $"
|
||||||
|
|
||||||
|
def expand_macro(self, formatter, name, content):
|
||||||
|
if 'RENDEZVOUS_VIEW' not in formatter.perm:
|
||||||
|
return ""
|
||||||
|
uperm = model.RendezVousTypePermissionSystem(self.env)
|
||||||
|
controller = api.RendezVousSystem(self.env).workflow_controller
|
||||||
|
ls = controller.expired_rendezvouses()
|
||||||
|
lsr = [tag.li(tag.a("%s: (%s)" % (i.name, i.status), href=formatter.href.rendezvous(i.rendezvous_id)))
|
||||||
|
for i in ls
|
||||||
|
if 'RENDEZVOUS_ADMIN' in formatter.perm or uperm.check_user_type_permissions(formatter.req.authname, i.type_id)]
|
||||||
|
if lsr:
|
||||||
|
return tag.div([tag.ul(lsr)])
|
||||||
|
return None
|
||||||
|
|
||||||
|
class CanceledRendezVousesMacro(WikiMacroBase):
|
||||||
|
|
||||||
|
"""Renders an overview of canceled or expired !RendezVouses"""
|
||||||
|
|
||||||
|
revision = "$Rev: 186 $"
|
||||||
|
|
||||||
|
def expand_macro(self, formatter, name, content):
|
||||||
|
if 'RENDEZVOUS_VIEW' not in formatter.perm:
|
||||||
|
return ""
|
||||||
|
uperm = model.RendezVousTypePermissionSystem(self.env)
|
||||||
|
controller = api.RendezVousSystem(self.env).workflow_controller
|
||||||
|
ls = controller.canceled_rendezvouses()
|
||||||
|
lsr = [tag.li(tag.a("%s: (%s)" % (i.name, i.status), href=formatter.href.rendezvous(i.rendezvous_id)))
|
||||||
|
for i in ls
|
||||||
|
if 'RENDEZVOUS_ADMIN' in formatter.perm or uperm.check_user_type_permissions(formatter.req.authname, i.type_id)]
|
||||||
|
if lsr:
|
||||||
|
return tag.div([tag.ul(lsr)])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentRendezVousesMacro(WikiMacroBase):
|
||||||
|
|
||||||
|
"""Renders an overview of current !RendezVouses"""
|
||||||
|
|
||||||
|
revision = "$Rev:$"
|
||||||
|
|
||||||
|
def expand_macro(self, formatter, name, content):
|
||||||
|
if 'RENDEZVOUS_VIEW' not in formatter.perm:
|
||||||
|
return ""
|
||||||
|
uperm = model.RendezVousTypePermissionSystem(self.env)
|
||||||
|
controller = api.RendezVousSystem(self.env).workflow_controller
|
||||||
|
ls = controller.voting_rendezvouses()
|
||||||
|
lsr = [tag.li(tag.a("%s: (%s)" % (i.name, i.status), href=formatter.href.rendezvous(i.rendezvous_id)))
|
||||||
|
for i in ls
|
||||||
|
if 'RENDEZVOUS_ADMIN' in formatter.perm or uperm.check_user_type_permissions(formatter.req.authname, i.type_id)]
|
||||||
|
if lsr:
|
||||||
|
return tag.div([tag.ul(lsr)])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduledRendezVousesMacro(WikiMacroBase):
|
||||||
|
|
||||||
|
"""Renders an overview of scheduled !RendezVouses"""
|
||||||
|
|
||||||
|
revision = "$Rev: 186 $"
|
||||||
|
|
||||||
|
def expand_macro(self, formatter, name, content):
|
||||||
|
if 'RENDEZVOUS_VIEW' not in formatter.perm:
|
||||||
|
return ""
|
||||||
|
uperm = model.RendezVousTypePermissionSystem(self.env)
|
||||||
|
controller = api.RendezVousSystem(self.env).workflow_controller
|
||||||
|
rendezvouses = controller.scheduled_rendezvouses(check=True)
|
||||||
|
lsr = []
|
||||||
|
for i in rendezvouses:
|
||||||
|
if i.elected and ('RENDEZVOUS_ADMIN' in formatter.perm or uperm.check_user_type_permissions(formatter.req.authname, i.type_id)):
|
||||||
|
lsr.append(tag.li(tag.a("%s: %s - %s" % (i.name, i.get_date(i.elected).time_begin.strftime('%Y.%m.%d %H:%M'), i.get_date(i.elected).time_end.strftime('%Y.%m.%d %H:%M')), href=formatter.href.rendezvous(i.rendezvous_id))))
|
||||||
|
if lsr:
|
||||||
|
return tag.div([tag.ul(lsr, style="list-style-image:url(%s)" % formatter.href("/chrome/hw/images/selected.png"))])
|
||||||
|
return None
|
|
@ -0,0 +1,850 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from trac.core import *
|
||||||
|
from trac.perm import PermissionSystem
|
||||||
|
from trac.db import Table, Column, Index
|
||||||
|
from trac.db.util import sql_escape_percent
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from trac.util.datefmt import utc, to_timestamp
|
||||||
|
from trac.util.text import to_unicode
|
||||||
|
from trac.env import IEnvironmentSetupParticipant
|
||||||
|
from ctdotools.utils import validate_id, gen_wiki_page
|
||||||
|
from dateutil.rrule import *
|
||||||
|
from collections import defaultdict
|
||||||
|
from tracrendezvous.location.model import ItemLocation as RendezVousLocation
|
||||||
|
from tracrendezvous.rendezvous import api
|
||||||
|
|
||||||
|
__all__ = ['RendezVous', 'RendezVousComment', 'RendezVousType', 'RendezVousDate',
|
||||||
|
'RendezVousVote', 'TypePermission', 'RendezVousModelProvider',
|
||||||
|
'RendezVousTypePermissionSystem']
|
||||||
|
|
||||||
|
class RendezVousVote(object):
|
||||||
|
|
||||||
|
def __init__(self, env, vote_id, date_id, user, email, time_created, time_begin, time_end):
|
||||||
|
self.env = env
|
||||||
|
self.vote_id = vote_id
|
||||||
|
self.date_id = date_id
|
||||||
|
self.user = to_unicode(user)
|
||||||
|
self.email = to_unicode(email)
|
||||||
|
self.time_created = time_created
|
||||||
|
self.time_begin = time_begin
|
||||||
|
self.time_end = time_end
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fetch_one(env, vote_id):
|
||||||
|
db = env.get_db_cnx()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute("""SELECT *
|
||||||
|
FROM rendezvous_vote
|
||||||
|
WHERE vote_id=%s""", (vote_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
vote_id, date_id, user, email, time_created, time_begin, time_end = row
|
||||||
|
return RendezVousVote(env, vote_id, date_id, user, email, datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(time_begin, utc), datetime.fromtimestamp(time_end, utc))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def exists(env, date_id, user, time_begin, time_end):
|
||||||
|
db = env.get_db_cnx()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute("""SELECT COUNT(*) FROM rendezvous_vote
|
||||||
|
WHERE date_id=%s
|
||||||
|
and user=%s
|
||||||
|
and time_begin=%s
|
||||||
|
and time_end=%s""", (date_id, user, to_timestamp(time_begin), to_timestamp(time_end)))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return row[0] > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fetch_by_date(env, date_id, userName=None):
|
||||||
|
db = env.get_db_cnx()
|
||||||
|
cursor = db.cursor()
|
||||||
|
if userName == None:
|
||||||
|
cursor.execute("""SELECT * FROM rendezvous_vote
|
||||||
|
WHERE date_id=%s
|
||||||
|
ORDER BY user""", (date_id,))
|
||||||
|
else:
|
||||||
|
cursor.execute("""SELECT * FROM rendezvous_vote
|
||||||
|
WHERE date_id=%s and user=%s
|
||||||
|
ORDER BY user;""", (date_id, userName))
|
||||||
|
res = []
|
||||||
|
for row in cursor:
|
||||||
|
vote_id, date_id, user, email, time_created, time_begin, time_end = row
|
||||||
|
res.append(RendezVousVote(env, vote_id, date_id, user, email, datetime.fromtimestamp(time_created, utc), datetime.fromtimestamp(time_begin, utc), datetime.fromtimestamp(time_end, utc)))
|
||||||
|
return res
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
db = self.env.get_db_cnx()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute("""INSERT INTO rendezvous_vote
|
||||||
|
(date_id, user, email, time_created, time_begin, time_end)
|
||||||
|
VALUES(%s, %s, %s, %s, %s, %s)""", (
|
||||||
|
self.date_id,
|
||||||
|
self.user,
|
||||||
|
self.email,
|
||||||
|
to_timestamp(self.time_created),
|
||||||
|
to_timestamp(self.time_begin),
|
||||||
|
to_timestamp(self.time_end)))
|
||||||
|
db.commit()
|
||||||
|
self.vote_id = db.get_last_id(cursor, 'rendezvous_vote')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete(env, vote_id):
|
||||||
|
db = env.get_db_cnx()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute("""DELETE FROM rendezvous_vote
|
||||||
|
WHERE vote_id =%s;""", (vote_id,))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_by_date(env, date_id):
|
||||||
|
db = env.get_db_cnx()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute("""DELETE FROM rendezvous_vote
|
||||||
|
WHERE date_id =%s;""", (date_id,))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
db = self.env.get_db_cnx()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute("""UPDATE rendezvous_vote
|
||||||
|
SET user=%s,
|
||||||
|
email=%s,
|
||||||
|
time_created=%s,
|
||||||
|
time_begin=%s,
|
||||||
|
time_end=%s
|
||||||
|
WHERE vote_id=%s;""", (self.user,
|
||||||
|
self.email,
|
||||||
|
to_timestamp(self.time_created),
|
||||||
|
to_timestamp(self.time_begin),
|
||||||
|
to_timestamp(self.time_end),
|
||||||
|
self.vote_id))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "<RendezVousVote: %d, %d, %s, %s, %s, %s, %s" %(self.vote_id, self.date_id, self.user, self.email, self.time_created, self.time_begin, self.time_end)
|
||||||
|
|
||||||
|
def timedelta_2_string(t):
|
||||||
|
return "%d,%d,%d" % (t.days, t.seconds,t.microseconds)
|
||||||
|
|
||||||
|
def string2_timedelta(t):
|
||||||
|
return timedelta(*map(int, "1,2,3".split(",",3)))
|
||||||
|
|
||||||
|
class RendezVousDate(object):
|
||||||
|
|
||||||
|
def __init__(self, env, date_id, rendezvous_id, author, email, time_created, time_begin, time_end, elected, fetch_votes=False):
|
||||||
|
self.env = env
|
||||||
|
self.date_id = date_id
|
||||||
|
self.rendezvous_id = rendezvous_id
|
||||||
|
self.author = to_unicode(author)
|
||||||
|
self.email = to_unicode(email)
|
||||||
|
self.time_created = time_created
|
||||||
|
self.time_begin = time_begin
|
||||||
|
self.time_end = time_end
|
||||||
|
self.elected = elected
|
||||||
|
if fetch_votes:
|
||||||
|
self.votes = RendezVousVote.fetch_by_date(env, self.date_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fetch_one(env, date_id, fetch_votes=True):
|
||||||
|
db = env.get_db_cnx()
|
||||||
|
cursor = db.cursor()
|
||||||
|
if int(date_id) > 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 "<RendezVousDate: %d, %d, %s, %s, %s, %s, %s, %s>" % (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 "<RendezVousComment: %d, %d, %s, %s, %s>" % (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 "<TypePermission: %d %s>" % (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 "<RendezVous: %d, %s, %s, %s, %s>" % (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
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE htm
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="admin.html" />
|
||||||
|
<head>
|
||||||
|
<title>RendezVous General Settings</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>General Settings</h2>
|
||||||
|
<form class='mod' id="modbasic" action="" method="post">
|
||||||
|
<fieldset>
|
||||||
|
<legend>RendezVous</legend>
|
||||||
|
<div class="field">
|
||||||
|
<label>max description length:<br/>
|
||||||
|
<input type="text" name="max_description_length" value="${max_description_length}"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>max votes per date:<br/>
|
||||||
|
<input type="text" name="max_votes_per_date" value="${max_votes_per_date}"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>max dates per rendezvous:<br/>
|
||||||
|
<input type="text" name="max_dates_per_rendezvous" value="${max_dates_per_rendezvous}"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>graph width:<br/>
|
||||||
|
<input type="text" name="graph_size_x" value="${graph_size_x}"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>graph height:<br/>
|
||||||
|
<input type="text" name="graph_size_y" value="${graph_size_y}"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>show vote graph:<br/>
|
||||||
|
<input type="checkbox" name="show_vote_graph" checked="${show_vote_graph and 'checked' or None}"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>show location map:<br/>
|
||||||
|
<input type="checkbox" name="show_location_map" checked="${show_location_map and 'checked' or None}"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="buttons">
|
||||||
|
<input type="submit" value="Apply changes" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<!DOCTYPE html
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/">
|
||||||
|
<xi:include href="admin.html" />
|
||||||
|
<head>
|
||||||
|
<title>$label_plural</title>
|
||||||
|
<script type="text/javascript">
|
||||||
|
/* <![CDATA[ */
|
||||||
|
/* ]]> */
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Overview Configuration</h2>
|
||||||
|
<form id="overview" name="overview" method="post" action="">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Overview Configuration</legend>
|
||||||
|
<div class="overview-row" py:for="l in all_status">
|
||||||
|
<div class="overview-set">
|
||||||
|
${l[0]}
|
||||||
|
<input type="text" id="cat_${l[1]}" name="${l[0]}" size="3" maxlength="3" value="${l[1]}"/><span py:if="len(l) == 4" class="error">wrong position - should be ${l[3]}!</span>
|
||||||
|
<input type="checkbox" name="show" value="${l[0]}" checked="${l[2] and 'checked' or None}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
help
|
||||||
|
</p>
|
||||||
|
<div class="buttons">
|
||||||
|
<input type="submit" name="save" value="Save" />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,95 @@
|
||||||
|
<!DOCTYPE html
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/">
|
||||||
|
<xi:include href="admin.html" />
|
||||||
|
<head>
|
||||||
|
<title>$label_plural</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Manage RendezVousTypes</h2>
|
||||||
|
<py:if test="'RENDEZVOUS_ADMIN' in perm">
|
||||||
|
<form id="addperm" class="addnew" method="post" action="">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Add Type:</legend>
|
||||||
|
<table>
|
||||||
|
<tr class="field">
|
||||||
|
<th><label for="gp_subject">RendezVous Type:</label></th>
|
||||||
|
<td><input id="gp_subject" type="text" name="rtype" /></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p class="help">
|
||||||
|
Create a new RendezVous Type. Note that <em>RendezVousType</em> names can't be all upper-case,
|
||||||
|
as that is reserved for permission names.
|
||||||
|
</p>
|
||||||
|
<div class="buttons">
|
||||||
|
<input type="submit" name="add" value=" Add " />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form id="addsubj" class="addnew" method="post" action="">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Grant Permission To Type:</legend>
|
||||||
|
<table>
|
||||||
|
<tr class="field">
|
||||||
|
<th><label for="rtype">RendezVousType:</label></th>
|
||||||
|
<td><select id="rtype" name="rtype">
|
||||||
|
<option py:for="rtype in rendezVousTypes">${rtype.name}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="field">
|
||||||
|
<th><label for="permission">Permission:</label></th>
|
||||||
|
<td><select id="permission" name="permission">
|
||||||
|
<option py:for="action in sorted(actions)">${action}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p class="help">
|
||||||
|
Grant permission to a RendezVousType.
|
||||||
|
</p>
|
||||||
|
<div class="buttons">
|
||||||
|
<input type="submit" name="add" value=" Add " />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</py:if>
|
||||||
|
|
||||||
|
<form method="post" action="">
|
||||||
|
<table class="listing" id="permlist">
|
||||||
|
<thead>
|
||||||
|
<tr><th>RendezVous Type</th><th>Default</th><th>Permission</th><th>delete</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr py:for="idx, rtype in enumerate(rendezVousTypes)"
|
||||||
|
class="${idx % 2 and 'odd' or 'even'}">
|
||||||
|
<td>${rtype.name}</td>
|
||||||
|
<td><input type="radio" name="default" value="${rtype.type_id}" checked="${rtype.type_id == default_rendezvous_type and 'checked' or None}" /></td>
|
||||||
|
<td>
|
||||||
|
<py:for each="typePerm in rtype.typePermissions">
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="${typePerm.type_id}:${typePerm.permission}" name="sel" value="${typePerm.type_id}:${typePerm.permission}" />
|
||||||
|
<label for="${typePerm.type_id}:${typePerm.permission}">${typePerm.permission}</label>
|
||||||
|
</div>
|
||||||
|
</py:for>
|
||||||
|
</td>
|
||||||
|
<td><input type="checkbox" id="delete:${rtype.type_id}" name="rsel" value="${rtype.type_id}"/></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="buttons">
|
||||||
|
<input type="submit" name="save" value="Save Changes" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="help">
|
||||||
|
Note that <em>RendezVousType</em> names can't be all upper-case,
|
||||||
|
as that is reserved for permission names.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,106 @@
|
||||||
|
<!DOCTYPE htm
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$("input[name^='date_begin']").datepicker({"dateFormat" : "dd.mm.yy", altField: "#date_end"});
|
||||||
|
$("input[name^='date_end']").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<title>Dates</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content" py:if="rendezvous">
|
||||||
|
<div id='rendezvous-main'>
|
||||||
|
<py:choose>
|
||||||
|
<py:when test="'RENDEZVOUS_DATE_MODIFY' in perm or 'RENDEZVOUS_DATE_ADD' in perm or 'RENDEZVOUS_DATE_DELETE' in perm">
|
||||||
|
<form py:if="len(dates) > 0" class='votes' method="post" action="">
|
||||||
|
<fieldset >
|
||||||
|
<legend>Dates for ${rendezvous.name}</legend>
|
||||||
|
<input type="hidden" name="item" value="${rendezvous.rendezvous_id}"/>
|
||||||
|
<table class="listing">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>author</th>
|
||||||
|
<th>email</th>
|
||||||
|
<th>date begin</th>
|
||||||
|
<th>time begin</th>
|
||||||
|
<th>date end</th>
|
||||||
|
<th>time end</th>
|
||||||
|
<th>delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<py:for each="date in dates">
|
||||||
|
<tr py:with="dt = selected_tz.fromutc(date.time_begin);
|
||||||
|
dt2 = selected_tz.fromutc(date.time_end)">
|
||||||
|
<td>${date.author}</td>
|
||||||
|
<td><input type="text" size="25" name="email:${date.date_id}" value="${date.email}"/></td>
|
||||||
|
<td><input type="text" size="10" maxlength="10" name="date_begin:${date.date_id}" value="${date.time_begin.strftime('%d.%m.%Y')}"/></td>
|
||||||
|
<td><input type="text" size="5" maxlength="5" name="time_begin:${date.date_id}" value="${dt.strftime('%H:%M')}"/> ${dt.tzinfo.tzname(None)}</td>
|
||||||
|
<td><input type="text" size="10" maxlength="10" id="date_end" name="date_end:${date.date_id}" value="${date.time_end.strftime('%d.%m.%Y')}"/></td>
|
||||||
|
<td><input type="text" size="5" maxlength="5" name="time_end:${date.date_id}" value="${dt2.strftime('%H:%M')}"/> ${dt2.tzinfo.tzname(None)}</td>
|
||||||
|
<td><input type="checkbox" name="delete" value="${date.date_id}"/></td>
|
||||||
|
</tr>
|
||||||
|
</py:for>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
<p class="help">Change or delete votes for an existing rendezvous date.</p>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" name="reset"/>
|
||||||
|
<input type="submit" name="savedates" value="Save Changes"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form class='rendezvous-wizard' method="post" mime-type="text/plain">
|
||||||
|
<input type="hidden" name="rendezvous_id" value="$rendezvous.rendezvous_id"/>
|
||||||
|
<fieldset id="properties">
|
||||||
|
<legend>New Date</legend>
|
||||||
|
<table class="rendezvous-wizard" py:with="dt = selected_tz.fromutc(new_date.time_begin);
|
||||||
|
dt2 = selected_tz.fromutc(new_date.time_end)">
|
||||||
|
<tr><th><label for="nd_email">Email:</label></th><td><input id="nd_email" type="text" size="24" maxlength="25" name="email"/></td></tr>
|
||||||
|
<tr><th><label for="nd_date_begin">Date begin:</label></th><td><input id="nd_date_begin" type="text" size="10" maxlength="10" name="date_begin" value="${new_date.time_begin.strftime('%d.%m.%Y')}"/>
|
||||||
|
<input id="nd_time_begin" type="text" size="5" maxlength="5" name="time_begin" value="${dt.strftime('%H:%M')}"/> ${dt.tzinfo.tzname(None)}</td></tr>
|
||||||
|
<tr><th><label for="date_end">Date end:</label></th><td><input id="date_end" type="text" size="10" maxlength="10" name="date_end" value="${new_date.time_end.strftime('%d.%m.%Y')}"/>
|
||||||
|
<input id="nd_time_end" type="text" size="5" maxlength="5" name="time_end" value="${dt2.strftime('%H:%M')}"/> ${dt2.tzinfo.tzname(None)}</td></tr>
|
||||||
|
<tr><th><label for="nd_autovoting">Vote that for me!</label></th><td><input id="nd_autovoting" type="checkbox" checked="checked" name="autovoting"/></td></tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" name="reset"/>
|
||||||
|
<input type="submit" name="newdate" value=" Add " />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</py:when>
|
||||||
|
<h3>Allowed date/time formats:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>yyyymmdd</li>
|
||||||
|
<li>yyyy.mm.dd</li>
|
||||||
|
</ul>
|
||||||
|
<py:otherwise>
|
||||||
|
<h2>Dates for ${rendezvous.name}</h2>
|
||||||
|
<table class="listing">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>author</th>
|
||||||
|
<th>day</th>
|
||||||
|
<th>day part</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr py:for="date in dates">
|
||||||
|
<td>${date.author}</td>
|
||||||
|
<td>${date.time_begin.strftime('%d.%m.%Y %H:%M')}</td>
|
||||||
|
<td>${date.time_end.strftime('%d.%m.%Y %H:%M')}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</py:otherwise>
|
||||||
|
</py:choose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,126 @@
|
||||||
|
<!DOCTYPE htm
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript">
|
||||||
|
/* <![CDATA[ */
|
||||||
|
jQuery(document).ready(function($) {$("#nr_location_search").get(0).focus()});
|
||||||
|
function setLocation()
|
||||||
|
{
|
||||||
|
for (i = 0; i < document.mylocation.location_sel.length; ++i)
|
||||||
|
if (document.mylocation.location_sel.options[i].selected == true)
|
||||||
|
{
|
||||||
|
document.mylocation.location_name.value = document.mylocation.location_sel.options[i].value;
|
||||||
|
document.mylocation.coordinates.value = document.getElementById("nr_coord_" + i.toString()).value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createIframe(lat, lon)
|
||||||
|
{
|
||||||
|
var iframe;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
iframe = document.getElementById("mapframe");
|
||||||
|
iframe.parentNode.removeChild(iframe);
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.createElement && (iframe =
|
||||||
|
document.createElement("iframe")))
|
||||||
|
{
|
||||||
|
var blonstart= lon - 0.00928;
|
||||||
|
var blonend= lon + 0.011276;
|
||||||
|
var blatstart = lat - 0.0033;
|
||||||
|
var bladend = lat + 0.0109;
|
||||||
|
iframe.name = iframe.id = "mapframe";
|
||||||
|
iframe.width = "100%";
|
||||||
|
iframe.height = "600px";
|
||||||
|
iframe.src = "http://www.openstreetmap.org/export/embed.html?bbox=" + blonstart.toString() + "," + blatstart.toString() + "," + blonend.toString() + "," + bladend.toString() + "&marker=" + lat + "," + lon + "&zoom=16";
|
||||||
|
document.getElementById("location-results").appendChild(iframe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ]]> */
|
||||||
|
</script>
|
||||||
|
<title>Locations</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<div id='rendezvous-main'>
|
||||||
|
<form name="mylocation" class='rendezvous-wizard' uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<input type="hidden" value="${rendezvous_id}"/>
|
||||||
|
<fieldset id="location">
|
||||||
|
<legend>Known Locations</legend>
|
||||||
|
<table class="listing">
|
||||||
|
<thead><tr><th>Name</th><th>Coordinates</th><th py:if="'RENDEZVOUS_LOCATION_MODIFY' in perm">Default</th><th py:if="'RENDEZVOUS_LOCATION_DELETE' in perm">Delete</th></tr></thead>
|
||||||
|
<tr py:for="location in locations">
|
||||||
|
<td><input name="name:${location.location_id}" type="text" size="40" maxlength="40" value="${location.name}"/></td>
|
||||||
|
<td><input name="location:${location.location_id}" type="text" size="32" maxlength="32" value="${location.coordinate_str()}"/></td>
|
||||||
|
<py:if test="'RENDEZVOUS_LOCATION_MODIFY' in perm">
|
||||||
|
<td><input type="radio" name="default" value="${location.location_id}" checked="${default_location == location.location_id and 'checked' or None}"/></td>
|
||||||
|
<td><input name="delete:${location.location_id}" type="checkbox" /></td>
|
||||||
|
</py:if>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p class="help">Change or delete existing locations.</p>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" name="reset"/>
|
||||||
|
<input type="submit" name="savelocations" value="Save Changes"/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<form name="newlocation" class='rendezvous-wizard' uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<fieldset id="new location">
|
||||||
|
<legend>New Location</legend>
|
||||||
|
<table class="rendezvous-wizard">
|
||||||
|
<tr><th><label for="nr_location_text">Name:</label></th><td><input id="nr_location_text" type="text" size="24" maxlength="25" name="location_name" value=""/></td></tr>
|
||||||
|
<tr><th><label for="nr_location_text">Coordinates:</label></th><td><input id="nr_coordinates_text" type="text" size="32" maxlength="32" name="coordinates" value=""/></td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" name="reset"/>
|
||||||
|
<input type="submit" name="addlocation" value="Add Location"/>
|
||||||
|
</div>
|
||||||
|
<p class="hint">
|
||||||
|
<h3>Allowed coordinates formats:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>DD format = 'lat,lon', e.g 53.235235,6.235235</li>
|
||||||
|
<li>DMS Format = 'N|Wdd°mm'ss" E|Wdddd°mm'ss", e.g N51°31'39.40000" E7°27'53.7200"</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<form name="search_location" class='rendezvous-wizard' uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<fieldset id="location-search">
|
||||||
|
<legend>Location Search</legend>
|
||||||
|
<div>
|
||||||
|
<label for="nr_location_search">Location Search:</label>
|
||||||
|
<input id="nr_location_search" type="text" size="40" maxlength="40" name="location_search"/>
|
||||||
|
<input type="submit" name="search" value="Search"/>
|
||||||
|
</div>
|
||||||
|
<p class="help">e.g "central station, cityname"</p>
|
||||||
|
<div py:if="location_results" id="location-results">
|
||||||
|
<h3>Search Results</h3>
|
||||||
|
<table>
|
||||||
|
<tr py:for="rx, result in enumerate(location_results)" >
|
||||||
|
<td>${result.attrib["info"]} <a class="location" onclick="createIframe(${result.attrib['lat']},${result.attrib['lon']});">${result.attrib["name"]}</a> ${result.attrib["is_in"]}
|
||||||
|
<py:with vars="nears = result.find('nearestplaces')">
|
||||||
|
<py:if test="nears">,
|
||||||
|
<py:with vars="places = nears.findall('named')">
|
||||||
|
<py:for each="place in places">${place.attrib["distance"]} km away of <b>${place.attrib["name"]}</b></py:for>
|
||||||
|
</py:with>
|
||||||
|
</py:if>
|
||||||
|
</py:with>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE html
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<title>RendezVous</title>
|
||||||
|
<script type="text/javascript" src="${chrome.htdocs_location}js/wikitoolbar.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content" class="rendezvous">
|
||||||
|
<!-- <div class="overview-content"> -->
|
||||||
|
<!-- <py:for each="ix, section in enumerate(all_rendezvouses.iteritems())"> -->
|
||||||
|
|
||||||
|
<div class="overview-set">
|
||||||
|
<table>
|
||||||
|
<tr><td><table><tr><td align="center"><img src="${href.chrome('/hw/images/new.png')}" border="3"/></td></tr><tr><td align="center"><h2>new</h2></td></tr></table></td>
|
||||||
|
<td><ul class="overview-set"><li py:for="item in all_rendezvouses['new']"><a class="overview" href="${href.rendezvous(item[0])}">${item[1]}</a></li></ul></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="overview-set">
|
||||||
|
<table>
|
||||||
|
<tr><td><table><tr><td align="center"><img src="${href.chrome('/hw/images/voting.png')}" border="3"/></td></tr><tr><td align="center"><h2>voting</h2></td></tr></table></td>
|
||||||
|
<td><ul class="overview-set"><li py:for="item in all_rendezvouses['voting']"><a class="overview" href="${href.rendezvous(item[0])}">${item[1]}</a></li></ul></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="overview-set">
|
||||||
|
<table>
|
||||||
|
<tr><td><table><tr><td align="center"><img src="${href.chrome('/hw/images/canceled.png')}" border="3"/></td></tr><tr><td align="center"><h2>canceled</h2></td></tr></table></td>
|
||||||
|
<td><ul class="overview-set"><li py:for="item in all_rendezvouses['canceled']"><a class="overview" href="${href.rendezvous(item[0])}">${item[1]}</a></li></ul></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="overview-set">
|
||||||
|
<table>
|
||||||
|
<tr><td><table><tr><td align="center"><img src="${href.chrome('/hw/images/expired.png')}" border="3"/></td></tr><tr><td align="center"><h2>expired</h2></td></tr></table></td>
|
||||||
|
<td><ul class="overview-set"><li py:for="item in all_rendezvouses['expired']"><a class="overview" href="${href.rendezvous(item[0])}">${item[1]}</a></li></ul></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--<div class="overview-set">
|
||||||
|
<table>
|
||||||
|
<tr><td><table><tr><td align="center"><img src="${href.chrome('/hw/images/%s' % icons[section[0]])}" border="3"/></td></tr><tr><td align="center"><h2>${section[0]}</h2></td></tr></table></td>
|
||||||
|
<td><ul class="overview-set"><li py:for="item in section[1]"><a class="overview" href="${href.rendezvous(item[0])}">${item[1]}</a></li></ul></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>-->
|
||||||
|
<!--<div class="overview-set">
|
||||||
|
<table>
|
||||||
|
<tr><td><table><tr><td align="center"><img src="${href.chrome('/hw/images/%s' % icons[section[0]])}" border="3"/></td></tr><tr><td align="center"><h2>${section[0]}</h2></td></tr></table></td>
|
||||||
|
<td><ul class="overview-set"><li py:for="item in section[1]"><a class="overview" href="${href.rendezvous(item[0])}">${item[1]}</a></li></ul></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>-->
|
||||||
|
<!-- </py:for> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,149 @@
|
||||||
|
<!DOCTYPE html
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<title>RendezVous</title>
|
||||||
|
<script type="text/javascript" src="${chrome.htdocs_location}js/wikitoolbar.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<div id="rendezvous-main">
|
||||||
|
<div id="rendezvous-details">
|
||||||
|
<h1>RendezVous #${rendezvous.rendezvous_id}<span style="color:#ff0000"> (${rendezvous.status})</span></h1>
|
||||||
|
<span class="rendezvous-header">Created on ${rendezvous.time_created.strftime('%d.%m.%Y %H:%M')} by ${rendezvous.author}</span>
|
||||||
|
<h3>${rendezvous.name}</h3><span></span>
|
||||||
|
<table class="properties" cellspacing="10px">
|
||||||
|
<tr>
|
||||||
|
<th id="h_type">Type</th>
|
||||||
|
<td headers="h_type">${rendezvous.type_name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr py:if="rendezvous.min_votes > 0">
|
||||||
|
<th id="h_minvotes">Min. Votes</th>
|
||||||
|
<td headers="h_minvotes">${rendezvous.min_votes}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th id="h_tags">Tagged with</th>
|
||||||
|
<td headers="h_tags">${rendezvous.tags and rendezvous.tags or None}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th id="h_location">Location</th>
|
||||||
|
<td headers="h_location">${location.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr py:if="location and location.lat != None">
|
||||||
|
<th id="h_coordinates">Coordinates</th>
|
||||||
|
<td headers="h_coordinates"><a href="${'http://www.openstreetmap.org/index.html?mlat=%s&mlon=%s&zoom=15&layers=B00TTT' % (location.lat, location.lon)}">${location.coordinate_str()}</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr py:if="show_location_map and location.lat != None"><td colspan="4"><iframe width="100%" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="${'http://www.openstreetmap.org/export/embed.html?bbox=%f,%f,%f,%f&layer=mapnik&marker=%s,%s&zoom=16' % (location.lon-0.00928, location.lat-0.0033, location.lon+0.011276, location.lat+0.0109, location.lat, location.lon)}" style="border: 1px solid black"></iframe></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div py:if="rendezvous.description">
|
||||||
|
<h3>Description</h3>
|
||||||
|
${wiki_to_html(context, rendezvous.description, escape_newlines=preserve_newlines)}
|
||||||
|
</div>
|
||||||
|
<h3>Votings</h3>
|
||||||
|
<table class="rendezvous-matrix">
|
||||||
|
<thead>
|
||||||
|
<tr class="rendezvous-matrix"><th class="rendezvous-matrix">Date proposals -></th>
|
||||||
|
<py:for each="date in rendezvous.dates">
|
||||||
|
<th class="rendezvous-matrix" py:with="dt_begin = selected_tz.fromutc(date.time_begin); dt_end = selected_tz.fromutc(date.time_end)">
|
||||||
|
<div py:if="date.elected" class="scheduled"><img src="${href.chrome('/hw/images/selected.png')}"/>scheduled</div>
|
||||||
|
<a py:strip="('RENDEZVOUS_DATE_VIEW' not in perm and date.author != authname) or 'RENDEZVOUS_ADMIN' not in perm" href="${href.date(rendezvous.rendezvous_id)}">${dt_begin.strftime('%d.%m.%Y %H:%M')} ${dt_begin.tzinfo.tzname(None)} - ${dt_end.strftime('%d.%m.%Y %H:%M')} ${dt_end.tzinfo.tzname(None)}</a>
|
||||||
|
</th>
|
||||||
|
</py:for>
|
||||||
|
<th class="rendezvous-matrix" py:if="'RENDEZVOUS_DATE_ADD' in perm and not rendezvous.elected">
|
||||||
|
<a title="create a new date for rendezvous #${rendezvous.rendezvous_id}" href="${href.date(rendezvous.rendezvous_id)}">new date</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="rendezvous-stats">
|
||||||
|
<td class="rendezvous-stats">VoteCount</td>
|
||||||
|
<py:for each="date in rendezvous.dates">
|
||||||
|
<td class="rendezvous-stats">${date.get_vote_count()}</td>
|
||||||
|
</py:for>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
<tbody>
|
||||||
|
<py:for each="row in table">
|
||||||
|
<tr class="rendezvous-matrix">
|
||||||
|
<td align="center" class="rendezvous-details">${row[0]}</td>
|
||||||
|
<py:for each="col in row[1:]" py:with="ismyrow = row[0] == req.authname">
|
||||||
|
<py:choose test="">
|
||||||
|
<td py:when="rendezvous.elected and not rendezvous.get_date(col[1]).elected" align="center" class="rendezvous-notelected"/>
|
||||||
|
<py:otherwise test=""><td align="center" class="${not col[0] and 'rendezvous-notvoted' or 'rendezvous-voted'}">
|
||||||
|
<py:if test="'RENDEZVOUS_VOTE_VIEW' in perm and ismyrow">
|
||||||
|
<py:choose>
|
||||||
|
<py:when test="not col[0] and 'RENDEZVOUS_VOTE_ADD' in perm">
|
||||||
|
<a class="voting" title="quick and dirty voting for the date" href="${href.quickvote(col[1])}">vote</a>
|
||||||
|
<a class="voting" title="advanced voting operations" href="${href.vote(col[1])}">...advanced</a>
|
||||||
|
</py:when>
|
||||||
|
<a py:when="'RENDEZVOUS_VOTE_MODIFY' in perm" class="voting" href="${href.vote(col[1])}">edit</a>
|
||||||
|
</py:choose>
|
||||||
|
</py:if>
|
||||||
|
</td>
|
||||||
|
</py:otherwise>
|
||||||
|
</py:choose>
|
||||||
|
</py:for>
|
||||||
|
</tr>
|
||||||
|
</py:for>
|
||||||
|
<tr class="rendezvous-details" py:if="not voted and len(rendezvous.dates) > 0 and 'RENDEZVOUS_VOTE_ADD' in perm" action="${urlbase}/rendezvous/addvote">
|
||||||
|
<td class="rendezvous-details"><input type="text" id="n" size="16" maxlength="15" name="newuser" value="${authname}"/></td>
|
||||||
|
<py:for each="date in rendezvous.dates">
|
||||||
|
<td align="center" class="rendezvous-details">
|
||||||
|
<a class="voting" title="quick and dirty voting for the date" href="${href.quickvote(date.date_id)}">vote</a>
|
||||||
|
<a class="voting" title="advanced voting operations" href="${href.vote(date.date_id)}">...advanced</a>
|
||||||
|
</td>
|
||||||
|
</py:for>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div py:if="has_votes and show_vote_graph == True and 'RENDEZVOUS_VOTE_GRAPH_VIEW' in perm">
|
||||||
|
<h2>Vote Distribution By RendezVousDate</h2>
|
||||||
|
<div py:if="date.votes" py:for="date in rendezvous.dates">
|
||||||
|
<img src="${href.chrome('site/rendezvous_graphs/date%s.png'%date.date_id)}" border="3" width="100%"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<py:if test="'RENDEZVOUS_COMMENT_VIEW' in perm">
|
||||||
|
<py:if test="comments">
|
||||||
|
<h3>Comments</h3>
|
||||||
|
<div py:for="comment in comments" class="comment">
|
||||||
|
<span class="comment-header">added on ${comment.time_created.strftime('%d.%m.%Y %H:%M')} by ${comment.author} <a py:if="authname == comment.author or 'RENDEZVOUS_ADMIN' in perm" href="${href.rendezvous(rendezvous.rendezvous_id, 'comment', comment.comment_id)}#edit">edit</a></span>
|
||||||
|
${wiki_to_html(context, comment.comment, escape_newlines=preserve_newlines)}
|
||||||
|
</div>
|
||||||
|
</py:if>
|
||||||
|
<h3 id="edit">New Comment</h3>
|
||||||
|
<div>
|
||||||
|
<py:if test="preview_comment">
|
||||||
|
<h3 style="color:#ff0000;">Preview!</h3>
|
||||||
|
<div class="graph">
|
||||||
|
<div class="comment">
|
||||||
|
<span class="comment-header">added on ${preview_comment.time_created.strftime('%d.%m.%Y %H:%M')} by ${preview_comment.author}</span>
|
||||||
|
${wiki_to_html(context, preview_comment.comment, escape_newlines=preserve_newlines)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</py:if>
|
||||||
|
<form name="my_comment" action="#edit" method="post" mime-type="text/plain">
|
||||||
|
<input py:if="preview_comment and preview_comment.comment_id > 0" type="hidden" name="comment_id" value="${preview_comment.comment_id}"/>
|
||||||
|
<textarea id="new_comment" class="wikitext" rows="20" cols="100" name="comment" py:content="preview_comment and preview_comment.comment or None"></textarea>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<input type="submit" name="preview" value="Preview"/>
|
||||||
|
<input type="submit" name="save" value="Save"/>
|
||||||
|
<input type="submit" name="delete" value="Delete"/>
|
||||||
|
</div>
|
||||||
|
<b>Note:</b> See <a href="${href.wiki('WikiFormatting')}">WikiFormatting</a> and
|
||||||
|
<a href="${href.wiki('TracWiki')}">TracWiki</a> for help on editing wiki content.
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</py:if>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,93 @@
|
||||||
|
<!DOCTYPE htm
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript" src="${chrome.htdocs_location}js/wikitoolbar.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
/* <![CDATA[ */
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
/* only enable control elements for the currently selected action */
|
||||||
|
var actions = $("#action input[name='action']");
|
||||||
|
function updateActionFields()
|
||||||
|
{
|
||||||
|
actions.each(function() {
|
||||||
|
$(this).siblings().find("*[@id]").enable($(this).checked());
|
||||||
|
$(this).siblings().filter("*[@id]").enable($(this).checked());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actions.click(updateActionFields);
|
||||||
|
updateActionFields();
|
||||||
|
$("#schedule_deadline").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
});
|
||||||
|
/* ]]> */
|
||||||
|
</script>
|
||||||
|
<title>${title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<div id='rendezvous-main'>
|
||||||
|
<form name="new_rendezvous" class='rendezvous-wizard' uri="" method="post" mime-type="text/plain" action="">
|
||||||
|
<input py:if="rendezvous.rendezvous_id != 0" type="hidden" name="rendezvous_id" value="${rendezvous.rendezvous_id}"/>
|
||||||
|
<fieldset id="properties">
|
||||||
|
<legend>${title}</legend>
|
||||||
|
<table class="rendezvous-wizard" py:with="dt = selected_tz.fromutc(rendezvous.schedule_deadline)">
|
||||||
|
<tr><th><label for="name">Title</label></th><td><input id="name" type="text" size="50" maxlength="50" name="name" value="${rendezvous.name}"/></td></tr>
|
||||||
|
<tr><th><label for="author">Author</label></th><td>
|
||||||
|
<py:choose test="">
|
||||||
|
<input py:when="not rendezvous.rendezvous_id or 'RENDEZVOUS_ADMIN' in perm" id="author" type="text" size="50" maxlength="50" name="author" value="${rendezvous.rendezvous_id and rendezvous.author or authname}"/>
|
||||||
|
<py:otherwise>${rendezvous.author}</py:otherwise>
|
||||||
|
</py:choose>
|
||||||
|
</td></tr>
|
||||||
|
<tr><th><label for="email">Email</label></th><td><input id="email" type="text" size="50" maxlength="100" name="email" value="${rendezvous.email}"/></td></tr>
|
||||||
|
<tr><th><label for="rtype">Type</label></th><td><select id="rtype" name="type_name" size="1"><option py:for="rtype in types" selected="${rtype.type_id == rendezvous.type_id and 'checked' or None}">${rtype.name}</option></select></td></tr>
|
||||||
|
<tr><th><label for="min_votes">Minimum votes</label></th>
|
||||||
|
<td><input id="min_votes" type="input" name="min_votes" size="4" maxlength="4" value="${rendezvous.min_votes}" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr><th><label for="schedule_deadline">Voting deadline</label></th>
|
||||||
|
<td><input id="schedule_deadline" type="input" name="schedule_deadline_date" size="10" maxlength="10" value="${dt.strftime('%d.%m.%Y')}" />
|
||||||
|
<input id="schedule_deadline_time" type="input" size="5" maxlength="5" name="schedule_deadline_time" value="${dt.strftime('%H:%M')}" /> ${dt.tzinfo.tzname(None)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr><th><label for="location">Locations</label></th>
|
||||||
|
<td><select id="location" name="location" size="1">
|
||||||
|
<option py:for="location in locations" selected="${location.location_id == rendezvous.location_id and 'checked' or None}">${location.name} :${location.coordinate_str()}</option>
|
||||||
|
</select>
|
||||||
|
<a py:if="'RENDEZVOUS_LOCATION_MODIFY' in perm" href="${href.location(location and location.location_id or 0)}">edit/search locations</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr><th><label for="description">Description</label></th><td><textarea id="description" class="wikitext" rows="20" cols="100" name="description" py:content="rendezvous.description"></textarea></td></tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
<!--! Workflow support -->
|
||||||
|
<fieldset py:if="rendezvous.rendezvous_id != 0" id="action">
|
||||||
|
<legend>${_("Workflow")}</legend>
|
||||||
|
<div py:for="key, label, controls, hints in action_controls">
|
||||||
|
<input type="radio" id="action_$key" name="action" value="$key" checked="${cstatus == key and 'checked' or None}" />
|
||||||
|
<label for="action_$key">$label</label>
|
||||||
|
$controls
|
||||||
|
<span class="hint" py:for="hint in hints">$hint</span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend >Tags</legend>
|
||||||
|
<div>
|
||||||
|
<input id="tags" type="text" size="70" maxlength="70" name="tags" value="${rendezvous.tags}"/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" value="Reset"/>
|
||||||
|
<py:choose test="">
|
||||||
|
<py:when test="rendezvous.rendezvous_id != 0">
|
||||||
|
<input type="submit" name="edit" value="edit"/>
|
||||||
|
<input type="submit" name="delete" value="delete"/>
|
||||||
|
</py:when>
|
||||||
|
<input py:otherwise="" type="submit" name="add" value="add"/>
|
||||||
|
</py:choose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,2 @@
|
||||||
|
$rendezvous_body_hdr
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
<!DOCTYPE htm
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="layout.html" />
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$("#rdate_begin").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
$("#rdate_end").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
$("#time_begin\:\\S*").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
$("#time_end\:\\S*").datepicker({"dateFormat" : "dd.mm.yy"});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<title>Votes</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<div id='rendezvous-main'>
|
||||||
|
<form py:with="canDelete = 'RENDEZVOUS_VOTE_DELETE' in perm; canModify = 'RENDEZVOUS_VOTE_MODIFY' in perm" py:if="votes" class='votes' method="post" action="">
|
||||||
|
<fieldset>
|
||||||
|
<py:choose test="">
|
||||||
|
<legend py:when="'RENDEZVOUS_VOTE_VIEW_OTHERS' in perm">All votes for ${rdate.time_begin.strftime('%x')}</legend>
|
||||||
|
<legend py:otherwise="">Votes for ${rdate.time_begin.strftime('%x')} made by $authname</legend>
|
||||||
|
</py:choose>
|
||||||
|
<table class="listing">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<py:if test="canModify">
|
||||||
|
<th>user</th>
|
||||||
|
<th>email</th>
|
||||||
|
<th>begin</th>
|
||||||
|
<th>end</th>
|
||||||
|
</py:if>
|
||||||
|
<th py:if="canDelete">delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody py:choose="">
|
||||||
|
<tr py:when="canModify and canDelete" py:for="vote in votes" py:with="dt = selected_tz.fromutc(vote.time_begin);dt2 = selected_tz.fromutc(vote.time_end)">
|
||||||
|
<td>${vote.user}</td>
|
||||||
|
<td><input type="text" size="40" name="email:${vote.vote_id}" value="${vote.email}"/></td>
|
||||||
|
<td><input type="text" size="8" id="time_begin:${vote.vote_id}" name="date_begin:${vote.vote_id}" value="${dt.strftime('%d.%m.%Y')}" />
|
||||||
|
<input type="text" size="5" name="time_begin:${vote.vote_id}" value="${dt.strftime('%H:%M')}"/> ${dt.tzinfo.tzname(None)}</td>
|
||||||
|
<td><input type="text" size="8" id="time_end:${vote.vote_id}" name="date_end:${vote.vote_id}" value="${dt2.strftime('%d.%m.%Y')}" />
|
||||||
|
<input type="text" size="5" name="time_end:${vote.vote_id}" value="${dt2.strftime('%H:%M')}"/> ${dt2.tzinfo.tzname(None)}</td>
|
||||||
|
<td><input type="checkbox" name="delete" value="${vote.vote_id}" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr py:when="not canModify and canDelete" py:for="vote in votes" py:with="dt = selected_tz.fromutc(vote.time_begin);dt2 = selected_tz.fromutc(vote.time_end)">
|
||||||
|
<td>${vote.user}</td>
|
||||||
|
<td>${vote.mail}</td>
|
||||||
|
<td>${dt.strftime('%d.%m.%Y')}
|
||||||
|
${dt.strftime('%H:%M')} ${dt.tzinfo.tzname(None)}</td>
|
||||||
|
<td>${dt2.strftime('%d.%m.%Y')}
|
||||||
|
${dt2.strftime('%H:%M')} ${dt2.tzinfo.tzname(None)}</td>
|
||||||
|
<td><input type="checkbox" name="delete" value="${vote.vote_id}"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr py:when="canModify and not canDelete" py:for="vote in votes" py:with="dt = selected_tz.fromutc(vote.time_begin);dt2 = selected_tz.fromutc(vote.time_end)">
|
||||||
|
<td><input type="text" size="40" name="email:${vote.vote_id}" value="${vote.email}" /></td>
|
||||||
|
<td><input type="text" size="8" id="time_begin:${vote.vote_id}" name="date_begin:${vote.vote_id}" value="${dt.strftime('%d.%m.%Y')}" />
|
||||||
|
<input type="text" size="5" name="time_begin:${vote.vote_id}" value="${dt.strftime('%H:%M')}"/> ${dt.tzinfo.tzname(None)}</td>
|
||||||
|
<td><input type="text" size="8" id="time_end:${vote.vote_id}" name="date_end:${vote.vote_id}" value="${dt2.strftime('%d.%m.%Y')}" />
|
||||||
|
<input type="text" size="5" name="time_end:${vote.vote_id}" value="${dt2.strftime('%H:%M')}"/> ${dt2.tzinfo.tzname(None)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr py:otherwise="" py:for="vote in votes">
|
||||||
|
<td>${vote.user}</td>
|
||||||
|
<td>${vote.time_begin.strftime('%d.%m.%Y')}</td>
|
||||||
|
<td>${dt.strftime('%H:%M')} ${dt.tzinfo.tzname(None)}</td>
|
||||||
|
<td>${dt2.strftime('%d.%m.%Y')}</td>
|
||||||
|
<td>${dt2.strftime('%H:%M')} ${dt2.tzinfo.tzname(None)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="reset" name="reset"/>
|
||||||
|
<input type="submit" name="savevotes" value="Save Changes"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="help">Change or delete votes for an existing rendezvous date.</p>
|
||||||
|
<form py:if="'RENDEZVOUS_VOTE_ADD' in perm" action="" method="post" mime-type="text/plain">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Add new vote for ${rdate.time_begin.strftime("%x")}:</legend>
|
||||||
|
<table>
|
||||||
|
<tr class="field">
|
||||||
|
<th><label for="dv_user">User:</label></th>
|
||||||
|
<td><input id="dv_user" type="text" name="user" value="${authname}"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="field">
|
||||||
|
<th><label for="dv_email">Email:</label></th>
|
||||||
|
<td><input id="dv_email" type="text" name="email" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="field" py:with="dt = selected_tz.fromutc(rdate.time_begin)">
|
||||||
|
<th><label for="dv_date_begin">Date Begin:</label></th>
|
||||||
|
<td><input id="rdate_begin" type="text" name="date_begin" value="${dt.strftime('%d.%m.%Y')}"/>
|
||||||
|
<input type="text" name="time_begin" value="${dt.strftime('%H:%M')}"/> ${dt.tzinfo.tzname(None)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="field" py:with="dt = selected_tz.fromutc(rdate.time_end)">
|
||||||
|
<th><label for="dv_date_end">Date End:</label></th>
|
||||||
|
<td><input id="rdate_end" type="text" name="date_end" value="${dt.strftime('%d.%m.%Y')}"/>
|
||||||
|
<input type="text" name="time_end" value="${dt.strftime('%H:%M')}"/> ${dt.tzinfo.tzname(None)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="mybuttons">
|
||||||
|
<input type="submit" name="add" value="Add" />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<p class="help">Add a vote to an existing rendezvous date.</p>
|
||||||
|
</div>
|
||||||
|
<h3>Allowed date/time formats:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>time:
|
||||||
|
<ul>
|
||||||
|
<li>'hhMM'</li>
|
||||||
|
<li>'hh:MM'</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>date:
|
||||||
|
<ul>
|
||||||
|
<li>'yyyymmdd'</li>
|
||||||
|
<li>'yyyy.mm.dd'</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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
|