imip-agent

Annotated imip_manager.py

128:d52e08731368
2014-12-10 Paul Boddie Changed manager-originating messages to use the agent identity, sending a Bcc to the outgoing agent instead of using the "sender Bcc" mechanism. This should work much better with any agent identity signing operations.
paul@69 1
#!/usr/bin/env python
paul@69 2
paul@69 3
import cgi, os, sys
paul@69 4
paul@69 5
sys.path.append("/var/lib/imip-agent")
paul@69 6
paul@79 7
from imiptools.content import Handler, \
paul@79 8
                              format_datetime, get_address, get_datetime, \
paul@117 9
                              get_item, get_uri, get_utc_datetime, get_value, \
paul@117 10
                              get_values, parse_object, to_part, to_timezone
paul@83 11
from imiptools.mail import Messenger
paul@114 12
from imiptools.period import have_conflict, get_slots, get_spans
paul@77 13
from vCalendar import to_node
paul@69 14
import markup
paul@69 15
import imip_store
paul@69 16
paul@69 17
getenv = os.environ.get
paul@69 18
setenv = os.environ.__setitem__
paul@69 19
paul@69 20
class CGIEnvironment:
paul@69 21
paul@69 22
    "A CGI-compatible environment."
paul@69 23
paul@69 24
    def __init__(self):
paul@69 25
        self.args = None
paul@69 26
        self.method = None
paul@69 27
        self.path = None
paul@69 28
        self.path_info = None
paul@69 29
        self.user = None
paul@69 30
paul@69 31
    def get_args(self):
paul@69 32
        if self.args is None:
paul@69 33
            if self.get_method() != "POST":
paul@69 34
                setenv("QUERY_STRING", "")
paul@69 35
            self.args = cgi.parse(keep_blank_values=True)
paul@69 36
        return self.args
paul@69 37
paul@69 38
    def get_method(self):
paul@69 39
        if self.method is None:
paul@69 40
            self.method = getenv("REQUEST_METHOD") or "GET"
paul@69 41
        return self.method
paul@69 42
paul@69 43
    def get_path(self):
paul@69 44
        if self.path is None:
paul@69 45
            self.path = getenv("SCRIPT_NAME") or ""
paul@69 46
        return self.path
paul@69 47
paul@69 48
    def get_path_info(self):
paul@69 49
        if self.path_info is None:
paul@69 50
            self.path_info = getenv("PATH_INFO") or ""
paul@69 51
        return self.path_info
paul@69 52
paul@69 53
    def get_user(self):
paul@69 54
        if self.user is None:
paul@69 55
            self.user = getenv("REMOTE_USER") or ""
paul@69 56
        return self.user
paul@69 57
paul@69 58
    def get_output(self):
paul@69 59
        return sys.stdout
paul@69 60
paul@69 61
    def get_url(self):
paul@69 62
        path = self.get_path()
paul@69 63
        path_info = self.get_path_info()
paul@69 64
        return "%s%s" % (path.rstrip("/"), path_info)
paul@69 65
paul@79 66
class ManagerHandler(Handler):
paul@79 67
paul@121 68
    """
paul@121 69
    A content handler for use by the manager, as opposed to operating within the
paul@121 70
    mail processing pipeline.
paul@121 71
    """
paul@79 72
paul@121 73
    def __init__(self, obj, user, messenger):
paul@121 74
        details, details_attr = obj.values()[0]
paul@79 75
        Handler.__init__(self, details)
paul@121 76
        self.obj = obj
paul@79 77
        self.user = user
paul@82 78
        self.messenger = messenger
paul@82 79
paul@79 80
        self.organisers = map(get_address, self.get_values("ORGANIZER"))
paul@79 81
paul@79 82
    # Communication methods.
paul@79 83
paul@79 84
    def send_message(self, sender):
paul@79 85
paul@79 86
        """
paul@128 87
        Create a full calendar object and send it to the organisers, sending a
paul@128 88
        copy to the 'sender'.
paul@79 89
        """
paul@79 90
paul@121 91
        node = to_node(self.obj)
paul@79 92
        part = to_part("REPLY", [node])
paul@128 93
        message = self.messenger.make_message([part], self.organisers, outgoing_bcc=sender)
paul@128 94
        self.messenger.sendmail(self.organisers, message.as_string(), outgoing_bcc=sender)
paul@79 95
paul@79 96
    # Action methods.
paul@79 97
paul@79 98
    def process_request(self, accept):
