imip-agent

Annotated imip_manager.py

166:095586ad9a1a
2015-01-24 Paul Boddie Added missing padding after active columns in period groups.
paul@69 1
#!/usr/bin/env python
paul@69 2
paul@146 3
"""
paul@146 4
A Web interface to a user's calendar.
paul@146 5
paul@146 6
Copyright (C) 2014, 2015 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@146 20
"""
paul@146 21
paul@146 22
# Edit this path to refer to the location of the imiptools libraries, if
paul@146 23
# necessary.
paul@146 24
paul@146 25
LIBRARY_PATH = "/var/lib/imip-agent"
paul@146 26
paul@149 27
import babel.dates
paul@149 28
import cgi, os, sys
paul@69 29
paul@146 30
sys.path.append(LIBRARY_PATH)
paul@69 31
paul@153 32
from imiptools.content import Handler, get_address, \
paul@117 33
                              get_item, get_uri, get_utc_datetime, get_value, \
paul@155 34
                              get_value_map, get_values, parse_object, to_part
paul@153 35
from imiptools.dates import format_datetime, get_datetime, get_start_of_day, \
paul@153 36
                            to_timezone
paul@83 37
from imiptools.mail import Messenger
paul@162 38
from imiptools.period import add_day_start_points, add_slots, convert_periods, \
paul@162 39
                             get_scale, have_conflict, get_slots, get_spans, \
paul@162 40
                             partition_by_day
paul@147 41
from imiptools.profile import Preferences
paul@77 42
from vCalendar import to_node
paul@69 43
import markup
paul@69 44
import imip_store
paul@69 45
paul@69 46
getenv = os.environ.get
paul@69 47
setenv = os.environ.__setitem__
paul@69 48
paul@69 49
class CGIEnvironment:
paul@69 50
paul@69 51
    "A CGI-compatible environment."
paul@69 52
paul@69 53
    def __init__(self):
paul@69 54
        self.args = None
paul@69 55
        self.method = None
paul@69 56
        self.path = None
paul@69 57
        self.path_info = None
paul@69 58
        self.user = None
paul@69 59
paul@69 60
    def get_args(self):
paul@69 61
        if self.args is None:
paul@69 62
            if self.get_method() != "POST":
paul@69 63
                setenv("QUERY_STRING", "")
paul@69 64
            self.args = cgi.parse(keep_blank_values=True)
paul@69 65
        return self.args
paul@69 66
paul@69 67
    def get_method(self):
paul@69 68
        if self.method is None:
paul@69 69
            self.method = getenv("REQUEST_METHOD") or "GET"
paul@69 70
        return self.method
paul@69 71
paul@69 72
    def get_path(self):
paul@69 73
        if self.path is None:
paul@69 74
            self.path = getenv("SCRIPT_NAME") or ""
paul@69 75
        return self.path
paul@69 76
paul@69 77
    def get_path_info(self):
paul@69 78
        if self.path_info is None:
paul@69 79
            self.path_info = getenv("PATH_INFO") or ""
paul@69 80
        return self.path_info
paul@69 81
paul@69 82
    def get_user(self):
paul@69 83
        if self.user is None:
paul@69 84
            self.user = getenv("REMOTE_USER") or ""
paul@69 85
        return self.user
paul@69 86
paul@69 87
    def get_output(self):
paul@69 88
        return sys.stdout
paul@69 89
paul@69 90
    def get_url(self):
paul@69 91
        path = self.get_path()
paul@69 92
        path_info = self.get_path_info()
paul@69 93
        return "%s%s" % (path.rstrip("/"), path_info)
paul@69 94
paul@154 95
    def new_url(self, path_info):
paul@154 96
        path = self.get_path()
paul@154 97
        return "%s/%s" % (path.rstrip("/"), path_info.lstrip("/"))
paul@154 98
paul@79 99
class ManagerHandler(Handler):
paul@79 100
paul@121 101
    """
paul@121 102
    A content handler for use by the manager, as opposed to operating within the
paul@121 103
    mail processing pipeline.
paul@121 104
    """
paul@79 105
paul@121 106
    def __init__(self, obj, user, messenger):
