source: weekplanplugin/trunk/weekplan/core.py

Last change on this file was 18468, checked in by lucid, 20 months ago

WeekPlanPlugin: Bump version to 1.5: ical support
(see #14110)

File size: 10.6 KB
Line 
1# -*- coding: utf-8 -*-
2
3import datetime
4from json.encoder import JSONEncoder
5import pkg_resources
6import re
7try:
8    from StringIO import StringIO # Python 2
9except ImportError:
10    from io import StringIO # Python 3
11
12
13from trac import __version__
14from trac.db.api import DatabaseManager
15from trac.config import OrderedExtensionsOption, ExtensionOption
16from trac.core import *
17from trac.env import IEnvironmentSetupParticipant
18from trac.perm import IPermissionRequestor
19from trac.web import IRequestHandler, RequestDone
20from trac.web.chrome import ITemplateProvider, web_context
21from trac.util.datefmt import format_date, format_datetime, parse_date, utc
22from trac.util.text import CRLF
23
24from weekplan.model import SCHEMA, WeekPlanEvent
25
26
27PLUGIN_NAME = 'WeekPlanPlugin'
28PLUGIN_VERSION = 2
29
30
31class IWeekPlanEventProvider(Interface):
32   
33    def get_plan_prefixes():
34        """Get all plan id prefixes handled by this provider.
35       
36        :return: a list of plan id prefix strings.
37        """
38   
39    def get_events(req, plans_ids, range):
40        """Get all events for the given plans that the provider handles.
41
42        :param req: the request object.
43        :param plan_ids: a list of plan id's for which events should be returned.
44        :param range: a tuple (datetime, datetime) or None to get all events.
45       
46        :return: a list of  WeekPlanEvent items.
47        """
48
49    def add_event(req, event):
50        """Add a new event. Optional method.
51       
52        :param req: the request object.
53        :param event: a new WeekPlanEvent to be added.
54        """
55
56    def update_event(req, event):
57        """Update an event. Optional method.
58       
59        :param req: the request object.
60        :param event: a WeekPlanEvent to be updated.
61        """
62
63    def delete_event(req, event):
64        """Delete an event. Optional method.
65       
66        :param req: the request object.
67        :param event: a WeekPlanEvent to be deleted.
68        """
69
70
71class DBWeekPlanEventProvider(Component):
72    """Provides events from the dedicated DB table."""
73   
74    implements(IWeekPlanEventProvider)
75   
76    def get_plan_prefixes(self):
77        return ['db:']
78   
79    def get_events(self, req, plan_ids, range):
80        if range:
81            return WeekPlanEvent.select_by_plans_and_time(self.env, plan_ids, range[0], range[1])
82        elif len(plan_ids) == 1:
83            return WeekPlanEvent.select_by_plan(self.env, plan_ids[0])
84        elif db_plan_ids:
85            return WeekPlanEvent.select_by_plans(self.env, plan_ids)
86        else:
87            return []
88
89    def add_event(self, req, event):
90        WeekPlanEvent.add(self.env, event)
91
92    def update_event(self, req, event):
93        WeekPlanEvent.update(self.env, event)
94
95    def delete_event(self, req, event):
96        WeekPlanEvent.delete_by_id(self.env, event.id)
97
98
99class WeekPlanModule(Component):
100    """Week-by-week plans."""
101
102    implements(IPermissionRequestor, IRequestHandler, IEnvironmentSetupParticipant, ITemplateProvider)
103
104    event_providers = OrderedExtensionsOption('weekplan', 'event_providers',
105        IWeekPlanEventProvider,
106        'DBWeekPlanEventProvider',
107        True,
108        """List of components implementing `IWeekPlanEventProvider`.""")
109
110    default_event_provider = ExtensionOption('weekplan', 'default_event_provider',
111        IWeekPlanEventProvider,
112        'DBWeekPlanEventProvider',
113        """Default component handling plans if no prefix matches.""")
114
115
116    def get_event_provider(self, plan_id):
117        for provider in self.event_providers:
118            for prefix in provider.get_plan_prefixes():
119                if plan_id.startswith(prefix):
120                    return provider
121        return self.default_event_provider
122
123    def get_events(self, req, plan_ids, range=None):
124        events = []
125        remaining_ids = set(plan_ids)
126        for provider in self.event_providers:
127            prefixes = provider.get_plan_prefixes()
128            ids = [id for id in remaining_ids
129                      for prefix in prefixes
130                      if id.startswith(prefix)]
131            remaining_ids -= set(ids)
132            if ids:
133                events.extend(provider.get_events(req, ids, range))
134        if remaining_ids:
135            provider = self.default_event_provider
136            ids = list(remaining_ids)
137            events.extend(provider.get_events(req, ids, range))
138        return events
139
140    def can_plan_add_event(self, plan_id):
141        return hasattr(self.get_event_provider(plan_id), 'add_event')
142
143    def can_plan_update_event(self, plan_id):
144        return hasattr(self.get_event_provider(plan_id), 'update_event')
145
146    def can_plan_delete_event(self, plan_id):
147        return hasattr(self.get_event_provider(plan_id), 'delete_event')
148
149    # IPermissionRequestor methods
150   
151    def get_permission_actions(self):
152        return ['WEEK_PLAN']
153
154    # IRequestHandler methods
155
156    MATCH_REQUEST_RE = re.compile(r'^/weekplan(:/(.+))?$')
157
158    def match_request(self, req):
159        match = self.MATCH_REQUEST_RE.match(req.path_info)
160        if match:
161            if match.group(1):
162                req.args['plan'] = match.group(1)
163            return True
164
165    def process_request(self, req):
166        req.perm.require('WEEK_PLAN')
167
168        action = req.args.get('action')
169
170        if 'POST' == req.method:
171            if action == 'add_event':
172                event = self._parse_event(req)
173                provider = self.get_event_provider(event.plan)
174                provider.add_event(req, event)
175                self._send_event(req, event)
176
177            elif action == 'update_event':
178                event = self._parse_event(req)
179                provider = self.get_event_provider(event.plan)
180                provider.update_event(req, event)
181                self._send_event(req, event)
182
183            elif action == 'delete_event':
184                event = self._parse_event(req)
185                provider = self.get_event_provider(event.plan)
186                provider.delete_event(req, event)
187                req.send_no_content()
188
189        elif 'GET' == req.method:
190            format = req.args.get('format')
191            if format == 'ics':
192                plan_id = req.args.get('plan')
193                events = self.get_events(req, [plan_id])
194                self._render_ics(req, plan_id, events)
195            elif format == 'json':
196                plan_ids = req.args.get('plan', '').split('|')
197                start = parse_date(req.args.get('start'), utc)
198                end = parse_date(req.args.get('end'), utc)
199                events = self.get_events(req, plan_ids, (start, end))
200                self._send_events(req, events)
201        raise TracError()
202
203    def _parse_event(self, req):
204        return WeekPlanEvent(
205            req.args.get('id'),
206            req.args.get('plan'),
207            req.args.get('title'),
208            parse_date(req.args.get('start'), utc),
209            parse_date(req.args.get('end'), utc))
210
211    def _send_event(self, req, event):
212        context = web_context(req, 'weekplan')
213        self._send_json(req, event.serialized(self.env, context))
214       
215    def _send_events(self, req, events):
216        context = web_context(req, 'weekplan')
217        self._send_json(req, [e.serialized(self.env, context) for e in events])
218
219    def _send_json(self, req, data):
220        content = JSONEncoder().encode(data).encode('utf-8')
221        req.send(content, 'application/json')
222
223    # Derived from http://trac.edgewall.org/browser/tags/trac-1.1.1/trac/ticket/roadmap.py?marks=466-580#L466
224    def _render_ics(self, req, plan_id, events):
225        req.send_response(200)
226        req.send_header('Content-Type', 'text/calendar;charset=utf-8')
227        buf = StringIO()
228
229        def escape_value(text):
230            s = ''.join(map(lambda c: '\\' + c if c in ';,\\' else c, text))
231            return '\\n'.join(re.split(r'[\r\n]+', s))
232
233        def write_prop(name, value, params={}):
234            text = ';'.join([name] + [k + '=' + v for k, v in params.items()]) \
235                 + ':' + escape_value(value)
236            firstline = 1
237            while text:
238                if not firstline:
239                    text = ' ' + text
240                else:
241                    firstline = 0
242                buf.write(text[:75] + CRLF)
243                text = text[75:]
244
245        def write_date(name, value, params={}):
246            params['VALUE'] = 'DATE'
247            write_prop(name, format_date(value, '%Y%m%d', req.tz), params)
248
249        def write_utctime(name, value, params={}):
250            write_prop(name, format_datetime(value, '%Y%m%dT%H%M%SZ', utc),
251                       params)
252
253        host = req.base_url[req.base_url.find('://') + 3:]
254        user = req.args.get('user', 'anonymous')
255
256        write_prop('BEGIN', 'VCALENDAR')
257        write_prop('VERSION', '2.0')
258        write_prop('PRODID', '-//Edgewall Software//NONSGML Trac %s//EN'
259                   % __version__)
260        write_prop('METHOD', 'PUBLISH')
261        write_prop('X-WR-CALNAME',
262                   self.env.project_name + ' - WeekPlan ' + plan_id)
263        write_prop('X-WR-CALDESC', self.env.project_description)
264        write_prop('X-WR-TIMEZONE', str(req.tz))
265
266        for event in events:
267            uid = '<%s/%s/%s@%s>' % (req.base_path, plan_id, event.id, host)
268
269            write_prop('BEGIN', 'VEVENT')
270            write_prop('UID', uid)
271            write_utctime('DTSTAMP', datetime.datetime.now())
272            write_date('DTSTART', event.start)
273            write_date('DTEND', event.end)
274            write_prop('SUMMARY', event.title)
275            # write_prop('URL', req.abs_href.weekplan(plan_id, event.id))
276            # write_prop('DESCRIPTION', event.description)
277            write_prop('END', 'VEVENT')
278
279        write_prop('END', 'VCALENDAR')
280
281        ics_str = buf.getvalue().encode('utf-8')
282        req.send_header('Content-Length', len(ics_str))
283        req.end_headers()
284        req.write(ics_str)
285        raise RequestDone
286
287    # IEnvironmentSetupParticipant
288
289    def environment_created(self):
290        dbm = DatabaseManager(self.env)
291        dbm.create_tables(SCHEMA)
292        dbm.set_database_version(PLUGIN_VERSION, PLUGIN_NAME)
293
294    def environment_needs_upgrade(self):
295        dbm = DatabaseManager(self.env)
296        return dbm.needs_upgrade(PLUGIN_VERSION, PLUGIN_NAME)
297
298    def upgrade_environment(self):
299        dbm = DatabaseManager(self.env)
300        if dbm.get_database_version(PLUGIN_NAME) == 0:
301            dbm.create_tables(SCHEMA)
302            dbm.set_database_version(PLUGIN_VERSION, PLUGIN_NAME)
303        else:
304            dbm.upgrade(PLUGIN_VERSION, PLUGIN_NAME, 'weekplan.upgrades')
305
306    # ITemplateProvider methods
307   
308    def get_htdocs_dirs(self):
309        return [('weekplan', pkg_resources.resource_filename('weekplan', 'htdocs'))]
310
311    def get_templates_dirs(self):
312        return []
Note: See TracBrowser for help on using the repository browser.