imip-agent

Annotated imip_manager.py

121:39f9ea9ec85a
2014-12-09 Paul Boddie Changed the Web interface to show the calendar and pending requests together. Tidied object inspection.
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@69 181
        print >>self.out, "Status:", code, message
paul@69 182
paul@69 183
    def no_user(self):
paul@69 184
        self.status(403, "Forbidden")
paul@69 185
        self.new_page(title="Forbidden")
paul@69 186
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@69 187
paul@70 188
    def no_page(self):
paul@70 189
        self.status(404, "Not Found")
paul@70 190
        self.new_page(title="Not Found")
paul@70 191
        self.page.p("No page is provided at the given address.")
paul@70 192
paul@121 193
    # Request logic and page fragment methods.
paul@121 194
paul@121 195
    def handle_request(self, uid, request):
paul@121 196
paul@121 197
        "Handle actions involving the given 'uid' and 'request' object."
paul@121 198
paul@121 199
        # Handle a submitted form.
paul@121 200
paul@121 201
        args = self.env.get_args()
paul@121 202
        show_form = False
paul@121 203
paul@121 204
        accept = args.has_key("accept")
paul@121 205
        decline = args.has_key("decline")
paul@121 206
paul@121 207
        if accept or decline:
paul@121 208
paul@121 209
            handler = ManagerHandler(request, self.user, self.messenger)
paul@121 210
paul@121 211
            if handler.process_request(accept):
paul@121 212
paul@121 213
                # Remove the request from the list.
paul@121 214
paul@121 215
                self.remove_request(uid)
paul@121 216
paul@121 217
        elif args.has_key("ignore"):
paul@121 218
paul@121 219
            # Remove the request from the list.
paul@121 220
paul@121 221
            self.remove_request(uid)
paul@121 222
paul@121 223
        else:
paul@121 224
            show_form = True
paul@121 225
paul@121 226
        return show_form
paul@121 227
paul@121 228
    def show_request_form(self):
paul@121 229
paul@121 230
        "Show a form for a request."
paul@121 231
paul@121 232
        self.page.p("Action to take for this request:")
paul@121 233
        self.page.form(method="POST")
paul@121 234
        self.page.p()
paul@121 235
        self.page.input(name="accept", type="submit", value="Accept")
paul@121 236
        self.page.add(" ")
paul@121 237
        self.page.input(name="decline", type="submit", value="Decline")
paul@121 238
        self.page.add(" ")
paul@121 239
        self.page.input(name="ignore", type="submit", value="Ignore")
paul@121 240
        self.page.p.close()
paul@121 241
        self.page.form.close()
paul@121 242
paul@121 243
    def show_object_on_page(self, uid, obj):
paul@121 244
paul@121 245
        """
paul@121 246
        Show the calendar object with the given 'uid' and representation 'obj'
paul@121 247
        on the current page.
paul@121 248
        """
paul@121 249
paul@121 250
        details = self._get_details(obj)
paul@121 251
paul@121 252
        # Provide a summary of the object.
paul@121 253
paul@121 254
        self.page.dl()
paul@121 255
paul@121 256
        for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]:
paul@121 257
            for value in get_values(details, name):
paul@121 258
                self.page.dt(name)
paul@121 259
                self.page.dd(value)
paul@121 260
paul@121 261
        self.page.dl.close()
paul@121 262
paul@121 263
        dtstart = format_datetime(get_utc_datetime(details, "DTSTART"))
paul@121 264
        dtend = format_datetime(get_utc_datetime(details, "DTEND"))
paul@121 265
paul@121 266
        # Indicate whether there are conflicting events.
paul@121 267
paul@121 268
        freebusy = self.store.get_freebusy(self.user)
paul@121 269
paul@121 270
        if freebusy:
paul@121 271
paul@121 272
            # Obtain any time zone details from the suggested event.
paul@121 273
paul@121 274
            _dtstart, attr = get_item(details, "DTSTART")
paul@121 275
            tzid = attr.get("TZID")
paul@121 276
paul@121 277
            # Show any conflicts.
paul@121 278
paul@121 279
            for t in have_conflict(freebusy, [(dtstart, dtend)], True):
paul@121 280
                start, end, found_uid = t[:3]
paul@121 281
                if uid != found_uid:
paul@121 282
                    start = format_datetime(to_timezone(get_datetime(start), tzid))
paul@121 283
                    end = format_datetime(to_timezone(get_datetime(end), tzid))
paul@121 284
                    self.page.p("Event conflicts with another from %s to %s." % (start, end))
paul@121 285
paul@121 286
    def show_requests_on_page(self):
paul@69 287
paul@69 288
        "Show requests for the current user."
paul@69 289
paul@69 290
        # NOTE: This list could be more informative, but it is envisaged that
paul@69 291
        # NOTE: the requests would be visited directly anyway.
paul@69 292
paul@121 293
        requests = self._get_requests()
paul@70 294
paul@80 295
        if requests:
paul@114 296
            self.page.p("Pending requests:")
