# rfc2445 VTODO list from tracker issues import re from roundup.cgi import templating from roundup.cgi.actions import Action class Calendar(Action): def unauthorized(self): """Return 403 response - the user is not allowed to view the context Note: exceptions.Unauthorised would result in HTTP response 200 (Ok) with error message shown on HTML page. """ self.client.header({'Content-Type':'text/plain'}, 403) return self._('You are not allowed to view %s%s calendar') % ( self.classname, self.nodeid or '') def property(self, context, path): """Traverse property path; return HTMLItem, HTMLProperty or None""" _names = path.split('/', 1) _propname = _names[0] try: # try list index try: # may raise ValueError _propnum = int(_propname) _prop = tuple(context)[_propnum] except ValueError: _prop = context[_propname] except (IndexError, KeyError): return None # 'id' property is not wrapped by HTMLClass if _propname == 'id': # (client, classname, nodeid, prop, name, value, anonymous=0) _prop = templating.NumberHTMLProperty(self.client, context._classname, _prop, context._props[_propname], _propname, _prop, context._anonymous) if len(_names) > 1: return self.property(_prop, _names[1]) else: return _prop def handle(self): _context = self.context['context'] # check if the context is accessible if not (_context and _context.is_view_ok()): return self.unauthorized() # if we are viewing a class (no nodeid), apply filter # otherwise compose a list of single item if self.nodeid: _lst = [_context] else: _lst = _context.filter(self.context['request']) _result = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Roundup Calendar//NONSGML v1.0//EN' ] for _item in _lst: if not _item.is_view_ok(): continue _result.extend(('BEGIN:VTODO', 'CATEGORIES:Issues')) for (_name, _property, _handler) in self.PROPERTIES: _value = self.property(_item, _property) if not (_value and _value.is_view_ok()): continue _value = _handler(self, _name, _value) if _value: _result.append(_value) _result.append('END:VTODO') _result.append('END:VCALENDAR') self.client.header({'Content-Type': 'text/calendar', 'Cache-Control': 'no-cache'}, 200) return '\r\n'.join(_result) ### content handlers def datetime(self, name, value): """Format date-time (UTC)""" return ':'.join((name, value.pretty('%Y%m%dT%H%M%SZ'))) PRIORITIES = { # Map Roundup priorities (1-5) to rfc2445 (1-9) '1': 1, '2': 3, '3': 5, '4': 7, '5': 9, } def priority(self, name, value): """Map Roundup priorities (1-5) to rfc2445 (1-9)""" # rfc2445: A value of zero specifies an undefined priority. return '%s:%i' % (name, self.PRIORITIES.get(value._value, 0)) _re_control_char = re.compile(r'[^\r\n -\xFF]') _re_newline = re.compile(r'\r\n|\r|\n') _re_escaped = re.compile(r'([\\;,])') def text(self, name, value, width=75): """Escape and fold text strings""" text = value._value if self._re_control_char.search(text): # cannot be sent as text # FIXME!!! return base64 encoding #return ':'.join((name, text)) text = self._re_control_char.sub(' ', text) # escape backslash, semicol and comma text = self._re_escaped.sub(r'\\\1', text) # convert newlines text = self._re_newline.sub(r'\\n', text) # build final string text = ':'.join((name, text)) lines = [] while len(text) > width: lines.append(text[:width]) text = ' ' + text[width:] lines.append(text) return '\r\n'.join(lines) def contexturl(self, name, value): """Produce direct URL for an issue. Class is taken from request context; value is item id. """ return '%s:%s%s%s' % (name, self.base, self.classname, value._value) def messages(self, name, value): """Build an rfc2445 comments from issue messages""" return '\r\n'.join([self.text(name, msg.content) # Note: the first message already shown in DESCRIPTION for msg in tuple(value)[1:]]) def files(self, name, value, klass='file'): """Build an rfc2445 attachments from issue files""" return '\r\n'.join(['%s:%s%s%s' % (name, self.base, klass, item.id) for item in value]) ### attribute mapping PROPERTIES = ( ('DTSTAMP', 'creation', datetime), ('LAST-MODIFIED', 'activity', datetime), ('PRIORITY', 'priority', priority), ('SUMMARY', 'title', text), ('URL', 'id', contexturl), ('ORGANIZER', 'assignedto/address', text), ('DESCRIPTION', 'messages/0/content', text), ('COMMENT', 'messages', messages), ('ATTACH', 'files', files), # eststart and estend are estimated start and end timestamps, # not in Roundup classic schema #('DTSTART', 'eststart', datetime), #('DUE', 'estend', datetime), ) def init(instance): instance.registerAction('calendar', Calendar) # vim: set et sts=4 sw=4 :