source: indico/indico/MaKaC/plugins/Collaboration/base.py @ 842982

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

[FIX] Fixes on previous vidyo booking improvements

  • fixes for make me moderator, booking per contrib/session and access for creation
  • Property mode set to 100644
File size: 104.3 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 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.
20from indico.util.contextManager import ContextManager
21import time
22from persistent import Persistent
23from hashlib import md5
24from MaKaC.common.Counter import Counter
25from MaKaC.common.utils import formatDateTime, parseDateTime
26from MaKaC.common.timezoneUtils import getAdjustedDate, setAdjustedDate,\
27    datetimeToUnixTimeInt
28from MaKaC.webinterface import wcomponents, urlHandlers
29from MaKaC.plugins import PluginsHolder
30from MaKaC.errors import MaKaCError, NoReportError
31from MaKaC.services.interface.rpc.common import ServiceError
32from MaKaC.common.timezoneUtils import nowutc
33from MaKaC.common.logger import Logger
34from MaKaC.common.indexes import IndexesHolder
35from MaKaC.plugins.Collaboration.collaborationTools import CollaborationTools,\
36    MailTools
37from MaKaC.conference import Observer
38from MaKaC.webinterface.common.tools import hasTags
39from MaKaC.plugins.Collaboration import mail
40from MaKaC.common.mail import GenericMailer
41import os, inspect
42import MaKaC.plugins.Collaboration as Collaboration
43from indico.modules.scheduler.client import Client
44from indico.modules.scheduler.tasks import HTTPTask
45from indico.util import json
46from indico.util.i18n import gettext_lazy
47from indico.util.date_time import now_utc
48from MaKaC.common.fossilize import Fossilizable, fossilizes
49from MaKaC.common.externalOperationsManager import ExternalOperationsManager
50from BTrees.OOBTree import OOBTree
51
52from MaKaC.plugins.Collaboration.fossils import ICSErrorBaseFossil, ICSSanitizationErrorFossil,\
53    ICSBookingBaseConfModifFossil, ICSBookingBaseIndexingFossil,\
54    ISpeakerWrapperBaseFossil
55
56
57class CSBookingManager(Persistent, Observer):
58    """ Class for managing the bookins of a meeting.
59        It will store the list of bookings. Adding / removing / editing bookings should be through this class.
60    """
61
62    _shouldBeTitleNotified = True
63    _shouldBeDateChangeNotified = True
64    _shouldBeLocationChangeNotified = True
65    _shouldBeDeletionNotified = True
66
67    def __init__(self, conf):
68        """ Constructor for the CSBookingManager class.
69            conf: a Conference object. The meeting that owns this CSBookingManager.
70        """
71        self._conf = conf
72        self._counter = Counter(1)
73        # a dict where the bookings will be stored. The key will be the booking id, the value a CSBookingBase object.
74        self._bookings = {}
75
76        # an index of bookings by type. The key will be a booking type (string), the value a list of booking id
77        self._bookingsByType = {}
78
79        # an index of bookings to video services by event.uniqueId : video.uniqueId pairind.
80        self._bookingsToVideoServices = OOBTree()
81
82        # a list of ids with hidden bookings
83        self._hiddenBookings = set()
84
85        # an index of video services managers for each plugin. key: plugin name, value: list of users
86        self._managers = {}
87
88        # list of speaker wrapper for a conference
89        self._speakerWrapperList = []
90        self.updateSpeakerWrapperList()
91
92    def getOwner(self):
93        """ Returns the Conference (the meeting) that owns this CSBookingManager object.
94        """
95        return self._conf
96
97    def isCSAllowed(self, user = None):
98        """ Returns if the associated event should display a Video Services tab
99            This can depend on the kind of event (meeting, lecture, conference), on the equipment of the room...
100            If a user is provided, we will take into account if the user can manage the plugin (for example,
101            an event manager cannot manage an admin-only plugin)
102        """
103        pluginsPerEventType = CollaborationTools.getCollaborationPluginType().getOption("pluginsPerEventType").getValue()
104        if pluginsPerEventType:
105            for plugin in pluginsPerEventType[self._conf.getType()]:
106                if plugin.isActive() and (user is None or CollaborationTools.canUserManagePlugin(self._conf, plugin, user)):
107                    return True
108        return False
109
110    def getAllowedPlugins(self):
111        """ Returns a list of allowed plugins (Plugin objects) for this event.
112            Only active plugins are returned.
113            This can depend on the kind of event (meeting, lecture, conference), on the equipment of the room...
114        """
115        pluginsPerEventType = CollaborationTools.getCollaborationPluginType().getOption("pluginsPerEventType").getValue()
116        if pluginsPerEventType is not None:
117            allowedForThisEvent = pluginsPerEventType[self._conf.getType()]
118            return [plugin for plugin in allowedForThisEvent if plugin.isActive()]
119
120
121    def getBookingList(self, sorted = False, filterByType = None, notify = False, onlyPublic = False):
122        """ Returns a list of all the bookings.
123            If sorted = True, the list of bookings will be sorted by id.
124            If filterByType = None, all bookings are returned.
125            Otherwise, just those of the type "filterByType" if filterByType is a string,
126            or if it is a list of strings, those who have a type included in filterByType.
127        """
128
129        if not hasattr(self, "_bookingsByType"): #TODO: remove when safe
130            self._bookingsByType = {}
131
132        if filterByType is not None:
133            if type(filterByType) == str:
134                keys = self._bookingsByType.get(filterByType, [])
135            if type(filterByType) == list:
136                keys = []
137                for pluginName in filterByType:
138                    keys.extend(self._bookingsByType.get(pluginName, []))
139        else:
140            keys = self._bookings.keys()
141
142        if onlyPublic and self.getHiddenBookings():
143            keys = set(keys)
144            keys = keys.difference(self.getHiddenBookings())
145            keys = list(keys)
146
147        if sorted:
148            keys.sort(key = lambda k: int(k))
149
150        bookingList = [self._bookings[k] for k in keys if not self._bookings[k].hasSessionOrContributionLink() or self._bookings[k].getLinkObject()]
151
152        #we notify all the bookings that they have been viewed. If a booking doesn't need to be viewed, nothing will happen
153        if notify:
154            for booking in bookingList:
155                if booking.needsToBeNotifiedOnView():
156                    try:
157                        booking._notifyOnView()
158                    except Exception, e:
159                        Logger.get('VideoServ').error("Exception while notifying to a booking that it is being viewed. Exception: " + str(e))
160
161        return bookingList
162
163    def getBooking(self, id):
164        """ Returns a booking given its id.
165        """
166        return self._bookings[id]
167
168    def getSingleBooking(self, type, notify = False):
169        """ Returns the single booking of a plugin who only allows one booking.
170            type: a string with the name of the plugin
171            If the plugin actually allows multiple bookings, an exception will be thrown
172            If the plugin has no booking, None will be returned.
173            Otherwise the booking will be returned
174        """
175        if CollaborationTools.getCSBookingClass(type)._allowMultiple:
176            raise CollaborationException("Plugin type " + str(type) + " is not a single-booking plugin")
177        blist = self._bookingsByType.get(type,[])
178        if blist:
179            booking = self._bookings[blist[0]]
180            if notify:
181                try:
182                    booking._notifyOnView()
183                except Exception, e:
184                    Logger.get('VideoServ').error("Exception while notifying to a booking that it is being viewed. Exception: " + str(e))
185            return booking
186        else:
187            return None
188
189    def getHiddenBookings(self):
190        if not hasattr(self, '_hiddenBookings'):
191            self._hiddenBookings = set()
192        return self._hiddenBookings
193
194    def hasBookings(self):
195        return len(self._bookings) > 0
196
197    def canCreateBooking(self, type):
198        """ Returns if it's possible to create a booking of this given type
199        """
200        if not CollaborationTools.getCSBookingClass(type)._allowMultiple:
201            return len(self.getBookingList(filterByType = type)) == 0
202        return True
203
204    def checkVideoLink(self, bookingParams):
205
206        if bookingParams.get('videoLinkType',"") == "session":
207            sessSlotId = bookingParams.get("videoLinkSession","")
208            import re
209            regExp = re.match(r"""(s[0-9a]*)(l[0-9]*)""", sessSlotId)
210            if not regExp:
211                raise CollaborationException(_('No session has been passed when the type is session.'))
212            sessionId = regExp.group(1)[1:]
213            slotId = regExp.group(2)[1:]
214            session = self._conf.getSessionById(sessionId)
215            if session is None:
216                raise CollaborationException(_('The session does not exist.'))
217            slot = session.getSlotById(slotId)
218            if slot is None:
219                raise CollaborationException(_('The session does not exist.'))
220            return slot.getUniqueId()
221
222        elif bookingParams.get('videoLinkType',"") == "contribution":
223            contId = bookingParams.get("videoLinkContribution","")
224            if contId == "":
225                raise CollaborationException(_('No contribution has been passed when the type is contribution.'))
226            cont = self._conf.getContributionById(contId)
227            if cont is None:
228                raise CollaborationException(_('The contribution does not exist.'))
229            return cont.getUniqueId()
230
231        return self._conf.getUniqueId()
232
233    def addBooking(self, booking):
234        """ Adds an existing booking to the list of bookings.
235
236            booking: The existing booking to be added.
237        """
238        self._bookings[booking.getId()] = booking
239        self._bookingsByType.setdefault(booking.getType(),[]).append(booking.getId())
240        if booking.isHidden():
241            self.getHiddenBookings().add(booking.getId())
242        self._indexBooking(booking)
243        self._notifyModification()
244
245    def createBooking(self, bookingType, bookingParams = {}):
246        """ Adds a new booking to the list of bookings.
247            The id of the new booking is auto-generated incrementally.
248            After generating the booking, its "performBooking" method will be called.
249
250            bookingType: a String with the booking's plugin. Example: "DummyPlugin", "EVO"
251            bookingParams: a dictionary with the parameters necessary to create the booking.
252                           "create the booking" usually means Indico deciding if the booking can take place.
253                           if "startDate" and "endDate" are among the keys, they will be taken out of the dictionary.
254        """
255        if self.canCreateBooking(bookingType):
256
257            uniqueId = self.checkVideoLink(bookingParams)
258
259            if (self.hasVideoService(uniqueId) and bookingParams.has_key("videoLinkType") and bookingParams.get("videoLinkType","") != "event"): # Restriction: 1 video service per session or contribution.
260                raise NoReportError(_('Only one video service per contribution or session is allowed.'))
261
262            newBooking = CollaborationTools.getCSBookingClass(bookingType)(bookingType, self._conf)
263            if bookingParams.has_key("videoLinkType"):
264                newBooking.setLinkType({bookingParams["videoLinkType"] : uniqueId})
265
266            error = newBooking.setBookingParams(bookingParams)
267
268            if isinstance(error, CSErrorBase):
269                return error
270            elif error:
271                raise CollaborationServiceException("Problem while creating a booking of type " + bookingType)
272            else:
273                newId = self._getNewBookingId()
274                newBooking.setId(newId)
275                createResult = newBooking._create()
276                if isinstance(createResult, CSErrorBase):
277                    return createResult
278                else:
279                    self._bookings[newId] = newBooking
280                    self._bookingsByType.setdefault(bookingType,[]).append(newId)
281                    if newBooking.isHidden():
282                        self.getHiddenBookings().add(newId)
283                    self._indexBooking(newBooking)
284                    self._notifyModification()
285
286                    if uniqueId is not None: # if we're here and uniqueId has a value, register the video service.
287                        self.addVideoService(uniqueId, newBooking)
288
289                    if MailTools.needToSendEmails(bookingType):
290                        newBooking._sendNotifications('new')
291
292                    return newBooking
293        else:
294            #we raise an exception because the web interface should take care of this never actually happening
295            raise CollaborationServiceException(bookingType + " only allows to create 1 booking per event")
296
297    def _indexBooking(self, booking):
298        if booking.shouldBeIndexed():
299            indexes = self._getIndexList(booking)
300            for index in indexes:
301                index.indexBooking(booking)
302
303    def changeBooking(self, bookingId, bookingParams):
304        """
305        Changes the bookingParams of a CSBookingBase object.
306        After updating the booking, its 'performBooking' method will be called.
307        bookingId: the id of the CSBookingBase object to change
308        bookingParams: a dictionary with the new parameters that will modify the booking
309        'modify the booking' can mean that maybe the booking will be rejected with the new parameters.
310        if 'startDate' and 'endDate' are among the keys, they will be taken out of the dictionary.
311        """
312        booking = self.getBooking(bookingId)
313
314        oldStartDate = booking.getStartDate()
315        oldModificationDate = booking.getModificationDate()
316        oldBookingParams = booking.getBookingParams() #this is a copy so it's ok
317
318        error = booking.setBookingParams(bookingParams)
319        if isinstance(error, CSSanitizationError):
320            return error
321        elif error:
322            CSBookingManager._rollbackChanges(booking, oldBookingParams, oldModificationDate)
323            if isinstance(error, CSErrorBase):
324                return error
325            raise CollaborationServiceException("Problem while modifying a booking of type " + booking.getType())
326        else:
327            modifyResult = booking._modify(oldBookingParams)
328            if isinstance(modifyResult, CSErrorBase):
329                CSBookingManager._rollbackChanges(booking, oldBookingParams, oldModificationDate)
330                return modifyResult
331            else:
332                modificationDate = now_utc()
333                booking.setModificationDate(modificationDate)
334
335                if booking.isHidden():
336                    self.getHiddenBookings().add(booking.getId())
337                elif booking.getId() in self.getHiddenBookings():
338                    self.getHiddenBookings().remove(booking.getId())
339
340                eventLinkUpdated = False
341                newLinkId = self.checkVideoLink(bookingParams)
342
343                if booking.hasSessionOrContributionLink():
344                    oldLinkData = booking.getLinkIdDict()
345                    oldLinkId = oldLinkData.values()[0]
346
347                    # Details changed, we need to remove the association and re-create it
348                    if not (oldLinkData.has_key(bookingParams.get('videoLinkType','')) and oldLinkId == newLinkId):
349                        self.removeVideoSingleService(booking.getLinkId(), booking)
350                        eventLinkUpdated = True
351
352                if eventLinkUpdated or (bookingParams.has_key("videoLinkType") and bookingParams.get("videoLinkType","") != "event"):
353                    if self.hasVideoService(booking.getLinkId(), booking):
354                        pass # No change in the event linking
355                    elif newLinkId is not None:
356                        if (self.hasVideoService(newLinkId) and bookingParams.has_key("videoLinkType") and bookingParams.get("videoLinkType","") != "event"): # Restriction: 1 video service per session or contribution.
357                            raise NoReportError(_('Only one video service per contribution or session is allowed.'))
358                        else:
359                            self.addVideoService(newLinkId, booking)
360                            if bookingParams.has_key("videoLinkType"):
361                                booking.setLinkType({bookingParams['videoLinkType']: newLinkId})
362                    else: # If it's still None, event linking has been completely removed.
363                        booking.resetLinkParams()
364
365                self._changeStartDateInIndex(booking, oldStartDate, booking.getStartDate())
366                self._changeModificationDateInIndex(booking, oldModificationDate, modificationDate)
367
368                if booking.hasAcceptReject():
369                    if booking.getAcceptRejectStatus() is not None:
370                        booking.clearAcceptRejectStatus()
371                        self._addToPendingIndex(booking)
372
373                self._notifyModification()
374
375                if MailTools.needToSendEmails(booking.getType()):
376                    booking._sendNotifications('modify')
377
378                return booking
379
380    @classmethod
381    def _rollbackChanges(cls, booking, oldBookingParams, oldModificationDate):
382        booking.setBookingParams(oldBookingParams)
383        booking.setModificationDate(oldModificationDate)
384
385    def _changeConfTitleInIndex(self, booking, oldTitle, newTitle):
386        if booking.shouldBeIndexed():
387            indexes = self._getIndexList(booking)
388            for index in indexes:
389                index.changeEventTitle(booking, oldTitle, newTitle)
390
391    def _changeStartDateInIndex(self, booking, oldStartDate, newStartDate):
392        if booking.shouldBeIndexed() and booking.hasStartDate():
393            indexes = self._getIndexList(booking)
394            for index in indexes:
395                index.changeStartDate(booking, oldStartDate, newStartDate)
396
397    def _changeModificationDateInIndex(self, booking, oldModificationDate, newModificationDate):
398        if booking.shouldBeIndexed():
399            indexes = self._getIndexList(booking)
400            for index in indexes:
401                index.changeModificationDate(booking, oldModificationDate, newModificationDate)
402
403    def _changeConfStartDateInIndex(self, booking, oldConfStartDate, newConfStartDate):
404        if booking.shouldBeIndexed():
405            indexes = self._getIndexList(booking)
406            for index in indexes:
407                index.changeConfStartDate(booking, oldConfStartDate, newConfStartDate)
408
409    def removeBooking(self, id):
410        """ Removes a booking given its id.
411        """
412        booking = self.getBooking(id)
413        bookingType = booking.getType()
414        bookingLinkId = booking.getLinkId()
415
416        removeResult = booking._delete()
417        if isinstance(removeResult, CSErrorBase):
418            return removeResult
419        else:
420            del self._bookings[id]
421            self._bookingsByType[bookingType].remove(id)
422            if not self._bookingsByType[bookingType]:
423                del self._bookingsByType[bookingType]
424            if id in self.getHiddenBookings():
425                self.getHiddenBookings().remove(id)
426
427            # If there is an association to a session or contribution, remove it
428            if bookingLinkId is not None:
429                self.removeVideoSingleService(bookingLinkId, booking)
430            self._unindexBooking(booking)
431
432            self._notifyModification()
433
434            if MailTools.needToSendEmails(booking.getType()):
435                booking._sendNotifications('remove')
436
437            return booking
438
439    def _unindexBooking(self, booking):
440        if booking.shouldBeIndexed():
441            indexes = self._getIndexList(booking)
442            for index in indexes:
443                index.unindexBooking(booking)
444
445    def startBooking(self, id):
446        booking = self._bookings[id]
447        if booking.canBeStarted():
448            booking._start()
449            return booking
450        else:
451            raise CollaborationException(_("Tried to start booking ") + str(id) + _(" of meeting ") + str(self._conf.getId()) + _(" but this booking cannot be started."))
452
453    def stopBooking(self, id):
454        booking = self._bookings[id]
455        if booking.canBeStopped():
456            booking._stop()
457            return booking
458        else:
459            raise CollaborationException(_("Tried to stop booking ") + str(id) + _(" of meeting ") + str(self._conf.getId()) + _(" but this booking cannot be stopped."))
460
461    def connectBooking(self, id):
462        booking = self._bookings[id]
463        if booking.canBeConnected():
464            return booking._connect()
465        else:
466            raise CollaborationException(_("Tried to connect booking ") + str(id) + _(" of meeting ") + str(self._conf.getId()) + _(" but this booking cannot be connected."))
467
468    def checkBookingStatus(self, id):
469        booking = self._bookings[id]
470        if booking.hasCheckStatus():
471            result = booking._checkStatus()
472            if isinstance(result, CSErrorBase):
473                return result
474            else:
475                return booking
476        else:
477            raise ServiceError(message=_("Tried to check status of booking ") + str(id) + _(" of meeting ") + str(self._conf.getId()) + _(" but this booking does not support the check status service."))
478
479    def acceptBooking(self, id, user = None):
480        booking = self._bookings[id]
481        if booking.hasAcceptReject():
482            if booking.getAcceptRejectStatus() is None:
483                self._removeFromPendingIndex(booking)
484            booking.accept(user)
485            return booking
486        else:
487            raise ServiceError(message=_("Tried to accept booking ") + str(id) + _(" of meeting ") + str(self._conf.getId()) + _(" but this booking cannot be accepted."))
488
489    def rejectBooking(self, id, reason):
490        booking = self._bookings[id]
491        if booking.hasAcceptReject():
492            if booking.getAcceptRejectStatus() is None:
493                self._removeFromPendingIndex(booking)
494            booking.reject(reason)
495            return booking
496        else:
497            raise ServiceError("ERR-COLL10", _("Tried to reject booking ") + str(id) + _(" of meeting ") + str(self._conf.getId()) + _(" but this booking cannot be rejected."))
498
499    def makeMeModeratorBooking(self, id, user):
500        booking = self._bookings[id]
501        bookingParams = booking.getBookingParams()
502        bookingParams["owner"] = user
503        return self.changeBooking(id,bookingParams)
504
505    def _addToPendingIndex(self, booking):
506        if booking.shouldBeIndexed():
507            indexes = self._getPendingIndexList(booking)
508            for index in indexes:
509                index.indexBooking(booking)
510
511    def _removeFromPendingIndex(self, booking):
512        if booking.shouldBeIndexed():
513            indexes = self._getPendingIndexList(booking)
514            for index in indexes:
515                index.unindexBooking(booking)
516
517    def _getNewBookingId(self):
518        return self._counter.newCount()
519
520    def _getIndexList(self, booking):
521        """ Returns a list of BookingsIndex objects where the booking should be indexed.
522            This list includes:
523            -an index of all bookings
524            -an index of bookings of the given type
525            -an index of all bookings in the category of the event
526            -an index of booking of the given type, in the category of the event
527            If the booking type declared common indexes:
528            -the common indexes
529            -the common indexes for the category of the event
530            If the booking is of the Accept/Reject type
531            -same indexes as above, but only for pending bookings
532        """
533        collaborationIndex = IndexesHolder().getById("collaboration")
534        indexes = [collaborationIndex.getAllBookingsIndex(),
535                   collaborationIndex.getIndex(booking.getType())]
536
537        for commonIndexName in booking.getCommonIndexes():
538            indexes.append(collaborationIndex.getIndex(commonIndexName))
539
540        if booking.hasAcceptReject() and booking.getAcceptRejectStatus() is None:
541            indexes.extend(self._getPendingIndexList(booking))
542
543        return indexes
544
545    def _getPendingIndexList(self, booking):
546        collaborationIndex = IndexesHolder().getById("collaboration")
547        indexes = [collaborationIndex.getIndex("all_pending"),
548                   collaborationIndex.getIndex(booking.getType() + "_pending")]
549
550        for commonIndexName in booking.getCommonIndexes():
551            indexes.append(collaborationIndex.getIndex(commonIndexName + "_pending"))
552
553        return indexes
554
555    def getManagers(self):
556        if not hasattr(self, "_managers"):
557            self._managers = {}
558        return self._managers
559
560    def addPluginManager(self, plugin, user):
561        #TODO: use .linkTo on the user. To be done when the list of roles of a user is actually needed for smth...
562        self.getManagers().setdefault(plugin, []).append(user)
563        self._notifyModification()
564
565    def removePluginManager(self, plugin, user):
566        #TODO: use .unlinkTo on the user. To be done when the list of roles of a user is actually needed for smth...
567        if user in self.getManagers().setdefault(plugin,[]):
568            self.getManagers()[plugin].remove(user)
569            self._notifyModification()
570
571    def getVideoServicesManagers(self):
572        return self.getManagers().setdefault('all', [])
573
574    def isVideoServicesManager(self, user):
575        return user in self.getManagers().setdefault('all', [])
576
577    def getPluginManagers(self, plugin):
578        return self.getManagers().setdefault(plugin, [])
579
580    def isPluginManager(self, plugin, user):
581        return user in self.getManagers().setdefault(plugin, [])
582
583    def getAllManagers(self):
584        """ Returns a list with all the managers, no matter their type
585            The returned list is not ordered.
586        """
587        managers = set()
588        for managerList in self.getManagers().itervalues():
589            managers = managers.union(managerList)
590        return list(managers)
591
592    def isPluginManagerOfAnyPlugin(self, user):
593        #TODO: this method is not optimal. to be optimal, we should store somewhere an index where the key
594        #is the user, and the value is a list of plugins where they are managers.
595        #this could be done with .getLinkTo, but we would need to change the .linkTo method to add extra information
596        #(since we cannot create a role for each plugin)
597        if self.isVideoServicesManager(user):
598            return True
599        else:
600            for plugin in self.getManagers().iterkeys():
601                if self.isPluginManager(plugin, user):
602                    return True
603            return False
604
605    def notifyTitleChange(self, oldTitle, newTitle):
606        """ Notifies the CSBookingManager that the title of the event (meeting) it's attached to has changed.
607            The CSBookingManager will reindex all its bookings in the event title index.
608            This method will be called by the event (meeting) object
609        """
610        for booking in self.getBookingList():
611            try:
612                self._changeConfTitleInIndex(booking, oldTitle, newTitle)
613            except Exception, e:
614                Logger.get('VideoServ').exception("Exception while reindexing a booking in the event title index because its event's title changed: " + str(e))
615
616    def notifyInfoChange(self):
617        self.updateSpeakerWrapperList()
618
619    def notifyEventDateChanges(self, oldStartDate = None, newStartDate = None, oldEndDate = None, newEndDate = None):
620        """ Notifies the CSBookingManager that the start and / or end dates of the event it's attached to have changed.
621            The CSBookingManager will change the dates of all the bookings that want to be updated.
622            If there are problems (such as a booking not being able to be modified)
623            it will write a list of strings describing the problems in the 'dateChangeNotificationProblems' context variable.
624            (each string is produced by the _booking2NotifyProblem method).
625            This method will be called by the event (meeting) object.
626        """
627        startDateChanged = oldStartDate is not None and newStartDate is not None and not oldStartDate == newStartDate
628        endDateChanged = oldEndDate is not None and newEndDate is not None and not oldEndDate == newEndDate
629        someDateChanged = startDateChanged or endDateChanged
630
631        Logger.get("VideoServ").info("""CSBookingManager: starting notifyEventDateChanges. Arguments: confId=%s, oldStartDate=%s, newStartDate=%s, oldEndDate=%s, newEndDate=%s""" %
632                                     (str(self._conf.getId()), str(oldStartDate), str(newStartDate), str(oldEndDate), str(newEndDate)))
633
634        if someDateChanged:
635            problems = []
636            for booking in self.getBookingList():
637                if booking.hasStartDate():
638                    if startDateChanged:
639                        try:
640                            self._changeConfStartDateInIndex(booking, oldStartDate, newStartDate)
641                        except Exception, e:
642                            Logger.get('VideoServ').error("Exception while reindexing a booking in the event start date index because its event's start date changed: " + str(e))
643
644                    if booking.needsToBeNotifiedOfDateChanges():
645                        Logger.get("VideoServ").info("""CSBookingManager: notifying date changes to booking %s of event %s""" %
646                                                     (str(booking.getId()), str(self._conf.getId())))
647                        oldBookingStartDate = booking.getStartDate()
648                        oldBookingEndDate = booking.getEndDate()
649                        oldBookingParams = booking.getBookingParams() #this is a copy so it's ok
650
651                        if startDateChanged:
652                            booking.setStartDate(oldBookingStartDate + (newStartDate - oldStartDate) )
653                        if endDateChanged:
654                            booking.setEndDate(oldBookingEndDate + (newEndDate - oldEndDate) )
655
656                        rollback = False
657                        modifyResult = None
658                        try:
659                            modifyResult = booking._modify(oldBookingParams)
660                            if isinstance(modifyResult, CSErrorBase):
661                                Logger.get('VideoServ').warning("""Error while changing the dates of booking %s of event %s after event dates changed: %s""" %
662                                                                (str(booking.getId()), str(self._conf.getId()), modifyResult.getLogMessage()))
663                                rollback = True
664                        except Exception, e:
665                            Logger.get('VideoServ').error("""Exception while changing the dates of booking %s of event %s after event dates changed: %s""" %
666                                                          (str(booking.getId()), str(self._conf.getId()), str(e)))
667                            rollback = True
668
669                        if rollback:
670                            booking.setStartDate(oldBookingStartDate)
671                            booking.setEndDate(oldBookingEndDate)
672                            problems.append(CSBookingManager._booking2NotifyProblem(booking, modifyResult))
673                        elif startDateChanged:
674                            self._changeStartDateInIndex(booking, oldBookingStartDate, booking.getStartDate())
675
676                if hasattr(booking, "notifyEventDateChanges"):
677                    try:
678                        booking.notifyEventDateChanges(oldStartDate, newStartDate, oldEndDate, newEndDate)
679                    except Exception, e:
680                        Logger.get('VideoServ').exception("Exception while notifying a plugin of an event date changed: " + str(e))
681
682            if problems:
683                ContextManager.get('dateChangeNotificationProblems')['Collaboration'] = [
684                    'Some Video Services bookings could not be moved:',
685                    problems,
686                    'Go to [[' + str(urlHandlers.UHConfModifCollaboration.getURL(self.getOwner(), secure = ContextManager.get('currentRH').use_https())) + ' the Video Services section]] to modify them yourself.'
687                ]
688
689
690    def notifyTimezoneChange(self, oldTimezone, newTimezone):
691        """ Notifies the CSBookingManager that the timezone of the event it's attached to has changed.
692            The CSBookingManager will change the dates of all the bookings that want to be updated.
693            This method will be called by the event (Conference) object
694        """
695        return []
696
697    def notifyLocationChange(self):
698        for booking in self.getBookingList():
699            if hasattr(booking, "notifyLocationChange"):
700                try:
701                    booking.notifyLocationChange()
702                except Exception, e:
703                    Logger.get('VideoServ').exception("Exception while notifying a plugin of a location change: " + str(e))
704
705    @classmethod
706    def _booking2NotifyProblem(cls, booking, modifyError):
707        """ Turns a booking into a string used to tell the user
708            why a date change of a booking triggered by the event's start or end date change
709            went bad.
710        """
711
712        message = []
713        message.extend(["The dates of the ", booking.getType(), " booking"])
714        if booking.hasTitle():
715            message.extend([': "', booking._getTitle(), '" (', booking.getStartDateAsString(), ' - ', booking.getEndDateAsString(), ')'])
716        else:
717            message.extend([' ongoing from ', booking.getStartDateAsString(), ' to ', booking.getEndDateAsString(), ''])
718
719        message.append(' could not be changed.')
720        if modifyError and modifyError.getUserMessage():
721            message.extend([' Reason: ', modifyError.getUserMessage()])
722        return "".join(message)
723
724
725    def notifyDeletion(self):
726        """ Notifies the CSBookingManager that the Conference object it is attached to has been deleted.
727            The CSBookingManager will change the dates of all the bookings that want to be updated.
728            This method will be called by the event (Conference) object
729        """
730        for booking in self.getBookingList():
731            try:
732                removeResult = booking._delete()
733                if isinstance(removeResult, CSErrorBase):
734                    Logger.get('VideoServ').warning("Error while deleting a booking of type %s after deleting an event: %s"%(booking.getType(), removeResult.getLogMessage() ))
735                self._unindexBooking(booking)
736            except Exception, e:
737                Logger.get('VideoServ').exception("Exception while deleting a booking of type %s after deleting an event: %s" % (booking.getType(), str(e)))
738
739    def getEventDisplayPlugins(self, sorted = False):
740        """ Returns a list of names (strings) of plugins which have been configured
741            as showing bookings in the event display page, and which have bookings
742            already (or previously) created in the event.
743            (does not check if the bookings are hidden or not)
744        """
745
746        pluginsWithEventDisplay = CollaborationTools.pluginsWithEventDisplay()
747        l = []
748        for pluginName in self._bookingsByType:
749            if pluginName in pluginsWithEventDisplay:
750                l.append(pluginName)
751        if sorted:
752            l.sort()
753        return l
754
755    def createTestBooking(self, bookingParams = {}):
756        """ Function that creates a 'test' booking for performance test.
757            Avoids to use any of the plugins except DummyPlugin
758        """
759        from MaKaC.plugins.Collaboration.DummyPlugin.collaboration import CSBooking as DummyBooking
760        bookingType = 'DummyPlugin'
761        newBooking = DummyBooking(bookingType, self._conf)
762        error = newBooking.setBookingParams(bookingParams)
763        if error:
764            raise CollaborationServiceException("Problem while creating a test booking")
765        else:
766            newId = self._getNewBookingId()
767            newBooking.setId(newId)
768            createResult = newBooking._create()
769            if isinstance(createResult, CSErrorBase):
770                return createResult
771            else:
772                self._bookings[newId] = newBooking
773                self._bookingsByType.setdefault(bookingType,[]).append(newId)
774                if newBooking.isHidden():
775                    self.getHiddenBookings().add(newId)
776                self._indexBooking(newBooking)
777                self._notifyModification()
778                return newBooking
779
780    def _notifyModification(self):
781        self._p_changed = 1
782
783    def getSortedContributionSpeaker(self, exclusive):
784        ''' This method will create a dictionary by sorting the contribution/speakers
785            that they are in recording, webcast or in both.
786            bool: exclusive - if True, every dicts (recording, webcast, both) will
787                                have different speaker list (no repetition allowed)
788                                if an element is present in 'both', it will be deleted from
789                                'recording and 'webcast'
790
791            returns d = { 'recording': {}, 'webcast' : {}, 'both': {} }
792        '''
793
794        d = {}
795
796        recordingBooking = self.getSingleBooking("RecordingRequest")
797        webcastBooking = self.getSingleBooking("WebcastRequest")
798
799        d["recording"] = recordingBooking.getContributionSpeakerSingleBooking() if recordingBooking else {}
800        d["webcast"] = webcastBooking.getContributionSpeakerSingleBooking() if webcastBooking else {}
801
802        contributions = {}
803        ''' Look for speaker intersections between 'recording' and 'webcast' dicts
804            and put them in 'both' dict. Additionally, if any intersection has been found,
805            we exclude them from the original dictionary.
806        '''
807        for cont in d["recording"].copy():
808            if cont in d["webcast"].copy():
809                # Check if same contribution/speaker in 'recording' and 'webcast'
810                intersection = set(d['recording'][cont]) & set(d['webcast'][cont])
811                if intersection:
812                    contributions[cont] = list(intersection)
813
814                    # if exclusive is True, and as we found same contribution/speaker,
815                    # we delete them from 'recording' and 'webcast' dicts
816                    if exclusive:
817                        exclusion = set(d['recording'][cont]) ^ set(contributions[cont])
818                        if not exclusion:
819                            del d["recording"][cont]
820                        else:
821                            d["recording"][cont] = list(exclusion)
822
823                        exclusion = set(d['webcast'][cont]) ^ set(contributions[cont])
824                        if not exclusion:
825                            del d["webcast"][cont]
826                        else:
827                            d["webcast"][cont] = list(exclusion)
828
829        d["both"] = contributions
830
831        return d
832
833    def getContributionSpeakerByType(self, requestType):
834        ''' Return a plain dict of contribution/speaker according to the requestType
835            if the request type is 'both', we need to merge the lists
836        '''
837        d = self.getSortedContributionSpeaker(False) # We want non exclusive dict
838
839        if requestType == "recording":
840            return d['recording']
841        elif requestType == "webcast":
842            return d['webcast']
843        elif requestType == "both":
844            # We merge 'recording' and 'webcast'
845            m = dict(((cont, list(set(spks) | \
846                set(d['webcast'].get(cont, [])))) for cont, spks in d['recording'].iteritems()))
847            m.update(dict((cont, spks) for cont, spks in d['webcast'].iteritems() if cont not in m))
848
849            return m
850        else:
851            return {}
852
853    def updateSpeakerWrapperList(self, newList = False):
854        """
855        if newList arg is True, don't check if there is an existing speakerWrapperList
856        and create a new one straight forward. (Done to avoid loops)
857        """
858        SWList = []
859        contributions = self.getSortedContributionSpeaker(True)
860        requestType = ['recording', 'webcast', 'both']
861
862        for type in requestType:
863            for cont in contributions[type]:
864                for spk in contributions[type][cont]:
865                    if newList:
866                        sw = None
867                    else:
868                        sw = self.getSpeakerWrapperByUniqueId("%s.%s"%(cont, spk.getId()))
869
870                    if sw:
871                        if not sw.getObject().getEmail():
872                            if sw.getStatus() not in [SpeakerStatusEnum.SIGNED,
873                                                      SpeakerStatusEnum.FROMFILE,
874                                                      SpeakerStatusEnum.REFUSED]:
875                                sw.setStatus(SpeakerStatusEnum.NOEMAIL)
876                        elif sw.getStatus() == SpeakerStatusEnum.NOEMAIL:
877                            sw.setStatus(SpeakerStatusEnum.NOTSIGNED)
878                        sw.setRequestType(type)
879                        SWList.append(sw)
880                    else:
881                        newSw = SpeakerWrapper(spk, cont, type)
882                        if not newSw.getObject().getEmail():
883                            newSw.setStatus(SpeakerStatusEnum.NOEMAIL)
884                        SWList.append(newSw)
885
886        self._speakerWrapperList = SWList
887
888    def getSpeakerWrapperList(self):
889        if not hasattr(self, "_speakerWrapperList"):#TODO: remove when safe
890            self.updateSpeakerWrapperList(True)
891
892        return self._speakerWrapperList
893
894    def getSpeakerWrapperByUniqueId(self, id):
895
896        if not hasattr(self, "_speakerWrapperList"):#TODO: remove when safe
897            self.updateSpeakerWrapperList(True)
898
899        for spkWrap in self._speakerWrapperList:
900            if spkWrap.getUniqueId() == id:
901                return spkWrap
902
903        return None
904
905    def areSignatureCompleted(self):
906        value = True;
907        for spkWrap in self._speakerWrapperList:
908            if spkWrap.getStatus() != SpeakerStatusEnum.FROMFILE and \
909                spkWrap.getStatus() != SpeakerStatusEnum.SIGNED:
910                value = False;
911
912        return value
913
914    def getSpeakerWrapperListByStatus(self, status):
915        '''Return a list of SpeakerWrapper matching the status.
916        '''
917        list = []
918        for spkWrap in self._speakerWrapperList:
919            if spkWrap.getStatus() == status:
920                list.append(spkWrap)
921
922        return list
923
924    def getSpeakerEmailByUniqueId(self, id, user):
925        ''' Return the email of a speaker according to the uniqueId.
926            id: uniqueId of the speaker wrapper.
927            user: user object of the sender of the emails, in order to check the rights.
928        '''
929
930        canManageRequest = CollaborationTools.getRequestTypeUserCanManage(self._conf, user)
931        requestTypeAccepted = ""
932
933        if canManageRequest == "recording":
934            requestTypeAccepted = ["recording"]
935        elif canManageRequest == "webcast":
936            requestTypeAccepted = ["webcast"]
937        elif canManageRequest == "both":
938            requestTypeAccepted = ["recording", "webcast", "both"]
939
940        list = []
941        for spkWrap in self._speakerWrapperList:
942            if spkWrap.getUniqueId() == id and \
943                   spkWrap.hasEmail() and spkWrap.getStatus() not in \
944                   [SpeakerStatusEnum.SIGNED, SpeakerStatusEnum.FROMFILE] and \
945                   spkWrap.getRequestType() in requestTypeAccepted:
946
947                list.append(spkWrap.getObject().getEmail())
948
949        return list
950
951    def addVideoService(self, uniqueId, videoService):
952        """ Adds a video service to Contribution / Session link in the tracking
953            dictionary in order {uniqueId : videoService}
954        """
955
956        if self.getVideoServices().has_key(uniqueId):
957            self.getVideoServices()[uniqueId].append(videoService)
958        else:
959            self.getVideoServices()[uniqueId] = [videoService]
960
961    def removeVideoAllServices(self, uniqueId):
962        """ Removes all associations of Contributions / Sessions with video
963            services from the dictionary, key included.
964        """
965
966        if not self.hasVideoService(uniqueId):
967            return None
968
969        del self.getVideoServices()[uniqueId]
970
971    def removeVideoSingleService(self, uniqueId, videoService):
972        """ Removes a specific video service from a specific contribution. As
973            the list of services is unordered, iterate through to match for
974            removal - performance cost therefore occurs here.
975        """
976
977        if not self.hasVideoService(uniqueId):
978            return None
979
980        target = self.getVideoServicesById(uniqueId)
981
982        for service in target:
983            if service == videoService:
984                target.remove(service)
985                break
986
987        # There are no more entries, therefore remove the dictionary entry too.
988        if len(target) == 0:
989            self.removeVideoAllServices(uniqueId)
990
991    def getVideoServices(self):
992        """ Returns the OOBTree associating event unique IDs with the List
993            of video services associated.
994        """
995
996        if not hasattr(self, "_bookingsToVideoServices"):
997            self._bookingsToVideoServices = OOBTree()
998
999        return self._bookingsToVideoServices
1000
1001    def getVideoServicesById(self, uniqueId):
1002        """ Returns a list of video services associated with the uniqueId
1003            for printing in event timetable. Returns None if no video services
1004            are found.
1005        """
1006
1007        if not self.hasVideoService(uniqueId):
1008            return None
1009
1010        return self.getVideoServices()[uniqueId]
1011
1012    def hasVideoService(self, uniqueId, service=None):
1013        """ Returns True if the uniqueId of the Contribution or Session provided
1014            has an entry in the self._bookingsToVideoServices dictionary, thusly
1015            denoting the presence of linked bookings. Second parameter is for more
1016            specific matching, i.e. returns True if unique ID is associated with
1017            specific service.
1018        """
1019        if service is None:
1020            return self.getVideoServices().has_key(uniqueId)
1021
1022        if self.getVideoServices().has_key(uniqueId):
1023            for serv in self.getVideoServicesById(uniqueId):
1024                if serv == service:
1025                    return True
1026        else:
1027            return self.getVideoServices().has_key(uniqueId)
1028
1029    def isAnyRequestAccepted(self):
1030        '''
1031            Return true if at least one between recording and webcast request
1032            has been accepted.
1033        '''
1034        value = False
1035        rr = self.getSingleBooking("RecordingRequest")
1036        wr = self.getSingleBooking("WebcastRequest")
1037
1038        if rr:
1039            value = rr.getAcceptRejectStatus()
1040
1041        if wr:
1042            value = value or wr.getAcceptRejectStatus()
1043
1044        return value
1045
1046    def isContributionReadyToBePublished(self, contId):
1047        if not hasattr(self, "_speakerWrapperList"):#TODO: remove when safe
1048            self.updateSpeakerWrapperList(True)
1049
1050        exists = False
1051        for spkWrap in self._speakerWrapperList:
1052            if spkWrap.getContId() == contId:
1053                exists = True
1054                if spkWrap.getStatus() != SpeakerStatusEnum.SIGNED and \
1055                    spkWrap.getStatus() != SpeakerStatusEnum.FROMFILE:
1056                    return False
1057
1058        #The list has to have at least one spkWrap with the given contId
1059        return exists
1060
1061class CSBookingBase(Persistent, Fossilizable):
1062    fossilizes(ICSBookingBaseConfModifFossil, ICSBookingBaseIndexingFossil)
1063
1064    """ Base class that represents a Collaboration Systems booking.
1065        Every Collaboration plugin will have to implement this class.
1066        In the base class are gathered all the functionalities / elements that are common for all plugins.
1067        A booking is Persistent (DateChangeObserver inherits from Persistent) so it will be stored in the database.
1068        Also, every CSBookingBase object in the server will be mirrored by a Javascript object in the client, through "Pickling".
1069
1070        Every class that implements the CSBookingBase has to declare the following class attributes:
1071            _hasStart : True if the plugin has a "start" concept. Otherwise, the "start" button will not appear, etc.
1072            _hasStop : True if the plugin has a "stop" concept. Otherwise, the "stop" button will not appear, etc.
1073            _hasConnect : True if the plugin has a "connect" concept. Otherwise, the "connect" button will not appear, etc.
1074            _hasCheckStatus: True if the plugin has a "check status" concept. Otherwise, the "check status" button will not appear, etc.
1075            _hasAcceptReject: True if the plugin has a "accept or reject" concept. Otherwise, the "accept" and "reject" buttons will not appear, etc.
1076            _requiresServerCallForStart : True if we should notify the server when the user presses the "start" button.
1077            _requiresServerCallForStop : True if we should notify the server when the user presses the "stop" button.
1078            _requiresServerCallForConnect : True if we should notify the server when the user presses the "connect" button.
1079            _requiresClientCallForStart : True if the browser should execute some JS action when the user presses the "start" button.
1080            _requiresClientCallForStop : True if the browser should execute some JS action when the user presses the "stop" button.
1081            _requiresClientCallForConnect : True if the browser should execute some JS action when the user presses the "connect" button.
1082            _needsBookingParamsCheck : True if the booking parameters should be checked after the booking is added / edited.
1083                                       If True, the _checkBookingParams method will be called by the setBookingParams method.
1084            _needsToBeNotifiedOnView: True if the booking object needs to be notified (through the "notifyOnView" method)
1085                                      when the user "sees" the booking, for example when returning the list of bookings.
1086            _canBeNotifiedOfEventDateChanges: True if bookings of this type should be able to be notified
1087                                              of their owner Event changing start date, end date or timezone.
1088            _allowMultiple: True if this booking type allows more than 1 booking per event.
1089    """
1090
1091    _hasStart = False
1092    _hasStop = False
1093    _hasConnect = False
1094    _hasCheckStatus = False
1095    _hasAcceptReject = False
1096    _hasStartStopAll = False
1097    _requiresServerCallForStart = False
1098    _requiresServerCallForStop = False
1099    _requiresServerCallForConnect = False
1100    _requiresClientCallForStart = False
1101    _requiresClientCallForStop = False
1102    _requiresClientCallForConnect = False
1103    _needsBookingParamsCheck = False
1104    _needsToBeNotifiedOnView = False
1105    _canBeNotifiedOfEventDateChanges = True
1106    _allowMultiple = True
1107    _shouldBeIndexed = True
1108    _commonIndexes = []
1109    _hasStartDate = True
1110    _hasEventDisplay = False
1111    _hasTitle = False
1112    _adminOnly = False
1113    _complexParameters = []
1114    _linkVideoType = None
1115    _linkVideoId = None
1116
1117    def __init__(self, bookingType, conf):
1118        """ Constructor for the CSBookingBase class.
1119            id: a string with the id of the booking
1120            bookingType: a string with the type of the booking. Example: "DummyPlugin", "EVO"
1121            conf: a Conference object to which this booking belongs (through the CSBookingManager object). The meeting of this booking.
1122            startTime: TODO
1123            endTime: TODO
1124
1125            Other attributes initialized by this constructor:
1126            -_bookingParams: the parameters necessary to perform the booking.
1127                             The plugins will decide if the booking gets authorized or not depending on this.
1128                             Needs to be defined by the implementing class, as keys with empty values.
1129            -_startingParams: the parameters necessary to start the booking.
1130                              They will be used on the client for the local start action.
1131                              Needs to be defined by the implementing class, as keys with empty values.
1132            -_warning: A warning is a plugin-defined object, with information to show to the user when
1133                       the operation went well but we still have to show some info to the user.
1134            -_permissionToStart : Even if the "start" button for a booking is able to be pushed, there may be cases where the booking should
1135                            not start. For example, if it's not the correct time yet.
1136                            In that case "permissionToStart" should be set to false so that the booking doesn't start.
1137            -_permissionToStop: Same as permissionToStart. Sometimes the booking should not be allowed to stop even if the "stop" button is available.
1138            -_permissionToConnect: Same as permissionToStart. Sometimes the booking should not be allowed to connect even if the "connect" button is available.
1139        """
1140        self._id = None
1141        self._type = bookingType
1142        self._plugin = CollaborationTools.getPlugin(self._type)
1143        self._conf = conf
1144        self._warning = None
1145        self._creationDate = nowutc()
1146        self._modificationDate = nowutc()
1147        self._creationDateTimestamp = int(datetimeToUnixTimeInt(self._creationDate))
1148        self._modificationDateTimestamp = int(datetimeToUnixTimeInt(self._modificationDate))
1149        self._startDate = None
1150        self._endDate = None
1151        self._startDateTimestamp = None
1152        self._endDateTimestamp = None
1153        self._acceptRejectStatus = None #None = not yet accepted / rejected; True = accepted; False = rejected
1154        self._rejectReason = ""
1155        self._bookingParams = {}
1156        self._canBeDeleted = True
1157        self._permissionToStart = False
1158        self._permissionToStop = False
1159        self._permissionToConnect = False
1160        self._needsToBeNotifiedOfDateChanges = self._canBeNotifiedOfEventDateChanges
1161        self._hidden = False
1162        self._play_status = None
1163
1164        setattr(self, "_" + bookingType + "Options", CollaborationTools.getPlugin(bookingType).getOptions())
1165        #NOTE:  Should maybe notify the creation of a new booking, specially if it's a single booking
1166        #       like that can update requestType of the speaker wrapper...
1167
1168    def getId(self):
1169        """ Returns the internal, per-conference id of the booking.
1170            This attribute will be available in Javascript with the "id" identifier.
1171        """
1172        return self._id
1173
1174    def setId(self, id):
1175        """ Sets the internal, per-conference id of the booking
1176        """
1177        self._id = id
1178
1179    def getUniqueId(self):
1180        """ Returns an unique Id that identifies this booking server-wide.
1181            Useful for ExternalOperationsManager
1182        """
1183        return "%scsbook%s" % (self.getConference().getUniqueId(), self.getId())
1184
1185    def getType(self):
1186        """ Returns the type of the booking, as a string: "EVO", "DummyPlugin"
1187            This attribute will be available in Javascript with the "type" identifier.
1188        """
1189        return self._type
1190
1191    def getConference(self):
1192        """ Returns the owner of this CSBookingBase object, which is a Conference object representing the meeting.
1193        """
1194        return self._conf
1195
1196    def setConference(self, conf):
1197        """ Sets the owner of this CSBookingBase object, which is a Conference object representing the meeting.
1198        """
1199        self._conf = conf
1200
1201    def getWarning(self):
1202        """ Returns a warning attached to this booking.
1203            A warning is a plugin-defined object, with information to show to the user when
1204            the operation went well but we still have to show some info to the user.
1205            To be overloaded by plugins.
1206        """
1207        if not hasattr(self, '_warning'):
1208            self._warning = None
1209        return self._warning
1210
1211    def setWarning(self, warning):
1212        """ Sets a warning attached to this booking.
1213            A warning is a plugin-defined object, with information to show to the user when
1214            the operation went well but we still have to show some info to the user.
1215            To be overloaded by plugins.
1216        """
1217        self._warning = warning
1218
1219    def getCreationDate(self):
1220        """ Returns the date this booking was created, as a timezone localized datetime object
1221        """
1222        if not hasattr(self, "_creationDate"): #TODO: remove when safe
1223            self._creationDate = nowutc()
1224        return self._creationDate
1225
1226    def getAdjustedCreationDate(self, tz=None):
1227        """ Returns the booking creation date, adjusted to a given timezone.
1228            If no timezone is provided, the event's timezone is used
1229        """
1230        return getAdjustedDate(self.getCreationDate(), self.getConference(), tz)
1231
1232    def getCreationDateTimestamp(self):
1233        if not hasattr(object, "_creationDateTimestamp"): #TODO: remove when safe
1234            self._creationDateTimestamp = int(datetimeToUnixTimeInt(self._creationDate))
1235        return self._creationDateTimestamp
1236
1237    def getModificationDate(self):
1238        """ Returns the date this booking was modified last
1239        """
1240        if not hasattr(self, "_modificationDate"):  #TODO: remove when safe
1241            self._modificationDate = nowutc()
1242        return self._modificationDate
1243
1244    def getAdjustedModificationDate(self, tz=None):
1245        """ Returns the booking last modification date, adjusted to a given timezone.
1246            If no timezone is provided, the event's timezone is used
1247        """
1248        return getAdjustedDate(self.getModificationDate(), self.getConference(), tz)
1249
1250    def getModificationDateTimestamp(self):
1251        if not hasattr(object, "_modificationDateTimestamp"): #TODO: remove when safe
1252            self._modificationDateTimestamp = int(datetimeToUnixTimeInt(self._modificationDate))
1253        return self._modificationDateTimestamp
1254
1255    def setModificationDate(self, date):
1256        """ Sets the date this booking was modified last
1257        """
1258        self._modificationDate = date
1259        if date:
1260            self._modificationDateTimestamp = int(datetimeToUnixTimeInt(date))
1261        else:
1262            self._modificationDateTimestamp = None
1263
1264    def getBookingsOfSameType(self, sorted = False):
1265        """ Returns a list of the bookings of the same type as this one (including this one)
1266            sorted: if true, bookings will be sorted by id
1267        """
1268        return self._conf.getCSBookingManager().getBookingList(sorted, self._type)
1269
1270    def getPlugin(self):
1271        """ Returns the Plugin object associated to this booking.
1272        """
1273        return self._plugin
1274
1275    def setPlugin(self, plugin):
1276        """ Sets the Plugin object associated to this booking.
1277        """
1278        self._plugin = plugin
1279
1280    def getPluginOptions(self):
1281        """ Utility method that returns the plugin options for this booking's type of plugin
1282        """
1283        return self._plugin.getOptions()
1284
1285    def getPluginOptionByName(self, optionName):
1286        """ Utility method that returns a plugin option, given its name, for this booking's type of plugin
1287        """
1288        return self.getPluginOptions()[optionName]
1289
1290    def getStartDate(self):
1291        """ Returns the start date as an datetime object with timezone information (adjusted to the meeting's timezone)
1292        """
1293        return self._startDate
1294
1295    def getAdjustedStartDate(self, tz=None):
1296        """ Returns the booking start date, adjusted to a given timezone.
1297            If no timezone is provided, the event's timezone is used
1298        """
1299        if self.getStartDate():
1300            return getAdjustedDate(self.getStartDate(), self.getConference(), tz)
1301        else:
1302            return None
1303
1304    def getStartDateTimestamp(self):
1305        if not hasattr(object, "_startDateTimestamp"): #TODO: remove when safe
1306            self._startDateTimestamp = int(datetimeToUnixTimeInt(self._startDate))
1307        return self._startDateTimestamp
1308
1309    def setStartDateTimestamp(self, startDateTimestamp):
1310        self._startDateTimestamp = startDateTimestamp
1311
1312    def getStartDateAsString(self):
1313        """ Returns the start date as a string, expressed in the meeting's timezone
1314        """
1315        if self._startDate == None:
1316            return ""
1317        else:
1318            return formatDateTime(self.getAdjustedStartDate(), locale='en_US')
1319
1320    def setStartDate(self, startDate):
1321        """ Sets the start date as an datetime object with timezone information (adjusted to the meeting's timezone)
1322        """
1323        self._startDate = startDate
1324        if startDate:
1325            self._startDateTimestamp = int(datetimeToUnixTimeInt(startDate))
1326        else:
1327            self._startDateTimestamp = None
1328
1329    def setStartDateFromString(self, startDateString):
1330        """ Sets the start date from a string. It is assumed that the date is expressed in the meeting's timezone
1331        """
1332        if startDateString == "":
1333            self.setStartDate(None)
1334        else:
1335            try:
1336                self.setStartDate(setAdjustedDate(parseDateTime(startDateString), self._conf))
1337            except ValueError:
1338                raise CollaborationServiceException("startDate parameter (" + startDateString +" ) is in an incorrect format for booking with id: " + str(self._id))
1339
1340    def getEndDate(self):
1341        """ Returns the end date as an datetime object with timezone information (adjusted to the meeting's timezone)
1342        """
1343        return self._endDate
1344
1345    def isHappeningNow(self):
1346        now = nowutc()
1347        return self.getStartDate() < now and self.getEndDate() > now
1348
1349    def hasHappened(self):
1350        now = nowutc()
1351        return now > self.getEndDate()
1352
1353    def getAdjustedEndDate(self, tz=None):
1354        """ Returns the booking end date, adjusted to a given timezone.
1355            If no timezone is provided, the event's timezone is used
1356        """
1357        return getAdjustedDate(self.getEndDate(), self.getConference(), tz)
1358
1359    def getEndDateTimestamp(self):
1360        if not hasattr(object, "_endDateTimestamp"): #TODO: remove when safe
1361            self._endDateTimestamp = int(datetimeToUnixTimeInt(self._endDate))
1362        return self._endDateTimestamp
1363
1364    def setEndDateTimestamp(self, endDateTimestamp):
1365        self._endDateTimestamp = endDateTimestamp
1366
1367    def getEndDateAsString(self):
1368        """ Returns the start date as a string, expressed in the meeting's timezone
1369        """
1370        if self._endDate == None:
1371            return ""
1372        else:
1373            return formatDateTime(self.getAdjustedEndDate(), locale='en_US')
1374
1375    def setEndDate(self, endDate):
1376        """ Sets the start date as an datetime object with timezone information (adjusted to the meeting's timezone)
1377        """
1378        self._endDate = endDate
1379        if endDate:
1380            self._endDateTimestamp = int(datetimeToUnixTimeInt(endDate))
1381        else:
1382            self._endDateTimestamp = None
1383
1384    def setEndDateFromString(self, endDateString):
1385        """ Sets the start date from a string. It is assumed that the date is expressed in the meeting's timezone
1386        """
1387        if endDateString == "":
1388            self.setEndDate(None)
1389        else:
1390            try:
1391                self.setEndDate(setAdjustedDate(parseDateTime(endDateString), self._conf))
1392            except ValueError:
1393                raise CollaborationServiceException("endDate parameter (" + endDateString +" ) is in an incorrect format for booking with id: " + str(self._id))
1394
1395    def getStatusMessage(self):
1396        """ Returns the status message as a string.
1397            This attribute will be available in Javascript with the "statusMessage"
1398        """
1399        status = self.getPlayStatus()
1400        if status == None:
1401            if self.isHappeningNow():
1402                return _("Ready to start!")
1403            elif self.hasHappened():
1404                return _("Already took place")
1405            else:
1406                return _("Booking created")
1407        elif status:
1408            return _("Conference started")
1409        elif not status:
1410            return _("Conference stopped")
1411
1412    def getStatusClass(self):
1413        """ Returns the status message CSS class as a string.
1414            This attribute will be available in Javascript with the "statusClass"
1415        """
1416        if self.getPlayStatus() == None or self.hasHappened():
1417            return "statusMessageOther"
1418        else:
1419            return "statusMessageOK"
1420
1421    def accept(self, user = None):
1422        """ Sets this booking as accepted
1423        """
1424        self._acceptRejectStatus = True
1425        self._accept(user)
1426
1427    def reject(self, reason):
1428        """ Sets this booking as rejected, and stores the reason
1429        """
1430        self._acceptRejectStatus = False
1431        self._rejectReason = reason
1432        self._reject()
1433
1434    def clearAcceptRejectStatus(self):
1435        """ Sets back the accept / reject status to None
1436        """
1437        self._acceptRejectStatus = None
1438
1439    def getAcceptRejectStatus(self):
1440        """ Returns the Accept/Reject status of the booking
1441            This attribute will be available in Javascript with the "acceptRejectStatus"
1442            Its value will be:
1443            -None if the booking has not been accepted or rejected yet,
1444            -True if it has been accepted,
1445            -False if it has been rejected
1446        """
1447        if not hasattr(self, "_acceptRejectStatus"):
1448            self._acceptRejectStatus = None
1449        return self._acceptRejectStatus
1450
1451    def getRejectReason(self):
1452        """ Returns the rejection reason.
1453            This attribute will be available in Javascript with the "rejectReason"
1454        """
1455        if not hasattr(self, "_rejectReason"):
1456            self._rejectReason = ""
1457        return self._rejectReason
1458
1459    ## methods relating to the linking of CSBooking objects to Contributions & Sessions
1460
1461    def hasSessionOrContributionLink(self):
1462        return (self.isLinkedToContribution() or self.isLinkedToSession())
1463
1464    def isLinkedToSession(self):
1465        return (self._linkVideoType == "session")
1466
1467    def isLinkedToContribution(self):
1468        return (self._linkVideoType == "contribution")
1469
1470    def getLinkId(self):
1471        """ Returns the unique ID of the Contribution or Session which this
1472            object is associated with, completely agnostic of the link type.
1473            Returns None if no association (default) found.
1474        """
1475        if not self.hasSessionOrContributionLink():
1476            return None
1477
1478        return self._linkVideoId
1479
1480    def getLinkIdDict(self):
1481        """ Returns a dictionary of structure linkType (session | contribution)
1482            : unique ID of referenced object.
1483            Returns None if no association is found.
1484        """
1485        linkId = self.getLinkId()
1486
1487        if linkId == None:
1488            return linkId
1489
1490        return {self._linkVideoType : linkId}
1491
1492    def getLinkType(self):
1493        """ Returns a string denoting the link type, that is whether linked
1494            to a session or contribution.
1495        """
1496
1497        return self._linkVideoType
1498
1499    def setLinkType(self, linkDict):
1500        """ Accepts a dictionary of linkType: linkId """
1501        self._linkVideoType = linkDict.keys()[0]
1502        self._linkVideoId = linkDict.values()[0]
1503
1504    def resetLinkParams(self):
1505        """ Removes all association with a Session or Contribution from this
1506            CSBooking only.
1507        """
1508
1509        self._linkVideoType = self._linkVideoId = None
1510
1511    def getBookingParams(self):
1512        """ Returns a dictionary with the booking params.
1513            This attribute will be available in Javascript with the "bookingParams"
1514
1515            If self._bookingParams has not been set by the implementing class, an exception is thrown.
1516
1517            Support for "complex" parameters, that are not defined in the self._bookingParams dict, but can
1518            be retrieved through getter methods.
1519            If a subclass defines a class attributes called _complexParameters (a list of strings),
1520            parameter names that are in  this list will also be included in the returned dictionary.
1521            Their value will be retrieved by calling the corresponding getXXX methods
1522            but instead the inheriting class's setXXX method will be called.
1523            Example: _complexParameters = ["communityName", "accessPassword", "hasAccessPassword"] correspond
1524            to the methods getCommunityName, getAccessPassword, getHasAccessPassword.
1525            If you include a parameter in the _complexParameters list, you always have to implement the corresponding getter method.
1526        """
1527        bookingParams = {}
1528        for k, v in self.__class__._simpleParameters.iteritems():
1529            if k in self._bookingParams:
1530                value = self._bookingParams[k]
1531            else:
1532                value = v[1] #we use the default value
1533            if v[0] is bool and value is True: #we assume it will be used in a single checkbox
1534                value = ["yes"]
1535            if value is not False: #we do not include False, it means the single checkbox is not checked
1536                bookingParams[k] = value
1537
1538        if hasattr(self.__class__, "_complexParameters") and len(self.__class__._complexParameters) > 0:
1539            getterMethods = dict(inspect.getmembers(self, lambda m: inspect.ismethod(m) and m.__name__.startswith('get')))
1540            for paramName in self.__class__._complexParameters:
1541                getMethodName = 'get' + paramName[0].upper() + paramName[1:]
1542                if getMethodName in getterMethods:
1543                    bookingParams[paramName] = getterMethods[getMethodName]()
1544                else:
1545                    raise CollaborationServiceException("Tried to retrieve complex parameter " + str(paramName) + " but the corresponding getter method " + getMethodName + " is not implemented")
1546
1547        bookingParams["startDate"] = self.getStartDateAsString()
1548        bookingParams["endDate"] = self.getEndDateAsString()
1549        if self.needsToBeNotifiedOfDateChanges():
1550            bookingParams["notifyOnDateChanges"] = ["yes"]
1551        if self.isHidden():
1552            bookingParams["hidden"] = ["yes"]
1553        return bookingParams
1554
1555
1556    def getBookingParamByName(self, paramName):
1557        if paramName in self.__class__._simpleParameters:
1558            if not paramName in self._bookingParams:
1559                self._bookingParams[paramName] = self.__class__._simpleParameters[paramName][1]
1560            return self._bookingParams[paramName]
1561        elif hasattr(self.__class__, "_complexParameters") and paramName in self.__class__._complexParameters:
1562            getterMethods = dict(inspect.getmembers(self, lambda m: inspect.ismethod(m) and m.__name__.startswith('get')))
1563            getMethodName = 'get' + paramName[0].upper() + paramName[1:]
1564            if getMethodName in getterMethods:
1565                return getterMethods[getMethodName]()
1566            else:
1567                raise CollaborationServiceException("Tried to retrieve complex parameter " + str(paramName) + " but the corresponding getter method " + getMethodName + " is not implemented")
1568        else:
1569            raise CollaborationServiceException("Tried to retrieve parameter " + str(paramName) + " but this parameter does not exist")
1570
1571    def getContributionSpeakerSingleBooking(self):
1572        ''' Return a dictionnary with the contributions and their speakers that need to be recorded
1573            e.g: {contId:[Spk1Object, Spk2Object, Spk3Object], cont2:[Spk1Object]}...
1574        '''
1575        request = {}
1576
1577        recordingTalksChoice = self.getBookingParams()["talks"] #either "all", "choose" or ""
1578        listTalksToRecord = self.getBookingParams()["talkSelection"]
1579
1580        if self._conf.getType() == "simple_event":
1581            request[self._conf.getId()] = []
1582            for chair in self._conf.getChairList():
1583                request[self._conf.getId()].append(chair)
1584        else:
1585            for cont in self._conf.getContributionList():
1586                ''' We select the contributions that respect the following conditions:
1587                    - They have Speakers assigned.
1588                    - They are scheduled. (to discuss...)
1589                    - They have been chosen for the recording request.
1590                '''
1591                if recordingTalksChoice != "choose" or cont.getId() in listTalksToRecord:
1592                    if cont.isScheduled():
1593                        request[cont.getId()] = []
1594                        for spk in cont.getSpeakerList():
1595                            request[cont.getId()].append(spk)
1596
1597        return request
1598
1599    def setBookingParams(self, params):
1600        """ Sets new booking parameters.
1601            params: a dict with key/value pairs with the new values for the booking parameters.
1602            If the plugin's _needsBookingParamsCheck is True, the _checkBookingParams() method will be called.
1603            This function will return False if all the checks were OK or if there were no checks, and otherwise will throw
1604            an exception or return a CSReturnedErrorBase error.
1605
1606            Support for "complex" parameters, that are not defined in the self._bookingParams dict, but can
1607            be set through setter methods.
1608            If a subclass defines a class attributes called _complexParameters (a list of strings),
1609            parameter names that are in 'params' and also in this list will not be assigned directly,
1610            but instead the inheriting class's setXXX method will be called.
1611
1612            Example: _complexParameters = ["communityName", "accessPassword", "hasAccessPassword"] corresponds
1613            to methods setCommunityName, setAccessPassword, setHasAccessPassword.
1614            Note: even if a parameter is in this list, you can decide not to implement its corresponding set
1615            method if you never expect the parameter name to come up inside 'params'.
1616        """
1617
1618        sanitizeResult = self.sanitizeParams(params)
1619        if sanitizeResult:
1620            return sanitizeResult
1621
1622        self.setHidden(params.pop("hidden", False) == ["yes"])
1623        self.setNeedsToBeNotifiedOfDateChanges(params.pop("notifyOnDateChanges", False) == ["yes"])
1624
1625        startDate = params.pop("startDate", None)
1626        if startDate is not None:
1627            self.setStartDateFromString(startDate)
1628        endDate = params.pop("endDate", None)
1629        if endDate is not None:
1630            self.setEndDateFromString(endDate)
1631
1632        for k,v in params.iteritems():
1633            if k in self.__class__._simpleParameters:
1634                if self.__class__._simpleParameters[k][0]:
1635                    try:
1636                        v = self.__class__._simpleParameters[k][0](v)
1637                    except ValueError:
1638                        raise CollaborationServiceException("Tried to set value of parameter with name " + str(k) + ", recognized as a simple parameter of type" + str(self._simpleParameters[k]) + ", but the conversion failed")
1639                self._bookingParams[k] = v
1640            elif k in self.__class__._complexParameters:
1641                setterMethods = dict(inspect.getmembers(self, lambda m: inspect.ismethod(m) and m.__name__.startswith('set')))
1642                setMethodName = 'set' + k[0].upper() + k[1:]
1643                if setMethodName in setterMethods:
1644                    setterMethods[setMethodName](v)
1645                else:
1646                    raise CollaborationServiceException("Tried to set value of parameter with name " + str(k) + ", recognized as a complex parameter, but the corresponding setter method " + setMethodName + " is not implemented")
1647            else:
1648                raise CollaborationServiceException("Tried to set the value of a parameter with name " + str(k) + " that was not declared")
1649
1650        for k, v in self.__class__._simpleParameters.iteritems():
1651            if not k in self._bookingParams:
1652                self._bookingParams[k] = self.__class__._simpleParameters[k][1]
1653
1654        if self.needsBookingParamsCheck():
1655            return self._checkBookingParams()
1656
1657        return False
1658
1659    def sanitizeParams(self, params):
1660        """ Checks if the fields introduced into the booking / request form
1661            have any kind of HTML or script tag.
1662        """
1663        if not isinstance(params, dict):
1664            raise CollaborationServiceException("Booking parameters are not a dictionary")
1665
1666        invalidFields = []
1667        for k, v in params.iteritems():
1668            if type(v) == str and hasTags(v):
1669                invalidFields.append(k)
1670
1671        if invalidFields:
1672            return CSSanitizationError(invalidFields)
1673        else:
1674            return None
1675
1676    def _getTypeDisplayName(self):
1677        return CollaborationTools.getXMLGenerator(self._type).getDisplayName()
1678
1679    def _getFirstLineInfo(self, tz):
1680        return CollaborationTools.getXMLGenerator(self._type).getFirstLineInfo(self, tz)
1681
1682    def _getTitle(self):
1683        if self.hasEventDisplay():
1684            raise CollaborationException("Method _getTitle was not overriden for the plugin type " + str(self._type))
1685
1686    def _getInformationDisplay(self, tz):
1687        templateClass = CollaborationTools.getTemplateClass(self.getType(), "WInformationDisplay")
1688        if templateClass:
1689            return templateClass(self, tz).getHTML()
1690        else:
1691            return None
1692
1693    def _getLaunchDisplayInfo(self):
1694        """ To be overloaded by plugins
1695        """
1696        return None
1697
1698    def _checkBookingParams(self):
1699        """ To be overriden by inheriting classes.
1700            Verifies that the booking parameters are correct. For example, that a numeric field is actually a number.
1701            Otherwise, an exception should be thrown.
1702            If there are no errors, the method should just return.
1703        """
1704        if self.needsBookingParamsCheck():
1705            raise CollaborationServiceException("Method _checkBookingParams was not overriden for the plugin type " + str(self._type))
1706
1707    def hasStart(self):
1708        """ Returns if this booking belongs to a plugin who has a "start" concept.
1709            This attribute will be available in Javascript with the "hasStart" attribute
1710        """
1711        return self._hasStart
1712
1713    def hasStartStopAll(self):
1714        """ Returns if this booking belongs to a plugin who has a "start" concept, and all of its bookings for a conference
1715            can be started simultanously.
1716            This attribute will be available in Javascript with the "hasStart" attribute
1717        """
1718        return self._hasStartStopAll
1719
1720    def hasStop(self):
1721        """ Returns if this booking belongs to a plugin who has a "stop" concept.
1722            This attribute will be available in Javascript with the "hasStop" attribute
1723        """
1724        return self._hasStop
1725
1726    def hasConnect(self):
1727        """ Returns if this booking belongs to a plugin who has a "connect" concept.
1728            This attribute will be available in Javascript with the "hasConnect" attribute
1729        """
1730        if not hasattr(self, '_hasConnect'):
1731            self._hasConnect = False
1732        return self._hasConnect
1733
1734    def hasCheckStatus(self):
1735        """ Returns if this booking belongs to a plugin who has a "check status" concept.
1736            This attribute will be available in Javascript with the "hasCheckStatus" attribute
1737        """
1738        return self._hasCheckStatus
1739
1740    def hasAcceptReject(self):
1741        """ Returns if this booking belongs to a plugin who has a "accept or reject" concept.
1742            This attribute will be available in Javascript with the "hasAcceptReject" attribute
1743        """
1744        return self._hasAcceptReject
1745
1746    def requiresServerCallForStart(self):
1747        """ Returns if this booking belongs to a plugin who requires a server call when the start button is pressed.
1748            This attribute will be available in Javascript with the "requiresServerCallForStart" attribute
1749        """
1750        return self._requiresServerCallForStart
1751
1752    def requiresServerCallForStop(self):
1753        """ Returns if this booking belongs to a plugin who requires a server call when the stop button is pressed.
1754            This attribute will be available in Javascript with the "requiresServerCallForStop" attribute
1755        """
1756        return self._requiresServerCallForStop
1757
1758    def requiresServerCallForConnect(self):
1759        """ Returns if this booking belongs to a plugin who requires a server call when the connect button is pressed.
1760            This attribute will be available in Javascript with the "requiresServerCallForConnect" attribute
1761        """
1762        if not hasattr(self, '_requiresServerCallForConnect'):
1763            self._requiresServerCallForConnect = False
1764        return self._requiresServerCallForConnect
1765
1766    def requiresClientCallForStart(self):
1767        """ Returns if this booking belongs to a plugin who requires a client call when the start button is pressed.
1768            This attribute will be available in Javascript with the "requiresClientCallForStart" attribute
1769        """
1770        return self._requiresClientCallForStart
1771
1772    def requiresClientCallForStop(self):
1773        """ Returns if this booking belongs to a plugin who requires a client call when the stop button is pressed.
1774            This attribute will be available in Javascript with the "requiresClientCallForStop" attribute
1775        """
1776        return self._requiresClientCallForStop
1777
1778    def requiresClientCallForConnect(self):
1779        """ Returns if this booking belongs to a plugin who requires a client call when the connect button is pressed.
1780            This attribute will be available in Javascript with the "requiresClientCallForConnect" attribute
1781        """
1782        if not hasattr(self, '_requiresClientCallForConnect'):
1783            self._requiresClientCallForConnect = False
1784        return self._requiresClientCallForConnect
1785
1786    def canBeDeleted(self):
1787        """ Returns if this booking can be deleted, in the sense that the "Remove" button will be active and able to be pressed.
1788            This attribute will be available in Javascript with the "canBeDeleted" attribute
1789        """
1790
1791        return self._canBeDeleted
1792
1793    def setCanBeDeleted(self, canBeDeleted):
1794        """ Sets if this booking can be deleted, in the sense that the "Remove" button will be active and able to be pressed.
1795            This attribute will be available in Javascript with the "canBeDeleted" attribute
1796        """
1797        self._canBeDeleted = canBeDeleted
1798
1799    def canBeStarted(self):
1800        """ Returns if this booking can be started, in the sense that the "Start" button will be active and able to be pressed.
1801            This attribute will be available in Javascript with the "canBeStarted" attribute
1802        """
1803        return self.isHappeningNow()
1804
1805    def canBeStopped(self):
1806        """ Returns if this booking can be stopped, in the sense that the "Stop" button will be active and able to be pressed.
1807            This attribute will be available in Javascript with the "canBeStopped" attribute
1808        """
1809        return self.isHappeningNow()
1810
1811    def canBeConnected(self):
1812        """ Returns if this booking can be connected, in the sense that the "Connect" button will be active and able to be pressed.
1813            This attribute will be available in Javascript with the "canBeConnected" attribute
1814        """
1815        return self.isHappeningNow()
1816
1817    def isPermittedToStart(self):
1818        """ Returns if this booking is allowed to start, in the sense that it will be started after the "Start" button is pressed.
1819            For example a booking should not be permitted to start before a given time, even if the button is active.
1820            This attribute will be available in Javascript with the "isPermittedToStart" attribute
1821        """
1822        return self._permissionToStart
1823
1824    def isPermittedToStop(self):
1825        """ Returns if this booking is allowed to stop, in the sense that it will be started after the "Stop" button is pressed.
1826            This attribute will be available in Javascript with the "isPermittedToStop" attribute
1827        """
1828        return self._permissionToStop
1829
1830    def isPermittedToConnect(self):
1831        """ Returns if this booking is allowed to stop, in the sense that it will be connect after the "Connect" button is pressed.
1832            This attribute will be available in Javascript with the "isPermittedToConnect" attribute
1833        """
1834        if not hasattr(self, '_permissionToConnect'):
1835            self._permissionToConnect = False
1836        return self._permissionToConnect
1837
1838    def needsBookingParamsCheck(self):
1839        """ Returns if this booking belongs to a plugin that needs to verify the booking parameters.
1840        """
1841        return self._needsBookingParamsCheck
1842
1843    def needsToBeNotifiedOnView(self):
1844        """ Returns if this booking needs to be notified when someone views it (for example when the list of bookings is returned)
1845        """
1846        return self._needsToBeNotifiedOnView
1847
1848    def canBeNotifiedOfEventDateChanges(self):
1849        """ Returns if bookings of this type should be able to be notified
1850            of their owner Event changing start date, end date or timezone.
1851        """
1852        return False
1853
1854    def needsToBeNotifiedOfDateChanges(self):
1855        """ Returns if this booking in particular needs to be notified
1856            of their owner Event changing start date, end date or timezone.
1857        """
1858        return self._needsToBeNotifiedOfDateChanges
1859
1860    def setNeedsToBeNotifiedOfDateChanges(self, needsToBeNotifiedOfDateChanges):
1861        """ Sets if this booking in particular needs to be notified
1862            of their owner Event changing start date, end date or timezone.
1863        """
1864        self._needsToBeNotifiedOfDateChanges = needsToBeNotifiedOfDateChanges
1865
1866    def isHidden(self):
1867        """ Return if this booking is "hidden"
1868            A hidden booking will not appear in display pages
1869        """
1870        if not hasattr(self, '_hidden'):
1871            self._hidden = False
1872        return self._hidden
1873
1874    def setHidden(self, hidden):
1875        """ Sets if this booking is "hidden"
1876            A hidden booking will not appear in display pages
1877            hidden: a Boolean
1878        """
1879        self._hidden = hidden
1880
1881    def isAllowMultiple(self):
1882        """ Returns if this booking belongs to a type that allows multiple bookings per event.
1883        """
1884        return self._allowMultiple
1885
1886    def shouldBeIndexed(self):
1887        """ Returns if bookings of this type should be indexed
1888        """
1889        return self._shouldBeIndexed
1890
1891    def getCommonIndexes(self):
1892        """ Returns a list of strings with the names of the
1893            common (shared) indexes that bookings of this type want to
1894            be included in.
1895        """
1896        return self._commonIndexes
1897
1898    def getModificationURL(self):
1899        return urlHandlers.UHConfModifCollaboration.getURL(self.getConference(),
1900                                                           secure = ContextManager.get('currentRH').use_https(),
1901                                                           tab = CollaborationTools.getPluginTab(self.getPlugin()))
1902
1903    def hasStartDate(self):
1904        """ Returns if bookings of this type have a start date
1905            (they may only have creation / modification date)
1906        """
1907        return self._hasStartDate
1908
1909    def hasTitle(self):
1910        """ Returns if bookings of this type have a title
1911        """
1912        return self._hasTitle
1913
1914    def hasEventDisplay(self):
1915        """ Returns if the type of this booking should display something on
1916            an event display page
1917        """
1918        return self._hasEventDisplay
1919
1920    def isAdminOnly(self):
1921        """ Returns if this booking / this booking's plugin pages should only be displayed
1922            to Server Admins, Video Service Admins, or the respective plugin admins.
1923        """
1924        return self._adminOnly
1925
1926    def _create(self):
1927        """ To be overriden by inheriting classes.
1928            This method is called when a booking is created, after setting the booking parameters.
1929            The plugin should decide if the booking is accepted or not.
1930            Often this will involve communication with another entity, like an MCU for the multi-point H.323 plugin,
1931            or a EVO HTTP server in the EVO case.
1932        """
1933        raise CollaborationException("Method _create was not overriden for the plugin type " + str(self._type))
1934
1935    def _modify(self, oldBookingParams):
1936        """ To be overriden by inheriting classes.
1937            This method is called when a booking is modifying, after setting the booking parameters.
1938            The plugin should decide if the booking is accepted or not.
1939            Often this will involve communication with another entity, like an MCU for the multi-point H.323 plugin
1940            or a EVO HTTP server in the EVO case.
1941            A dictionary with the previous booking params is passed. This dictionary is the one obtained
1942            by the method self.getBookingParams() before the new params input by the user are applied.
1943        """
1944        raise CollaborationException("Method _modify was not overriden for the plugin type " + str(self._type))
1945
1946    def _start(self):
1947        """ To be overriden by inheriting classes
1948            This method is called when the user presses the "Start" button in a plugin who has a "Start" concept
1949            and whose flag _requiresServerCallForStart is True.
1950            Often this will involve communication with another entity.
1951        """
1952        if self.hasStart():
1953            raise CollaborationException("Method _start was not overriden for the plugin type " + str(self._type))
1954        else:
1955            pass
1956
1957    def _stop(self):
1958        """ To be overriden by inheriting classes
1959            This method is called when the user presses the "Stop" button in a plugin who has a "Stop" concept
1960            and whose flag _requiresServerCallForStop is True.
1961            Often this will involve communication with another entity.
1962        """
1963        if self.hasStop():
1964            raise CollaborationException("Method _stop was not overriden for the plugin type " + str(self._type))
1965        else:
1966            pass
1967
1968    def _connect(self):
1969        """ To be overriden by inheriting classes
1970            This method is called when the user presses the "Connect" button in a plugin who has a "Connect" concept
1971            and whose flag _requiresServerCallForConnect is True.
1972            Often this will involve communication with another entity.
1973        """
1974        if self.hasConnect():
1975            raise CollaborationException("Method _connect was not overriden for the plugin type " + str(self._type))
1976        else:
1977            pass
1978
1979    def _checkStatus(self):
1980        """ To be overriden by inheriting classes
1981            This method is called when the user presses the "Check Status" button in a plugin who has a "check status" concept.
1982            Often this will involve communication with another entity.
1983        """
1984        if self.hasCheckStatus():
1985            raise CollaborationException("Method _checkStatus was not overriden for the plugin type " + str(self._type))
1986        else:
1987            pass
1988
1989    def _accept(self, user = None):
1990        """ To be overriden by inheriting classes
1991            This method is called when a user with privileges presses the "Accept" button
1992            in a plugin who has a "accept or reject" concept.
1993            Often this will involve communication with another entity.
1994        """
1995        if self.hasAcceptReject():
1996            raise CollaborationException("Method _accept was not overriden for the plugin type " + str(self._type))
1997        else:
1998            pass
1999
2000    def _reject(self):
2001        """ To be overriden by inheriting classes
2002            This method is called when a user with privileges presses the "Reject" button
2003            in a plugin who has a "accept or reject" concept.
2004            Often this will involve communication with another entity.
2005        """
2006        if self.hasAcceptReject():
2007            raise CollaborationException("Method _reject was not overriden for the plugin type " + str(self._type))
2008        else:
2009            pass
2010
2011    def _notifyOnView(self):
2012        """ To be overriden by inheriting classes
2013            This method is called when a user "sees" a booking, for example when the list of bookings is displayed.
2014            Maybe in this moment the booking wants to update its status.
2015        """
2016        if self.needsToBeNotifiedOnView():
2017            raise CollaborationException("Method _notifyOnView was not overriden for the plugin type " + str(self._type))
2018        else:
2019            pass
2020
2021    def _delete(self):
2022        """ To be overriden by inheriting classes
2023            This method is called whent he user removes a booking. Maybe the plugin will need to liberate
2024            ressources that were allocated to it.
2025            This method does not unregister the booking from the list of date change observer of the meeting
2026        """
2027        raise CollaborationException("Method _delete was not overriden for the plugin type " + str(self._type))
2028
2029    def _sendNotifications(self, operation):
2030        """
2031        Sends a mail, wrapping it with ExternalOperationsManager
2032        """
2033        ExternalOperationsManager.execute(self, "sendMail_" + operation, self._sendMail, operation)
2034
2035    def _sendMail(self, operation):
2036        if operation == 'new':
2037            try:
2038                notification = mail.NewBookingNotification(self)
2039                GenericMailer.sendAndLog(notification, self._conf,
2040                                     "MaKaC/plugins/Collaboration/base.py",
2041                                     self._conf.getCreator())
2042            except Exception, e:
2043                Logger.get('VideoServ').error(
2044                    """Could not send NewBookingNotification for booking with id %s of event with id %s, exception: %s""" %
2045                    (self.getId(), self._conf.getId(), str(e)))
2046                raise
2047
2048        elif operation == 'modify':
2049            try:
2050                notification = mail.BookingModifiedNotification(self)
2051                GenericMailer.sendAndLog(notification, self._conf,
2052                                     "MaKaC/plugins/Collaboration/base.py",
2053                                     self._conf.getCreator())
2054            except Exception, e:
2055                Logger.get('VideoServ').error(
2056                    """Could not send BookingModifiedNotification for booking with id %s of event with id %s, exception: %s""" %
2057                    (self.getId(), self._conf.getId(), str(e)))
2058                raise
2059
2060        elif operation == 'remove':
2061            try:
2062                notification = mail.BookingDeletedNotification(self)
2063                GenericMailer.sendAndLog(notification, self._conf,
2064                                     "MaKaC/plugins/Collaboration/base.py",
2065                                     self._conf.getCreator())
2066            except Exception, e:
2067                Logger.get('VideoServ').error(
2068                    """Could not send BookingDeletedNotification for booking with id %s of event with id %s, exception: %s""" %
2069                    (self.getId(), self._conf.getId(), str(e)))
2070                raise
2071
2072    def getPlayStatus(self):
2073        if not hasattr(self, '_play_status'):
2074            self._play_status = None
2075        return self._play_status
2076
2077class WCSTemplateBase(wcomponents.WTemplated):
2078    """ Base class for Collaboration templates.
2079        It stores the following attributes:
2080            _conf : the corresponding Conference object.
2081            _pluginName: the corresponding plugin ("EVO", "DummyPlugin", etc.).
2082            _XXXOptions: a dictionary whose values are the options of the plugin called pluginName.
2083                         So, for example, if an EVO template inherits from this class, an attribute self._EVOOptions will be available.
2084        This class also overloads the _setTPLFile method so that Indico knows where each plugin's *.tpl files are.
2085    """
2086
2087    def __init__(self, pluginId):
2088        """ Constructor for the WCSTemplateBase class.
2089            conf: a Conference object
2090            plugin: the corresponding plugin
2091        """
2092        self._plugin = CollaborationTools.getPlugin(pluginId)
2093        self._pluginId = self._plugin.getId()
2094        self._ph = PluginsHolder()
2095
2096        setattr(self, "_" + self._pluginId + "Options", self._plugin.getOptions())
2097
2098    def _setTPLFile(self, extension='tpl'):
2099        tplDir = os.path.join(self._plugin.getModule().__path__[0], "tpls")
2100
2101        fname = "%s.%s" % (self.tplId, extension)
2102        self.tplFile = os.path.join(tplDir, fname)
2103
2104        hfile = self._getSpecificTPL(os.path.join(tplDir,self._pluginId,'chelp'), self.tplId,extension='wohl')
2105        self.helpFile = os.path.join(tplDir,'chelp',hfile)
2106
2107
2108class WCSPageTemplateBase(WCSTemplateBase):
2109    """ Base class for Collaboration templates for the create / modify booking form.
2110    """
2111
2112    def __init__(self, conf, pluginId, user):
2113        WCSTemplateBase.__init__(self, pluginId)
2114        self._conf = conf
2115        self._user = user
2116
2117
2118class WJSBase(WCSTemplateBase):
2119    """ Base class for Collaboration templates for Javascript code template.
2120        It overloads _setTPLFile so that indico can find the Main.js, Extra.js and Indexing.js files.
2121    """
2122    def __init__(self, conf, plugin, user):
2123        WCSTemplateBase.__init__(self, plugin)
2124        self._conf = conf
2125        self._user = user
2126
2127    def _setTPLFile(self):
2128        WCSTemplateBase._setTPLFile(self, extension='js')
2129        self.helpFile = ''
2130
2131
2132class WCSCSSBase(WCSTemplateBase):
2133    """ Base class for Collaboration templates for CSS code template
2134        It overloads _setTPLFile so that indico can find the style.css files.
2135    """
2136
2137    def _setTPLFile(self):
2138        tplDir = self._plugin.getModule().__path__[0]
2139        fname = "%s.css" % self.tplId
2140        self.tplFile = os.path.join(tplDir, fname)
2141        self.helpFile = ''
2142
2143
2144class CSErrorBase(Fossilizable):
2145    fossilizes(ICSErrorBaseFossil)
2146
2147    """ When _create, _modify or _remove want to return an error,
2148        they should return an error that inherits from this class
2149    """
2150
2151    def __init__(self):
2152        pass
2153
2154    def getUserMessage(self):
2155        """ To be overloaded.
2156            Returns the string that will be shown to the user when this error will happen.
2157        """
2158        raise CollaborationException("Method getUserMessage was not overriden for the a CSErrorBase object of class " + self.__class__.__name__)
2159
2160    def getLogMessage(self):
2161        """ To be overloaded.
2162            Returns the string that will be printed in Indico's log when this error will happen.
2163        """
2164        raise CollaborationException("Method getLogMessage was not overriden for the a CSErrorBase object of class " + self.__class__.__name__)
2165
2166class CSSanitizationError(CSErrorBase): #already Fossilizable
2167    fossilizes(ICSSanitizationErrorFossil)
2168
2169    """ Class used to return which fields have a sanitization error (invalid html / script tags)
2170    """
2171
2172    def __init__(self, invalidFields):
2173        self._invalidFields = invalidFields
2174
2175    def invalidFields(self):
2176        return self._invalidFields
2177
2178class CollaborationException(MaKaCError):
2179    """ Error for the Collaboration System "core". Each plugin should declare their own EVOError, etc.
2180    """
2181    def __init__(self, msg, area = 'Collaboration', inner = None):
2182        MaKaCError.__init__(self, msg, area)
2183        self._inner = inner
2184
2185    def getInner(self):
2186        return self._inner
2187
2188    def __str__(self):
2189        return MaKaCError.__str__(self) + '. Inner: ' + str(self._inner)
2190
2191class CollaborationServiceException(ServiceError):
2192    """ Error for the Collaboration System "core", for Service calls.
2193    """
2194    def __init__(self, message, inner = None):
2195        ServiceError.__init__(self, "ERR-COLL", message, inner)
2196
2197class SpeakerStatusEnum:
2198    (NOEMAIL, NOTSIGNED, SIGNED, FROMFILE, PENDING, REFUSED) = xrange(6)
2199
2200class SpeakerWrapper(Persistent, Fossilizable):
2201
2202    fossilizes(ISpeakerWrapperBaseFossil)
2203
2204    def __init__(self, speaker, contId, requestType):
2205        self.status = not speaker.getEmail() and SpeakerStatusEnum.NOEMAIL or SpeakerStatusEnum.NOTSIGNED
2206        self.speaker = speaker
2207        self.contId = contId
2208        self.requestType = requestType
2209        self.reason = ""
2210        self.localFile = None
2211        self.dateAgreement = 0
2212        self.ipSignature = None
2213        self.modificationDate = nowutc()
2214        self.uniqueIdHash = md5("%s.%s"%(time.time(), self.getUniqueId())).hexdigest()
2215
2216    def getUniqueId(self):
2217        return "%s.%s"%(self.contId, self.speaker.getId())
2218
2219    def getUniqueIdHash(self):
2220        # to remove once saved
2221        if not hasattr(self, "uniqueIdHash"):#TODO: remove when safe
2222            return md5(self.getUniqueId()).hexdigest()
2223        else:
2224            return self.uniqueIdHash
2225
2226    def getStatus(self):
2227        return self.status
2228
2229    def setStatus(self, newStatus, ip=None):
2230        try:
2231            self.status = newStatus;
2232            if newStatus == SpeakerStatusEnum.SIGNED or newStatus == SpeakerStatusEnum.FROMFILE:
2233                self.dateAgreement = now_utc()
2234                if newStatus == SpeakerStatusEnum.SIGNED:
2235                    self.ipSignature = ip
2236        except Exception, e:
2237            Logger.get('VideoServ').error("Exception while changing the speaker status. Exception: " + str(e))
2238
2239    def getDateAgreementSigned(self):
2240        if hasattr(self, "dateAgreement"):#TODO: remove when safe
2241            return self.dateAgreement
2242        return 0
2243
2244    def getIpAddressWhenSigned(self):
2245        if hasattr(self, "ipSignature"):#TODO: remove when safe
2246            return self.ipSignature
2247        return None
2248
2249    def getRejectReason(self):
2250        if hasattr(self, "reason"):#TODO: remove when safe
2251            if self.status == SpeakerStatusEnum.REFUSED and hasattr(self, "reason"):
2252                return self.reason
2253            else:
2254                return "This speaker has not refused the agreement."
2255        else:
2256            return "Information not available."
2257
2258    def setRejectReason(self, reason):
2259        if hasattr(self, "reason"):#TODO: remove when safe
2260            self.reason = reason
2261
2262    def getObject(self):
2263        return self.speaker
2264
2265    def getContId(self):
2266        return self.contId
2267
2268    def getRequestType(self):
2269        if hasattr(self, "requestType"):#TODO: remove when safe
2270            return self.requestType
2271        return "NA"
2272
2273    def setRequestType(self, type):
2274        self.requestType = type
2275
2276    def getSpeakerId(self):
2277        return self.speaker.getId()
2278
2279    def getLocalFile(self):
2280        '''
2281        If exists, return path to paper agreement
2282        '''
2283        if hasattr(self, "localFile"):#TODO: remove when safe
2284            return self.localFile
2285
2286    def setLocalFile(self, localFile):
2287        '''
2288        Set localFile of paper agreement
2289        '''
2290        if hasattr(self, "localFile"):#TODO: remove when safe
2291            self.localFile = localFile
2292
2293    def hasEmail(self):
2294        if self.speaker.getEmail():
2295            return True
2296        return False
2297
2298    def getCategory(self):
2299        return None
2300
2301    def getConference(self):
2302        return self.speaker.getConference()
2303
2304    def getContribution(self):
2305        # if the conference is a lecture, the getContribution will fail.
2306        if self.getConference().getType() == "simple_event":
2307            return None
2308        else:
2309            return self.speaker.getContribution()
2310
2311    def getSession(self):
2312        return None
2313
2314    def getSubContribution(self):
2315        return None
2316
2317    def getModificationDate(self):
2318        if hasattr(self, "modificationDate"):  # TODO: remove when safe
2319            return self.modificationDate
2320        return None
2321
2322    def setModificationDate(self):
2323        if hasattr(self, "modificationDate"):  # TODO: remove when safe
2324            self.modificationDate = now_utc()
2325
2326    def getLocator(self):
2327        return self.getContribution().getLocator()
2328
2329    def triggerNotification(self):
2330        if self.getRequestType() in ('recording', 'webcast'):
2331            self._triggerNotification(self.getRequestType())
2332        elif self.getRequestType() == 'both':
2333            self._triggerNotification('recording')
2334            self._triggerNotification('webcast')
2335
2336    def _triggerNotification(self, type):
2337        url = None
2338        if type == 'recording':
2339            url =  CollaborationTools.getOptionValue('RecordingRequest', 'AgreementNotificationURL')
2340        elif type == 'webcast':
2341            url =  CollaborationTools.getOptionValue('WebcastRequest', 'AgreementNotificationURL')
2342        if not url:
2343            return
2344        signed = None
2345        if self.getStatus() in (SpeakerStatusEnum.FROMFILE, SpeakerStatusEnum.SIGNED):
2346            signed = True
2347        elif self.getStatus() == SpeakerStatusEnum.REFUSED:
2348            signed = False
2349        spk = self.getObject()
2350        payload = {
2351            'confId': self.getConference().getId(),
2352            'contrib': self.getContId(),
2353            'type': type,
2354            'status': self.getStatus(),
2355            'signed': signed,
2356            'speaker': {
2357                'id': spk.getId(),
2358                'name': spk.getFullName(),
2359                'email': spk.getEmail()
2360            }
2361        }
2362        cl = Client()
2363        cl.enqueue(HTTPTask(url, {'data': json.dumps(payload)}))
Note: See TracBrowser for help on using the repository browser.