1001 lines
45 KiB
Python
1001 lines
45 KiB
Python
# -*- 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
|
|
|
|
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.utcfromtimestamp(ts), 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 = utc.localize(datetime.utcnow())
|
|
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 = utc.localize(datetime(int(year), int(month), int(day), 17, tzinfo=selected_tz))
|
|
except ValueError:
|
|
new_date = date_now
|
|
event = Event(myenv, 0, "", req.authname, date_now, date_now, new_date, selected_tz.normalize(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)
|
|
print "time_begin", event.time_begin, req.args["time_begin"]
|
|
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)
|
|
print "time_end", event.time_end, req.args["time_end"]
|
|
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 = utc.localize(datetime.utcnow())
|
|
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)
|
|
|
|
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 = utc.localize(datetime.utcfromtimestamp(time_begin)).astimezone(selected_tz)
|
|
time_end = utc.localize(datetime.datetime.utcfromtimestamp(time_end)).astimezone(selected_tz)
|
|
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')
|