imip-agent

Annotated imiptools/handlers/resource.py

1031:fc1efc973849
2016-01-31 Paul Boddie Introduced the access control list scheduling function plus argument support. Moved scheduling function lookup into the scheduling module as part of the revised function invocation parsing activity, employing a new text processing module in imiptools. Added tests of the access control list functionality.
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@1031 25
from imiptools.handlers.scheduling import apply_scheduling_functions
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@937 31
    def _process(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@859 72
        # Remove any previous cancellations involving this event.
paul@859 73
paul@859 74
        self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@859 75
paul@676 76
        # Update free/busy information.
paul@676 77
paul@676 78
        self.update_event_in_freebusy(for_organiser=False)
paul@676 79
paul@676 80
    def _schedule_for_attendee(self):
paul@676 81
paul@676 82
        "Attempt to schedule the current object for the current user."
paul@676 83
paul@737 84
        attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user]
paul@936 85
        scheduled = self.schedule()
paul@655 86
paul@936 87
        # Update the participation of the resource in the object.
paul@936 88
        # Update free/busy information.
paul@662 89
paul@936 90
        if scheduled in ("ACCEPTED", "DECLINED"):
paul@936 91
            method = "REPLY"
paul@936 92
            attendee_attr = self.update_participation(scheduled)
paul@662 93
paul@936 94
            self.update_event_in_freebusy(for_organiser=False)
paul@936 95
            self.remove_event_from_freebusy_offers()
paul@131 96
paul@936 97
            # Set the complete event or an additional occurrence.
paul@334 98
paul@711 99
            event = self.obj.to_node()
paul@711 100
            self.store.set_event(self.user, self.uid, self.recurrenceid, event)
paul@381 101
paul@711 102
            # Remove additional recurrences if handling a complete event.
paul@796 103
            # Also remove any previous cancellations involving this event.
paul@381 104
paul@711 105
            if not self.recurrenceid:
paul@711 106
                self.store.remove_recurrences(self.user, self.uid)
paul@796 107
                self.store.remove_cancellations(self.user, self.uid)
paul@796 108
            else:
paul@796 109
                self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@361 110
paul@936 111
        # For countered proposals, record the offer in the resource's
paul@936 112
        # free/busy collection.
paul@936 113
paul@936 114
        elif scheduled == "COUNTER":
paul@936 115
            method = "COUNTER"
paul@936 116
            self.update_event_in_freebusy_offers()
paul@936 117
paul@936 118
        # For inappropriate periods, reply declining participation.
paul@936 119
paul@936 120
        else:
paul@936 121
            method = "REPLY"
paul@936 122
            attendee_attr = self.update_participation("DECLINED")
paul@936 123
paul@580 124
        # Make a version of the object with just this attendee, update the
paul@580 125
        # DTSTAMP in the response, and return the object for sending.
paul@574 126
paul@745 127
        self.update_sender(attendee_attr)
paul@574 128
        self.obj["ATTENDEE"] = [(self.user, attendee_attr)]
paul@574 129
        self.update_dtstamp()
paul@662 130
        self.add_result(method, map(get_address, self.obj.get_values("ORGANIZER")), to_part(method, [self.obj.to_node()]))
paul@131 131
paul@468 132
    def _cancel_for_attendee(self):
paul@131 133
paul@420 134
        """
paul@468 135
        Cancel for the current user their attendance of the event described by
paul@468 136
        the current object.
paul@420 137
        """
paul@420 138
paul@580 139
        # Update free/busy information.
paul@131 140
paul@580 141
        self.remove_event_from_freebusy()
paul@580 142
paul@672 143
        # Update the stored event and cancel it.
paul@672 144
paul@672 145
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@672 146
        self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@672 147
paul@710 148
    def _revoke_for_attendee(self):
paul@710 149
paul@710 150
        "Revoke any counter-proposal recorded as a free/busy offer."
paul@710 151
paul@710 152
        self.remove_event_from_freebusy_offers()
