1 # -*- coding: utf-8 -*-
4 ## This file is part of Indico.
5 ## Copyright (C) 2002 - 2013 European Organization for Nuclear Research (CERN).
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
25 # python stdlib imports
32 from ZODB
.POSException
import ConflictError
33 from datetime
import datetime
, timedelta
, time
34 from zope
.index
.text
import parsetree
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):
79 class ArgumentValueError(Exception):
83 class LimitExceededException(Exception):
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.
93 TYPES
= None # abstract
94 PREFIX
= 'export' # url prefix. must exist in indico_wsgi_url_parser.py, too! also used as function prefix
96 DEFAULT_DETAIL
= None # abstract
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
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
())
111 Logger
.get('HTTPAPIHook.parseRequest').debug(expCls
)
112 m
= expCls
._matchPath
(path
)
118 if format
not in DataFetcher
.getAllowedFormats():
120 elif expCls
.VALID_FORMATS
and format
not in expCls
.VALID_FORMATS
:
122 return expCls(queryParams
, type, gd
), format
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
)
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
)
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
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()
163 tzName
= self
._serverTZ
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
)
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
):
190 def _getMethodName(self
):
191 return self
.PREFIX
+ '_' + self
._type
193 def _performCall(self
, func
, aw
):
198 if isinstance(res
, types
.GeneratorType
):
200 resultList
.append(obj
)
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
)
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)
222 raise NotImplementedError(method_name
)
225 # Just execute the function, we'll never have to repeat it
226 resultList
, complete
= self
._performCall
(func
, aw
)
228 # Try it a few times until commit succeeds
229 dbi
= DBMgr
.getInstance()
230 for _retry
in xrange(10):
232 resultList
, complete
= self
._performCall
(func
, aw
)
235 except ConflictError
:
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
):
262 def getAllowedFormats(cls
):
263 return Serializer
.getAllFormats()
266 def _parseDateTime(cls
, dateTime
, allowNegativeOffset
):
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
277 # if it's a an "alias", return immediately
279 if dateTime
in cls
._deltas
:
280 return ('ctx', now
+ cls
._deltas
[dateTime
])
281 elif dateTime
== 'now':
283 elif dateTime
== 'today':
286 m
= re
.match(r
'^([+-])?(?:(\d{1,3})d)?(?:(\d{1,2})h)?(?:(\d{1,2})m)?$', dateTime
)
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]))
299 return ('abs', datetime
.strptime(dateTime
, "%Y-%m-%dT%H:%M"))
303 return ('ctx', datetime
.strptime(dateTime
, "%Y-%m-%d"))
305 raise ArgumentParseError("Impossible to parse '%s'" % dateTime
)
308 def _getDateTime(cls
, ctx
, dateTime
, tz
, aux
=None):
311 rel
, value
= cls
._parseDateTime
(dateTime
, ctx
=='from')
312 except ArgumentParseError
, e
:
313 raise HTTPAPIError(e
.message
, apache
.HTTP_BAD_REQUEST
)
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
322 return tz
.localize(value
.combine(value
.date(), time(0, 0, 0)))
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
)
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
):
347 # this set acts as a checklist to know if a record has already been sent
349 self
._intermediateResults
= []
353 raise LimitExceededException()
354 if obj
not in exclude
and (not hasattr(obj
, 'canAccess') or obj
.canAccess(self
._aw
)):
355 self
._intermediateResults
.append(obj
)
360 def _sortedIterator(self
, iterator
, limit
, orderBy
, descending
):
363 if orderBy
or descending
:
364 sortingKey
= self
._sortingKeys
.get(orderBy
)
366 limitedIterable
= sorted(self
._limitIterator
(iterator
, limit
),
367 key
=sortingKey
, reverse
=descending
)
368 except LimitExceededException
:
370 limitedIterable
= sorted(self
._intermediateResults
,
371 key
=sortingKey
, reverse
=descending
)
373 limitedIterable
= self
._limitIterator
(iterator
, limit
)
375 # iterate over result
376 for obj
in limitedIterable
:
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
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
):
403 def _process(self
, iterator
, filter=None, iface
=None):
405 iface
= self
.DETAIL_INTERFACES
.get(self
._detail
)
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'}),
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()
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)
436 @HTTPAPIHook.register
437 class EventSearchHook(HTTPAPIHook
):
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')
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
:
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()})
462 @HTTPAPIHook.register
463 class UserInfoHook(HTTPAPIHook
):
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
)
475 if not requested_user
:
476 raise HTTPAPIError('Requested user not found', apache
.HTTP_NOT_FOUND
)
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'
491 'contributions': 500,
492 'subcontributions': 500,
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
))
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
)
547 def getCategoryPath(cls
, idList
, aw
):
551 '_type': 'CategoryPath',
553 'path': cls
._getCategoryPath
(id, aw
)
558 def _getCategoryPath(id, aw
):
560 firstCat
= cat
= CategoryManager().getById(id)
561 visibility
= cat
.getVisibility()
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
))
567 if visibility
> len(path
):
568 visibilityName
= "Everywhere"
569 elif visibility
== 0:
570 visibilityName
= "Nowhere"
572 categId
= path
[visibility
-1]["id"]
573 visibilityName
= CategoryManager().getById(categId
).getName()
575 path
.append({"visibility": {"name": visibilityName
}})
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
)
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
)
602 def category(self
, idlist
):
603 idx
= IndexesHolder().getById('categoryDateAll')
606 if self
._room
or self
._location
or self
._eventType
:
608 if self
._eventType
and obj
.getType() != self
._eventType
:
611 name
= obj
.getLocation() and obj
.getLocation().getName()
612 if not name
or not fnmatch
.fnmatch(name
.lower(), self
._location
.lower()):
615 name
= obj
.getRoom() and obj
.getRoom().getName()
616 if not name
or not fnmatch
.fnmatch(name
.lower(), self
._room
.lower()):
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
):
628 obj
= ch
.getById(objId
, True)
632 return self
._process
(_iterate_objs(idlist
))
634 class SessionContribHook(HTTPAPIHook
):
635 DEFAULT_DETAIL
= 'contributions'
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']
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
):
686 obj
= event
.getSessionById(objId
)
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
):
712 obj
= event
.getContributionById(objId
)
716 return self
._process
(_iterate_objs(idlist
))
719 @HTTPAPIHook.register
720 class UserEventHook(HTTPAPIHook
):
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']
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
)
735 raise HTTPAPIError('Avatar does not exist')
737 def _getMethodName(self
):
738 return self
.PREFIX
+ '_' + self
._what
740 def _checkProtection(self
, aw
):
742 # No avatar specified => use self. No need to check any permissinos.
743 self
._avatar
= aw
.getUser()
745 elif not self
._avatar
.canUserModify(aw
.getUser()):
746 raise HTTPAPIError('Access denied', 403)
748 def export_linked_events(self
, aw
):
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
)
783 def _postprocess(self
, obj
, fossil
, iface
):
784 fossil
['roles'] = list(self
._roles
[obj
.getId()])
787 def events(self
, eventIds
):
788 ch
= ConferenceHolder()
790 def _iterate_objs(objIds
):
792 obj
= ch
.getById(objId
, True)
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
)