paul@121 107
        details, details_attr = obj.values()[0]
paul@79 108
        Handler.__init__(self, details)
paul@121 109
        self.obj = obj
paul@79 110
        self.user = user
paul@82 111
        self.messenger = messenger
paul@82 112
paul@79 113
        self.organisers = map(get_address, self.get_values("ORGANIZER"))
paul@79 114
paul@79 115
    # Communication methods.
paul@79 116
paul@79 117
    def send_message(self, sender):
paul@79 118
paul@79 119
        """
paul@128 120
        Create a full calendar object and send it to the organisers, sending a
paul@128 121
        copy to the 'sender'.
paul@79 122
        """
paul@79 123
paul@121 124
        node = to_node(self.obj)
paul@79 125
        part = to_part("REPLY", [node])
paul@128 126
        message = self.messenger.make_message([part], self.organisers, outgoing_bcc=sender)
paul@128 127
        self.messenger.sendmail(self.organisers, message.as_string(), outgoing_bcc=sender)
paul@79 128
paul@79 129
    # Action methods.
paul@79 130
paul@155 131
    def process_request(self, accept, update=False):
paul@79 132
paul@79 133
        """
paul@79 134
        Process the current request for the given 'user', accepting any request
paul@79 135
        when 'accept' is true, declining requests otherwise. Return whether any
paul@79 136
        action was taken.
paul@155 137
paul@155 138
        If 'update' is given, the sequence number will be incremented in order
paul@155 139
        to override any previous response.
paul@79 140
        """
paul@79 141
paul@79 142
        # When accepting or declining, do so only on behalf of this user,
paul@79 143
        # preserving any other attributes set as an attendee.
paul@79 144
paul@79 145
        for attendee, attendee_attr in self.get_items("ATTENDEE"):
paul@79 146
paul@79 147
            if attendee == self.user:
paul@79 148
                freebusy = self.store.get_freebusy(attendee)
paul@79 149
paul@79 150
                attendee_attr["PARTSTAT"] = accept and "ACCEPTED" or "DECLINED"
paul@128 151
                if self.messenger and self.messenger.sender != get_address(attendee):
paul@128 152
                    attendee_attr["SENT-BY"] = get_uri(self.messenger.sender)
paul@79 153
                self.details["ATTENDEE"] = [(attendee, attendee_attr)]
paul@155 154
                if update:
paul@155 155
                    sequence = self.get_value("SEQUENCE") or "0"
paul@155 156
                    self.details["SEQUENCE"] = [(str(int(sequence) + 1), {})]
paul@158 157
                self.update_dtstamp()
paul@155 158
paul@79 159
                self.send_message(get_address(attendee))
paul@79 160
paul@79 161
                return True
paul@79 162
paul@79 163
        return False
paul@79 164
paul@69 165
class Manager:
paul@69 166
paul@69 167
    "A simple manager application."
paul@69 168
paul@82 169
    def __init__(self, messenger=None):
paul@82 170
        self.messenger = messenger or Messenger()
paul@82 171
paul@69 172
        self.env = CGIEnvironment()
paul@69 173
        user = self.env.get_user()
paul@77 174
        self.user = user and get_uri(user) or None
paul@147 175
        self.preferences = None
paul@149 176
        self.locale = None
paul@121 177
        self.requests = None
paul@121 178
paul@69 179
        self.out = self.env.get_output()
paul@69 180
        self.page = markup.page()
paul@69 181
        self.encoding = "utf-8"
paul@69 182
paul@77 183
        self.store = imip_store.FileStore()
paul@162 184
        self.objects = {}
paul@77 185
paul@77 186
        try:
paul@77 187
            self.publisher = imip_store.FilePublisher()
paul@77 188
        except OSError:
paul@77 189
            self.publisher = None
paul@77 190
paul@121 191
    def _get_uid(self, path_info):
paul@121 192
        return path_info.lstrip("/").split("/", 1)[0]
paul@121 193
paul@117 194
    def _get_object(self, uid):
paul@162 195
        if self.objects.has_key(uid):
paul@162 196
            return self.objects[uid]
paul@162 197
paul@117 198
        f = uid and self.store.get_event(self.user, uid) or None
