[IMP] Use 403 code for access denied api error
[cds-indico.git] / indico / web / http_api / api.py
blob47fcca9c04c79dd6bd1eeb82a19178ec67cbfc76
1 # -*- coding: utf-8 -*-
2 ##
3 ##
4 ## This file is part of Indico.
5 ## Copyright (C) 2002 - 2013 European Organization for Nuclear Research (CERN).
6 ##
7 ## 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 3 of the
10 ## License, or (at your option) any later version.
12 ## 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.
17 ## You should have received a copy of the GNU General Public License
18 ## along with Indico;if not, see <http://www.gnu.org/licenses/>.
19 from MaKaC.user import AvatarHolder
21 """
22 Main export interface
23 """
25 # python stdlib imports
26 import fnmatch
27 import itertools
28 import pytz
29 import re
30 import types
31 import urllib
32 from ZODB.POSException import ConflictError
33 from datetime import datetime, timedelta, time
34 from zope.index.text import parsetree
36 # indico imports
37 from indico.util.date_time import nowutc
38 from indico.util.fossilize import fossilize
39 from indico.util.redis import client as redis_client
40 import indico.util.redis.avatar_links as avatar_links
42 from indico.web.http_api.metadata import Serializer
43 from indico.web.http_api.metadata.html import HTML4Serializer
44 from indico.web.http_api.metadata.jsonp import JSONPSerializer
45 from indico.web.http_api.metadata.ical import ICalSerializer
46 from indico.web.http_api.metadata.atom import AtomSerializer
47 from indico.web.http_api.fossils import IConferenceMetadataFossil,\
48 IConferenceMetadataWithContribsFossil, IConferenceMetadataWithSubContribsFossil,\
49 IConferenceMetadataWithSessionsFossil, IPeriodFossil, ICategoryMetadataFossil,\
50 ICategoryProtectedMetadataFossil, ISessionMetadataFossil, ISessionMetadataWithContributionsFossil,\
51 ISessionMetadataWithSubContribsFossil, IContributionMetadataFossil,\
52 IContributionMetadataWithSubContribsFossil, IBasicConferenceMetadataFossil
53 from indico.web.http_api.responses import HTTPAPIError
54 from indico.web.http_api.util import get_query_parameter
55 from indico.web.wsgi import webinterface_handler_config as apache
57 # indico legacy imports
58 from MaKaC.common.db import DBMgr
59 from MaKaC.conference import CategoryManager
60 from MaKaC.common.indexes import IndexesHolder
61 from MaKaC.common.info import HelperMaKaCInfo
62 from MaKaC.conference import ConferenceHolder
63 from MaKaC.plugins.base import PluginsHolder
64 from MaKaC.rb_tools import Period, datespan
65 from MaKaC.schedule import ScheduleToJson
66 from MaKaC.common.logger import Logger
67 from MaKaC.errors import NoReportError
68 from MaKaC.user import AvatarHolder
70 utc = pytz.timezone('UTC')
71 MAX_DATETIME = utc.localize(datetime(2099, 12, 31, 23, 59, 0))
72 MIN_DATETIME = utc.localize(datetime(2000, 1, 1))
75 class ArgumentParseError(Exception):
76 pass
79 class ArgumentValueError(Exception):
80 pass
83 class LimitExceededException(Exception):
84 pass
87 class HTTPAPIHook(object):
88 """This class is the hook between the query (path+params) and the generator of the results (fossil).
89 It is also in charge of checking the parameters and the access rights.
90 """
92 HOOK_LIST = []
93 TYPES = None # abstract
94 PREFIX = 'export' # url prefix. must exist in indico_wsgi_url_parser.py, too! also used as function prefix
95 RE = None # abstract
96 DEFAULT_DETAIL = None # abstract
97 MAX_RECORDS = {}
98 SERIALIZER_TYPE_MAP = {} # maps fossil type names to friendly names (useful for plugins e.g. RoomCERN --> Room)
99 VALID_FORMATS = None # None = all formats
100 GUEST_ALLOWED = True # When False, it forces authentication
101 COMMIT = False # commit database changes
102 HTTP_POST = False # require (and allow) HTTP POST
103 NO_CACHE = False
105 @classmethod
106 def parseRequest(cls, path, queryParams):
107 """Parse a request path and return a hook and the requested data type."""
108 path = urllib.unquote(path)
109 hooks = itertools.chain(cls.HOOK_LIST, cls._getPluginHooks())
110 for expCls in hooks:
111 Logger.get('HTTPAPIHook.parseRequest').debug(expCls)
112 m = expCls._matchPath(path)
113 if m:
114 gd = m.groupdict()
115 g = m.groups()
116 type = g[0]
117 format = g[-1]
118 if format not in DataFetcher.getAllowedFormats():
119 return None, None
120 elif expCls.VALID_FORMATS and format not in expCls.VALID_FORMATS:
121 return None, None
122 return expCls(queryParams, type, gd), format
123 return None, None
125 @staticmethod
126 def register(cls):
127 """Register a hook that is not part of a plugin.
129 To use it, simply decorate the hook class with this method."""
130 assert cls.RE is not None
131 HTTPAPIHook.HOOK_LIST.append(cls)
132 return cls
134 @classmethod
135 def _matchPath(cls, path):
136 if not hasattr(cls, '_RE'):
137 types = '|'.join(cls.TYPES)
138 cls._RE = re.compile(r'/' + cls.PREFIX + '/(' + types + r')/' + cls.RE + r'\.(\w+)$')
139 return cls._RE.match(path)
141 @classmethod
142 def _getPluginHooks(cls):
143 for plugin in PluginsHolder().getPluginTypes():
144 for expClsName in plugin.getHTTPAPIHookList():
145 yield getattr(plugin.getModule().http_api, expClsName)
147 def __init__(self, queryParams, type, pathParams):
148 self._queryParams = queryParams
149 self._type = type
150 self._pathParams = pathParams
152 def _getParams(self):
153 self._offset = get_query_parameter(self._queryParams, ['O', 'offset'], 0, integer=True)
154 self._orderBy = get_query_parameter(self._queryParams, ['o', 'order'])
155 self._descending = get_query_parameter(self._queryParams, ['c', 'descending'], 'no') == 'yes'
156 self._detail = get_query_parameter(self._queryParams, ['d', 'detail'], self.DEFAULT_DETAIL)
157 tzName = get_query_parameter(self._queryParams, ['tz'], None)
159 info = HelperMaKaCInfo.getMaKaCInfoInstance()
160 self._serverTZ = info.getTimezone()
162 if tzName is None:
163 tzName = self._serverTZ
164 try:
165 self._tz = pytz.timezone(tzName)
166 except pytz.UnknownTimeZoneError, e:
167 raise HTTPAPIError("Bad timezone: '%s'" % e.message, apache.HTTP_BAD_REQUEST)
168 max = self.MAX_RECORDS.get(self._detail, 1000)
169 self._userLimit = get_query_parameter(self._queryParams, ['n', 'limit'], 0, integer=True)
170 if self._userLimit > max:
171 raise HTTPAPIError("You can only request up to %d records per request with the detail level '%s'" %
172 (max, self._detail), apache.HTTP_BAD_REQUEST)
173 self._limit = self._userLimit if self._userLimit > 0 else max
175 fromDT = get_query_parameter(self._queryParams, ['f', 'from'])
176 toDT = get_query_parameter(self._queryParams, ['t', 'to'])
177 dayDT = get_query_parameter(self._queryParams, ['day'])
179 if (fromDT or toDT) and dayDT:
180 raise HTTPAPIError("'day' can only be used without 'from' and 'to'", apache.HTTP_BAD_REQUEST)
181 elif dayDT:
182 fromDT = toDT = dayDT
184 self._fromDT = DataFetcher._getDateTime('from', fromDT, self._tz) if fromDT else None
185 self._toDT = DataFetcher._getDateTime('to', toDT, self._tz, aux=self._fromDT) if toDT else None
187 def _hasAccess(self, aw):
188 return True
190 def _getMethodName(self):
191 return self.PREFIX + '_' + self._type
193 def _performCall(self, func, aw):
194 resultList = []
195 complete = True
196 try:
197 res = func(aw)
198 if isinstance(res, types.GeneratorType):
199 for obj in res:
200 resultList.append(obj)
201 else:
202 resultList = res
203 except LimitExceededException:
204 complete = (self._limit == self._userLimit)
205 return resultList, complete
207 def __call__(self, aw, req):
208 """Perform the actual exporting"""
209 if self.HTTP_POST != (req.method == 'POST'):
210 raise HTTPAPIError('This action requires %s' % ('POST' if self.HTTP_POST else 'GET'), apache.HTTP_METHOD_NOT_ALLOWED)
211 self._req = req
212 self._getParams()
213 req = self._req
214 if not self.GUEST_ALLOWED and not aw.getUser():
215 raise HTTPAPIError('Guest access to this resource is forbidden.', apache.HTTP_FORBIDDEN)
216 if not self._hasAccess(aw):
217 raise HTTPAPIError('Access to this resource is restricted.', apache.HTTP_FORBIDDEN)
219 method_name = self._getMethodName()
220 func = getattr(self, method_name, None)
221 if not func:
222 raise NotImplementedError(method_name)
224 if not self.COMMIT:
225 # Just execute the function, we'll never have to repeat it
226 resultList, complete = self._performCall(func, aw)
227 else:
228 # Try it a few times until commit succeeds
229 dbi = DBMgr.getInstance()
230 for _retry in xrange(10):
231 dbi.sync()
232 resultList, complete = self._performCall(func, aw)
233 try:
234 dbi.commit()
235 except ConflictError:
236 pass # retry
237 else:
238 break
239 else:
240 raise HTTPAPIError('An unresolvable database conflict has occured', apache.HTTP_INTERNAL_SERVER_ERROR)
242 extraFunc = getattr(self, method_name + '_extra', None)
243 extra = extraFunc(aw, resultList) if extraFunc else None
244 return resultList, extra, complete, self.SERIALIZER_TYPE_MAP
247 class DataFetcher(object):
249 _deltas = {'yesterday': timedelta(-1),
250 'tomorrow': timedelta(1)}
252 _sortingKeys = {'id': lambda x: x.getId(),
253 'start': lambda x: x.getStartDate(),
254 'end': lambda x: x.getEndDate(),
255 'title': lambda x: x.getTitle()}
257 def __init__(self, aw, hook):
258 self._aw = aw
259 self._hook = hook
261 @classmethod
262 def getAllowedFormats(cls):
263 return Serializer.getAllFormats()
265 @classmethod
266 def _parseDateTime(cls, dateTime, allowNegativeOffset):
268 Accepted formats:
269 * ISO 8601 subset - YYYY-MM-DD[THH:MM]
270 * 'today', 'yesterday', 'tomorrow' and 'now'
271 * days in the future/past: '[+/-]DdHHhMMm'
273 'ctx' means that the date will change according to its function
274 ('from' or 'to')
277 # if it's a an "alias", return immediately
278 now = nowutc()
279 if dateTime in cls._deltas:
280 return ('ctx', now + cls._deltas[dateTime])
281 elif dateTime == 'now':
282 return ('abs', now)
283 elif dateTime == 'today':
284 return ('ctx', now)
286 m = re.match(r'^([+-])?(?:(\d{1,3})d)?(?:(\d{1,2})h)?(?:(\d{1,2})m)?$', dateTime)
287 if m:
288 mod = -1 if m.group(1) == '-' else 1
289 if not allowNegativeOffset and mod == -1:
290 raise ArgumentParseError('End date cannot be a negative offset')
292 atoms = list(0 if a == None else int(a) * mod for a in m.groups()[1:])
293 if atoms[1] > 23 or atoms[2] > 59:
294 raise ArgumentParseError("Invalid time!")
295 return ('ctx', timedelta(days=atoms[0], hours=atoms[1], minutes=atoms[2]))
296 else:
297 # iso 8601 subset
298 try:
299 return ('abs', datetime.strptime(dateTime, "%Y-%m-%dT%H:%M"))
300 except ValueError:
301 pass
302 try:
303 return ('ctx', datetime.strptime(dateTime, "%Y-%m-%d"))
304 except ValueError:
305 raise ArgumentParseError("Impossible to parse '%s'" % dateTime)
307 @classmethod
308 def _getDateTime(cls, ctx, dateTime, tz, aux=None):
310 try:
311 rel, value = cls._parseDateTime(dateTime, ctx=='from')
312 except ArgumentParseError, e:
313 raise HTTPAPIError(e.message, apache.HTTP_BAD_REQUEST)
315 if rel == 'abs':
316 return tz.localize(value) if not value.tzinfo else value
317 elif rel == 'ctx' and type(value) == timedelta:
318 value = nowutc() + value
320 # from here on, 'value' has to be a datetime
321 if ctx == 'from':
322 return tz.localize(value.combine(value.date(), time(0, 0, 0)))
323 else:
324 return tz.localize(value.combine(value.date(), time(23, 59, 59)))
327 class IteratedDataFetcher(DataFetcher):
328 DETAIL_INTERFACES = {}
330 def __init__(self, aw, hook):
331 super(IteratedDataFetcher, self).__init__(aw, hook)
332 self._tz = hook._tz
333 self._serverTZ = hook._serverTZ
334 self._offset = hook._offset
335 self._limit = hook._limit
336 self._detail = hook._detail
337 self._orderBy = hook._orderBy
338 self._descending = hook._descending
339 self._fromDT = hook._fromDT
340 self._toDT = hook._toDT
342 def _userAccessFilter(self, obj):
343 return obj.canAccess(self._aw)
345 def _limitIterator(self, iterator, limit):
346 counter = 0
347 # this set acts as a checklist to know if a record has already been sent
348 exclude = set()
349 self._intermediateResults = []
351 for obj in iterator:
352 if counter >= limit:
353 raise LimitExceededException()
354 if obj not in exclude and (not hasattr(obj, 'canAccess') or obj.canAccess(self._aw)):
355 self._intermediateResults.append(obj)
356 yield obj
357 exclude.add(obj)
358 counter += 1
360 def _sortedIterator(self, iterator, limit, orderBy, descending):
362 exceeded = False
363 if orderBy or descending:
364 sortingKey = self._sortingKeys.get(orderBy)
365 try:
366 limitedIterable = sorted(self._limitIterator(iterator, limit),
367 key=sortingKey, reverse=descending)
368 except LimitExceededException:
369 exceeded = True
370 limitedIterable = sorted(self._intermediateResults,
371 key=sortingKey, reverse=descending)
372 else:
373 limitedIterable = self._limitIterator(iterator, limit)
375 # iterate over result
376 for obj in limitedIterable:
377 yield obj
379 # in case the limit was exceeded while sorting the results,
380 # raise the exception as if we were truly consuming an iterator
381 if orderBy and exceeded:
382 raise LimitExceededException()
384 def _iterateOver(self, iterator, offset, limit, orderBy, descending, filter=None):
386 Iterates over a maximum of `limit` elements, starting at the
387 element number `offset`. The elements will be ordered according
388 to `orderby` and `descending` (slooooow) and filtered by the
389 callable `filter`:
392 if filter:
393 iterator = itertools.ifilter(filter, iterator)
394 # offset + limit because offset records are skipped and do not count
395 sortedIterator = self._sortedIterator(iterator, offset + limit, orderBy, descending)
396 # Skip offset elements - http://docs.python.org/library/itertools.html#recipes
397 next(itertools.islice(sortedIterator, offset, offset), None)
398 return sortedIterator
400 def _postprocess(self, obj, fossil, iface):
401 return fossil
403 def _process(self, iterator, filter=None, iface=None):
404 if iface is None:
405 iface = self.DETAIL_INTERFACES.get(self._detail)
406 if iface is None:
407 raise HTTPAPIError('Invalid detail level: %s' % self._detail, apache.HTTP_BAD_REQUEST)
408 for obj in self._iterateOver(iterator, self._offset, self._limit, self._orderBy, self._descending, filter):
409 yield self._postprocess(obj,
410 fossilize(obj, iface, tz=self._tz, naiveTZ=self._serverTZ,
411 filters={'access': self._userAccessFilter},
412 mapClassType={'AcceptedContribution': 'Contribution'}),
413 iface)
416 @HTTPAPIHook.register
417 class EventTimeTableHook(HTTPAPIHook):
418 TYPES = ('timetable',)
419 RE = r'(?P<idlist>\w+(?:-\w+)*)'
421 def _getParams(self):
422 super(EventTimeTableHook, self)._getParams()
423 self._idList = self._pathParams['idlist'].split('-')
426 def export_timetable(self, aw):
427 ch = ConferenceHolder()
428 d = {}
429 for cid in self._idList:
430 conf = ch.getById(cid)
431 d[cid] = ScheduleToJson.process(conf.getSchedule(), self._tz.tzname(None),
432 aw, days = None, mgmtMode = False)
433 return d
436 @HTTPAPIHook.register
437 class EventSearchHook(HTTPAPIHook):
438 TYPES = ('event',)
439 RE = r'search/(?P<search_term>[^\/]+)'
441 def _getParams(self):
442 super(EventSearchHook, self)._getParams()
443 self._search = self._pathParams['search_term']
446 def export_event(self, aw):
447 ch = ConferenceHolder()
448 index = IndexesHolder().getIndex('conferenceTitle')
449 try:
450 query = ' AND '.join(map(lambda y: "*%s*" % y, filter(lambda x: len(x) > 0, self._search.split(' '))))
451 results = index.search(query)
452 except parsetree.ParseError:
453 results = []
454 d = []
455 for id, v in results:
456 event = ch.getById(id)
457 if event.canAccess(aw):
458 d.append({'id': id, 'title': event.getTitle(), 'startDate': event.getStartDate(), 'hasAnyProtection': event.hasAnyProtection()})
459 return d
462 @HTTPAPIHook.register
463 class UserInfoHook(HTTPAPIHook):
464 TYPES = ('user',)
465 RE = r'(?P<user_id>[\d]+)'
467 def _getParams(self):
468 super(UserInfoHook, self)._getParams()
469 self._user_id = self._pathParams['user_id']
472 def export_user(self, aw):
473 requested_user = AvatarHolder().getById(self._user_id)
474 user = aw.getUser()
475 if not requested_user:
476 raise HTTPAPIError('Requested user not found', apache.HTTP_NOT_FOUND)
477 if user:
478 if requested_user.canUserModify(user):
479 return requested_user.fossilize()
480 raise HTTPAPIError('You do not have access to that info', apache.HTTP_FORBIDDEN)
481 raise HTTPAPIError('You need to be logged in', apache.HTTP_FORBIDDEN)
484 @HTTPAPIHook.register
485 class CategoryEventHook(HTTPAPIHook):
486 TYPES = ('event', 'categ')
487 RE = r'(?P<idlist>\w+(?:-\w+)*)'
488 DEFAULT_DETAIL = 'events'
489 MAX_RECORDS = {
490 'events': 1000,
491 'contributions': 500,
492 'subcontributions': 500,
493 'sessions': 100,
496 def _getParams(self):
497 super(CategoryEventHook, self)._getParams()
498 self._idList = self._pathParams['idlist'].split('-')
499 self._wantFavorites = False
500 if 'favorites' in self._idList:
501 self._idList.remove('favorites')
502 self._wantFavorites = True
503 self._eventType = get_query_parameter(self._queryParams, ['T', 'type'])
504 if self._eventType == 'lecture':
505 self._eventType = 'simple_event'
506 self._occurrences = get_query_parameter(self._queryParams, ['occ', 'occurrences'], 'no') == 'yes'
507 self._location = get_query_parameter(self._queryParams, ['l', 'location'])
508 self._room = get_query_parameter(self._queryParams, ['r', 'room'])
510 def export_categ(self, aw):
511 expInt = CategoryEventFetcher(aw, self)
512 idList = list(self._idList)
513 if self._wantFavorites and aw.getUser():
514 idList += [c.getId() for c in aw.getUser().getLinkTo('category', 'favorite')]
515 return expInt.category(idList)
517 def export_categ_extra(self, aw, resultList):
518 ids = set((event['categoryId'] for event in resultList))
519 return {
520 'eventCategories': CategoryEventFetcher.getCategoryPath(ids, aw)
523 def export_event(self, aw):
524 expInt = CategoryEventFetcher(aw, self)
525 return expInt.event(self._idList)
528 class CategoryEventFetcher(IteratedDataFetcher):
529 DETAIL_INTERFACES = {
530 'events': IConferenceMetadataFossil,
531 'contributions': IConferenceMetadataWithContribsFossil,
532 'subcontributions': IConferenceMetadataWithSubContribsFossil,
533 'sessions': IConferenceMetadataWithSessionsFossil
536 def __init__(self, aw, hook):
537 super(CategoryEventFetcher, self).__init__(aw, hook)
538 self._eventType = hook._eventType
539 self._occurrences = hook._occurrences
540 self._location = hook._location
541 self._room = hook._room
543 def _postprocess(self, obj, fossil, iface):
544 return self._addOccurrences(fossil, obj, self._fromDT, self._toDT)
546 @classmethod
547 def getCategoryPath(cls, idList, aw):
548 res = []
549 for id in idList:
550 res.append({
551 '_type': 'CategoryPath',
552 'categoryId': id,
553 'path': cls._getCategoryPath(id, aw)
555 return res
557 @staticmethod
558 def _getCategoryPath(id, aw):
559 path = []
560 firstCat = cat = CategoryManager().getById(id)
561 visibility = cat.getVisibility()
562 while cat:
563 # the first category (containing the event) is always shown, others only with access
564 iface = ICategoryMetadataFossil if firstCat or cat.canAccess(aw) else ICategoryProtectedMetadataFossil
565 path.append(fossilize(cat, iface))
566 cat = cat.getOwner()
567 if visibility > len(path):
568 visibilityName= "Everywhere"
569 elif visibility == 0:
570 visibilityName = "Nowhere"
571 else:
572 categId = path[visibility-1]["id"]
573 visibilityName = CategoryManager().getById(categId).getName()
574 path.reverse()
575 path.append({"visibility": {"name": visibilityName}})
576 return path
578 @staticmethod
579 def _eventDaysIterator(conf):
581 Iterates over the daily times of an event
583 sched = conf.getSchedule()
584 for day in datespan(conf.getStartDate(), conf.getEndDate()):
585 # ignore days that have no occurrences
586 if sched.getEntriesOnDay(day):
587 startDT = sched.calculateDayStartDate(day)
588 endDT = sched.calculateDayEndDate(day)
589 if startDT != endDT:
590 yield Period(startDT, endDT)
592 def _addOccurrences(self, fossil, obj, startDT, endDT):
593 if self._occurrences:
594 (startDT, endDT) = (startDT or MIN_DATETIME,
595 endDT or MAX_DATETIME)
596 # get occurrences in the date interval
597 fossil['occurrences'] = fossilize(itertools.ifilter(
598 lambda x: x.startDT >= startDT and x.endDT <= endDT, self._eventDaysIterator(obj)),
599 {Period: IPeriodFossil}, tz=self._tz, naiveTZ=self._serverTZ)
600 return fossil
602 def category(self, idlist):
603 idx = IndexesHolder().getById('categoryDateAll')
605 filter = None
606 if self._room or self._location or self._eventType:
607 def filter(obj):
608 if self._eventType and obj.getType() != self._eventType:
609 return False
610 if self._location:
611 name = obj.getLocation() and obj.getLocation().getName()
612 if not name or not fnmatch.fnmatch(name.lower(), self._location.lower()):
613 return False
614 if self._room:
615 name = obj.getRoom() and obj.getRoom().getName()
616 if not name or not fnmatch.fnmatch(name.lower(), self._room.lower()):
617 return False
618 return True
620 iters = itertools.chain(*(idx.iterateObjectsIn(catId, self._fromDT, self._toDT) for catId in idlist))
621 return self._process(iters, filter)
623 def event(self, idlist):
624 ch = ConferenceHolder()
626 def _iterate_objs(objIds):
627 for objId in objIds:
628 obj = ch.getById(objId, True)
629 if obj is not None:
630 yield obj
632 return self._process(_iterate_objs(idlist))
634 class SessionContribHook(HTTPAPIHook):
635 DEFAULT_DETAIL = 'contributions'
636 MAX_RECORDS = {
637 'contributions': 500,
638 'subcontributions': 500,
641 def _getParams(self):
642 super(SessionContribHook, self)._getParams()
643 self._idList = self._pathParams['idlist'].split('-')
644 self._eventId = self._pathParams['event']
646 @classmethod
647 def _matchPath(cls, path):
648 if not hasattr(cls, '_RE'):
649 cls._RE = re.compile(r'/' + cls.PREFIX + '/event/' + cls.RE + r'\.(\w+)$')
650 return cls._RE.match(path)
652 def export_session(self, aw):
653 expInt = SessionFetcher(aw, self)
654 return expInt.session(self._idList)
656 def export_contribution(self, aw):
657 expInt = ContributionFetcher(aw, self)
658 return expInt.contribution(self._idList)
660 class SessionContribFetcher(IteratedDataFetcher):
662 def __init__(self, aw, hook):
663 super(SessionContribFetcher, self).__init__(aw, hook)
664 self._eventId = hook._eventId
666 @HTTPAPIHook.register
667 class SessionHook(SessionContribHook):
668 RE = r'(?P<event>[\w\s]+)/session/(?P<idlist>\w+(?:-\w+)*)'
670 def _getParams(self):
671 super(SessionHook, self)._getParams()
672 self._type = 'session'
674 class SessionFetcher(SessionContribFetcher):
675 DETAIL_INTERFACES = {
676 'contributions': ISessionMetadataWithContributionsFossil,
677 'subcontributions': ISessionMetadataWithSubContribsFossil,
680 def session(self, idlist):
681 ch = ConferenceHolder()
682 event = ch.getById(self._eventId)
684 def _iterate_objs(objIds):
685 for objId in objIds:
686 obj = event.getSessionById(objId)
687 if obj is not None:
688 yield obj
690 return self._process(_iterate_objs(idlist))
692 @HTTPAPIHook.register
693 class ContributionHook(SessionContribHook):
694 RE = r'(?P<event>[\w\s]+)/contribution/(?P<idlist>\w+(?:-\w+)*)'
696 def _getParams(self):
697 super(ContributionHook, self)._getParams()
698 self._type = 'contribution'
700 class ContributionFetcher(SessionContribFetcher):
701 DETAIL_INTERFACES = {
702 'contributions': IContributionMetadataFossil,
703 'subcontributions': IContributionMetadataWithSubContribsFossil,
706 def contribution(self, idlist):
707 ch = ConferenceHolder()
708 event = ch.getById(self._eventId)
710 def _iterate_objs(objIds):
711 for objId in objIds:
712 obj = event.getContributionById(objId)
713 if obj is not None:
714 yield obj
716 return self._process(_iterate_objs(idlist))
719 @HTTPAPIHook.register
720 class UserEventHook(HTTPAPIHook):
721 TYPES = ('user',)
722 RE = r'(?P<what>linked_events|categ_events)'
723 DEFAULT_DETAIL = 'basic_events'
724 GUEST_ALLOWED = False
726 def _getParams(self):
727 super(UserEventHook, self)._getParams()
728 self._what = self._pathParams['what']
729 self._avatar = None
730 # User-specified avatar
731 userId = get_query_parameter(self._queryParams, ['uid', 'userid'])
732 if userId is not None:
733 self._avatar = AvatarHolder().getById(userId)
734 if not self._avatar:
735 raise HTTPAPIError('Avatar does not exist')
737 def _getMethodName(self):
738 return self.PREFIX + '_' + self._what
740 def _checkProtection(self, aw):
741 if not self._avatar:
742 # No avatar specified => use self. No need to check any permissinos.
743 self._avatar = aw.getUser()
744 return
745 elif not self._avatar.canUserModify(aw.getUser()):
746 raise HTTPAPIError('Access denied', 403)
748 def export_linked_events(self, aw):
749 if not redis_client:
750 raise HTTPAPIError('This API is only available when using Redis')
751 self._checkProtection(aw)
752 links = avatar_links.get_links(redis_client, self._avatar)
753 return UserRelatedEventFetcher(aw, self, links).events(links.keys())
755 def export_categ_events(self, aw):
756 self._checkProtection(aw)
757 catIds = [item['categ'].getId() for item in self._avatar.getRelatedCategories().itervalues()]
758 return UserCategoryEventFetcher(aw, self).category_events(catIds)
761 class UserCategoryEventFetcher(IteratedDataFetcher):
763 DETAIL_INTERFACES = {
764 'basic_events': IBasicConferenceMetadataFossil
767 def category_events(self, catIds):
768 idx = IndexesHolder().getById('categoryDateAll')
769 iters = itertools.chain(*(idx.iterateObjectsIn(catId, self._fromDT, self._toDT) for catId in catIds))
770 return self._process(iters)
773 class UserRelatedEventFetcher(IteratedDataFetcher):
775 DETAIL_INTERFACES = {
776 'basic_events': IBasicConferenceMetadataFossil
779 def __init__(self, aw, hook, roles):
780 super(UserRelatedEventFetcher, self).__init__(aw, hook)
781 self._roles = roles
783 def _postprocess(self, obj, fossil, iface):
784 fossil['roles'] = list(self._roles[obj.getId()])
785 return fossil
787 def events(self, eventIds):
788 ch = ConferenceHolder()
790 def _iterate_objs(objIds):
791 for objId in objIds:
792 obj = ch.getById(objId, True)
793 if obj is not None:
794 yield obj
796 return self._process(_iterate_objs(eventIds))
798 Serializer.register('html', HTML4Serializer)
799 Serializer.register('jsonp', JSONPSerializer)
800 Serializer.register('ics', ICalSerializer)
801 Serializer.register('atom', AtomSerializer)