paul@79 99
paul@79 100
        """
paul@79 101
        Process the current request for the given 'user', accepting any request
paul@79 102
        when 'accept' is true, declining requests otherwise. Return whether any
paul@79 103
        action was taken.
paul@79 104
        """
paul@79 105
paul@79 106
        # When accepting or declining, do so only on behalf of this user,
paul@79 107
        # preserving any other attributes set as an attendee.
paul@79 108
paul@79 109
        for attendee, attendee_attr in self.get_items("ATTENDEE"):
paul@79 110
paul@79 111
            if attendee == self.user:
paul@79 112
                freebusy = self.store.get_freebusy(attendee)
paul@79 113
paul@79 114
                attendee_attr["PARTSTAT"] = accept and "ACCEPTED" or "DECLINED"
paul@128 115
                if self.messenger and self.messenger.sender != get_address(attendee):
paul@128 116
                    attendee_attr["SENT-BY"] = get_uri(self.messenger.sender)
paul@79 117
                self.details["ATTENDEE"] = [(attendee, attendee_attr)]
paul@79 118
                self.send_message(get_address(attendee))
paul@79 119
paul@79 120
                return True
paul@79 121
paul@79 122
        return False
paul@79 123
paul@69 124
class Manager:
paul@69 125
paul@69 126
    "A simple manager application."
paul@69 127
paul@82 128
    def __init__(self, messenger=None):
paul@82 129
        self.messenger = messenger or Messenger()
paul@82 130
paul@69 131
        self.env = CGIEnvironment()
paul@69 132
        user = self.env.get_user()
paul@77 133
        self.user = user and get_uri(user) or None
paul@121 134
        self.requests = None
paul@121 135
paul@69 136
        self.out = self.env.get_output()
paul@69 137
        self.page = markup.page()
paul@69 138
        self.encoding = "utf-8"
paul@69 139
paul@77 140
        self.store = imip_store.FileStore()
paul@77 141
paul@77 142
        try:
paul@77 143
            self.publisher = imip_store.FilePublisher()
paul@77 144
        except OSError:
paul@77 145
            self.publisher = None
paul@77 146
paul@121 147
    def _get_uid(self, path_info):
paul@121 148
        return path_info.lstrip("/").split("/", 1)[0]
paul@121 149
paul@117 150
    def _get_object(self, uid):
paul@117 151
        f = uid and self.store.get_event(self.user, uid) or None
paul@117 152
paul@117 153
        if not f:
paul@117 154
            return None
paul@117 155
paul@117 156
        obj = parse_object(f, "utf-8")
paul@117 157
paul@117 158
        if not obj:
paul@117 159
            return None
paul@117 160
paul@121 161
        return obj
paul@121 162
paul@121 163
    def _get_details(self, obj):
paul@121 164
        details, details_attr = obj.values()[0]
paul@121 165
        return details
paul@121 166
paul@121 167
    def _get_requests(self):
paul@121 168
        if self.requests is None:
paul@121 169
            self.requests = self.store.get_requests(self.user)
paul@121 170
        return self.requests
paul@117 171
paul@78 172
    # Data management methods.
paul@78 173
paul@78 174
    def remove_request(self, uid):
paul@105 175
        return self.store.dequeue_request(self.user, uid)
paul@78 176
paul@78 177
    # Presentation methods.
paul@78 178
paul@69 179
    def new_page(self, title):
paul@69 180
        self.page.init(title=title, charset=self.encoding)
paul@69 181
paul@69 182
    def status(self, code, message):
paul@123 183
        self.header("Status", "%s %s" % (code, message))
paul@123 184
paul@123 185
    def header(self, header, value):
paul@123 186
        print >>self.out, "%s: %s" % (header, value)
paul@69 187
paul@69 188
    def no_user(self):
paul@69 189
        self.status(403, "Forbidden")
paul@69 190
        self.new_page(title="Forbidden")
paul@69 191
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@69 192
paul@70 193
    def no_page(self):
paul@70 194
        self.status(404, "Not Found")
paul@70 195
        self.new_page(title="Not Found")
paul@70 196
        self.page.p("No page is provided at the given address.")
paul@70 197
paul@123 198
    def redirect(self, url):
paul@123 199
        self.status(302, "Redirect")
paul@123 200
        self.header("Location", url)
paul@123 201
        self.new_page(title="Redirect")
paul@123 202
        self.page.p("Redirecting to: %s" % url)
paul@123 203
paul@121 204
    # Request logic and page fragment methods.
paul@121 205
paul@121 206
    def handle_request(self, uid, request):
paul@121 207
paul@121 208
        "Handle actions involving the given 'uid' and 'request' object."