paul@117 199
paul@117 200
        if not f:
paul@117 201
            return None
paul@117 202
paul@162 203
        self.objects[uid] = obj = parse_object(f, "utf-8")
paul@117 204
paul@117 205
        if not obj:
paul@117 206
            return None
paul@117 207
paul@121 208
        return obj
paul@121 209
paul@121 210
    def _get_details(self, obj):
paul@121 211
        details, details_attr = obj.values()[0]
paul@121 212
        return details
paul@121 213
paul@121 214
    def _get_requests(self):
paul@121 215
        if self.requests is None:
paul@121 216
            self.requests = self.store.get_requests(self.user)
paul@121 217
        return self.requests
paul@117 218
paul@162 219
    def _get_request_summary(self):
paul@162 220
        summary = []
paul@162 221
        for uid in self._get_requests():
paul@162 222
            obj = self._get_object(uid)
paul@162 223
            if obj:
paul@162 224
                details = self._get_details(obj)
paul@162 225
                summary.append((
paul@162 226
                    get_value(details, "DTSTART"),
paul@162 227
                    get_value(details, "DTEND"),
paul@162 228
                    uid
paul@162 229
                    ))
paul@162 230
        return summary
paul@162 231
paul@147 232
    # Preference methods.
paul@147 233
paul@149 234
    def get_user_locale(self):
paul@149 235
        if not self.locale:
paul@149 236
            self.locale = self.get_preferences().get("LANG", "C")
paul@149 237
        return self.locale
paul@147 238
paul@147 239
    def get_preferences(self):
paul@147 240
        if not self.preferences:
paul@147 241
            self.preferences = Preferences(self.user)
paul@147 242
        return self.preferences
paul@147 243
paul@162 244
    # Prettyprinting of dates and times.
paul@162 245
paul@149 246
    def format_date(self, dt, format):
paul@149 247
        return self._format_datetime(babel.dates.format_date, dt, format)
paul@149 248
paul@149 249
    def format_time(self, dt, format):
paul@149 250
        return self._format_datetime(babel.dates.format_time, dt, format)
paul@149 251
paul@149 252
    def format_datetime(self, dt, format):
paul@149 253
        return self._format_datetime(babel.dates.format_datetime, dt, format)
paul@149 254
paul@149 255
    def _format_datetime(self, fn, dt, format):
paul@149 256
        return fn(dt, format=format, locale=self.get_user_locale())
paul@149 257
paul@78 258
    # Data management methods.
paul@78 259
paul@78 260
    def remove_request(self, uid):
paul@105 261
        return self.store.dequeue_request(self.user, uid)
paul@78 262
paul@78 263
    # Presentation methods.
paul@78 264
paul@69 265
    def new_page(self, title):
paul@69 266
        self.page.init(title=title, charset=self.encoding)
paul@69 267
paul@69 268
    def status(self, code, message):
paul@123 269
        self.header("Status", "%s %s" % (code, message))
paul@123 270
paul@123 271
    def header(self, header, value):
paul@123 272
        print >>self.out, "%s: %s" % (header, value)
paul@69 273
paul@69 274
    def no_user(self):
paul@69 275
        self.status(403, "Forbidden")
paul@69 276
        self.new_page(title="Forbidden")
paul@69 277
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@69 278
paul@70 279
    def no_page(self):
paul@70 280
        self.status(404, "Not Found")
paul@70 281
        self.new_page(title="Not Found")
paul@70 282
        self.page.p("No page is provided at the given address.")
paul@70 283
paul@123 284
    def redirect(self, url):
paul@123 285
        self.status(302, "Redirect")
paul@123 286
        self.header("Location", url)
paul@123 287
        self.new_page(title="Redirect")
paul@123 288
        self.page.p("Redirecting to: %s" % url)
paul@123 289
paul@121 290
    # Request logic and page fragment methods.
paul@121 291
paul@155 292
    def handle_request(self, uid, request, queued):
paul@121 293
paul@155 294
        """
paul@155 295
        Handle actions involving the given 'uid' and 'request' object, where
paul@155 296
        'queued' indicates that the object has not yet been handled.
paul@155 297
        """
paul@121 298
paul@121 299
        # Handle a submitted form.
