imip-agent

Annotated imiptools/handlers/resource.py

796:d5dc0ff83bcc
2015-10-02 Paul Boddie Remove any previous cancellations when handling events.
paul@48 1
#!/usr/bin/env python
paul@48 2
paul@48 3
"""
paul@48 4
Handlers for a resource.
paul@146 5
paul@146 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@146 7
paul@146 8
This program is free software; you can redistribute it and/or modify it under
paul@146 9
the terms of the GNU General Public License as published by the Free Software
paul@146 10
Foundation; either version 3 of the License, or (at your option) any later
paul@146 11
version.
paul@146 12
paul@146 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 16
details.
paul@146 17
paul@146 18
You should have received a copy of the GNU General Public License along with
paul@146 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@48 20
"""
paul@48 21
paul@737 22
from imiptools.data import get_address, to_part, uri_dict
paul@662 23
from imiptools.dates import ValidityError
paul@418 24
from imiptools.handlers import Handler
paul@683 25
from imiptools.handlers.common import CommonFreebusy, CommonEvent
paul@48 26
paul@725 27
class ResourceHandler(CommonEvent, Handler):
paul@131 28
paul@131 29
    "Handling mechanisms specific to resources."
paul@131 30
paul@131 31
    def _record_and_respond(self, handle_for_attendee):
paul@131 32
paul@420 33
        """
paul@420 34
        Record details from the incoming message, using the given
paul@420 35
        'handle_for_attendee' callable to process any valid message
paul@420 36
        appropriately.
paul@420 37
        """
paul@420 38
paul@131 39
        oa = self.require_organiser_and_attendees()
paul@131 40
        if not oa:
paul@131 41
            return None
paul@131 42
paul@131 43
        organiser_item, attendees = oa
paul@131 44
paul@468 45
        # Process for the current user, a resource as attendee.
paul@131 46
paul@738 47
        if not self.have_new_object():
paul@468 48
            return None
paul@131 49
paul@468 50
        # Collect response objects produced when handling the request.
paul@131 51
paul@662 52
        handle_for_attendee()
paul@131 53
paul@676 54
    def _add_for_attendee(self):
paul@131 55
paul@420 56
        """
paul@676 57
        Attempt to add a recurrence to an existing object for the current user.
paul@676 58
        This does not request a response concerning participation, apparently.
paul@420 59
        """
paul@420 60
paul@737 61
        # Request details where configured, doing so for unknown objects anyway.
paul@676 62
paul@737 63
        if self.will_refresh():
paul@737 64
            self.make_refresh()
paul@676 65
            return
paul@676 66
paul@676 67
        # Record the event as a recurrence of the parent object.
paul@676 68
paul@676 69
        self.update_recurrenceid()
paul@737 70
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@676 71
paul@676 72
        # Update free/busy information.
paul@676 73
paul@676 74
        self.update_event_in_freebusy(for_organiser=False)
paul@676 75
paul@676 76
    def _schedule_for_attendee(self):
paul@676 77
paul@676 78
        "Attempt to schedule the current object for the current user."
paul@676 79
paul@662 80
        method = "REPLY"
paul@737 81
        attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user]
paul@662 82
paul@655 83
        # Check any constraints on the request.
paul@655 84
paul@655 85
        try:
paul@662 86
            corrected = self.correct_object()
paul@655 87
paul@655 88
        # Refuse to schedule obviously invalid requests.
paul@655 89
paul@662 90
        except ValidityError:
paul@710 91
            attendee_attr = self.update_participation(self.obj, "DECLINED")
paul@291 92
paul@655 93
        # With a valid request, determine whether the event can be scheduled.
paul@655 94
paul@655 95
        else:
paul@655 96
            # Interpretation of periods can depend on the time zone.
paul@655 97
paul@655 98
            tzid = self.get_tzid()
paul@131 99
paul@655 100
            # If newer than any old version, discard old details from the
paul@655 101
            # free/busy record and check for suitability.
paul@655 102
paul@655 103
            periods = self.obj.get_periods(tzid, self.get_window_end())
paul@710 104
paul@730 105
            freebusy = self.store.get_freebusy(self.user)
paul@730 106
            offers = self.store.get_freebusy_offers(self.user)
paul@730 107
paul@730 108
            # Check the periods against any scheduled events and against
paul@730 109
            # any outstanding offers.
paul@655 110
paul@730 111
            scheduled = self.can_schedule(freebusy, periods)
paul@730 112
            scheduled = scheduled and self.can_schedule(offers, periods)
paul@662 113
paul@730 114
            # Where the corrected object can be scheduled, issue a counter
paul@730 115
            # request.
paul@662 116
paul@730 117
            if scheduled and corrected:
paul@730 118
                method = "COUNTER"
paul@662 119
paul@730 120
            # Find the next available slot if the event cannot be scheduled.
paul@662 121
paul@730 122
            #elif not scheduled and len(periods) == 1:
paul@662 123
paul@730 124
            #    # Find a free period, update the object with the details.
paul@710 125
paul@730 126
            #    duration = periods[0].get_duration()
paul@730 127
            #    free = invert_freebusy(freebusy)
paul@662 128
paul@730 129
            #    for found in periods_from(free, periods[0]):
paul@730 130
            #        # NOTE: Correct the found period first.
paul@730 131
            #        if found.get_duration() >= duration
paul@730 132
            #            scheduled = True
paul@730 133
            #            method = "COUNTER"
paul@730 134
            #            # NOTE: Set the period using the original duration.
paul@730 135
            #            break
paul@131 136
paul@730 137
            # Update the participation of the resource in the object.
paul@730 138
            # Update free/busy information.