paul@710 153
paul@936 154
    # Scheduling details.
paul@936 155
paul@936 156
    def schedule(self):
paul@936 157
paul@936 158
        """
paul@936 159
        Attempt to schedule the current object, returning an indication of the
paul@936 160
        kind of response to be returned: "COUNTER" for counter-proposals,
paul@936 161
        "ACCEPTED" for acceptances, "DECLINED" for rejections, and None for
paul@936 162
        invalid requests.
paul@936 163
        """
paul@936 164
paul@1025 165
        # Obtain a list of scheduling functions.
paul@936 166
paul@1025 167
        functions = self.get_preferences().get("scheduling_function",
paul@1025 168
            "schedule_in_freebusy").split("\n")
paul@936 169
paul@1025 170
        return apply_scheduling_functions(functions, self)
paul@936 171
paul@131 172
class Event(ResourceHandler):
paul@48 173
paul@48 174
    "An event handler."
paul@48 175
paul@48 176
    def add(self):
paul@676 177
paul@676 178
        "Add a new occurrence to an existing event."
paul@676 179
paul@937 180
        self._process(self._add_for_attendee)
paul@48 181
paul@48 182
    def cancel(self):
paul@131 183
paul@131 184
        "Cancel attendance for attendees."
paul@131 185
paul@937 186
        self._process(self._cancel_for_attendee)
paul@48 187
paul@48 188
    def counter(self):
paul@48 189
paul@48 190
        "Since this handler does not send requests, it will not handle replies."
paul@48 191
paul@48 192
        pass
paul@48 193
paul@48 194
    def declinecounter(self):
paul@48 195
paul@710 196
        "Revoke any counter-proposal."
paul@48 197
paul@937 198
        self._process(self._revoke_for_attendee)
paul@48 199
paul@48 200
    def publish(self):
paul@676 201
paul@676 202
        """
paul@676 203
        Resources only consider events sent as requests, not generally published
paul@676 204
        events.
paul@676 205
        """
paul@676 206
paul@48 207
        pass
paul@48 208
paul@48 209
    def refresh(self):
paul@626 210
paul@626 211
        """
paul@626 212
        Refresh messages are typically sent to event organisers, but resources
paul@626 213
        do not act as organisers themselves.
paul@626 214
        """
paul@48 215
paul@676 216
        pass
paul@676 217
paul@48 218
    def reply(self):
paul@48 219
paul@48 220
        "Since this handler does not send requests, it will not handle replies."
paul@48 221
paul@48 222
        pass
paul@48 223
paul@48 224
    def request(self):
paul@48 225
paul@48 226
        """
paul@48 227
        Respond to a request by preparing a reply containing accept/decline
paul@468 228
        information for the recipient.
paul@48 229
paul@48 230
        No support for countering requests is implemented.
paul@48 231
        """
paul@48 232
paul@937 233
        self._process(self._schedule_for_attendee)
paul@48 234
paul@725 235
class Freebusy(CommonFreebusy, Handler):
paul@48 236
paul@48 237
    "A free/busy handler."
paul@48 238
paul@48 239
    def publish(self):
paul@676 240
paul@676 241
        "Resources ignore generally published free/busy information."
paul@676 242
paul@943 243
        self._record_freebusy(from_organiser=True)
paul@48 244
paul@48 245
    def reply(self):
paul@48 246
paul@48 247
        "Since this handler does not send requests, it will not handle replies."
paul@48 248
paul@48 249
        pass
paul@48 250
paul@108 251
    # request provided by CommonFreeBusy.request
paul@48 252
paul@48 253
# Handler registry.
paul@48 254
paul@48 255
handlers = [
paul@48 256
    ("VFREEBUSY",   Freebusy),
paul@48 257
    ("VEVENT",      Event),
paul@48 258
    ]
paul@48 259
paul@48 260
# vim: tabstop=4 expandtab shiftwidth=4