paul@121 300
paul@121 301
        args = self.env.get_args()
paul@123 302
        handled = True
paul@121 303
paul@121 304
        accept = args.has_key("accept")
paul@121 305
        decline = args.has_key("decline")
paul@155 306
        update = not queued and args.has_key("update")
paul@121 307
paul@121 308
        if accept or decline:
paul@121 309
paul@121 310
            handler = ManagerHandler(request, self.user, self.messenger)
paul@121 311
paul@155 312
            if handler.process_request(accept, update):
paul@121 313
paul@121 314
                # Remove the request from the list.
paul@121 315
paul@121 316
                self.remove_request(uid)
paul@121 317
paul@121 318
        elif args.has_key("ignore"):
paul@121 319
paul@121 320
            # Remove the request from the list.
paul@121 321
paul@121 322
            self.remove_request(uid)
paul@121 323
paul@121 324
        else:
paul@123 325
            handled = False
paul@121 326
paul@123 327
        if handled:
paul@123 328
            self.redirect(self.env.get_path())
paul@123 329
paul@123 330
        return handled
paul@121 331
paul@155 332
    def show_request_form(self, obj, needs_action):
paul@155 333
paul@155 334
        """
paul@155 335
        Show a form for a request concerning 'obj', indicating whether action is
paul@155 336
        needed if 'needs_action' is specified as a true value.
paul@155 337
        """
paul@155 338
paul@155 339
        details = self._get_details(obj)
paul@155 340
paul@155 341
        attendees = get_value_map(details, "ATTENDEE")
paul@155 342
        attendee_attr = attendees.get(self.user)
paul@121 343
paul@155 344
        if attendee_attr:
paul@155 345
            partstat = attendee_attr.get("PARTSTAT")
paul@155 346
            if partstat == "ACCEPTED":
paul@155 347
                self.page.p("This request has been accepted.")
paul@155 348
            elif partstat == "DECLINED":
paul@155 349
                self.page.p("This request has been declined.")
paul@155 350
            else:
paul@155 351
                self.page.p("This request has been ignored.")
paul@121 352
paul@155 353
        if needs_action:
paul@155 354
            self.page.p("An action is required for this request:")
paul@155 355
        else:
paul@155 356
            self.page.p("This request can be updated as follows:")
paul@155 357
paul@121 358
        self.page.form(method="POST")
paul@121 359
        self.page.p()
paul@121 360
        self.page.input(name="accept", type="submit", value="Accept")
paul@121 361
        self.page.add(" ")
paul@121 362
        self.page.input(name="decline", type="submit", value="Decline")
paul@121 363
        self.page.add(" ")
paul@121 364
        self.page.input(name="ignore", type="submit", value="Ignore")
paul@155 365
        if not needs_action:
paul@155 366
            self.page.input(name="update", type="hidden", value="true")
paul@121 367
        self.page.p.close()
paul@121 368
        self.page.form.close()
paul@121 369
paul@121 370
    def show_object_on_page(self, uid, obj):
paul@121 371
paul@121 372
        """
paul@121 373
        Show the calendar object with the given 'uid' and representation 'obj'
paul@121 374
        on the current page.
paul@121 375
        """
paul@121 376
paul@154 377
        # Obtain the user's timezone.
paul@154 378
paul@154 379
        prefs = self.get_preferences()
paul@154 380
        tzid = prefs.get("TZID", "UTC")
paul@121 381
paul@121 382
        # Provide a summary of the object.
paul@121 383
paul@154 384
        details = self._get_details(obj)
paul@154 385
paul@121 386
        self.page.dl()
paul@121 387
paul@121 388
        for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]:
paul@147 389
            if name in ["DTSTART", "DTEND"]:
paul@147 390
                value, attr = get_item(details, name)
paul@154 391
                tzid = attr.get("TZID", tzid)
paul@149 392
                value = self.format_datetime(to_timezone(get_datetime(value), tzid), "full")
paul@121 393
                self.page.dt(name)
paul@121 394
                self.page.dd(value)
paul@147 395
            else:
paul@147 396
                for value in get_values(details, name):
paul@147 397
                    self.page.dt(name)