paul@121 209
paul@121 210
        # Handle a submitted form.
paul@121 211
paul@121 212
        args = self.env.get_args()
paul@123 213
        handled = True
paul@121 214
paul@121 215
        accept = args.has_key("accept")
paul@121 216
        decline = args.has_key("decline")
paul@121 217
paul@121 218
        if accept or decline:
paul@121 219
paul@121 220
            handler = ManagerHandler(request, self.user, self.messenger)
paul@121 221
paul@121 222
            if handler.process_request(accept):
paul@121 223
paul@121 224
                # Remove the request from the list.
paul@121 225
paul@121 226
                self.remove_request(uid)
paul@121 227
paul@121 228
        elif args.has_key("ignore"):
paul@121 229
paul@121 230
            # Remove the request from the list.
paul@121 231
paul@121 232
            self.remove_request(uid)
paul@121 233
paul@121 234
        else:
paul@123 235
            handled = False
paul@121 236
paul@123 237
        if handled:
paul@123 238
            self.redirect(self.env.get_path())
paul@123 239
paul@123 240
        return handled
paul@121 241
paul@121 242
    def show_request_form(self):
paul@121 243
paul@121 244
        "Show a form for a request."
paul@121 245
paul@121 246
        self.page.p("Action to take for this request:")
paul@121 247
        self.page.form(method="POST")
paul@121 248
        self.page.p()
paul@121 249
        self.page.input(name="accept", type="submit", value="Accept")
paul@121 250
        self.page.add(" ")
paul@121 251
        self.page.input(name="decline", type="submit", value="Decline")
paul@121 252
        self.page.add(" ")
paul@121 253
        self.page.input(name="ignore", type="submit", value="Ignore")
paul@121 254
        self.page.p.close()
paul@121 255
        self.page.form.close()
paul@121 256
paul@121 257
    def show_object_on_page(self, uid, obj):
paul@121 258
paul@121 259
        """
paul@121 260
        Show the calendar object with the given 'uid' and representation 'obj'
paul@121 261
        on the current page.
paul@121 262
        """
paul@121 263
paul@121 264
        details = self._get_details(obj)
paul@121 265
paul@121 266
        # Provide a summary of the object.
paul@121 267
paul@121 268
        self.page.dl()
paul@121 269
paul@121 270
        for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]:
paul@121 271
            for value in get_values(details, name):
paul@121 272
                self.page.dt(name)
paul@121 273
                self.page.dd(value)
paul@121 274
paul@121 275
        self.page.dl.close()
paul@121 276
paul@121 277
        dtstart = format_datetime(get_utc_datetime(details, "DTSTART"))
paul@121 278
        dtend = format_datetime(get_utc_datetime(details, "DTEND"))
paul@121 279
paul@121 280
        # Indicate whether there are conflicting events.
paul@121 281
paul@121 282
        freebusy = self.store.get_freebusy(self.user)
paul@121 283
paul@121 284
        if freebusy:
paul@121 285
paul@121 286
            # Obtain any time zone details from the suggested event.
paul@121 287
paul@121 288
            _dtstart, attr = get_item(details, "DTSTART")
paul@121 289
            tzid = attr.get("TZID")
paul@121 290
paul@121 291
            # Show any conflicts.
paul@121 292
paul@121 293
            for t in have_conflict(freebusy, [(dtstart, dtend)], True):
paul@121 294
                start, end, found_uid = t[:3]
paul@121 295
                if uid != found_uid:
paul@121 296
                    start = format_datetime(to_timezone(get_datetime(start), tzid))
paul@121 297
                    end = format_datetime(to_timezone(get_datetime(end), tzid))
paul@121 298
                    self.page.p("Event conflicts with another from %s to %s." % (start, end))
paul@121 299
paul@121 300
    def show_requests_on_page(self):
paul@69 301
paul@69 302
        "Show requests for the current user."
paul@69 303
paul@69 304
        # NOTE: This list could be more informative, but it is envisaged that
paul@69 305
        # NOTE: the requests would be visited directly anyway.
paul@69 306
paul@121 307
        requests = self._get_requests()
paul@70 308
paul@80 309
        if requests:
paul@114 310
            self.page.p("Pending requests:")
paul@114 311
paul@80 312
            self.page.ul()
paul@69 313
paul@80 314
            for request in requests:
paul@80 315
                self.page.li()
paul@80 316
                self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request))
paul@80 317
                self.page.li.close()
paul@80 318
paul@80 319
            self.page.ul.close()
paul@80 320
paul@80 321
        else:
paul@80 322
            self.page.p("There are no pending requests.")
paul@69 323
paul@121 324
    # Full page output methods.
paul@70 325
paul@121 326
    def show_object(self, path_info):
paul@70 327
paul@121 328
        "Show an object request using the given 'path_info' for the current user."
paul@70 329
paul@121 330
        uid = self._get_uid(path_info)
paul@121 331
        obj = self._get_object(uid)
paul@121 332
paul@121 333
        if not obj:
paul@70 334
            return False
paul@70 335
paul@123 336
        is_request = uid in self._get_requests()
paul@123 337
        handled = is_request and self.handle_request(uid, obj)
paul@77 338
paul@123 339
        if handled:
paul@123 340
            return True
paul@73 341
paul@123 342
        self.new_page(title="Event")
paul@79 343
paul@121 344
        self.show_object_on_page(uid, obj)
paul@73 345
paul@123 346
        if is_request and not handled:
paul@121 347
            self.show_request_form()
paul@73 348
paul@70 349
        return True
paul@70 350
paul@114 351
    def show_calendar(self):
paul@114 352
paul@114 353
        "Show the calendar for the current user."
paul@114 354
paul@114 355
        self.new_page(title="Calendar")
paul@121 356
        self.show_requests_on_page()
paul@114 357
paul@114 358
        freebusy = self.store.get_freebusy(self.user)
paul@114 359
        page = self.page
paul@114 360
paul@114 361
        if not freebusy:
paul@114 362
            page.p("No events scheduled.")
paul@114 363
            return
paul@114 364
paul@114 365
        # Day view: start at the earliest known day and produce days until the
paul@114 366
        # latest known day, perhaps with expandable sections of empty days.
paul@114 367
paul@114 368
        # Month view: start at the earliest known month and produce months until
paul@114 369
        # the latest known month, perhaps with expandable sections of empty
paul@114 370
        # months.
paul@114 371
paul@114 372
        # Details of users to invite to new events could be superimposed on the
paul@114 373
        # calendar.
paul@114 374
paul@114 375
        # Requests could be listed and linked to their tentative positions in
paul@114 376
        # the calendar.
paul@114 377
paul@114 378
        slots = get_slots(freebusy)
paul@114 379
        spans = get_spans(slots)
paul@114 380
paul@114 381
        page.table(border=1, cellspacing=0, cellpadding=5)
paul@114 382
paul@114 383
        for point, active in slots:
paul@114 384
            page.tr()
paul@114 385
            page.th(class_="timeslot")
paul@114 386
            page.add(point)
paul@114 387
            page.th.close()
paul@114 388
paul@114 389
            for t in active:
paul@114 390
                if t:
paul@116 391
                    start, end, uid = t[:3]
paul@114 392
                    span = spans[uid]
paul@114 393
                    if point == start:
paul@117 394
paul@114 395
                        page.td(class_="event", rowspan=span)
paul@117 396
                        obj = self._get_object(uid)
paul@117 397
                        if obj:
paul@121 398
                            details = self._get_details(obj)
paul@121 399
                            page.a(get_value(details, "SUMMARY"), href="%s/%s" % (self.env.get_url().rstrip("/"), uid))
paul@114 400
                        page.td.close()
paul@114 401
                else:
paul@114 402
                    page.td(class_="empty")
paul@114 403
                    page.td.close()
paul@114 404
paul@114 405
            page.tr.close()
paul@114 406
paul@114 407
        page.table.close()
paul@114 408
paul@69 409
    def select_action(self):
paul@69 410
paul@69 411
        "Select the desired action and show the result."
paul@69 412
paul@121 413
        path_info = self.env.get_path_info().strip("/")
paul@121 414
paul@69 415
        if not path_info:
paul@114 416
            self.show_calendar()
paul@121 417
        elif self.show_object(path_info):
paul@70 418
            pass
paul@70 419
        else:
paul@70 420
            self.no_page()
paul@69 421
paul@82 422
    def __call__(self):
paul@69 423
paul@69 424
        "Interpret a request and show an appropriate response."
paul@69 425
paul@69 426
        if not self.user:
paul@69 427
            self.no_user()
paul@69 428
        else:
paul@69 429
            self.select_action()
paul@69 430
paul@70 431
        # Write the headers and actual content.
paul@70 432
paul@69 433
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 434
        print >>self.out
paul@69 435
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 436
paul@69 437
if __name__ == "__main__":
paul@128 438
    Manager()()
paul@69 439
paul@69 440
# vim: tabstop=4 expandtab shiftwidth=4