imip-agent

Annotated imiptools/handlers/resource.py

1126:df5219c279f5
2016-04-19 Paul Boddie Avoid comparing floating datetimes where only date values should be compared. freebusy-collections
paul@48 1
#!/usr/bin/env python
paul@48 2
paul@48 3
"""
paul@48 4
Handlers for a resource.
paul@146 5
paul@1025 6
Copyright (C) 2014, 2015, 2016 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@418 23
from imiptools.handlers import Handler
paul@683 24
from imiptools.handlers.common import CommonFreebusy, CommonEvent
paul@1039 25
from imiptools.handlers.scheduling import apply_scheduling_functions, \
paul@1040 26
                                          confirm_scheduling, \
paul@1040 27
                                          finish_scheduling, \
paul@1040 28
                                          retract_scheduling
paul@48 29
paul@725 30
class ResourceHandler(CommonEvent, Handler):
paul@131 31
paul@131 32
    "Handling mechanisms specific to resources."
paul@131 33
paul@937 34
    def _process(self, handle_for_attendee):
paul@131 35
paul@420 36
        """
paul@420 37
        Record details from the incoming message, using the given
paul@420 38
        'handle_for_attendee' callable to process any valid message
paul@420 39
        appropriately.
paul@420 40
        """
paul@420 41
paul@131 42
        oa = self.require_organiser_and_attendees()
paul@131 43
        if not oa:
paul@131 44
            return None
paul@131 45
paul@131 46
        organiser_item, attendees = oa
paul@131 47
paul@468 48
        # Process for the current user, a resource as attendee.
paul@131 49
paul@738 50
        if not self.have_new_object():
paul@468 51
            return None
paul@131 52
paul@468 53
        # Collect response objects produced when handling the request.
paul@131 54
paul@662 55
        handle_for_attendee()
paul@131 56
paul@676 57
    def _add_for_attendee(self):
paul@131 58
paul@420 59
        """
paul@676 60
        Attempt to add a recurrence to an existing object for the current user.
paul@676 61
        This does not request a response concerning participation, apparently.
paul@420 62
        """
paul@420 63
paul@737 64
        # Request details where configured, doing so for unknown objects anyway.
paul@676 65
paul@737 66
        if self.will_refresh():
paul@737 67
            self.make_refresh()
paul@676 68
            return
paul@676 69
paul@676 70
        # Record the event as a recurrence of the parent object.
paul@676 71
paul@676 72
        self.update_recurrenceid()
paul@737 73
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@676 74
paul@859 75
        # Remove any previous cancellations involving this event.
paul@859 76
paul@859 77
        self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@859 78
paul@676 79
        # Update free/busy information.
paul@676 80
paul@676 81
        self.update_event_in_freebusy(for_organiser=False)
paul@676 82
paul@1039 83
        # Confirm the scheduling of the recurrence.
paul@1039 84
paul@1039 85
        self.confirm_scheduling()
paul@1039 86
paul@676 87
    def _schedule_for_attendee(self):
paul@676 88
paul@676 89
        "Attempt to schedule the current object for the current user."
paul@676 90
paul@737 91
        attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user]
paul@1040 92
paul@1040 93
        # Attempt to schedule the event.
paul@1040 94
paul@936 95
        scheduled = self.schedule()
paul@655 96
paul@1040 97
        try:
paul@1040 98
            # Update the participation of the resource in the object.
paul@1040 99
            # Update free/busy information.
paul@662 100
paul@1040 101
            if scheduled in ("ACCEPTED", "DECLINED"):
paul@1040 102
                method = "REPLY"
paul@1040 103
                attendee_attr = self.update_participation(scheduled)
paul@662 104
paul@1040 105
                self.update_event_in_freebusy(for_organiser=False)
paul@1040 106
                self.remove_event_from_freebusy_offers()
paul@131 107
paul@1040 108
                # Set the complete event or an additional occurrence.
paul@334 109
paul@1040 110
                event = self.obj.to_node()
paul@1040 111
                self.store.set_event(self.user, self.uid, self.recurrenceid, event)
paul@381 112
paul@1040 113
                # Remove additional recurrences if handling a complete event.
paul@1040 114
                # Also remove any previous cancellations involving this event.
paul@381 115
paul@1040 116
                if not self.recurrenceid:
paul@1040 117
                    self.store.remove_recurrences(self.user, self.uid)
paul@1040 118
                    self.store.remove_cancellations(self.user, self.uid)
paul@1040 119
                else:
paul@1040 120
                    self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@361 121
paul@1040 122
                if scheduled == "ACCEPTED":
paul@1040 123
                    self.confirm_scheduling()
paul@1039 124
paul@1040 125
            # For countered proposals, record the offer in the resource's
paul@1040 126
            # free/busy collection.
paul@1039 127
paul@1040 128
            elif scheduled == "COUNTER":
paul@1040 129
                method = "COUNTER"
paul@1040 130
                self.update_event_in_freebusy_offers()
paul@1040 131
paul@1040 132
            # For inappropriate periods, reply declining participation.
paul@936 133
paul@1040 134
            else:
paul@1040 135
                method = "REPLY"
paul@1040 136
                attendee_attr = self.update_participation("DECLINED")
paul@936 137
paul@1040 138
        # Confirm any scheduling.
paul@936 139
paul@1040 140
        finally:
paul@1040 141
            self.finish_scheduling()
paul@936 142
paul@580 143
        # Make a version of the object with just this attendee, update the
paul@580 144
        # DTSTAMP in the response, and return the object for sending.
paul@574 145
paul@745 146
        self.update_sender(attendee_attr)
paul@574 147
        self.obj["ATTENDEE"] = [(self.user, attendee_attr)]
paul@574 148
        self.update_dtstamp()
paul@662 149
        self.add_result(method, map(get_address, self.obj.get_values("ORGANIZER")), to_part(method, [self.obj.to_node()]))
paul@131 150
paul@468 151
    def _cancel_for_attendee(self):
paul@131 152
paul@420 153
        """