paul@147 398
                    self.page.dd(value)
paul@121 399
paul@121 400
        self.page.dl.close()
paul@121 401
paul@121 402
        dtstart = format_datetime(get_utc_datetime(details, "DTSTART"))
paul@121 403
        dtend = format_datetime(get_utc_datetime(details, "DTEND"))
paul@121 404
paul@121 405
        # Indicate whether there are conflicting events.
paul@121 406
paul@121 407
        freebusy = self.store.get_freebusy(self.user)
paul@121 408
paul@121 409
        if freebusy:
paul@121 410
paul@121 411
            # Obtain any time zone details from the suggested event.
paul@121 412
paul@121 413
            _dtstart, attr = get_item(details, "DTSTART")
paul@154 414
            tzid = attr.get("TZID", tzid)
paul@121 415
paul@121 416
            # Show any conflicts.
paul@121 417
paul@121 418
            for t in have_conflict(freebusy, [(dtstart, dtend)], True):
paul@121 419
                start, end, found_uid = t[:3]
paul@154 420
paul@154 421
                # Provide details of any conflicting event.
paul@154 422
paul@121 423
                if uid != found_uid:
paul@149 424
                    start = self.format_datetime(to_timezone(get_datetime(start), tzid), "full")
paul@149 425
                    end = self.format_datetime(to_timezone(get_datetime(end), tzid), "full")
paul@154 426
                    self.page.p("Event conflicts with another from %s to %s: " % (start, end))
paul@154 427
paul@154 428
                    # Show the event summary for the conflicting event.
paul@154 429
paul@154 430
                    found_obj = self._get_object(found_uid)
paul@154 431
                    if found_obj:
paul@154 432
                        found_details = self._get_details(found_obj)
paul@154 433
                        self.page.a(get_value(found_details, "SUMMARY"), href=self.env.new_url(found_uid))
paul@121 434
paul@121 435
    def show_requests_on_page(self):
paul@69 436
paul@69 437
        "Show requests for the current user."
paul@69 438
paul@69 439
        # NOTE: This list could be more informative, but it is envisaged that
paul@69 440
        # NOTE: the requests would be visited directly anyway.
paul@69 441
paul@121 442
        requests = self._get_requests()
paul@70 443
paul@80 444
        if requests:
paul@114 445
            self.page.p("Pending requests:")
paul@114 446
paul@80 447
            self.page.ul()
paul@69 448
paul@80 449
            for request in requests:
paul@165 450
                obj = self._get_object(request)
paul@165 451
                if obj:
paul@165 452
                    details = self._get_details(obj)
paul@165 453
                    self.page.li()
paul@165 454
                    self.page.a(get_value(details, "SUMMARY"), href="#%s" % request)
paul@165 455
                    self.page.li.close()
paul@80 456
paul@80 457
            self.page.ul.close()
paul@80 458
paul@80 459
        else:
paul@80 460
            self.page.p("There are no pending requests.")
paul@69 461
paul@121 462
    # Full page output methods.
paul@70 463
paul@121 464
    def show_object(self, path_info):
paul@70 465
paul@121 466
        "Show an object request using the given 'path_info' for the current user."
paul@70 467
paul@121 468
        uid = self._get_uid(path_info)
paul@121 469
        obj = self._get_object(uid)
paul@121 470
paul@121 471
        if not obj:
paul@70 472
            return False
paul@70 473
paul@123 474
        is_request = uid in self._get_requests()
paul@155 475
        handled = self.handle_request(uid, obj, is_request)
paul@77 476
paul@123 477
        if handled:
paul@123 478
            return True
paul@73 479
paul@123 480
        self.new_page(title="Event")
paul@79 481
paul@121 482
        self.show_object_on_page(uid, obj)
paul@73 483
paul@155 484
        self.show_request_form(obj, is_request and not handled)
paul@73 485
paul@70 486
        return True
paul@70 487
paul@114 488
    def show_calendar(self):
paul@114 489
paul@114 490
        "Show the calendar for the current user."
paul@114 491
paul@114 492
        self.new_page(title="Calendar")
paul@162 493
        page = self.page
paul@162 494
paul@121 495
        self.show_requests_on_page()