paul@114 297
paul@80 298
            self.page.ul()
paul@69 299
paul@80 300
            for request in requests:
paul@80 301
                self.page.li()
paul@80 302
                self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request))
paul@80 303
                self.page.li.close()
paul@80 304
paul@80 305
            self.page.ul.close()
paul@80 306
paul@80 307
        else:
paul@80 308
            self.page.p("There are no pending requests.")
paul@69 309
paul@121 310
    # Full page output methods.
paul@70 311
paul@121 312
    def show_object(self, path_info):
paul@70 313
paul@121 314
        "Show an object request using the given 'path_info' for the current user."
paul@70 315
paul@121 316
        uid = self._get_uid(path_info)
paul@121 317
        obj = self._get_object(uid)
paul@121 318
paul@121 319
        if not obj:
paul@70 320
            return False
paul@70 321
paul@121 322
        self.new_page(title="Event")
paul@77 323
paul@121 324
        is_request = uid in self._get_requests()
paul@73 325
paul@121 326
        show_form = is_request and self.handle_request(uid, obj)
paul@79 327
paul@121 328
        self.show_object_on_page(uid, obj)
paul@73 329
paul@73 330
        if show_form:
paul@121 331
            self.show_request_form()
paul@73 332
paul@70 333
        return True
paul@70 334
paul@114 335
    def show_calendar(self):
paul@114 336
paul@114 337
        "Show the calendar for the current user."
paul@114 338
paul@114 339
        self.new_page(title="Calendar")
paul@121 340
        self.show_requests_on_page()
paul@114 341
paul@114 342
        freebusy = self.store.get_freebusy(self.user)
paul@114 343
        page = self.page
paul@114 344
paul@114 345
        if not freebusy:
paul@114 346
            page.p("No events scheduled.")
paul@114 347
            return
paul@114 348
paul@114 349
        # Day view: start at the earliest known day and produce days until the
paul@114 350
        # latest known day, perhaps with expandable sections of empty days.
paul@114 351
paul@114 352
        # Month view: start at the earliest known month and produce months until
paul@114 353
        # the latest known month, perhaps with expandable sections of empty
paul@114 354
        # months.
paul@114 355
paul@114 356
        # Details of users to invite to new events could be superimposed on the
paul@114 357
        # calendar.
paul@114 358
paul@114 359
        # Requests could be listed and linked to their tentative positions in
paul@114 360
        # the calendar.
paul@114 361
paul@114 362
        slots = get_slots(freebusy)
paul@114 363
        spans = get_spans(slots)
paul@114 364
paul@114 365
        page.table(border=1, cellspacing=0, cellpadding=5)
paul@114 366
paul@114 367
        for point, active in slots:
paul@114 368
            page.tr()
paul@114 369
            page.th(class_="timeslot")
paul@114 370
            page.add(point)
paul@114 371
            page.th.close()
paul@114 372
paul@114 373
            for t in active:
paul@114 374
                if t:
paul@116 375
                    start, end, uid = t[:3]
paul@114 376
                    span = spans[uid]
paul@114 377
                    if point == start:
paul@117 378
paul@114 379
                        page.td(class_="event", rowspan=span)
paul@117 380
                        obj = self._get_object(uid)
paul@117 381
                        if obj:
paul@121 382
                            details = self._get_details(obj)
paul@121 383
                            page.a(get_value(details, "SUMMARY"), href="%s/%s" % (self.env.get_url().rstrip("/"), uid))
paul@114 384
                        page.td.close()
paul@114 385
                else:
paul@114 386
                    page.td(class_="empty")
paul@114 387
                    page.td.close()
paul@114 388
paul@114 389
            page.tr.close()
paul@114 390
paul@114 391
        page.table.close()
paul@114 392
paul@69 393
    def select_action(self):
paul@69 394
paul@69 395
        "Select the desired action and show the result."
paul@69 396
paul@121 397
        path_info = self.env.get_path_info().strip("/")
paul@121 398
paul@69 399
        if not path_info:
paul@114 400
            self.show_calendar()
paul@121 401
        elif self.show_object(path_info):
paul@70 402
            pass
paul@70 403
        else:
paul@70 404
            self.no_page()
paul@69 405
paul@82 406
    def __call__(self):
paul@69 407
paul@69 408
        "Interpret a request and show an appropriate response."
paul@69 409
paul@69 410
        if not self.user:
paul@69 411
            self.no_user()
paul@69 412
        else:
paul@69 413
            self.select_action()
paul@69 414
paul@70 415
        # Write the headers and actual content.
paul@70 416
paul@69 417
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 418
        print >>self.out
paul@69 419
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 420
paul@69 421
if __name__ == "__main__":
paul@85 422
    Manager(
paul@85 423
        Messenger(
paul@85 424
            "imip-agent@example.com",
paul@85 425
            "Calendar system message",
paul@85 426
            "This is a message from the calendar system."
paul@85 427
            )
paul@85 428
        )()
paul@69 429
paul@69 430
# vim: tabstop=4 expandtab shiftwidth=4