imip-agent

Annotated imiptools/handlers/person_outgoing.py

1126:df5219c279f5
2016-04-19 Paul Boddie Avoid comparing floating datetimes where only date values should be compared. freebusy-collections
paul@96 1
#!/usr/bin/env python
paul@96 2
paul@96 3
"""
paul@96 4
Handlers for a person for whom scheduling is performed, inspecting outgoing
paul@96 5
messages to obtain scheduling done externally.
paul@146 6
paul@1123 7
Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
paul@146 8
paul@146 9
This program is free software; you can redistribute it and/or modify it under
paul@146 10
the terms of the GNU General Public License as published by the Free Software
paul@146 11
Foundation; either version 3 of the License, or (at your option) any later
paul@146 12
version.
paul@146 13
paul@146 14
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 15
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 16
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 17
details.
paul@146 18
paul@146 19
You should have received a copy of the GNU General Public License along with
paul@146 20
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@96 21
"""
paul@96 22
paul@705 23
from imiptools.client import Client
paul@847 24
from imiptools.data import get_uri, uri_dict, uri_values
paul@418 25
from imiptools.handlers import Handler
paul@683 26
from imiptools.handlers.common import CommonEvent
paul@96 27
paul@725 28
class PersonHandler(CommonEvent, Handler):
paul@96 29
paul@96 30
    "Handling mechanisms specific to people."
paul@96 31
paul@729 32
    def set_identity(self, method):
paul@130 33
paul@140 34
        """
paul@729 35
        Set the current user for the current object in the context of the given
paul@729 36
        'method'. It is usually set when initialising the handler, using the
paul@729 37
        recipient details, but outgoing messages do not reference the recipient
paul@729 38
        in this way.
paul@140 39
        """
paul@130 40
paul@832 41
        if self.obj and not self.user:
paul@729 42
            from_organiser = method in self.organiser_methods
paul@832 43
            if from_organiser:
paul@832 44
                self.user = get_uri(self.obj.get_value("ORGANIZER"))
paul@832 45
paul@832 46
            # Since there may be many attendees in an attendee-provided outgoing
paul@832 47
            # message, because counter-proposals can have more than one
paul@832 48
            # attendee, the attendee originating from the calendar system is
paul@832 49
            # chosen.
paul@832 50
paul@832 51
            else:
paul@847 52
                self.user = self.get_sending_attendee()
paul@140 53
paul@684 54
    def _add(self):
paul@684 55
paul@684 56
        "Add a recurrence for the current object."
paul@684 57
paul@705 58
        if not Client.is_participating(self):
paul@705 59
            return False
paul@684 60
paul@832 61
        # Check for event using UID.
paul@684 62
paul@832 63
        if not self.have_new_object():
paul@684 64
            return False
paul@684 65
paul@684 66
        # Ignore unknown objects.
paul@684 67
paul@684 68
        if not self.get_stored_object_version():
paul@684 69
            return
paul@684 70
paul@684 71
        # Record the event as a recurrence of the parent object.
paul@684 72
paul@684 73
        self.update_recurrenceid()
paul@684 74
paul@733 75
        # Set the additional occurrence.
paul@733 76
paul@733 77
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@733 78
paul@859 79
        # Remove any previous cancellations involving this event.
paul@859 80
paul@859 81
        self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@859 82
paul@684 83
        # Update free/busy information.
paul@684 84
paul@684 85
        self.update_event_in_freebusy()
paul@684 86
paul@684 87
        return True
paul@684 88
paul@799 89
    def _record(self, from_organiser=True, counter=False):
paul@140 90
paul@420 91
        """
paul@420 92
        Record details from the current object given a message originating
paul@580 93
        from an organiser if 'from_organiser' is set to a true value.
paul@420 94
        """
paul@140 95
paul@705 96
        if not Client.is_participating(self):
paul@705 97
            return False
paul@468 98
paul@682 99
        # Check for a new event, tolerating not-strictly-new events if the