paul@468 154
        Cancel for the current user their attendance of the event described by
paul@468 155
        the current object.
paul@420 156
        """
paul@420 157
paul@580 158
        # Update free/busy information.
paul@131 159
paul@580 160
        self.remove_event_from_freebusy()
paul@580 161
paul@672 162
        # Update the stored event and cancel it.
paul@672 163
paul@672 164
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@672 165
        self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@672 166
paul@1039 167
        # Retract the scheduling of the event.
paul@1039 168
paul@1039 169
        self.retract_scheduling()
paul@1039 170
paul@710 171
    def _revoke_for_attendee(self):
paul@710 172
paul@710 173
        "Revoke any counter-proposal recorded as a free/busy offer."
paul@710 174
paul@710 175
        self.remove_event_from_freebusy_offers()
paul@710 176
paul@936 177
    # Scheduling details.
paul@936 178
paul@1058 179
    def get_scheduling_functions(self):
paul@1058 180
paul@1058 181
        "Return the scheduling functions for the resource."
paul@1058 182
paul@1058 183
        return self.get_preferences().get("scheduling_function",
paul@1058 184
            "schedule_in_freebusy").split("\n")
paul@1058 185
paul@936 186
    def schedule(self):
paul@936 187
paul@936 188
        """
paul@936 189
        Attempt to schedule the current object, returning an indication of the
paul@936 190
        kind of response to be returned: "COUNTER" for counter-proposals,
paul@936 191
        "ACCEPTED" for acceptances, "DECLINED" for rejections, and None for
paul@936 192
        invalid requests.
paul@936 193
        """
paul@936 194
paul@1058 195
        return apply_scheduling_functions(self)
paul@936 196
paul@1039 197
    def confirm_scheduling(self):
paul@1039 198
paul@1039 199
        "Confirm that this event has been scheduled."
paul@1039 200
paul@1058 201
        confirm_scheduling(self)
paul@1039 202
paul@1040 203
    def finish_scheduling(self):
paul@1040 204
paul@1040 205
        "Finish the scheduling, unlocking resources where appropriate."
paul@1040 206
paul@1058 207
        finish_scheduling(self)
paul@1040 208
paul@1039 209
    def retract_scheduling(self):
paul@1039 210
paul@1039 211
        "Retract this event from scheduling records."
paul@1039 212
paul@1058 213
        retract_scheduling(self)
paul@1039 214
paul@131 215
class Event(ResourceHandler):
paul@48 216
paul@48 217
    "An event handler."
paul@48 218
paul@48 219
    def add(self):
paul@676 220
paul@676 221
        "Add a new occurrence to an existing event."
paul@676 222
paul@937 223
        self._process(self._add_for_attendee)
paul@48 224
paul@48 225
    def cancel(self):
paul@131 226
paul@131 227
        "Cancel attendance for attendees."
paul@131 228
paul@937 229
        self._process(self._cancel_for_attendee)
paul@48 230
paul@48 231
    def counter(self):
paul@48 232
paul@48 233
        "Since this handler does not send requests, it will not handle replies."
paul@48 234
paul@48 235
        pass
paul@48 236
paul@48 237
    def declinecounter(self):
paul@48 238
paul@710 239
        "Revoke any counter-proposal."
paul@48 240
paul@937 241
        self._process(self._revoke_for_attendee)
paul@48 242
paul@48 243
    def publish(self):
paul@676 244
paul@676 245
        """
paul@676 246
        Resources only consider events sent as requests, not generally published
paul@676 247
        events.
paul@676 248
        """
paul@676 249
paul@48 250
        pass
paul@48 251
paul@48 252
    def refresh(self):
paul@626 253
paul@626 254
        """
paul@626 255
        Refresh messages are typically sent to event organisers, but resources
paul@626 256
        do not act as organisers themselves.
paul@626 257
        """
paul@48 258
paul@676 259
        pass
paul@676 260
paul@48 261
    def reply(self):
paul@48 262
paul@48 263
        "Since this handler does not send requests, it will not handle replies."
paul@48 264
paul@48 265
        pass
paul@48 266
paul@48 267
    def request(self):
paul@48 268
paul@48 269
        """
paul@48 270
        Respond to a request by preparing a reply containing accept/decline
paul@468 271
        information for the recipient.
paul@48 272
paul@48 273
        No support for countering requests is implemented.
paul@48 274
        """
paul@48 275
paul@937 276
        self._process(self._schedule_for_attendee)
paul@48 277
paul@725 278
class Freebusy(CommonFreebusy, Handler):
paul@48 279
paul@48 280
    "A free/busy handler."
paul@48 281
paul@48 282
    def publish(self):
paul@676 283
paul@676 284
        "Resources ignore generally published free/busy information."
paul@676 285
paul@943 286
        self._record_freebusy(from_organiser=True)
paul@48 287
paul@48 288
    def reply(self):
paul@48 289
paul@48 290
        "Since this handler does not send requests, it will not handle replies."
paul@48 291
paul@48 292
        pass
paul@48 293
paul@108 294
    # request provided by CommonFreeBusy.request
paul@48 295
paul@48 296
# Handler registry.
paul@48 297
paul@48 298
handlers = [
paul@48 299
    ("VFREEBUSY",   Freebusy),
paul@48 300
    ("VEVENT",      Event),
paul@48 301
    ]
paul@48 302
paul@48 303
# vim: tabstop=4 expandtab shiftwidth=4