imip-agent

Annotated imip_manager.py

123:884560298d08
2014-12-09 Paul Boddie Redirect to the root page upon processing a request.
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@79 87
        Create a full calendar object and send it to the organisers from the
paul@79 88
        given 'sender'.
paul@79 89
        """
paul@79 90
paul@121 91
        node = to_node(self.obj)
paul@79 92
        part = to_part("REPLY", [node])
paul@86 93
        message = self.messenger.make_message([part], self.organisers, sender=sender)
paul@86 94
        self.messenger.sendmail(self.organisers, message.as_string(), sender=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@79 115
                self.details["ATTENDEE"] = [(attendee, attendee_attr)]
paul@79 116
                self.send_message(get_address(attendee))
paul@79 117
paul@79 118
                return True
paul@79 119
paul@79 120
        return False
paul@79 121
paul@69 122
class Manager:
paul@69 123
paul@69 124
    "A simple manager application."
paul@69 125
paul@82 126
    def __init__(self, messenger=None):
paul@82 127
        self.messenger = messenger or Messenger()
paul@82 128
paul@69 129
        self.env = CGIEnvironment()
paul@69 130
        user = self.env.get_user()
paul@77 131
        self.user = user and get_uri(user) or None
paul@121 132
        self.requests = None
paul@121 133
paul@69 134
        self.out = self.env.get_output()
paul@69 135
        self.page = markup.page()
paul@69 136
        self.encoding = "utf-8"
paul@69 137
paul@77 138
        self.store = imip_store.FileStore()
paul@77 139
paul@77 140
        try:
paul@77 141
            self.publisher = imip_store.FilePublisher()
paul@77 142
        except OSError:
paul@77 143
            self.publisher = None
paul@77 144
paul@121 145
    def _get_uid(self, path_info):
paul@121 146
        return path_info.lstrip("/").split("/", 1)[0]
paul@121 147
paul@117 148
    def _get_object(self, uid):
paul@117 149
        f = uid and self.store.get_event(self.user, uid) or None
paul@117 150
paul@117 151
        if not f:
paul@117 152
            return None
paul@117 153
paul@117 154
        obj = parse_object(f, "utf-8")
paul@117 155
paul@117 156
        if not obj:
paul@117 157
            return None
paul@117 158
paul@121 159
        return obj
paul@121 160
paul@121 161
    def _get_details(self, obj):
paul@121 162
        details, details_attr = obj.values()[0]
paul@121 163
        return details
paul@121 164
paul@121 165
    def _get_requests(self):
paul@121 166
        if self.requests is None:
paul@121 167
            self.requests = self.store.get_requests(self.user)
paul@121 168
        return self.requests
paul@117 169
paul@78 170
    # Data management methods.
paul@78 171
paul@78 172
    def remove_request(self, uid):
paul@105 173
        return self.store.dequeue_request(self.user, uid)
paul@78 174
paul@78 175
    # Presentation methods.
paul@78 176
paul@69 177
    def new_page(self, title):
paul@69 178
        self.page.init(title=title, charset=self.encoding)
paul@69 179
paul@69 180
    def status(self, code, message):
paul@123 181
        self.header("Status", "%s %s" % (code, message))
paul@123 182
paul@123 183
    def header(self, header, value):
paul@123 184
        print >>self.out, "%s: %s" % (header, value)
paul@69 185
paul@69 186
    def no_user(self):
paul@69 187
        self.status(403, "Forbidden")
paul@69 188
        self.new_page(title="Forbidden")
paul@69 189
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@69 190
paul@70 191
    def no_page(self):
paul@70 192
        self.status(404, "Not Found")
paul@70 193
        self.new_page(title="Not Found")
paul@70 194
        self.page.p("No page is provided at the given address.")
paul@70 195
paul@123 196
    def redirect(self, url):
paul@123 197
        self.status(302, "Redirect")
paul@123 198
        self.header("Location", url)
paul@123 199
        self.new_page(title="Redirect")
paul@123 200
        self.page.p("Redirecting to: %s" % url)
paul@123 201
paul@121 202
    # Request logic and page fragment methods.
paul@121 203
paul@121 204
    def handle_request(self, uid, request):
paul@121 205
paul@121 206
        "Handle actions involving the given 'uid' and 'request' object."
paul@121 207
paul@121 208
        # Handle a submitted form.
paul@121 209
paul@121 210
        args = self.env.get_args()
paul@123 211
        handled = True
paul@121 212
paul@121 213
        accept = args.has_key("accept")
paul@121 214
        decline = args.has_key("decline")
paul@121 215
paul@121 216
        if accept or decline:
paul@121 217
paul@121 218
            handler = ManagerHandler(request, self.user, self.messenger)
paul@121 219
paul@121 220
            if handler.process_request(accept):
paul@121 221
paul@121 222
                # Remove the request from the list.
paul@121 223
paul@121 224
                self.remove_request(uid)
paul@121 225
paul@121 226
        elif args.has_key("ignore"):
paul@121 227
paul@121 228
            # Remove the request from the list.
paul@121 229
paul@121 230
            self.remove_request(uid)
paul@121 231
paul@121 232
        else:
paul@123 233
            handled = False
paul@121 234
paul@123 235
        if handled:
paul@123 236
            self.redirect(self.env.get_path())
paul@123 237
paul@123 238
        return handled
paul@121 239
paul@121 240
    def show_request_form(self):
paul@121 241
paul@121 242
        "Show a form for a request."
paul@121 243
paul@121 244
        self.page.p("Action to take for this request:")
paul@121 245
        self.page.form(method="POST")
paul@121 246
        self.page.p()
paul@121 247
        self.page.input(name="accept", type="submit", value="Accept")
paul@121 248
        self.page.add(" ")
paul@121 249
        self.page.input(name="decline", type="submit", value="Decline")
paul@121 250
        self.page.add(" ")
paul@121 251
        self.page.input(name="ignore", type="submit", value="Ignore")
paul@121 252
        self.page.p.close()
paul@121 253
        self.page.form.close()
paul@121 254
paul@121 255
    def show_object_on_page(self, uid, obj):
paul@121 256
paul@121 257
        """