paul@131 139
paul@730 140
            if method == "REPLY":
paul@745 141
                attendee_attr = self.update_participation(self.obj,
paul@745 142
                    scheduled and "ACCEPTED" or "DECLINED")
paul@745 143
paul@730 144
                self.update_event_in_freebusy(for_organiser=False)
paul@730 145
                self.remove_event_from_freebusy_offers()
paul@334 146
paul@730 147
            # For countered proposals, record the offer in the resource's
paul@730 148
            # free/busy collection.
paul@131 149
paul@730 150
            elif method == "COUNTER":
paul@730 151
                self.update_event_in_freebusy_offers()
paul@662 152
paul@710 153
        # Set the complete event or an additional occurrence.
paul@710 154
paul@711 155
        if method == "REPLY":
paul@711 156
            event = self.obj.to_node()
paul@711 157
            self.store.set_event(self.user, self.uid, self.recurrenceid, event)
paul@381 158
paul@711 159
            # Remove additional recurrences if handling a complete event.
paul@796 160
            # Also remove any previous cancellations involving this event.
paul@381 161
paul@711 162
            if not self.recurrenceid:
paul@711 163
                self.store.remove_recurrences(self.user, self.uid)
paul@796 164
                self.store.remove_cancellations(self.user, self.uid)
paul@796 165
            else:
paul@796 166
                self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@361 167
paul@580 168
        # Make a version of the object with just this attendee, update the
paul@580 169
        # DTSTAMP in the response, and return the object for sending.
paul@574 170
paul@745 171
        self.update_sender(attendee_attr)
paul@574 172
        self.obj["ATTENDEE"] = [(self.user, attendee_attr)]
paul@574 173
        self.update_dtstamp()
paul@662 174
        self.add_result(method, map(get_address, self.obj.get_values("ORGANIZER")), to_part(method, [self.obj.to_node()]))
paul@131 175
paul@468 176
    def _cancel_for_attendee(self):
paul@131 177
paul@420 178
        """
paul@468 179
        Cancel for the current user their attendance of the event described by
paul@468 180
        the current object.
paul@420 181
        """
paul@420 182
paul@580 183
        # Update free/busy information.
paul@131 184
paul@580 185
        self.remove_event_from_freebusy()
paul@580 186
paul@672 187
        # Update the stored event and cancel it.
paul@672 188
paul@672 189
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@672 190
        self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@672 191
paul@710 192
    def _revoke_for_attendee(self):
paul@710 193
paul@710 194
        "Revoke any counter-proposal recorded as a free/busy offer."
paul@710 195
paul@710 196
        self.remove_event_from_freebusy_offers()
paul@710 197
paul@131 198
class Event(ResourceHandler):
paul@48 199
paul@48 200
    "An event handler."
paul@48 201
paul@48 202
    def add(self):
paul@676 203
paul@676 204
        "Add a new occurrence to an existing event."
paul@676 205
paul@676 206
        self._record_and_respond(self._add_for_attendee)
paul@48 207
paul@48 208
    def cancel(self):
paul@131 209
paul@131 210
        "Cancel attendance for attendees."
paul@131 211
paul@131 212
        self._record_and_respond(self._cancel_for_attendee)
paul@48 213
paul@48 214
    def counter(self):
paul@48 215
paul@48 216
        "Since this handler does not send requests, it will not handle replies."
paul@48 217
paul@48 218
        pass
paul@48 219
paul@48 220
    def declinecounter(self):
paul@48 221
paul@710 222
        "Revoke any counter-proposal."
paul@48 223
paul@710 224
        self._record_and_respond(self._revoke_for_attendee)
paul@48 225
paul@48 226
    def publish(self):
paul@676 227
paul@676 228
        """
paul@676 229
        Resources only consider events sent as requests, not generally published
paul@676 230
        events.
paul@676 231
        """
paul@676 232
paul@48 233
        pass
paul@48 234
paul@48 235
    def refresh(self):
paul@626 236
paul@626 237
        """
paul@626 238
        Refresh messages are typically sent to event organisers, but resources
paul@626 239
        do not act as organisers themselves.
paul@626 240
        """
paul@48 241
paul@676 242
        pass
paul@676 243
paul@48 244
    def reply(self):
paul@48 245
paul@48 246
        "Since this handler does not send requests, it will not handle replies."
paul@48 247
paul@48 248
        pass
paul@48 249
paul@48 250
    def request(self):
paul@48 251
paul@48 252
        """
paul@48 253
        Respond to a request by preparing a reply containing accept/decline
paul@468 254
        information for the recipient.
paul@48 255
paul@48 256
        No support for countering requests is implemented.
paul@48 257
        """
paul@48 258
paul@662 259
        self._record_and_respond(self._schedule_for_attendee)
paul@48 260
paul@725 261
class Freebusy(CommonFreebusy, Handler):
paul@48 262
paul@48 263
    "A free/busy handler."
paul@48 264
paul@48 265
    def publish(self):
paul@676 266
paul@676 267
        "Resources ignore generally published free/busy information."
paul@676 268
paul@48 269
        pass
paul@48 270
paul@48 271
    def reply(self):
paul@48 272
paul@48 273
        "Since this handler does not send requests, it will not handle replies."
paul@48 274
paul@48 275
        pass
paul@48 276
paul@108 277
    # request provided by CommonFreeBusy.request
paul@48 278
paul@48 279
# Handler registry.
paul@48 280
paul@48 281
handlers = [
paul@48 282
    ("VFREEBUSY",   Freebusy),
paul@48 283
    ("VEVENT",      Event),
paul@48 284
    ]
paul@48 285
paul@48 286
# vim: tabstop=4 expandtab shiftwidth=4