paul@682 100
        # attendee is responding.
paul@468 101
paul@682 102
        if not self.have_new_object(strict=from_organiser):
paul@99 103
            return False
paul@97 104
paul@268 105
        # Update the object.
paul@96 106
paul@268 107
        if from_organiser:
paul@334 108
paul@361 109
            # Set the complete event or an additional occurrence.
paul@334 110
paul@468 111
            self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@334 112
paul@381 113
            # Remove additional recurrences if handling a complete event.
paul@796 114
            # Also remove any previous cancellations involving this event.
paul@381 115
paul@381 116
            if not self.recurrenceid:
paul@468 117
                self.store.remove_recurrences(self.user, self.uid)
paul@796 118
                self.store.remove_cancellations(self.user, self.uid)
paul@796 119
            else:
paul@796 120
                self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@381 121
paul@268 122
        else:
paul@1125 123
            # Occurrences that are still part of a parent object are separated,
paul@1125 124
            # attendance information transferred, and the free/busy details
paul@1125 125
            # updated.
paul@1125 126
paul@1125 127
            if self.is_newly_separated_occurrence():
paul@1125 128
                self.make_separate_occurrence(for_organiser=not from_organiser)
paul@1123 129
paul@420 130
            # Obtain valid attendees, merging their attendance with the stored
paul@420 131
            # object.
paul@420 132
paul@1125 133
            else:
paul@1125 134
                attendees = self.require_attendees(from_organiser)
paul@1125 135
                self.merge_attendance(attendees)
paul@96 136
paul@160 137
        # Remove any associated request.
paul@160 138
paul@917 139
        if from_organiser or self.has_indicated_attendance():
paul@917 140
            self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@809 141
        self.store.remove_counters(self.user, self.uid, self.recurrenceid)
paul@160 142
paul@96 143
        # Update free/busy information.
paul@96 144
paul@799 145
        if not counter:
paul@799 146
            self.update_event_in_freebusy(from_organiser)
paul@799 147
paul@799 148
        # For countered proposals, record the offer in the resource's
paul@799 149
        # free/busy collection.
paul@799 150
paul@799 151
        else:
paul@799 152
            self.update_event_in_freebusy_offers()
paul@96 153
paul@96 154
        return True
paul@96 155
paul@697 156
    def _remove(self):
paul@140 157
paul@580 158
        """
paul@580 159
        Remove details from the current object given a message originating
paul@580 160
        from an organiser if 'from_organiser' is set to a true value.
paul@580 161
        """
paul@140 162
paul@705 163
        if not Client.is_participating(self):
paul@705 164
            return False
paul@468 165
paul@468 166
        # Check for event using UID.
paul@468 167
paul@468 168
        if not self.have_new_object():
paul@140 169
            return False
paul@140 170
paul@527 171
        # Obtain any stored object, using parent object details if a newly-
paul@527 172
        # indicated occurrence is referenced.
paul@312 173
paul@606 174
        obj = self.get_stored_object_version()
paul@527 175
        old = not obj and self.get_parent_object() or obj
paul@527 176
paul@527 177
        if not old:
paul@420 178
            return False
paul@261 179
paul@527 180
        # Only cancel the event completely if all attendees are given.
paul@527 181
paul@527 182
        attendees = uri_dict(old.get_value_map("ATTENDEE"))
paul@312 183
        all_attendees = set(attendees.keys())
paul@312 184
        given_attendees = set(uri_values(self.obj.get_values("ATTENDEE")))
paul@690 185
        cancel_entire_event = not all_attendees.difference(given_attendees)
paul@312 186
paul@697 187
        # Update the recipient's record of the organiser's schedule.
paul@697 188
paul@697 189
        self.remove_freebusy_from_organiser(self.obj.get_value("ORGANIZER"))
paul@697 190
paul@312 191
        # Otherwise, remove the given attendees and update the event.
paul@312 192
paul@694 193
        if not cancel_entire_event and obj:
paul@312 194
            for attendee in given_attendees:
paul@314 195
                if attendees.has_key(attendee):
paul@314 196
                    del attendees[attendee]
paul@312 197
            obj["ATTENDEE"] = attendees.items()
paul@264 198
paul@312 199
        # Update the stored object with sequence information.
paul@312 200
paul@527 201
        if obj:
paul@577 202
            obj["SEQUENCE"] = self.obj.get_items("SEQUENCE") or []
paul@577 203
            obj["DTSTAMP"] = self.obj.get_items("DTSTAMP") or []
paul@312 204
paul@672 205
        # Update free/busy information.
paul@672 206
paul@672 207
        if cancel_entire_event or self.user in given_attendees:
paul@672 208
            self.remove_event_from_freebusy()
paul@911 209
            self.remove_freebusy_from_attendees(attendees)
paul@672 210
paul@527 211
        # Set the complete event if not an additional occurrence. For any newly-
paul@527 212
        # indicated occurrence, use the received event details.
paul@334 213
paul@527 214
        self.store.set_event(self.user, self.uid, self.recurrenceid, (obj or self.obj).to_node())
paul@264 215
paul@694 216
        # Perform any cancellation after recording the latest state of the
paul@694 217
        # event.
paul@694 218
paul@694 219
        if cancel_entire_event:
paul@694 220
            self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@694 221
paul@261 222
        # Remove any associated request.
paul@261 223
paul@468 224
        self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@809 225
        self.store.remove_counters(self.user, self.uid, self.recurrenceid)
paul@261 226
paul@140 227
        return True
paul@140 228
paul@804 229
    def _declinecounter(self):
paul@804 230
paul@804 231
        "Remove any counter-proposals for the given event."
paul@804 232
paul@804 233
        if not Client.is_participating(self):
paul@804 234
            return False
paul@804 235
paul@804 236
        # Check for event using UID.
paul@804 237
paul@804 238
        if not self.have_new_object():
paul@804 239
            return False
paul@804 240
paul@809 241
        self.remove_counters(uri_values(self.obj.get_values("ATTENDEE")))
paul@804 242
paul@96 243
class Event(PersonHandler):
paul@96 244
paul@96 245
    "An event handler."
paul@96 246
paul@96 247
    def add(self):
paul@686 248
paul@686 249
        "Record the addition of a recurrence to an event."
paul@686 250
paul@684 251
        self._add()
paul@96 252
paul@96 253
    def cancel(self):
paul@686 254
paul@686 255
        "Remove an event or a recurrence."
paul@686 256
paul@697 257
        self._remove()
paul@96 258
paul@96 259
    def counter(self):
paul@686 260
paul@799 261
        "Record an offer made by a counter-proposal."
paul@686 262
paul@799 263
        self._record(False, True)
paul@96 264
paul@96 265
    def declinecounter(self):
paul@686 266
paul@804 267
        "Expire any offer made by a counter-proposal."
paul@686 268
paul@804 269
        self._declinecounter()
paul@96 270
paul@96 271
    def publish(self):
paul@686 272
paul@686 273
        "Published events are recorded."
paul@686 274
paul@580 275
        self._record(True)
paul@96 276
paul@96 277
    def refresh(self):
paul@686 278
paul@686 279
        "Requests to refresh events do not provide event information."
paul@686 280
paul@353 281
        pass
paul@96 282
paul@96 283
    def reply(self):
paul@686 284
paul@686 285
        "Replies to requests are inspected for attendee information."
paul@686 286
paul@580 287
        self._record(False)
paul@96 288
paul@96 289
    def request(self):
paul@686 290
paul@686 291
        "Record events sent for potential scheduling."
paul@686 292
paul@580 293
        self._record(True)
paul@96 294
paul@96 295
# Handler registry.
paul@96 296
paul@96 297
handlers = [
paul@96 298
    ("VEVENT",      Event),
paul@96 299
    ]
paul@96 300
paul@96 301
# vim: tabstop=4 expandtab shiftwidth=4