paul@121 258
        Show the calendar object with the given 'uid' and representation 'obj'
paul@121 259
        on the current page.
paul@121 260
        """
paul@121 261
paul@121 262
        details = self._get_details(obj)
paul@121 263
paul@121 264
        # Provide a summary of the object.
paul@121 265
paul@121 266
        self.page.dl()
paul@121 267
paul@121 268
        for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]:
paul@121 269
            for value in get_values(details, name):
paul@121 270
                self.page.dt(name)
paul@121 271
                self.page.dd(value)
paul@121 272
paul@121 273
        self.page.dl.close()
paul@121 274
paul@121 275
        dtstart = format_datetime(get_utc_datetime(details, "DTSTART"))
paul@121 276
        dtend = format_datetime(get_utc_datetime(details, "DTEND"))
paul@121 277
paul@121 278
        # Indicate whether there are conflicting events.
paul@121 279
paul@121 280
        freebusy = self.store.get_freebusy(self.user)
paul@121 281
paul@121 282
        if freebusy:
paul@121 283
paul@121 284
            # Obtain any time zone details from the suggested event.
paul@121 285
paul@121 286
            _dtstart, attr = get_item(details, "DTSTART")
paul@121 287
            tzid = attr.get("TZID")
paul@121 288
paul@121 289
            # Show any conflicts.
paul@121 290
paul@121 291
            for t in have_conflict(freebusy, [(dtstart, dtend)], True):
paul@121 292
                start, end, found_uid = t[:3]
paul@121 293
                if uid != found_uid:
paul@121 294
                    start = format_datetime(to_timezone(get_datetime(start), tzid))
paul@121 295
                    end = format_datetime(to_timezone(get_datetime(end), tzid))
paul@121 296
                    self.page.p("Event conflicts with another from %s to %s." % (start, end))
paul@121 297
paul@121 298
    def show_requests_on_page(self):
paul@69 299
paul@69 300
        "Show requests for the current user."
paul@69 301
paul@69 302
        # NOTE: This list could be more informative, but it is envisaged that
paul@69 303
        # NOTE: the requests would be visited directly anyway.
paul@69 304
paul@121 305
        requests = self._get_requests()
paul@70 306
paul@80 307
        if requests:
paul@114 308
            self.page.p("Pending requests:")
paul@114 309
paul@80 310
            self.page.ul()
paul@69 311
paul@80 312
            for request in requests:
paul@80 313
                self.page.li()
paul@80 314
                self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request))
paul@80 315
                self.page.li.close()
paul@80 316
paul@80 317
            self.page.ul.close()
paul@80 318
paul@80 319
        else:
paul@80 320
            self.page.p("There are no pending requests.")
paul@69 321
paul@121 322
    # Full page output methods.
paul@70 323
paul@121 324
    def show_object(self, path_info):
paul@70 325
paul@121 326
        "Show an object request using the given 'path_info' for the current user."
paul@70 327
paul@121 328
        uid = self._get_uid(path_info)
paul@121 329
        obj = self._get_object(uid)
paul@121 330
paul@121 331
        if not obj:
paul@70 332
            return False
paul@70 333
paul@123 334
        is_request = uid in self._get_requests()
paul@123 335
        handled = is_request and self.handle_request(uid, obj)
paul@77 336
paul@123 337
        if handled:
paul@123 338
            return True
paul@73 339
paul@123 340
        self.new_page(title="Event")
paul@79 341
paul@121 342
        self.show_object_on_page(uid, obj)
paul@73 343
paul@123 344
        if is_request and not handled:
paul@121 345
            self.show_request_form()
paul@73 346
paul@70 347
        return True
paul@70 348
paul@114 349
    def show_calendar(self):
paul@114 350
paul@114 351
        "Show the calendar for the current user."
paul@114 352
paul@114 353
        self.new_page(title="Calendar")
paul@121 354
        self.show_requests_on_page()
paul@114 355
paul@114 356
        freebusy = self.store.get_freebusy(self.user)
paul@114 357
        page = self.page
paul@114 358
paul@114 359
        if not freebusy:
paul@114 360
            page.p("No events scheduled.")
paul@114 361
            return
paul@114 362
paul@114 363
        # Day view: start at the earliest known day and produce days until the
paul@114 364
        # latest known day, perhaps with expandable sections of empty days.
paul@114 365
paul@114 366
        # Month view: start at the earliest known month and produce months until
paul@114 367
        # the latest known month, perhaps with expandable sections of empty
paul@114 368
        # months.
paul@114 369
paul@114 370
        # Details of users to invite to new events could be superimposed on the
paul@114 371
        # calendar.
paul@114 372
paul@114 373
        # Requests could be listed and linked to their tentative positions in
paul@114 374
        # the calendar.
paul@114 375
paul@114 376
        slots = get_slots(freebusy)
paul@114 377
        spans = get_spans(slots)
paul@114 378
paul@114 379
        page.table(border=1, cellspacing=0, cellpadding=5)
paul@114 380
paul@114 381
        for point, active in slots:
paul@114 382
            page.tr()
paul@114 383
            page.th(class_="timeslot")
paul@114 384
            page.add(point)
paul@114 385
            page.th.close()
paul@114 386
paul@114 387
            for t in active:
paul@114 388
                if t:
paul@116 389
                    start, end, uid = t[:3]
paul@114 390
                    span = spans[uid]
paul@114 391
                    if point == start:
paul@117 392
paul@114 393
                        page.td(class_="event", rowspan=span)
paul@117 394
                        obj = self._get_object(uid)
paul@117 395
                        if obj:
paul@121 396
                            details = self._get_details(obj)
paul@121 397
                            page.a(get_value(details, "SUMMARY"), href="%s/%s" % (self.env.get_url().rstrip("/"), uid))
paul@114 398
                        page.td.close()
paul@114 399
                else:
paul@114 400
                    page.td(class_="empty")
paul@114 401
                    page.td.close()
paul@114 402
paul@114 403
            page.tr.close()
paul@114 404
paul@114 405
        page.table.close()
paul@114 406
paul@69 407
    def select_action(self):
paul@69 408
paul@69 409
        "Select the desired action and show the result."
paul@69 410
paul@121 411
        path_info = self.env.get_path_info().strip("/")
paul@121 412
paul@69 413
        if not path_info:
paul@114 414
            self.show_calendar()
paul@121 415
        elif self.show_object(path_info):
paul@70 416
            pass
paul@70 417
        else:
paul@70 418
            self.no_page()
paul@69 419
paul@82 420
    def __call__(self):
paul@69 421
paul@69 422
        "Interpret a request and show an appropriate response."
paul@69 423
paul@69 424
        if not self.user:
paul@69 425
            self.no_user()
paul@69 426
        else:
paul@69 427
            self.select_action()
paul@69 428
paul@70 429
        # Write the headers and actual content.
paul@70 430
paul@69 431
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 432
        print >>self.out
paul@69 433
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 434
paul@69 435
if __name__ == "__main__":
paul@85 436
    Manager(
paul@85 437
        Messenger(
paul@85 438
            "imip-agent@example.com",
paul@85 439
            "Calendar system message",
paul@85 440
            "This is a message from the calendar system."
paul@85 441
            )
paul@85 442
        )()
paul@69 443
paul@69 444
# vim: tabstop=4 expandtab shiftwidth=4