paul@114 496
paul@162 497
        request_summary = self._get_request_summary()
paul@114 498
        freebusy = self.store.get_freebusy(self.user)
paul@114 499
paul@114 500
        if not freebusy:
paul@114 501
            page.p("No events scheduled.")
paul@114 502
            return
paul@114 503
paul@154 504
        # Obtain the user's timezone.
paul@147 505
paul@147 506
        prefs = self.get_preferences()
paul@153 507
        tzid = prefs.get("TZID", "UTC")
paul@147 508
paul@114 509
        # Day view: start at the earliest known day and produce days until the
paul@114 510
        # latest known day, perhaps with expandable sections of empty days.
paul@114 511
paul@114 512
        # Month view: start at the earliest known month and produce months until
paul@114 513
        # the latest known month, perhaps with expandable sections of empty
paul@114 514
        # months.
paul@114 515
paul@114 516
        # Details of users to invite to new events could be superimposed on the
paul@114 517
        # calendar.
paul@114 518
paul@114 519
        # Requests could be listed and linked to their tentative positions in
paul@114 520
        # the calendar.
paul@114 521
paul@162 522
        groups = []
paul@162 523
        group_columns = []
paul@162 524
        all_points = set()
paul@162 525
paul@162 526
        # Obtain time point information for each group of periods.
paul@162 527
paul@162 528
        for periods in [request_summary, freebusy]:
paul@162 529
            periods = convert_periods(periods, tzid)
paul@162 530
paul@162 531
            # Get the time scale with start and end points.
paul@162 532
paul@162 533
            scale = get_scale(periods)
paul@162 534
paul@162 535
            # Get the time slots for the periods.
paul@162 536
paul@162 537
            slots = get_slots(scale)
paul@162 538
paul@162 539
            # Add start of day time points for multi-day periods.
paul@162 540
paul@162 541
            add_day_start_points(slots)
paul@162 542
paul@162 543
            # Record the slots and all time points employed.
paul@162 544
paul@162 545
            groups.append(slots)
paul@162 546
            all_points.update([point for point, slot in slots])
paul@162 547
paul@162 548
        # Partition the groups into days.
paul@162 549
paul@162 550
        days = {}
paul@162 551
        partitioned_groups = []
paul@162 552
paul@162 553
        for slots in groups:
paul@162 554
paul@162 555
            # Propagate time points to all groups of time slots.
paul@162 556
paul@162 557
            add_slots(slots, all_points)
paul@162 558
paul@162 559
            # Count the number of columns employed by the group.
paul@162 560
paul@162 561
            columns = 0
paul@162 562
paul@162 563
            # Partition the time slots by day.
paul@162 564
paul@162 565
            partitioned = {}
paul@162 566
paul@162 567
            for day, day_slots in partition_by_day(slots).items():
paul@162 568
                columns = max(columns, max(map(lambda i: len(i[1]), day_slots)))
paul@162 569
paul@162 570
                if not days.has_key(day):
paul@162 571
                    days[day] = set()
paul@162 572
paul@162 573
                # Convert each partition to a mapping from points to active
paul@162 574
                # periods.
paul@162 575
paul@162 576
                day_slots = dict(day_slots)
paul@162 577
                partitioned[day] = day_slots
paul@162 578
                days[day].update(day_slots.keys())
paul@162 579
paul@162 580
            if partitioned:
paul@162 581
                group_columns.append(columns + 1)
paul@162 582
                partitioned_groups.append(partitioned)
paul@114 583
paul@114 584
        page.table(border=1, cellspacing=0, cellpadding=5)
paul@162 585
        self.show_calendar_days(days, partitioned_groups, group_columns)
paul@162 586
        page.table.close()
paul@114 587
paul@162 588
    def show_calendar_days(self, days, partitioned_groups, group_columns):
paul@162 589
        page = self.page
paul@162 590
paul@162 591
        # Determine the number of columns required, the days providing time
paul@162 592
        # slots.
paul@147 593
paul@162 594
        all_columns = sum(group_columns)
paul@162 595
        all_days = days.items()
paul@162 596
        all_days.sort()
paul@162 597
paul@162 598
        # Produce a heading and time points for each day.
