source: indico/indico/web/http_api/export.py @ 5dc223

hello-world-walkthroughipv6v0.98-seriesv0.98.2v0.98.3v0.98b2v0.99v1.0v1.1
Last change on this file since 5dc223 was 5dc223, checked in by Jose Benito <jose.benito.gonzalez@…>, 21 months ago

[FIX] event occurences without from/to date

  • Property mode set to 100644
File size: 16.6 KB
Line 
1# -*- coding: utf-8 -*-
2##
3##
4## This file is part of CDS Indico.
5## Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 CERN.
6##
7## CDS Indico is free software; you can redistribute it and/or
8## modify it under the terms of the GNU General Public License as
9## published by the Free Software Foundation; either version 2 of the
10## License, or (at your option) any later version.
11##
12## CDS Indico is distributed in the hope that it will be useful, but
13## WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15## General Public License for more details.
16##
17## You should have received a copy of the GNU General Public License
18## along with CDS Indico; if not, write to the Free Software Foundation, Inc.,
19## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20
21"""
22Main export interface
23"""
24
25# python stdlib imports
26import fnmatch
27import itertools
28import pytz
29import re
30from zope.interface import Interface, implements
31from datetime import datetime, timedelta, date, time
32
33# external lib imports
34from simplejson import dumps
35
36# indico imports
37from indico.util.date_time import nowutc
38from indico.util.fossilize import fossilize
39
40from indico.util.metadata import Serializer
41from indico.web.http_api.html import HTML4Serializer
42from indico.web.http_api.jsonp import JSONPSerializer
43from indico.web.http_api.ical import ICalSerializer
44from indico.web.http_api.atom import AtomSerializer
45from indico.web.http_api.fossils import IConferenceMetadataFossil,\
46    IConferenceMetadataWithContribsFossil, IConferenceMetadataWithSubContribsFossil,\
47    IConferenceMetadataWithSessionsFossil, IPeriodFossil
48from indico.web.http_api.responses import HTTPAPIError
49from indico.web.wsgi import webinterface_handler_config as apache
50
51# indico legacy imports
52from MaKaC.common.indexes import IndexesHolder
53from MaKaC.common.info import HelperMaKaCInfo
54from MaKaC.conference import ConferenceHolder
55from MaKaC.plugins.base import PluginsHolder
56from MaKaC.rb_tools import Period, datespan
57
58from indico.web.http_api.util import get_query_parameter, remove_lists
59
60utc = pytz.timezone('UTC')
61MAX_DATETIME = utc.localize(datetime(2099, 12, 31, 23, 59, 0))
62MIN_DATETIME = utc.localize(datetime(2000, 1, 1))
63
64class ArgumentParseError(Exception):
65    pass
66
67
68class ArgumentValueError(Exception):
69    pass
70
71
72class LimitExceededException(Exception):
73    pass
74
75
76class Exporter(object):
77    EXPORTER_LIST = []
78    TYPES = None # abstract
79    RE = None # abstract
80    DEFAULT_DETAIL = None # abstract
81    MAX_RECORDS = None # abstract
82    SERIALIZER_TYPE_MAP = {}
83    VALID_FORMATS = None # None = all formats
84    GUEST_ALLOWED = True
85
86    @classmethod
87    def parseRequest(cls, path, qdata):
88        """Parse a request path and return an exporter and the requested data type."""
89        exporters = itertools.chain(cls.EXPORTER_LIST, cls._getPluginExporters())
90        for expCls in exporters:
91            m = expCls._matchPath(path)
92            if m:
93                gd = m.groupdict()
94                g = m.groups()
95                type = g[0]
96                format = g[-1]
97                if format not in ExportInterface.getAllowedFormats():
98                    return None, None
99                elif expCls.VALID_FORMATS and format not in expCls.VALID_FORMATS:
100                    return None, None
101                return expCls(qdata, type, gd), format
102        return None, None
103
104    @staticmethod
105    def register(cls):
106        """Register an exporter that is not part of a plugin.
107
108        To use it, simply decorate the exporter class with this method."""
109        assert cls.RE is not None
110        Exporter.EXPORTER_LIST.append(cls)
111        return cls
112
113    @classmethod
114    def _matchPath(cls, path):
115        if not hasattr(cls, '_RE'):
116            types = '|'.join(cls.TYPES)
117            cls._RE = re.compile(r'/export/(' + types + r')/' + cls.RE + r'\.(\w+)$')
118        return cls._RE.match(path)
119
120    @classmethod
121    def _getPluginExporters(cls):
122        for plugin in PluginsHolder().getPluginTypes():
123            for expClsName in plugin.getExporterList():
124                yield getattr(plugin.getModule().export, expClsName)
125
126    def __init__(self, qdata, type, urlParams):
127        self._qdata = qdata
128        self._type = type
129        self._urlParams = urlParams
130
131    def _getParams(self):
132        self._offset = get_query_parameter(self._qdata, ['O', 'offset'], 0, integer=True)
133        self._orderBy = get_query_parameter(self._qdata, ['o', 'order'], 'start')
134        self._descending = get_query_parameter(self._qdata, ['c', 'descending'], False)
135        self._detail = get_query_parameter(self._qdata, ['d', 'detail'], self.DEFAULT_DETAIL)
136        tzName = get_query_parameter(self._qdata, ['tz'], None)
137
138        info = HelperMaKaCInfo.getMaKaCInfoInstance()
139        self._serverTZ = info.getTimezone()
140
141        if tzName is None:
142            tzName = self._serverTZ
143        try:
144            self._tz = pytz.timezone(tzName)
145        except pytz.UnknownTimeZoneError, e:
146            raise HTTPAPIError("Bad timezone: '%s'" % e.message, apache.HTTP_BAD_REQUEST)
147        max = self.MAX_RECORDS.get(self._detail, 1000)
148        self._userLimit = get_query_parameter(self._qdata, ['n', 'limit'], 0, integer=True)
149        if self._userLimit > max:
150            raise HTTPAPIError("You can only request up to %d records per request with the detail level '%s'" %
151                (max, self._detail), apache.HTTP_BAD_REQUEST)
152        self._limit = self._userLimit if self._userLimit > 0 else max
153
154    def _hasAccess(self, aw):
155        return True
156
157    def __call__(self, aw):
158        """Perform the actual exporting"""
159        self._getParams()
160        if not self.GUEST_ALLOWED and not aw.getUser():
161            raise HTTPAPIError('Guest access to this exporter is forbidden.', apache.HTTP_FORBIDDEN)
162        if not self._hasAccess(aw):
163            raise HTTPAPIError('Access to this exporter is restricted.', apache.HTTP_FORBIDDEN)
164        resultList = []
165        complete = True
166
167        func = getattr(self, 'export_' + self._type, None)
168        if not func:
169            raise NotImplementedError('export_' + self._type)
170
171        try:
172            for obj in func(aw):
173                resultList.append(obj)
174        except LimitExceededException:
175            complete = (self._limit == self._userLimit)
176
177        return resultList, complete, self.SERIALIZER_TYPE_MAP
178
179
180class ExportInterface(object):
181    DETAIL_INTERFACES = {}
182
183    _deltas =  {'yesterday': timedelta(-1),
184                'tomorrow': timedelta(1)}
185
186    _sortingKeys = {'id': lambda x: x.getId(),
187                    'end': lambda x: x.getEndDate(),
188                    'title': lambda x: x.getTitle()}
189
190    def __init__(self, aw, exporter):
191        self._aw = aw
192        self._tz = exporter._tz
193        self._serverTZ = exporter._serverTZ
194        self._offset = exporter._offset
195        self._limit = exporter._limit
196        self._detail = exporter._detail
197        self._orderBy = exporter._orderBy
198        self._descending = exporter._descending
199
200    @classmethod
201    def getAllowedFormats(cls):
202        return Serializer.getAllFormats()
203
204    @classmethod
205    def _parseDateTime(cls, dateTime, allowNegativeOffset):
206        """
207        Accepted formats:
208         * ISO 8601 subset - YYYY-MM-DD[THH:MM]
209         * 'today', 'yesterday', 'tomorrow' and 'now'
210         * days in the future/past: '[+/-]DdHHhMMm'
211
212         'ctx' means that the date will change according to its function
213         ('from' or 'to')
214        """
215
216        # if it's a an "alias", return immediately
217        now = nowutc()
218        if dateTime in cls._deltas:
219            return ('ctx', now + cls._deltas[dateTime])
220        elif dateTime == 'now':
221            return ('abs', now)
222        elif dateTime == 'today':
223            return ('ctx', now)
224
225        m = re.match(r'^([+-])?(?:(\d{1,3})d)?(?:(\d{1,2})h)?(?:(\d{1,2})m)?$', dateTime)
226        if m:
227            mod = -1 if m.group(1) == '-' else 1
228            if not allowNegativeOffset and mod == -1:
229                raise ArgumentParseError('End date cannot be a negative offset')
230
231            atoms = list(0 if a == None else int(a) * mod for a in m.groups()[1:])
232            if atoms[1] > 23  or atoms[2] > 59:
233                raise ArgumentParseError("Invalid time!")
234            return ('ctx', timedelta(days=atoms[0], hours=atoms[1], minutes=atoms[2]))
235        else:
236            # iso 8601 subset
237            try:
238                return ('abs', datetime.strptime(dateTime, "%Y-%m-%dT%H:%M"))
239            except ValueError:
240                pass
241            try:
242                return ('ctx', datetime.strptime(dateTime, "%Y-%m-%d"))
243            except ValueError:
244                raise ArgumentParseError("Impossible to parse '%s'" % dateTime)
245
246    @classmethod
247    def _getDateTime(cls, ctx, dateTime, tz, aux=None):
248
249        try:
250            rel, value = cls._parseDateTime(dateTime, ctx=='from')
251        except ArgumentParseError, e:
252            raise HTTPAPIError(e.message, apache.HTTP_BAD_REQUEST)
253
254        if rel == 'abs':
255            return tz.localize(value) if not value.tzinfo else value
256        elif rel == 'ctx' and type(value) == timedelta:
257            if ctx == 'from':
258                value = nowutc() + value
259            else:
260                value = aux + value
261
262        # from here on, 'value' has to be a datetime
263        if ctx == 'from':
264            return tz.localize(value.combine(value.date(), time(0, 0, 0)))
265        else:
266            return tz.localize(value.combine(value.date(), time(23, 59, 59)))
267
268    def _getQueryParams(self, qdata):
269        fromDT = get_query_parameter(qdata, ['f', 'from'])
270        toDT = get_query_parameter(qdata, ['t', 'to'])
271        dayDT = get_query_parameter(qdata, ['day'])
272
273        if (fromDT or toDT) and dayDT:
274            raise HTTPAPIError("'day' can only be used without 'from' and 'to'", apache.HTTP_BAD_REQUEST)
275        elif dayDT:
276            fromDT = toDT = dayDT
277
278        self._fromDT = ExportInterface._getDateTime('from', fromDT, self._tz) if fromDT else None
279        self._toDT = ExportInterface._getDateTime('to', toDT, self._tz, aux=self._fromDT) if toDT else None
280
281    def _limitIterator(self, iterator, limit):
282        counter = 0
283        # this set acts as a checklist to know if a record has already been sent
284        exclude = set()
285        self._intermediateResults = []
286
287        for obj in iterator:
288            if counter >= limit:
289                raise LimitExceededException()
290            if obj not in exclude and (not hasattr(obj, 'canAccess') or obj.canAccess(self._aw)):
291                self._intermediateResults.append(obj)
292                yield obj
293                exclude.add(obj)
294                counter += 1
295
296    def _sortedIterator(self, iterator, limit, orderBy, descending):
297
298        exceeded = False
299        if (orderBy and orderBy != 'start') or descending:
300            sortingKey = self._sortingKeys.get(orderBy)
301            try:
302                limitedIterable = sorted(self._limitIterator(iterator, limit),
303                                         key=sortingKey)
304            except LimitExceededException:
305                exceeded = True
306                limitedIterable = sorted(self._intermediateResults,
307                                         key=sortingKey)
308
309            if descending:
310                limitedIterable.reverse()
311        else:
312            limitedIterable = self._limitIterator(iterator, limit)
313
314        # iterate over result
315        for obj in limitedIterable:
316            yield obj
317
318        # in case the limit was exceeded while sorting the results,
319        # raise the exception as if we were truly consuming an iterator
320        if orderBy and exceeded:
321            raise LimitExceededException()
322
323    def _iterateOver(self, iterator, offset, limit, orderBy, descending, filter=None):
324        """
325        Iterates over a maximum of `limit` elements, starting at the
326        element number `offset`. The elements will be ordered according
327        to `orderby` and `descending` (slooooow) and filtered by the
328        callable `filter`:
329        """
330
331        if filter:
332            iterator = itertools.ifilter(filter, iterator)
333        # offset + limit because offset records are skipped and do not count
334        sortedIterator = self._sortedIterator(iterator, offset + limit, orderBy, descending)
335        # Skip offset elements - http://docs.python.org/library/itertools.html#recipes
336        next(itertools.islice(sortedIterator, offset, offset), None)
337        return sortedIterator
338
339    def _postprocess(self, obj, fossil, iface):
340        return fossil
341
342    def _process(self, iterator, filter=None, iface=None):
343        if iface is None:
344            iface = self.DETAIL_INTERFACES.get(self._detail)
345            if iface is None:
346                raise HTTPAPIError('Invalid detail level: %s' % self._detail, apache.HTTP_BAD_REQUEST)
347        for obj in self._iterateOver(iterator, self._offset, self._limit, self._orderBy, self._descending, filter):
348            yield self._postprocess(obj, fossilize(obj, iface, tz=self._tz, naiveTZ=self._serverTZ), iface)
349
350
351@Exporter.register
352class CategoryEventExporter(Exporter):
353    TYPES = ('event', 'categ')
354    RE = r'(?P<idlist>\w+(?:-\w+)*)'
355    DEFAULT_DETAIL = 'events'
356    MAX_RECORDS = {
357        'events': 1000,
358        'contributions': 500,
359        'subcontributions': 500,
360        'sessions': 100,
361    }
362
363    def _getParams(self):
364        super(CategoryEventExporter, self)._getParams()
365        self._idList = self._urlParams['idlist'].split('-')
366
367    def export_categ(self, aw):
368        expInt = CategoryEventExportInterface(aw, self)
369        return expInt.category(self._idList, self._qdata)
370
371    def export_event(self, aw):
372        expInt = CategoryEventExportInterface(aw, self)
373        return expInt.event(self._idList, self._qdata)
374
375
376class CategoryEventExportInterface(ExportInterface):
377    DETAIL_INTERFACES = {
378        'events': IConferenceMetadataFossil,
379        'contributions': IConferenceMetadataWithContribsFossil,
380        'subcontributions': IConferenceMetadataWithSubContribsFossil,
381        'sessions': IConferenceMetadataWithSessionsFossil
382    }
383
384    def _getQueryParams(self, qdata):
385        super(CategoryEventExportInterface, self)._getQueryParams(qdata)
386        self._occurrences = get_query_parameter(qdata, ['occ', 'occurrences'], 'no') == 'yes'
387
388    def _postprocess(self, obj, fossil, iface):
389        return self._addOccurrences(fossil, obj, self._fromDT, self._toDT)
390
391    @staticmethod
392    def _eventDaysIterator(conf):
393        """
394        Iterates over the daily times of an event
395        """
396        sched = conf.getSchedule()
397        for day in datespan(conf.getStartDate(), conf.getEndDate()):
398            startDT = sched.calculateDayStartDate(day)
399            endDT = sched.calculateDayEndDate(day)
400            if startDT != endDT:
401                yield Period(startDT, endDT)
402
403    def _addOccurrences(self, fossil, obj, startDT, endDT):
404        if self._occurrences:
405            (startDT, endDT) = (startDT or MIN_DATETIME,
406                                endDT or MAX_DATETIME)
407            # get occurrences in the date interval
408            fossil['occurrences'] = fossilize(itertools.ifilter(
409                lambda x: x.startDT >= startDT and x.endDT <= endDT, self._eventDaysIterator(obj)),
410                                             {Period: IPeriodFossil}, tz=self._tz, naiveTZ=self._serverTZ)
411
412        return fossil
413
414    def category(self, idlist, qdata):
415        self._getQueryParams(qdata)
416        location = get_query_parameter(qdata, ['l', 'location'])
417        room = get_query_parameter(qdata, ['r', 'room'])
418
419        idx = IndexesHolder().getById('categoryDate')
420
421        filter = None
422        if room or location:
423            def filter(obj):
424                if location:
425                    name = obj.getLocation() and obj.getLocation().getName()
426                    if not name or not fnmatch.fnmatch(name.lower(), location.lower()):
427                        return False
428                if room:
429                    name = obj.getRoom() and obj.getRoom().getName()
430                    if not name or not fnmatch.fnmatch(name.lower(), room.lower()):
431                        return False
432                return True
433
434        for catId in idlist:
435            for obj in self._process(idx.iterateObjectsIn(catId, self._fromDT, self._toDT), filter, IConferenceMetadataFossil):
436                yield obj
437
438    def event(self, idlist, qdata):
439        self._getQueryParams(qdata)
440        ch = ConferenceHolder()
441
442        def _iterate_objs(objIds):
443            for objId in objIds:
444                obj = ch.getById(objId, True)
445                if obj is not None:
446                    yield obj
447
448        return self._process(_iterate_objs(idlist))
449
450Serializer.register('html', HTML4Serializer)
451Serializer.register('jsonp', JSONPSerializer)
452Serializer.register('ics', ICalSerializer)
453Serializer.register('atom', AtomSerializer)
Note: See TracBrowser for help on using the repository browser.