paul@162 599
paul@162 600
        for day, points in all_days:
paul@114 601
            page.tr()
paul@162 602
            page.th(class_="dayheading", colspan=all_columns)
paul@153 603
            page.add(self.format_date(day, "full"))
paul@114 604
            page.th.close()
paul@153 605
            page.tr.close()
paul@114 606
paul@162 607
            groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups]
paul@162 608
paul@162 609
            self.show_calendar_points(points, groups_for_day, group_columns)
paul@162 610
paul@162 611
    def show_calendar_points(self, points, groups, group_columns):
paul@162 612
        page = self.page
paul@162 613
paul@162 614
        # Produce a row for each time point.
paul@162 615
paul@162 616
        points = list(points)
paul@162 617
        points.sort()
paul@162 618
paul@162 619
        for point in points:
paul@162 620
            continuation = point == get_start_of_day(point)
paul@153 621
paul@162 622
            page.tr()
paul@162 623
            page.th(class_="timeslot")
paul@162 624
            page.add(self.format_time(point, "long"))
paul@162 625
            page.th.close()
paul@162 626
paul@162 627
            # Obtain slots for the time point from each group.
paul@162 628
paul@162 629
            for columns, slots in zip(group_columns, groups):
paul@162 630
                active = slots and slots.get(point)
paul@162 631
paul@162 632
                if not active:
paul@162 633
                    page.td(class_="empty", colspan=columns)
paul@162 634
                    page.td.close()
paul@162 635
                    continue
paul@162 636
paul@162 637
                slots = slots.items()
paul@162 638
                slots.sort()
paul@162 639
                spans = get_spans(slots)
paul@162 640
paul@162 641
                # Show a column for each active period.
paul@117 642
paul@153 643
                for t in active:
paul@153 644
                    if t:
paul@153 645
                        start, end, uid = t[:3]
paul@153 646
                        span = spans[uid]
paul@153 647
                        if point == start or continuation:
paul@153 648
paul@153 649
                            page.td(class_="event", rowspan=span)
paul@153 650
                            obj = self._get_object(uid)
paul@153 651
                            if obj:
paul@153 652
                                details = self._get_details(obj)
paul@164 653
                                summary = get_value(details, "SUMMARY")
paul@164 654
                                href = "%s/%s" % (self.env.get_url().rstrip("/"), uid)
paul@164 655
                                if point == start:
paul@164 656
                                    page.a(summary, href=href, id=uid)
paul@164 657
                                else:
paul@164 658
                                    page.a(summary, href=href)
paul@153 659
                            page.td.close()
paul@153 660
                    else:
paul@153 661
                        page.td(class_="empty")
paul@114 662
                        page.td.close()
paul@114 663
paul@166 664
                # Pad with empty columns.
paul@166 665
paul@166 666
                i = columns - len(active)
paul@166 667
                while i > 0:
paul@166 668
                    i -= 1
paul@166 669
                    page.td(class_="empty")
paul@166 670
                    page.td.close()
paul@166 671
paul@162 672
            page.tr.close()
paul@114 673
paul@69 674
    def select_action(self):
paul@69 675
paul@69 676
        "Select the desired action and show the result."
paul@69 677
paul@121 678
        path_info = self.env.get_path_info().strip("/")
paul@121 679
paul@69 680
        if not path_info:
paul@114 681
            self.show_calendar()
paul@121 682
        elif self.show_object(path_info):
paul@70 683
            pass
paul@70 684
        else:
paul@70 685
            self.no_page()
paul@69 686
paul@82 687
    def __call__(self):
paul@69 688
paul@69 689
        "Interpret a request and show an appropriate response."
paul@69 690
paul@69 691
        if not self.user:
paul@69 692
            self.no_user()
paul@69 693
        else:
paul@69 694
            self.select_action()
paul@69 695
paul@70 696
        # Write the headers and actual content.
paul@70 697
paul@69 698
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 699
        print >>self.out
paul@69 700
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 701
paul@69 702
if __name__ == "__main__":
paul@128 703
    Manager()()
paul@69 704
paul@69 705
# vim: tabstop=4 expandtab shiftwidth=4