imip-agent

Annotated imip_manager.py

440:ab91c238fbb6
2015-03-25 Paul Boddie Created an imipweb package, moving the CGIEnvironment class into the env module.
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@232 27
from datetime import date, datetime, timedelta
paul@149 28
import babel.dates
paul@401 29
import pytz
paul@440 30
import sys
paul@69 31
paul@146 32
sys.path.append(LIBRARY_PATH)
paul@69 33
paul@360 34
from imiptools.data import get_address, get_uri, get_window_end, make_freebusy, \
paul@360 35
                           Object, to_part, \
paul@309 36
                           uri_dict, uri_item, uri_items, uri_values
paul@427 37
from imiptools.dates import format_datetime, format_time, to_date, get_datetime, \
paul@291 38
                            get_datetime_item, get_default_timezone, \
paul@427 39
                            get_end_of_day, get_period_item, get_start_of_day, \
paul@427 40
                            get_start_of_next_day, get_timestamp, ends_on_same_day, \
paul@427 41
                            to_timezone
paul@418 42
from imiptools.handlers import Handler
paul@83 43
from imiptools.mail import Messenger
paul@279 44
from imiptools.period import add_day_start_points, add_empty_days, add_slots, \
paul@279 45
                             convert_periods, get_freebusy_details, \
paul@162 46
                             get_scale, have_conflict, get_slots, get_spans, \
paul@380 47
                             partition_by_day, remove_period, remove_affected_period, \
paul@380 48
                             update_freebusy
paul@147 49
from imiptools.profile import Preferences
paul@440 50
from imipweb.env import CGIEnvironment
paul@213 51
import imip_store
paul@69 52
import markup
paul@69 53
paul@305 54
class Common:
paul@305 55
paul@305 56
    "Common handler and manager methods."
paul@305 57
paul@305 58
    def __init__(self, user):
paul@305 59
        self.user = user
paul@305 60
        self.preferences = None
paul@305 61
paul@305 62
    def get_preferences(self):
paul@305 63
        if not self.preferences:
paul@305 64
            self.preferences = Preferences(self.user)
paul@305 65
        return self.preferences
paul@305 66
paul@305 67
    def get_tzid(self):
paul@305 68
        prefs = self.get_preferences()
paul@305 69
        return prefs.get("TZID") or get_default_timezone()
paul@305 70
paul@360 71
    def get_window_size(self):
paul@360 72
        prefs = self.get_preferences()
paul@360 73
        try:
paul@360 74
            return int(prefs.get("window_size"))
paul@360 75
        except (TypeError, ValueError):
paul@360 76
            return 100
paul@360 77
paul@360 78
    def get_window_end(self):
paul@360 79
        return get_window_end(self.get_tzid(), self.get_window_size())
paul@360 80
paul@366 81
    def update_attendees(self, obj, added, removed):
paul@366 82
paul@366 83
        """
paul@366 84
        Update the attendees in 'obj' with the given 'added' and 'removed'
paul@370 85
        attendee lists. A list is returned containing the attendees whose
paul@370 86
        attendance should be cancelled.
paul@366 87
        """
paul@366 88
paul@366 89
        to_cancel = []
paul@366 90
paul@366 91
        if added or removed:
paul@366 92
            attendees = uri_items(obj.get_items("ATTENDEE") or [])
paul@366 93
paul@366 94
            if removed:
paul@366 95
                remaining = []
paul@366 96
paul@366 97
                for attendee, attendee_attr in attendees:
paul@366 98
                    if attendee in removed:
paul@438 99
                        if attendee_attr.get("PARTSTAT") in ("ACCEPTED", "TENTATIVE"):
paul@438 100
                            to_cancel.append((attendee, attendee_attr))
paul@366 101
                    else:
paul@366 102
                        remaining.append((attendee, attendee_attr))
paul@366 103
paul@366 104
                attendees = remaining
paul@366 105
paul@366 106
            if added:
paul@366 107
                for attendee in added:
paul@366 108
                    attendees.append((attendee, {"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}))
paul@366 109
paul@366 110
            obj["ATTENDEE"] = attendees
paul@366 111
paul@370 112
        return to_cancel
paul@366 113
paul@361 114
class ManagerHandler(Common, Handler):
paul@79 115
paul@121 116
    """
paul@121 117
    A content handler for use by the manager, as opposed to operating within the
paul@121 118
    mail processing pipeline.
paul@121 119
    """
paul@79 120
paul@121 121
    def __init__(self, obj, user, messenger):
paul@224 122
        Handler.__init__(self, messenger=messenger)
paul@305 123
        Common.__init__(self, user)
paul@305 124
paul@224 125
        self.set_object(obj)
paul@82 126
paul@79 127
    # Communication methods.
paul@79 128
paul@253 129
    def send_message(self, method, sender, for_organiser):
paul@79 130
paul@79 131
        """
paul@207 132
        Create a full calendar object employing the given 'method', and send it
paul@253 133
        to the appropriate recipients, also sending a copy to the 'sender'. The
paul@253 134
        'for_organiser' value indicates whether the organiser is sending this
paul@253 135
        message.
paul@79 136
        """
paul@79 137
paul@219 138
        parts = [self.obj.to_part(method)]
paul@207 139
paul@260 140
        # As organiser, send an invitation to attendees, excluding oneself if
paul@260 141
        # also attending. The updated event will be saved by the outgoing
paul@260 142
        # handler.
paul@260 143
paul@323 144
        organiser = get_uri(self.obj.get_value("ORGANIZER"))
paul@309 145
        attendees = uri_values(self.obj.get_values("ATTENDEE"))
paul@308 146
paul@253 147
        if for_organiser:
paul@308 148
            recipients = [get_address(attendee) for attendee in attendees if attendee != self.user]
paul@207 149
        else:
paul@308 150
            recipients = [get_address(organiser)]
paul@207 151
paul@219 152
        # Bundle free/busy information if appropriate.
paul@219 153
paul@219 154
        preferences = Preferences(self.user)
paul@219 155
paul@219 156
        if preferences.get("freebusy_sharing") == "share" and \
paul@219 157
           preferences.get("freebusy_bundling") == "always":
paul@219 158
paul@222 159
            # Invent a unique identifier.
paul@222 160
paul@222 161
            utcnow = get_timestamp()
paul@222 162
            uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
paul@222 163
paul@222 164
            freebusy = self.store.get_freebusy(self.user)
paul@305 165
paul@305 166
            # Replace the non-updated free/busy details for this event with
paul@305 167
            # newer details (since the outgoing handler updates this user's
paul@305 168
            # free/busy details).
paul@305 169
paul@361 170
            update_freebusy(freebusy,
paul@360 171
                self.obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()),
paul@345 172
                self.obj.get_value("TRANSP") or "OPAQUE",
paul@395 173
                self.uid, self.recurrenceid,
paul@395 174
                self.obj.get_value("SUMMARY"),
paul@395 175
                organiser)
paul@305 176
paul@292 177
            user_attr = self.messenger and self.messenger.sender != get_address(self.user) and \
paul@292 178
                {"SENT-BY" : get_uri(self.messenger.sender)} or {}
paul@292 179
paul@292 180
            parts.append(to_part("PUBLISH", [
paul@292 181
                make_freebusy(freebusy, uid, self.user, user_attr)
paul@292 182
                ]))
paul@219 183
paul@219 184
        message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)
paul@207 185
        self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)
paul@79 186
paul@79 187
    # Action methods.
paul@79 188
paul@266 189
    def process_received_request(self, update=False):
paul@79 190
paul@79 191
        """
paul@266 192
        Process the current request for the given 'user'. Return whether any
paul@79 193
        action was taken.
paul@155 194
paul@155 195
        If 'update' is given, the sequence number will be incremented in order
paul@155 196
        to override any previous response.
paul@79 197
        """
paul@79 198
paul@266 199
        # Reply only on behalf of this user.
paul@79 200
paul@309 201
        for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")):
paul@79 202
paul@79 203
            if attendee == self.user:
paul@266 204
                if attendee_attr.has_key("RSVP"):
paul@266 205
                    del attendee_attr["RSVP"]
paul@128 206
                if self.messenger and self.messenger.sender != get_address(attendee):
paul@128 207
                    attendee_attr["SENT-BY"] = get_uri(self.messenger.sender)
paul@213 208
                self.obj["ATTENDEE"] = [(attendee, attendee_attr)]
paul@273 209
paul@158 210
                self.update_dtstamp()
paul@273 211
                self.set_sequence(update)
paul@155 212
paul@253 213
                self.send_message("REPLY", get_address(attendee), for_organiser=False)
paul@79 214
paul@79 215
                return True
paul@79 216
paul@79 217
        return False
paul@79 218
paul@315 219
    def process_created_request(self, method, update=False, removed=None, added=None):
paul@207 220
paul@207 221
        """
paul@207 222
        Process the current request for the given 'user', sending a created
paul@255 223
        request of the given 'method' to attendees. Return whether any action
paul@255 224
        was taken.
paul@207 225
paul@207 226
        If 'update' is given, the sequence number will be incremented in order
paul@207 227
        to override any previous message.
paul@308 228
paul@308 229
        If 'removed' is specified, a list of participants to be removed is
paul@308 230
        provided.
paul@315 231
paul@315 232
        If 'added' is specified, a list of participants to be added is provided.
paul@207 233
        """
paul@207 234
paul@309 235
        organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER"))
paul@213 236
paul@213 237
        if self.messenger and self.messenger.sender != get_address(organiser):
paul@213 238
            organiser_attr["SENT-BY"] = get_uri(self.messenger.sender)
paul@273 239
paul@366 240
        # Update the attendees in the event.
paul@311 241
paul@370 242
        to_cancel = self.update_attendees(self.obj, added, removed)
paul@308 243
paul@207 244
        self.update_dtstamp()
paul@273 245
        self.set_sequence(update)
paul@207 246
paul@308 247
        self.send_message(method, get_address(organiser), for_organiser=True)
paul@308 248
paul@311 249
        # When cancelling, replace the attendees with those for whom the event
paul@311 250
        # is now cancelled.
paul@311 251
paul@311 252
        if to_cancel:
paul@370 253
            remaining = self.obj["ATTENDEE"]
paul@311 254
            self.obj["ATTENDEE"] = to_cancel
paul@311 255
            self.send_message("CANCEL", get_address(organiser), for_organiser=True)
paul@311 256
paul@311 257
            # Just in case more work is done with this event, the attendees are
paul@311 258
            # now restored.
paul@311 259
paul@311 260
            self.obj["ATTENDEE"] = remaining
paul@311 261
paul@207 262
        return True
paul@207 263
paul@305 264
class Manager(Common):
paul@69 265
paul@69 266
    "A simple manager application."
paul@69 267
paul@82 268
    def __init__(self, messenger=None):
paul@82 269
        self.messenger = messenger or Messenger()
paul@212 270
        self.encoding = "utf-8"
paul@212 271
        self.env = CGIEnvironment(self.encoding)
paul@212 272
paul@69 273
        user = self.env.get_user()
paul@305 274
        Common.__init__(self, user and get_uri(user) or None)
paul@305 275
paul@149 276
        self.locale = None
paul@121 277
        self.requests = None
paul@121 278
paul@69 279
        self.out = self.env.get_output()
paul@69 280
        self.page = markup.page()
paul@398 281
        self.html_ids = None
paul@69 282
paul@77 283
        self.store = imip_store.FileStore()
paul@162 284
        self.objects = {}
paul@77 285
paul@77 286
        try:
paul@77 287
            self.publisher = imip_store.FilePublisher()
paul@77 288
        except OSError:
paul@77 289
            self.publisher = None
paul@77 290
paul@427 291
    def _suffixed_name(self, name, index=None):
paul@427 292
        return index is not None and "%s-%d" % (name, index) or name
paul@427 293
paul@427 294
    def _simple_suffixed_name(self, name, suffix, index=None):
paul@427 295
        return index is not None and "%s-%s" % (name, suffix) or name
paul@427 296
paul@345 297
    def _get_identifiers(self, path_info):
paul@345 298
        parts = path_info.lstrip("/").split("/")
paul@345 299
        if len(parts) == 1:
paul@345 300
            return parts[0], None
paul@345 301
        else:
paul@345 302
            return parts[:2]
paul@121 303
paul@343 304
    def _get_object(self, uid, recurrenceid=None):
paul@343 305
        if self.objects.has_key((uid, recurrenceid)):
paul@343 306
            return self.objects[(uid, recurrenceid)]
paul@162 307
paul@343 308
        fragment = uid and self.store.get_event(self.user, uid, recurrenceid) or None
paul@343 309
        obj = self.objects[(uid, recurrenceid)] = fragment and Object(fragment)
paul@121 310
        return obj
paul@121 311
paul@380 312
    def _get_recurrences(self, uid):
paul@380 313
        return self.store.get_recurrences(self.user, uid)
paul@380 314
paul@121 315
    def _get_requests(self):
paul@121 316
        if self.requests is None:
paul@331 317
            cancellations = self.store.get_cancellations(self.user)
paul@331 318
            requests = set(self.store.get_requests(self.user))
paul@331 319
            self.requests = requests.difference(cancellations)
paul@121 320
        return self.requests
paul@117 321
paul@162 322
    def _get_request_summary(self):
paul@162 323
        summary = []
paul@343 324
        for uid, recurrenceid in self._get_requests():
paul@343 325
            obj = self._get_object(uid, recurrenceid)
paul@162 326
            if obj:
paul@380 327
                periods = obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end())
paul@380 328
                recurrenceids = self._get_recurrences(uid)
paul@380 329
paul@395 330
                # Convert the periods to more substantial free/busy items.
paul@395 331
paul@380 332
                for start, end in periods:
paul@395 333
paul@395 334
                    # Subtract any recurrences from the free/busy details of a
paul@395 335
                    # parent object.
paul@395 336
paul@380 337
                    if recurrenceid or start not in recurrenceids:
paul@395 338
                        summary.append((
paul@395 339
                            start, end, uid,
paul@395 340
                            obj.get_value("TRANSP"),
paul@395 341
                            recurrenceid,
paul@395 342
                            obj.get_value("SUMMARY"),
paul@395 343
                            obj.get_value("ORGANIZER")
paul@395 344
                            ))
paul@162 345
        return summary
paul@162 346
paul@147 347
    # Preference methods.
paul@147 348
paul@149 349
    def get_user_locale(self):
paul@149 350
        if not self.locale:
paul@350 351
            self.locale = self.get_preferences().get("LANG", "en")
paul@149 352
        return self.locale
paul@147 353
paul@162 354
    # Prettyprinting of dates and times.
paul@162 355
paul@149 356
    def format_date(self, dt, format):
paul@149 357
        return self._format_datetime(babel.dates.format_date, dt, format)
paul@149 358
paul@149 359
    def format_time(self, dt, format):
paul@149 360
        return self._format_datetime(babel.dates.format_time, dt, format)
paul@149 361
paul@149 362
    def format_datetime(self, dt, format):
paul@232 363
        return self._format_datetime(
paul@232 364
            isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,
paul@232 365
            dt, format)
paul@232 366
paul@149 367
    def _format_datetime(self, fn, dt, format):
paul@149 368
        return fn(dt, format=format, locale=self.get_user_locale())
paul@149 369
paul@78 370
    # Data management methods.
paul@78 371
paul@343 372
    def remove_request(self, uid, recurrenceid=None):
paul@343 373
        return self.store.dequeue_request(self.user, uid, recurrenceid)
paul@78 374
paul@343 375
    def remove_event(self, uid, recurrenceid=None):
paul@343 376
        return self.store.remove_event(self.user, uid, recurrenceid)
paul@234 377
paul@343 378
    def update_freebusy(self, uid, recurrenceid, obj):
paul@371 379
paul@371 380
        """
paul@371 381
        Update stored free/busy details for the event with the given 'uid' and
paul@371 382
        'recurrenceid' having a representation of 'obj'.
paul@371 383
        """
paul@371 384
paul@371 385
        is_only_organiser = self.user not in uri_values(obj.get_values("ATTENDEE"))
paul@371 386
paul@296 387
        freebusy = self.store.get_freebusy(self.user)
paul@380 388
paul@361 389
        update_freebusy(freebusy,
paul@360 390
            obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()),
paul@371 391
            is_only_organiser and "ORG" or obj.get_value("TRANSP"),
paul@395 392
            uid, recurrenceid,
paul@395 393
            obj.get_value("SUMMARY"),
paul@395 394
            obj.get_value("ORGANIZER"))
paul@380 395
paul@380 396
        # Subtract any recurrences from the free/busy details of a parent
paul@380 397
        # object.
paul@380 398
paul@380 399
        for recurrenceid in self._get_recurrences(uid):
paul@380 400
            remove_affected_period(freebusy, uid, recurrenceid)
paul@380 401
paul@361 402
        self.store.set_freebusy(self.user, freebusy)
paul@296 403
paul@343 404
    def remove_from_freebusy(self, uid, recurrenceid=None):
paul@296 405
        freebusy = self.store.get_freebusy(self.user)
paul@361 406
        remove_period(freebusy, uid, recurrenceid)
paul@361 407
        self.store.set_freebusy(self.user, freebusy)
paul@296 408
paul@78 409
    # Presentation methods.
paul@78 410
paul@69 411
    def new_page(self, title):
paul@192 412
        self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css"))
paul@398 413
        self.html_ids = set()
paul@69 414
paul@69 415
    def status(self, code, message):
paul@123 416
        self.header("Status", "%s %s" % (code, message))
paul@123 417
paul@123 418
    def header(self, header, value):
paul@123 419
        print >>self.out, "%s: %s" % (header, value)
paul@69 420
paul@69 421
    def no_user(self):
paul@69 422
        self.status(403, "Forbidden")
paul@69 423
        self.new_page(title="Forbidden")
paul@69 424
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@69 425
paul@70 426
    def no_page(self):
paul@70 427
        self.status(404, "Not Found")
paul@70 428
        self.new_page(title="Not Found")
paul@70 429
        self.page.p("No page is provided at the given address.")
paul@70 430
paul@123 431
    def redirect(self, url):
paul@123 432
        self.status(302, "Redirect")
paul@123 433
        self.header("Location", url)
paul@123 434
        self.new_page(title="Redirect")
paul@123 435
        self.page.p("Redirecting to: %s" % url)
paul@123 436
paul@345 437
    def link_to(self, uid, recurrenceid=None):
paul@345 438
        if recurrenceid:
paul@345 439
            return self.env.new_url("/".join([uid, recurrenceid]))
paul@345 440
        else:
paul@345 441
            return self.env.new_url(uid)
paul@345 442
paul@246 443
    # Request logic methods.
paul@121 444
paul@202 445
    def handle_newevent(self):
paul@202 446
paul@207 447
        """
paul@207 448
        Handle any new event operation, creating a new event and redirecting to
paul@207 449
        the event page for further activity.
paul@207 450
        """
paul@202 451
paul@202 452
        # Handle a submitted form.
paul@202 453
paul@202 454
        args = self.env.get_args()
paul@202 455
paul@202 456
        if not args.has_key("newevent"):
paul@202 457
            return
paul@202 458
paul@202 459
        # Create a new event using the available information.
paul@202 460
paul@236 461
        slots = args.get("slot", [])
paul@202 462
        participants = args.get("participants", [])
paul@202 463
paul@236 464
        if not slots:
paul@202 465
            return
paul@202 466
paul@273 467
        # Obtain the user's timezone.
paul@273 468
paul@273 469
        tzid = self.get_tzid()
paul@273 470
paul@236 471
        # Coalesce the selected slots.
paul@236 472
paul@236 473
        slots.sort()
paul@236 474
        coalesced = []
paul@236 475
        last = None
paul@236 476
paul@236 477
        for slot in slots:
paul@236 478
            start, end = slot.split("-")
paul@273 479
            start = get_datetime(start, {"TZID" : tzid})
paul@273 480
            end = end and get_datetime(end, {"TZID" : tzid}) or get_start_of_next_day(start, tzid)
paul@248 481
paul@236 482
            if last:
paul@248 483
                last_start, last_end = last
paul@248 484
paul@248 485
                # Merge adjacent dates and datetimes.
paul@248 486
paul@390 487
                if start == last_end or \
paul@390 488
                    not isinstance(start, datetime) and \
paul@390 489
                    get_start_of_day(last_end, tzid) == get_start_of_day(start, tzid):
paul@390 490
paul@248 491
                    last = last_start, end
paul@236 492
                    continue
paul@248 493
paul@248 494
                # Handle datetimes within dates.
paul@248 495
                # Datetime periods are within single days and are therefore
paul@248 496
                # discarded.
paul@248 497
paul@390 498
                elif not isinstance(last_start, datetime) and \
paul@390 499
                    get_start_of_day(start, tzid) == get_start_of_day(last_start, tzid):
paul@390 500
paul@248 501
                    continue
paul@248 502
paul@248 503
                # Add separate dates and datetimes.
paul@248 504
paul@236 505
                else:
paul@236 506
                    coalesced.append(last)
paul@248 507
paul@236 508
            last = start, end
paul@236 509
paul@236 510
        if last:
paul@236 511
            coalesced.append(last)
paul@202 512
paul@202 513
        # Invent a unique identifier.
paul@202 514
paul@222 515
        utcnow = get_timestamp()
paul@202 516
        uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
paul@202 517
paul@391 518
        # Create a calendar object and store it as a request.
paul@391 519
paul@391 520
        record = []
paul@391 521
        rwrite = record.append
paul@391 522
paul@236 523
        # Define a single occurrence if only one coalesced slot exists.
paul@391 524
paul@391 525
        start, end = coalesced[0]
paul@391 526
        start_value, start_attr = get_datetime_item(start, tzid)
paul@391 527
        end_value, end_attr = get_datetime_item(end, tzid)
paul@391 528
paul@391 529
        rwrite(("UID", {}, uid))
paul@391 530
        rwrite(("SUMMARY", {}, "New event at %s" % utcnow))
paul@391 531
        rwrite(("DTSTAMP", {}, utcnow))
paul@391 532
        rwrite(("DTSTART", start_attr, start_value))
paul@391 533
        rwrite(("DTEND", end_attr, end_value))
paul@391 534
        rwrite(("ORGANIZER", {}, self.user))
paul@391 535
paul@391 536
        participants = uri_values(filter(None, participants))
paul@391 537
paul@391 538
        for participant in participants:
paul@391 539
            rwrite(("ATTENDEE", {"RSVP" : "TRUE", "PARTSTAT" : "NEEDS-ACTION"}, participant))
paul@391 540
paul@391 541
        if self.user not in participants:
paul@391 542
            rwrite(("ATTENDEE", {"PARTSTAT" : "ACCEPTED"}, self.user))
paul@391 543
paul@391 544
        # Define additional occurrences if many slots are defined.
paul@391 545
paul@391 546
        rdates = []
paul@391 547
paul@391 548
        for start, end in coalesced[1:]:
paul@252 549
            start_value, start_attr = get_datetime_item(start, tzid)
paul@252 550
            end_value, end_attr = get_datetime_item(end, tzid)
paul@391 551
            rdates.append("%s/%s" % (start_value, end_value))
paul@391 552
paul@391 553
        if rdates:
paul@391 554
            rwrite(("RDATE", {"VALUE" : "PERIOD", "TZID" : tzid}, rdates))
paul@391 555
paul@391 556
        node = ("VEVENT", {}, record)
paul@391 557
paul@391 558
        self.store.set_event(self.user, uid, None, node=node)
paul@391 559
        self.store.queue_request(self.user, uid)
paul@202 560
paul@236 561
        # Redirect to the object (or the first of the objects), where instead of
paul@236 562
        # attendee controls, there will be organiser controls.
paul@236 563
paul@391 564
        self.redirect(self.link_to(uid))
paul@202 565
paul@375 566
    def handle_request(self, uid, recurrenceid, obj):
paul@121 567
paul@299 568
        """
paul@375 569
        Handle actions involving the given 'uid', 'recurrenceid', and 'obj' as
paul@375 570
        the object's representation, returning an error if one occurred, or None
paul@375 571
        if the request was successfully handled.
paul@299 572
        """
paul@121 573
paul@121 574
        # Handle a submitted form.
paul@121 575
paul@121 576
        args = self.env.get_args()
paul@299 577
paul@299 578
        # Get the possible actions.
paul@299 579
paul@299 580
        reply = args.has_key("reply")
paul@299 581
        discard = args.has_key("discard")
paul@299 582
        invite = args.has_key("invite")
paul@299 583
        cancel = args.has_key("cancel")
paul@299 584
        save = args.has_key("save")
paul@410 585
        ignore = args.has_key("ignore")
paul@410 586
paul@410 587
        have_action = reply or discard or invite or cancel or save or ignore
paul@299 588
paul@299 589
        if not have_action:
paul@299 590
            return ["action"]
paul@121 591
paul@410 592
        # If ignoring the object, return to the calendar.
paul@410 593
paul@410 594
        if ignore:
paul@410 595
            self.redirect(self.env.get_path())
paul@410 596
            return None
paul@410 597
paul@212 598
        # Update the object.
paul@212 599
paul@212 600
        if args.has_key("summary"):
paul@213 601
            obj["SUMMARY"] = [(args["summary"][0], {})]
paul@212 602
paul@309 603
        attendees = uri_dict(obj.get_value_map("ATTENDEE"))
paul@308 604
paul@257 605
        if args.has_key("partstat"):
paul@372 606
            if attendees.has_key(self.user):
paul@372 607
                attendees[self.user]["PARTSTAT"] = args["partstat"][0]
paul@372 608
                if attendees[self.user].has_key("RSVP"):
paul@372 609
                    del attendees[self.user]["RSVP"]
paul@286 610
paul@309 611
        is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
paul@286 612
paul@286 613
        # Obtain the user's timezone and process datetime values.
paul@286 614
paul@286 615
        update = False
paul@286 616
paul@286 617
        if is_organiser:
paul@427 618
            periods, errors = self.handle_all_period_controls()
paul@427 619
            if errors:
paul@427 620
                return errors
paul@427 621
            elif periods:
paul@427 622
                self.set_period_in_object(obj, periods[0])
paul@427 623
                self.set_periods_in_object(obj, periods[1:])
paul@257 624
paul@315 625
        # Obtain any participants to be added or removed.
paul@315 626
paul@315 627
        removed = args.get("remove")
paul@315 628
        added = args.get("added")
paul@315 629
paul@212 630
        # Process any action.
paul@212 631
paul@299 632
        handled = True
paul@121 633
paul@266 634
        if reply or invite or cancel:
paul@121 635
paul@212 636
            handler = ManagerHandler(obj, self.user, self.messenger)
paul@121 637
paul@212 638
            # Process the object and remove it from the list of requests.
paul@121 639
paul@266 640
            if reply and handler.process_received_request(update) or \
paul@308 641
               is_organiser and (invite or cancel) and \
paul@315 642
               handler.process_created_request(invite and "REQUEST" or "CANCEL", update, removed, added):
paul@121 643
paul@375 644
                self.remove_request(uid, recurrenceid)
paul@121 645
paul@257 646
        # Save single user events.
paul@121 647
paul@257 648
        elif save:
paul@370 649
            to_cancel = self.update_attendees(obj, added, removed)
paul@375 650
            self.store.set_event(self.user, uid, recurrenceid, node=obj.to_node())
paul@395 651
            self.update_freebusy(uid, recurrenceid, obj)
paul@375 652
            self.remove_request(uid, recurrenceid)
paul@121 653
paul@257 654
        # Remove the request and the object.
paul@257 655
paul@257 656
        elif discard:
paul@375 657
            self.remove_from_freebusy(uid, recurrenceid)
paul@375 658
            self.remove_event(uid, recurrenceid)
paul@375 659
            self.remove_request(uid, recurrenceid)
paul@121 660
paul@121 661
        else:
paul@123 662
            handled = False
paul@121 663
paul@212 664
        # Upon handling an action, redirect to the main page.
paul@212 665
paul@123 666
        if handled:
paul@123 667
            self.redirect(self.env.get_path())
paul@123 668
paul@299 669
        return None
paul@121 670
paul@427 671
    def handle_all_period_controls(self):
paul@155 672
paul@155 673
        """
paul@427 674
        Handle datetime controls for a particular period, where 'index' may be
paul@427 675
        used to indicate a recurring period, or the main start and end datetimes
paul@427 676
        are handled.
paul@155 677
        """
paul@155 678
paul@286 679
        args = self.env.get_args()
paul@286 680
paul@427 681
        periods = []
paul@427 682
paul@427 683
        # Get the main period details.
paul@427 684
paul@436 685
        dtend_enabled = args.get("dtend-control", [None])[0]
paul@436 686
        dttimes_enabled = args.get("dttimes-control", [None])[0]
paul@427 687
        start_values = self.get_date_control_values("dtstart")
paul@427 688
        end_values = self.get_date_control_values("dtend")
paul@427 689
paul@427 690
        period, errors = self.handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled)
paul@427 691
paul@427 692
        if errors:
paul@427 693
            return None, errors
paul@427 694
paul@427 695
        periods.append(period)
paul@427 696
paul@427 697
        # Get the recurring period details.
paul@427 698
paul@436 699
        all_dtend_enabled = args.get("dtend-control-recur", [])
paul@436 700
        all_dttimes_enabled = args.get("dttimes-control-recur", [])
paul@427 701
        all_start_values = self.get_date_control_values("dtstart-recur", multiple=True)
paul@427 702
        all_end_values = self.get_date_control_values("dtend-recur", multiple=True)
paul@427 703
paul@436 704
        for index, (start_values, end_values, dtend_enabled, dttimes_enabled) in \
paul@436 705
            enumerate(map(None, all_start_values, all_end_values, all_dtend_enabled, all_dttimes_enabled)):
paul@436 706
paul@436 707
            dtend_enabled = str(index) in all_dtend_enabled
paul@436 708
            dttimes_enabled = str(index) in all_dttimes_enabled
paul@427 709
            period, errors = self.handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled)
paul@427 710
paul@427 711
            if errors:
paul@427 712
                return None, errors
paul@427 713
paul@427 714
            periods.append(period)
paul@427 715
paul@427 716
        return periods, None
paul@427 717
paul@427 718
    def handle_period_controls(self, start_values, end_values, dtend_enabled, dttimes_enabled):
paul@427 719
paul@427 720
        """
paul@427 721
        Handle datetime controls for a particular period, described by the given
paul@427 722
        'start_values' and 'end_values', with 'dtend_enabled' and
paul@427 723
        'dttimes_enabled' affecting the usage of the provided values.
paul@427 724
        """
paul@427 725
paul@427 726
        t = self.handle_date_control_values(start_values, dttimes_enabled)
paul@427 727
        if t:
paul@427 728
            dtstart, dtstart_attr = t
paul@427 729
        else:
paul@427 730
            return None, ["dtstart"]
paul@427 731
paul@427 732
        # Handle specified end datetimes.
paul@427 733
paul@427 734
        if dtend_enabled:
paul@427 735
            t = self.handle_date_control_values(end_values, dttimes_enabled)
paul@427 736
            if t:
paul@427 737
                dtend, dtend_attr = t
paul@427 738
paul@427 739
                # Convert end dates to iCalendar "next day" dates.
paul@427 740
paul@427 741
                if not isinstance(dtend, datetime):
paul@427 742
                    dtend += timedelta(1)
paul@300 743
            else:
paul@427 744
                return None, ["dtend"]
paul@427 745
paul@427 746
        # Otherwise, treat the end date as the start date. Datetimes are
paul@427 747
        # handled by making the event occupy the rest of the day.
paul@427 748
paul@427 749
        else:
paul@427 750
            dtend = dtstart + timedelta(1)
paul@427 751
            dtend_attr = dtstart_attr
paul@427 752
paul@427 753
            if isinstance(dtstart, datetime):
paul@427 754
                dtend = get_start_of_day(dtend, attr["TZID"])
paul@427 755
paul@427 756
        if dtstart >= dtend:
paul@427 757
            return None, ["dtstart", "dtend"]
paul@427 758
paul@427 759
        return ((dtstart, dtstart_attr), (dtend, dtend_attr)), None
paul@427 760
paul@427 761
    def handle_date_control_values(self, values, with_time=True):
paul@427 762
paul@427 763
        """
paul@427 764
        Handle date control information for the given 'values', returning a
paul@427 765
        (datetime, attr) tuple, or None if the fields cannot be used to
paul@427 766
        construct a datetime object.
paul@427 767
        """
paul@427 768
paul@427 769
        if not values or not values["date"]:
paul@427 770
            return None
paul@427 771
        elif with_time:
paul@427 772
            value = "%s%s" % (values["date"], values["time"])
paul@427 773
            attr = {"TZID" : values["tzid"], "VALUE" : "DATE-TIME"}
paul@427 774
            dt = get_datetime(value, attr)
paul@427 775
        else:
paul@427 776
            attr = {"VALUE" : "DATE"}
paul@427 777
            dt = get_datetime(values["date"])
paul@427 778
paul@427 779
        if dt:
paul@427 780
            return dt, attr
paul@286 781
paul@286 782
        return None
paul@286 783
paul@427 784
    def get_date_control_values(self, name, multiple=False):
paul@427 785
paul@427 786
        """
paul@427 787
        Return a dictionary containing date, time and tzid entries for fields
paul@427 788
        starting with 'name'.
paul@427 789
        """
paul@427 790
paul@427 791
        args = self.env.get_args()
paul@427 792
paul@427 793
        dates = args.get("%s-date" % name, [])
paul@427 794
        hours = args.get("%s-hour" % name, [])
paul@427 795
        minutes = args.get("%s-minute" % name, [])
paul@427 796
        seconds = args.get("%s-second" % name, [])
paul@427 797
        tzids = args.get("%s-tzid" % name, [])
paul@427 798
paul@427 799
        # Handle absent values by employing None values.
paul@427 800
paul@427 801
        field_values = map(None, dates, hours, minutes, seconds, tzids)
paul@427 802
        if not field_values and not multiple:
paul@427 803
            field_values = [(None, None, None, None, None)]
paul@427 804
paul@427 805
        all_values = []
paul@427 806
paul@427 807
        for date, hour, minute, second, tzid in field_values:
paul@427 808
paul@427 809
            # Construct a usable dictionary of values.
paul@427 810
paul@427 811
            time = (hour or minute or second) and \
paul@427 812
                "T%s%s%s" % (
paul@427 813
                    (hour or "").rjust(2, "0")[:2],
paul@427 814
                    (minute or "").rjust(2, "0")[:2],
paul@427 815
                    (second or "").rjust(2, "0")[:2]
paul@427 816
                    ) or ""
paul@427 817
paul@427 818
            value = {
paul@427 819
                "date" : date,
paul@427 820
                "time" : time,
paul@427 821
                "tzid" : tzid or self.get_tzid()
paul@427 822
                }
paul@427 823
paul@427 824
            # Return a single value or append to a collection of all values.
paul@427 825
paul@427 826
            if not multiple:
paul@427 827
                return value
paul@427 828
            else:
paul@427 829
                all_values.append(value)
paul@427 830
paul@427 831
        return all_values
paul@427 832
paul@427 833
    def set_period_in_object(self, obj, period):
paul@427 834
paul@427 835
        "Set in the given 'obj' the given 'period' as the main start and end."
paul@427 836
paul@427 837
        (dtstart, dtstart_attr), (dtend, dtend_attr) = period
paul@427 838
paul@427 839
        return self.set_datetime_in_object(dtstart, dtstart_attr.get("TZID"), "DTSTART", obj) or \
paul@427 840
            self.set_datetime_in_object(dtend, dtend_attr.get("TZID"), "DTEND", obj)
paul@427 841
paul@427 842
    def set_periods_in_object(self, obj, periods):
paul@427 843
paul@427 844
        "Set in the given 'obj' the given 'periods'."
paul@427 845
paul@427 846
        update = False
paul@427 847
paul@427 848
        old_values = obj.get_values("RDATE")
paul@427 849
        new_rdates = []
paul@427 850
paul@427 851
        del obj["RDATE"]
paul@427 852
paul@427 853
        for period in periods:
paul@427 854
            (dtstart, dtstart_attr), (dtend, dtend_attr) = period
paul@427 855
            tzid = dtstart_attr.get("TZID") or dtend_attr.get("TZID")
paul@427 856
            new_rdates.append(get_period_item(dtstart, dtend, tzid))
paul@427 857
paul@427 858
        obj["RDATE"] = new_rdates
paul@427 859
paul@427 860
        # NOTE: To do: calculate the update status.
paul@427 861
        return update
paul@427 862
paul@286 863
    def set_datetime_in_object(self, dt, tzid, property, obj):
paul@286 864
paul@286 865
        """
paul@286 866
        Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether
paul@286 867
        an update has occurred.
paul@286 868
        """
paul@286 869
paul@286 870
        if dt:
paul@286 871
            old_value = obj.get_value(property)
paul@286 872
            obj[property] = [get_datetime_item(dt, tzid)]
paul@286 873
            return format_datetime(dt) != old_value
paul@286 874
paul@286 875
        return False
paul@286 876
paul@421 877
    def handle_new_attendees(self, obj):
paul@421 878
paul@421 879
        "Add or remove new attendees. This does not affect the stored object."
paul@421 880
paul@421 881
        args = self.env.get_args()
paul@421 882
paul@421 883
        existing_attendees = uri_values(obj.get_values("ATTENDEE") or [])
paul@421 884
        new_attendees = args.get("added", [])
paul@421 885
        new_attendee = args.get("attendee", [""])[0]
paul@421 886
paul@421 887
        if args.has_key("add"):
paul@421 888
            if new_attendee.strip():
paul@421 889
                new_attendee = get_uri(new_attendee.strip())
paul@421 890
                if new_attendee not in new_attendees and new_attendee not in existing_attendees:
paul@421 891
                    new_attendees.append(new_attendee)
paul@421 892
                new_attendee = ""
paul@421 893
paul@421 894
        if args.has_key("removenew"):
paul@421 895
            removed_attendee = args["removenew"][0]
paul@421 896
            if removed_attendee in new_attendees:
paul@421 897
                new_attendees.remove(removed_attendee)
paul@421 898
paul@421 899
        return new_attendees, new_attendee
paul@421 900
paul@421 901
    def get_event_period(self, obj):
paul@421 902
paul@421 903
        """
paul@421 904
        Return (dtstart, dtstart attributes), (dtend, dtend attributes) for
paul@421 905
        'obj'.
paul@421 906
        """
paul@421 907
paul@421 908
        dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")
paul@421 909
        if obj.has_key("DTEND"):
paul@421 910
            dtend, dtend_attr = obj.get_datetime_item("DTEND")
paul@421 911
        elif obj.has_key("DURATION"):
paul@421 912
            duration = obj.get_duration("DURATION")
paul@421 913
            dtend = dtstart + duration
paul@421 914
            dtend_attr = dtstart_attr
paul@421 915
        else:
paul@421 916
            dtend, dtend_attr = dtstart, dtstart_attr
paul@421 917
        return (dtstart, dtstart_attr), (dtend, dtend_attr)
paul@421 918
paul@286 919
    # Page fragment methods.
paul@286 920
paul@286 921
    def show_request_controls(self, obj):
paul@286 922
paul@286 923
        "Show form controls for a request concerning 'obj'."
paul@286 924
paul@212 925
        page = self.page
paul@326 926
        args = self.env.get_args()
paul@212 927
paul@309 928
        is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
paul@207 929
paul@416 930
        attendees = uri_values((obj.get_values("ATTENDEE") or []) + filter(None, args.get("attendee", [])))
paul@326 931
        is_attendee = self.user in attendees
paul@121 932
paul@343 933
        is_request = (obj.get_value("UID"), obj.get_value("RECURRENCE-ID")) in self._get_requests()
paul@276 934
paul@257 935
        have_other_attendees = len(attendees) > (is_attendee and 1 or 0)
paul@257 936
paul@257 937
        # Show appropriate options depending on the role of the user.
paul@257 938
paul@257 939
        if is_attendee and not is_organiser:
paul@286 940
            page.p("An action is required for this request:")
paul@253 941
paul@255 942
            page.p()
paul@410 943
            page.input(name="reply", type="submit", value="Send reply")
paul@255 944
            page.add(" ")
paul@410 945
            page.input(name="discard", type="submit", value="Discard event")
paul@410 946
            page.add(" ")
paul@410 947
            page.input(name="ignore", type="submit", value="Do nothing for now")
paul@255 948
            page.p.close()
paul@207 949
paul@255 950
        if is_organiser:
paul@404 951
            page.p("As organiser, you can perform the following:")
paul@404 952
paul@257 953
            if have_other_attendees:
paul@257 954
                page.p()
paul@410 955
                page.input(name="invite", type="submit", value="Invite/notify attendees")
paul@257 956
                page.add(" ")
paul@276 957
                if is_request:
paul@410 958
                    page.input(name="discard", type="submit", value="Discard event")
paul@276 959
                else:
paul@410 960
                    page.input(name="cancel", type="submit", value="Cancel event")
paul@410 961
                page.add(" ")
paul@410 962
                page.input(name="ignore", type="submit", value="Do nothing for now")
paul@257 963
                page.p.close()
paul@257 964
            else:
paul@257 965
                page.p()
paul@410 966
                page.input(name="save", type="submit", value="Save event")
paul@276 967
                page.add(" ")
paul@410 968
                page.input(name="discard", type="submit", value="Discard event")
paul@410 969
                page.add(" ")
paul@410 970
                page.input(name="ignore", type="submit", value="Do nothing for now")
paul@257 971
                page.p.close()
paul@207 972
paul@287 973
    property_items = [
paul@287 974
        ("SUMMARY", "Summary"),
paul@287 975
        ("DTSTART", "Start"),
paul@287 976
        ("DTEND", "End"),
paul@287 977
        ("ORGANIZER", "Organiser"),
paul@287 978
        ("ATTENDEE", "Attendee"),
paul@287 979
        ]
paul@210 980
paul@257 981
    partstat_items = [
paul@257 982
        ("NEEDS-ACTION", "Not confirmed"),
paul@257 983
        ("ACCEPTED", "Attending"),
paul@259 984
        ("TENTATIVE", "Tentatively attending"),
paul@257 985
        ("DECLINED", "Not attending"),
paul@277 986
        ("DELEGATED", "Delegated"),
paul@355 987
        (None, "Not indicated"),
paul@257 988
        ]
paul@257 989
paul@299 990
    def show_object_on_page(self, uid, obj, error=None):
paul@121 991
paul@121 992
        """
paul@121 993
        Show the calendar object with the given 'uid' and representation 'obj'
paul@299 994
        on the current page. If 'error' is given, show a suitable message.
paul@121 995
        """
paul@121 996
paul@210 997
        page = self.page
paul@212 998
        page.form(method="POST")
paul@210 999
paul@436 1000
        page.input(name="editing", type="hidden", value="true")
paul@436 1001
paul@363 1002
        args = self.env.get_args()
paul@363 1003
paul@154 1004
        # Obtain the user's timezone.
paul@154 1005
paul@244 1006
        tzid = self.get_tzid()
paul@121 1007
paul@363 1008
        # Obtain basic event information, showing any necessary editing controls.
paul@315 1009
paul@363 1010
        is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
paul@315 1011
paul@363 1012
        if is_organiser:
paul@363 1013
            new_attendees, new_attendee = self.handle_new_attendees(obj)
paul@290 1014
        else:
paul@363 1015
            new_attendees = []
paul@363 1016
            new_attendee = ""
paul@300 1017
paul@436 1018
        (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_event_period(obj)
paul@408 1019
        self.show_object_datetime_controls(dtstart, dtend)
paul@408 1020
paul@121 1021
        # Provide a summary of the object.
paul@121 1022
paul@230 1023
        page.table(class_="object", cellspacing=5, cellpadding=5)
paul@212 1024
        page.thead()
paul@212 1025
        page.tr()
paul@286 1026
        page.th("Event", class_="mainheading", colspan=2)
paul@212 1027
        page.tr.close()
paul@212 1028
        page.thead.close()
paul@212 1029
        page.tbody()
paul@121 1030
paul@287 1031
        for name, label in self.property_items:
paul@409 1032
            field = name.lower()
paul@409 1033
paul@409 1034
            items = obj.get_items(name) or []
paul@409 1035
            rowspan = len(items)
paul@409 1036
paul@409 1037
            if name == "ATTENDEE":
paul@409 1038
                rowspan += len(new_attendees) + 1
paul@409 1039
            elif not items:
paul@409 1040
                continue
paul@409 1041
paul@210 1042
            page.tr()
paul@409 1043
            page.th(label, class_="objectheading %s%s" % (field, error and field in error and " error" or ""), rowspan=rowspan)
paul@210 1044
paul@210 1045
            # Handle datetimes specially.
paul@210 1046
paul@147 1047
            if name in ["DTSTART", "DTEND"]:
paul@290 1048
paul@297 1049
                # Obtain the datetime.
paul@297 1050
paul@290 1051
                if name == "DTSTART":
paul@411 1052
                    dt, attr = dtstart, dtstart_attr
paul@297 1053
paul@297 1054
                # Where no end datetime exists, use the start datetime as the
paul@297 1055
                # basis of any potential datetime specified if dt-control is
paul@297 1056
                # set.
paul@297 1057
paul@290 1058
                else:
paul@411 1059
                    dt, attr = dtend or dtstart, dtend_attr or dtstart_attr
paul@293 1060
paul@413 1061
                self.show_datetime_controls(obj, dt, attr, name == "DTSTART")
paul@286 1062
paul@210 1063
                page.tr.close()
paul@210 1064
paul@212 1065
            # Handle the summary specially.
paul@212 1066
paul@212 1067
            elif name == "SUMMARY":
paul@290 1068
                value = args.get("summary", [obj.get_value(name)])[0]
paul@290 1069
paul@286 1070
                page.td()
paul@269 1071
                if is_organiser:
paul@269 1072
                    page.input(name="summary", type="text", value=value, size=80)
paul@269 1073
                else:
paul@269 1074
                    page.add(value)
paul@212 1075
                page.td.close()
paul@212 1076
                page.tr.close()
paul@212 1077
paul@210 1078
            # Handle potentially many values.
paul@210 1079
paul@147 1080
            else:
paul@210 1081
                first = True
paul@210 1082
paul@308 1083
                for i, (value, attr) in enumerate(items):
paul@210 1084
                    if not first:
paul@210 1085
                        page.tr()
paul@210 1086
                    else:
paul@210 1087
                        first = False
paul@121 1088
paul@372 1089
                    if name == "ATTENDEE":
paul@309 1090
                        value = get_uri(value)
paul@309 1091
paul@326 1092
                        page.td(class_="objectvalue")
paul@265 1093
                        page.add(value)
paul@286 1094
                        page.add(" ")
paul@210 1095
paul@210 1096
                        partstat = attr.get("PARTSTAT")
paul@372 1097
                        if value == self.user:
paul@315 1098
                            self._show_menu("partstat", partstat, self.partstat_items, "partstat")
paul@265 1099
                        else:
paul@286 1100
                            page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat")
paul@308 1101
paul@372 1102
                        if is_organiser:
paul@315 1103
                            if value in args.get("remove", []):
paul@315 1104
                                page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove", checked="checked")
paul@315 1105
                            else:
paul@315 1106
                                page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove")
paul@308 1107
                            page.label("Remove", for_="remove-%d" % i, class_="remove")
paul@308 1108
                            page.label("Uninvited", for_="remove-%d" % i, class_="removed")
paul@308 1109
paul@265 1110
                    else:
paul@326 1111
                        page.td(class_="objectvalue")
paul@265 1112
                        page.add(value)
paul@210 1113
paul@210 1114
                    page.td.close()
paul@210 1115
                    page.tr.close()
paul@210 1116
paul@315 1117
                # Allow more attendees to be specified.
paul@315 1118
paul@315 1119
                if is_organiser and name == "ATTENDEE":
paul@315 1120
                    for i, attendee in enumerate(new_attendees):
paul@326 1121
                        if not first:
paul@326 1122
                            page.tr()
paul@326 1123
                        else:
paul@326 1124
                            first = False
paul@326 1125
paul@315 1126
                        page.td()
paul@315 1127
                        page.input(name="added", type="value", value=attendee)
paul@315 1128
                        page.input(name="removenew", type="submit", value=attendee, id="removenew-%d" % i, class_="remove")
paul@315 1129
                        page.label("Remove", for_="removenew-%d" % i, class_="remove")
paul@315 1130
                        page.td.close()
paul@315 1131
                        page.tr.close()
paul@326 1132
paul@326 1133
                    if not first:
paul@326 1134
                        page.tr()
paul@326 1135
paul@315 1136
                    page.td()
paul@315 1137
                    page.input(name="attendee", type="value", value=new_attendee)
paul@315 1138
                    page.input(name="add", type="submit", value="add", id="add-%d" % i, class_="add")
paul@315 1139
                    page.label("Add", for_="add-%d" % i, class_="add")
paul@315 1140
                    page.td.close()
paul@315 1141
                    page.tr.close()
paul@315 1142
paul@212 1143
        page.tbody.close()
paul@210 1144
        page.table.close()
paul@121 1145
paul@321 1146
        self.show_recurrences(obj)
paul@307 1147
        self.show_conflicting_events(uid, obj)
paul@307 1148
        self.show_request_controls(obj)
paul@307 1149
paul@307 1150
        page.form.close()
paul@307 1151
paul@427 1152
    def show_object_datetime_controls(self, start, end, index=None):
paul@427 1153
paul@427 1154
        """
paul@427 1155
        Show datetime-related controls if already active or if an object needs
paul@427 1156
        them for the given 'start' to 'end' period. The given 'index' is used to
paul@427 1157
        parameterise individual controls for dynamic manipulation.
paul@427 1158
        """
paul@427 1159
paul@427 1160
        page = self.page
paul@427 1161
        args = self.env.get_args()
paul@427 1162
        sn = self._suffixed_name
paul@427 1163
        ssn = self._simple_suffixed_name
paul@427 1164
paul@427 1165
        # Add a dynamic stylesheet to permit the controls to modify the display.
paul@427 1166
        # NOTE: The style details need to be coordinated with the static
paul@427 1167
        # NOTE: stylesheet.
paul@427 1168
paul@427 1169
        if index is not None:
paul@427 1170
            page.style(type="text/css")
paul@427 1171
paul@427 1172
            # Unlike the rules for object properties, these affect recurrence
paul@427 1173
            # properties.
paul@427 1174
paul@427 1175
            page.add("""\
paul@427 1176
input#dttimes-enable-%(index)d,
paul@427 1177
input#dtend-enable-%(index)d,
paul@427 1178
input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
paul@427 1179
input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
paul@427 1180
input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
paul@427 1181
input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
paul@427 1182
    display: none;
paul@427 1183
}""" % {"index" : index})
paul@427 1184
paul@427 1185
            page.style.close()
paul@427 1186
paul@436 1187
        dtend_control = args.get(ssn("dtend-control", "recur", index), [])
paul@436 1188
        dttimes_control = args.get(ssn("dttimes-control", "recur", index), [])
paul@436 1189
paul@436 1190
        dtend_enabled = index is not None and str(index) in dtend_control or index is None and dtend_control
paul@436 1191
        dttimes_enabled = index is not None and str(index) in dttimes_control or index is None and dttimes_control
paul@436 1192
paul@436 1193
        initial_load = not args.has_key("editing")
paul@436 1194
paul@436 1195
        dtend_enabled = dtend_enabled or initial_load and (isinstance(end, datetime) or start != end - timedelta(1))
paul@436 1196
        dttimes_enabled = dttimes_enabled or initial_load and (isinstance(start, datetime) or isinstance(end, datetime))
paul@427 1197
paul@427 1198
        if dtend_enabled:
paul@436 1199
            page.input(name=ssn("dtend-control", "recur", index), type="checkbox",
paul@436 1200
                       value=(index is not None and str(index) or "enable"), id=sn("dtend-enable", index), checked="checked")
paul@427 1201
        else:
paul@436 1202
            page.input(name=ssn("dtend-control", "recur", index), type="checkbox",
paul@436 1203
                       value=(index is not None and str(index) or "enable"), id=sn("dtend-enable", index))
paul@427 1204
paul@427 1205
        if dttimes_enabled:
paul@436 1206
            page.input(name=ssn("dttimes-control", "recur", index), type="checkbox",
paul@436 1207
                       value=(index is not None and str(index) or "enable"), id=sn("dttimes-enable", index), checked="checked")
paul@427 1208
        else:
paul@436 1209
            page.input(name=ssn("dttimes-control", "recur", index), type="checkbox",
paul@436 1210
                       value=(index is not None and str(index) or "enable"), id=sn("dttimes-enable", index))
paul@427 1211
paul@427 1212
    def show_datetime_controls(self, obj, dt, attr, show_start):
paul@413 1213
paul@413 1214
        """
paul@413 1215
        Show datetime details from the given 'obj' for the datetime 'dt' and
paul@427 1216
        attributes 'attr', showing start details if 'show_start' is set
paul@413 1217
        to a true value. Details will appear as controls for organisers and
paul@413 1218
        labels for attendees.
paul@413 1219
        """
paul@413 1220
paul@413 1221
        page = self.page
paul@413 1222
        is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
paul@413 1223
paul@429 1224
        # Change end dates to refer to the actual dates, not the iCalendar
paul@429 1225
        # "next day" dates.
paul@429 1226
paul@429 1227
        if not show_start and not isinstance(dt, datetime):
paul@429 1228
            dt -= timedelta(1)
paul@429 1229
paul@413 1230
        # Show controls for editing as organiser.
paul@413 1231
paul@413 1232
        if is_organiser:
paul@427 1233
            page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
paul@427 1234
paul@427 1235
            if show_start:
paul@413 1236
                page.div(class_="dt enabled")
paul@423 1237
                self._show_date_controls("dtstart", dt, attr.get("TZID"))
paul@413 1238
                page.br()
paul@413 1239
                page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
paul@427 1240
                page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
paul@413 1241
                page.div.close()
paul@413 1242
paul@413 1243
            else:
paul@413 1244
                page.div(class_="dt disabled")
paul@413 1245
                page.label("Specify end date", for_="dtend-enable", class_="enable")
paul@413 1246
                page.div.close()
paul@413 1247
                page.div(class_="dt enabled")
paul@423 1248
                self._show_date_controls("dtend", dt, attr.get("TZID"))
paul@413 1249
                page.br()
paul@427 1250
                page.label("End on same day", for_="dtend-enable", class_="disable")
paul@413 1251
                page.div.close()
paul@413 1252
paul@413 1253
            page.td.close()
paul@413 1254
paul@413 1255
        # Show a label as attendee.
paul@413 1256
paul@413 1257
        else:
paul@413 1258
            page.td(self.format_datetime(dt, "full"))
paul@413 1259
paul@427 1260
    def show_recurrence_controls(self, obj, index, start, end, origin, recurrenceid, recurrenceids, show_start):
paul@408 1261
paul@408 1262
        """
paul@427 1263
        Show datetime details from the given 'obj' for the recurrence having the
paul@427 1264
        given 'index', with the recurrence period described by the datetimes
paul@427 1265
        'start' and 'end', indicating the 'origin' of the period from the event
paul@427 1266
        details, employing any 'recurrenceid' and 'recurrenceids' for the object
paul@427 1267
        to configure the displayed information.
paul@427 1268
paul@427 1269
        If 'show_start' is set to a true value, the start details will be shown;
paul@427 1270
        otherwise, the end details will be shown.
paul@408 1271
        """
paul@408 1272
paul@408 1273
        page = self.page
paul@427 1274
        sn = self._suffixed_name
paul@427 1275
        ssn = self._simple_suffixed_name
paul@427 1276
paul@427 1277
        is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
paul@427 1278
paul@429 1279
        # Change end dates to refer to the actual dates, not the iCalendar
paul@429 1280
        # "next day" dates.
paul@429 1281
paul@429 1282
        if not isinstance(end, datetime):
paul@429 1283
            end -= timedelta(1)
paul@429 1284
paul@427 1285
        start_utc = format_datetime(to_timezone(start, "UTC"))
paul@427 1286
        replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""
paul@427 1287
        css = " ".join([
paul@427 1288
            replaced,
paul@427 1289
            recurrenceid and start_utc == recurrenceid and "affected" or ""
paul@427 1290
            ])
paul@427 1291
paul@427 1292
        # Show controls for editing as organiser.
paul@427 1293
paul@427 1294
        if is_organiser and not replaced and origin != "RRULE":
paul@427 1295
            page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
paul@427 1296
paul@427 1297
            if show_start:
paul@427 1298
                page.div(class_="dt enabled")
paul@435 1299
                self._show_date_controls(ssn("dtstart", "recur", index), start, None, index)
paul@427 1300
                page.br()
paul@427 1301
                page.label("Specify times", for_=sn("dttimes-enable", index), class_="time disabled enable")
paul@427 1302
                page.label("Specify dates only", for_=sn("dttimes-enable", index), class_="time enabled disable")
paul@427 1303
                page.div.close()
paul@427 1304
paul@427 1305
            else:
paul@427 1306
                page.div(class_="dt disabled")
paul@427 1307
                page.label("Specify end date", for_=sn("dtend-enable", index), class_="enable")
paul@427 1308
                page.div.close()
paul@427 1309
                page.div(class_="dt enabled")
paul@435 1310
                self._show_date_controls(ssn("dtend", "recur", index), end, None, index)
paul@427 1311
                page.br()
paul@427 1312
                page.label("End on same day", for_=sn("dtend-enable", index), class_="disable")
paul@427 1313
                page.div.close()
paul@427 1314
paul@427 1315
            page.td.close()
paul@427 1316
paul@427 1317
        # Show label as attendee.
paul@427 1318
paul@363 1319
        else:
paul@427 1320
            page.td(self.format_datetime(show_start and start or end, "long"), class_=css)
paul@363 1321
paul@321 1322
    def show_recurrences(self, obj):
paul@321 1323
paul@321 1324
        "Show recurrences for the object having the given representation 'obj'."
paul@321 1325
paul@321 1326
        page = self.page
paul@427 1327
        is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
paul@321 1328
paul@357 1329
        # Obtain any parent object if this object is a specific recurrence.
paul@357 1330
paul@380 1331
        uid = obj.get_value("UID")
paul@357 1332
        recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
paul@357 1333
paul@357 1334
        if recurrenceid:
paul@380 1335
            obj = self._get_object(uid)
paul@357 1336
            if not obj:
paul@357 1337
                return
paul@357 1338
paul@357 1339
            page.p("This event modifies a recurring event.")
paul@357 1340
paul@360 1341
        # Obtain the periods associated with the event in the user's time zone.
paul@321 1342
paul@427 1343
        periods = obj.get_periods(self.get_tzid(), self.get_window_end(), origin=True)
paul@380 1344
        recurrenceids = self._get_recurrences(uid)
paul@321 1345
paul@321 1346
        if len(periods) == 1:
paul@321 1347
            return
paul@321 1348
paul@427 1349
        if is_organiser:
paul@427 1350
            page.p("This event recurs on the following occasions within the next %d days:" % self.get_window_size())
paul@427 1351
        else:
paul@427 1352
            page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())
paul@427 1353
paul@427 1354
        # Determine whether any periods are explicitly created or are part of a
paul@427 1355
        # rule.
paul@427 1356
paul@427 1357
        explicit_periods = filter(lambda t: t[2] != "RRULE", periods)
paul@427 1358
paul@427 1359
        # Show each recurrence in a separate table if editable.
paul@427 1360
paul@427 1361
        if is_organiser and explicit_periods:
paul@436 1362
paul@427 1363
            for index, (start, end, origin) in enumerate(periods[1:]):
paul@427 1364
paul@427 1365
                # Isolate the controls from neighbouring tables.
paul@427 1366
paul@427 1367
                page.div()
paul@427 1368
paul@427 1369
                self.show_object_datetime_controls(start, end, index)
paul@427 1370
paul@427 1371
                # NOTE: Need to customise the TH classes according to errors and
paul@427 1372
                # NOTE: index information.
paul@427 1373
paul@427 1374
                page.table(cellspacing=5, cellpadding=5, class_="recurrence")
paul@427 1375
                page.caption("Occurrence")
paul@427 1376
                page.tbody()
paul@427 1377
                page.tr()
paul@427 1378
                page.th("Start", class_="objectheading start")
paul@427 1379
                self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, True)
paul@427 1380
                page.tr.close()
paul@427 1381
                page.tr()
paul@427 1382
                page.th("End", class_="objectheading end")
paul@427 1383
                self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, False)
paul@427 1384
                page.tr.close()
paul@427 1385
                page.tbody.close()
paul@427 1386
                page.table.close()
paul@427 1387
paul@427 1388
                page.div.close()
paul@427 1389
paul@427 1390
        # Otherwise, use a compact single table.
paul@427 1391
paul@427 1392
        else:
paul@427 1393
            page.table(cellspacing=5, cellpadding=5, class_="recurrence")
paul@427 1394
            page.caption("Occurrences")
paul@427 1395
            page.thead()
paul@321 1396
            page.tr()
paul@427 1397
            page.th("Start", class_="objectheading start")
paul@427 1398
            page.th("End", class_="objectheading end")
paul@321 1399
            page.tr.close()
paul@427 1400
            page.thead.close()
paul@427 1401
            page.tbody()
paul@439 1402
paul@439 1403
            # Show only subsequent periods if organiser, since the principal
paul@439 1404
            # period will be the start and end datetimes.
paul@439 1405
paul@439 1406
            for index, (start, end, origin) in enumerate(is_organiser and periods[1:] or periods):
paul@427 1407
                page.tr()
paul@427 1408
                self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, True)
paul@427 1409
                self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, False)
paul@427 1410
                page.tr.close()
paul@427 1411
            page.tbody.close()
paul@427 1412
            page.table.close()
paul@321 1413
paul@307 1414
    def show_conflicting_events(self, uid, obj):
paul@307 1415
paul@307 1416
        """
paul@307 1417
        Show conflicting events for the object having the given 'uid' and
paul@307 1418
        representation 'obj'.
paul@307 1419
        """
paul@307 1420
paul@307 1421
        page = self.page
paul@307 1422
paul@307 1423
        # Obtain the user's timezone.
paul@307 1424
paul@307 1425
        tzid = self.get_tzid()
paul@386 1426
        periods = obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end())
paul@386 1427
paul@121 1428
        # Indicate whether there are conflicting events.
paul@121 1429
paul@121 1430
        freebusy = self.store.get_freebusy(self.user)
paul@121 1431
paul@121 1432
        if freebusy:
paul@121 1433
paul@121 1434
            # Obtain any time zone details from the suggested event.
paul@121 1435
paul@213 1436
            _dtstart, attr = obj.get_item("DTSTART")
paul@154 1437
            tzid = attr.get("TZID", tzid)
paul@121 1438
paul@121 1439
            # Show any conflicts.
paul@121 1440
paul@386 1441
            conflicts = [t for t in have_conflict(freebusy, periods, True) if t[2] != uid]
paul@154 1442
paul@302 1443
            if conflicts:
paul@302 1444
                page.p("This event conflicts with others:")
paul@154 1445
paul@302 1446
                page.table(cellspacing=5, cellpadding=5, class_="conflicts")
paul@302 1447
                page.thead()
paul@302 1448
                page.tr()
paul@302 1449
                page.th("Event")
paul@302 1450
                page.th("Start")
paul@302 1451
                page.th("End")
paul@302 1452
                page.tr.close()
paul@302 1453
                page.thead.close()
paul@302 1454
                page.tbody()
paul@302 1455
paul@302 1456
                for t in conflicts:
paul@407 1457
                    start, end, found_uid, transp, found_recurrenceid, summary = t[:6]
paul@302 1458
paul@302 1459
                    # Provide details of any conflicting event.
paul@302 1460
paul@302 1461
                    start = self.format_datetime(to_timezone(get_datetime(start), tzid), "long")
paul@302 1462
                    end = self.format_datetime(to_timezone(get_datetime(end), tzid), "long")
paul@302 1463
paul@302 1464
                    page.tr()
paul@154 1465
paul@154 1466
                    # Show the event summary for the conflicting event.
paul@154 1467
paul@302 1468
                    page.td()
paul@407 1469
                    page.a(summary, href=self.link_to(found_uid))
paul@302 1470
                    page.td.close()
paul@302 1471
paul@302 1472
                    page.td(start)
paul@302 1473
                    page.td(end)
paul@302 1474
paul@302 1475
                    page.tr.close()
paul@302 1476
paul@302 1477
                page.tbody.close()
paul@302 1478
                page.table.close()
paul@121 1479
paul@121 1480
    def show_requests_on_page(self):
paul@69 1481
paul@69 1482
        "Show requests for the current user."
paul@69 1483
paul@399 1484
        page = self.page
paul@399 1485
paul@69 1486
        # NOTE: This list could be more informative, but it is envisaged that
paul@69 1487
        # NOTE: the requests would be visited directly anyway.
paul@69 1488
paul@121 1489
        requests = self._get_requests()
paul@70 1490
paul@399 1491
        page.div(id="pending-requests")
paul@185 1492
paul@80 1493
        if requests:
paul@399 1494
            page.p("Pending requests:")
paul@399 1495
paul@399 1496
            page.ul()
paul@69 1497
paul@343 1498
            for uid, recurrenceid in requests:
paul@343 1499
                obj = self._get_object(uid, recurrenceid)
paul@165 1500
                if obj:
paul@399 1501
                    page.li()
paul@399 1502
                    page.a(obj.get_value("SUMMARY"), href="#request-%s-%s" % (uid, recurrenceid or ""))
paul@399 1503
                    page.li.close()
paul@399 1504
paul@399 1505
            page.ul.close()
paul@80 1506
paul@80 1507
        else:
paul@399 1508
            page.p("There are no pending requests.")
paul@399 1509
paul@399 1510
        page.div.close()
paul@185 1511
paul@185 1512
    def show_participants_on_page(self):
paul@185 1513
paul@185 1514
        "Show participants for scheduling purposes."
paul@185 1515
paul@399 1516
        page = self.page
paul@185 1517
        args = self.env.get_args()
paul@185 1518
        participants = args.get("participants", [])
paul@185 1519
paul@185 1520
        try:
paul@185 1521
            for name, value in args.items():
paul@185 1522
                if name.startswith("remove-participant-"):
paul@185 1523
                    i = int(name[len("remove-participant-"):])
paul@185 1524
                    del participants[i]
paul@185 1525
                    break
paul@185 1526
        except ValueError:
paul@185 1527
            pass
paul@185 1528
paul@185 1529
        # Trim empty participants.
paul@185 1530
paul@185 1531
        while participants and not participants[-1].strip():
paul@185 1532
            participants.pop()
paul@185 1533
paul@185 1534
        # Show any specified participants together with controls to remove and
paul@185 1535
        # add participants.
paul@185 1536
paul@399 1537
        page.div(id="participants")
paul@399 1538
paul@399 1539
        page.p("Participants for scheduling:")
paul@185 1540
paul@185 1541
        for i, participant in enumerate(participants):
paul@399 1542
            page.p()
paul@399 1543
            page.input(name="participants", type="text", value=participant)
paul@399 1544
            page.input(name="remove-participant-%d" % i, type="submit", value="Remove")
paul@399 1545
            page.p.close()
paul@399 1546
paul@399 1547
        page.p()
paul@399 1548
        page.input(name="participants", type="text")
paul@399 1549
        page.input(name="add-participant", type="submit", value="Add")
paul@399 1550
        page.p.close()
paul@399 1551
paul@399 1552
        page.div.close()
paul@185 1553
paul@185 1554
        return participants
paul@185 1555
paul@121 1556
    # Full page output methods.
paul@70 1557
paul@121 1558
    def show_object(self, path_info):
paul@70 1559
paul@121 1560
        "Show an object request using the given 'path_info' for the current user."
paul@70 1561
paul@345 1562
        uid, recurrenceid = self._get_identifiers(path_info)
paul@345 1563
        obj = self._get_object(uid, recurrenceid)
paul@121 1564
paul@121 1565
        if not obj:
paul@70 1566
            return False
paul@70 1567
paul@375 1568
        error = self.handle_request(uid, recurrenceid, obj)
paul@77 1569
paul@299 1570
        if not error:
paul@123 1571
            return True
paul@73 1572
paul@123 1573
        self.new_page(title="Event")
paul@299 1574
        self.show_object_on_page(uid, obj, error)
paul@73 1575
paul@70 1576
        return True
paul@70 1577
paul@114 1578
    def show_calendar(self):
paul@114 1579
paul@114 1580
        "Show the calendar for the current user."
paul@114 1581
paul@202 1582
        handled = self.handle_newevent()
paul@202 1583
paul@114 1584
        self.new_page(title="Calendar")
paul@162 1585
        page = self.page
paul@162 1586
paul@196 1587
        # Form controls are used in various places on the calendar page.
paul@196 1588
paul@196 1589
        page.form(method="POST")
paul@196 1590
paul@121 1591
        self.show_requests_on_page()
paul@185 1592
        participants = self.show_participants_on_page()
paul@114 1593
paul@196 1594
        # Show a button for scheduling a new event.
paul@196 1595
paul@230 1596
        page.p(class_="controls")
paul@313 1597
        page.input(name="newevent", type="submit", value="New event", id="newevent", accesskey="N")
paul@196 1598
        page.p.close()
paul@196 1599
paul@280 1600
        # Show controls for hiding empty days and busy slots.
paul@203 1601
        # The positioning of the control, paragraph and table are important here.
paul@203 1602
paul@288 1603
        page.input(name="showdays", type="checkbox", value="show", id="showdays", accesskey="D")
paul@282 1604
        page.input(name="hidebusy", type="checkbox", value="hide", id="hidebusy", accesskey="B")
paul@203 1605
paul@230 1606
        page.p(class_="controls")
paul@237 1607
        page.label("Hide busy time periods", for_="hidebusy", class_="hidebusy enable")
paul@237 1608
        page.label("Show busy time periods", for_="hidebusy", class_="hidebusy disable")
paul@288 1609
        page.label("Show empty days", for_="showdays", class_="showdays disable")
paul@288 1610
        page.label("Hide empty days", for_="showdays", class_="showdays enable")
paul@396 1611
        page.input(name="reset", type="submit", value="Clear selections", id="reset")
paul@396 1612
        page.label("Clear selections", for_="reset", class_="reset")
paul@203 1613
        page.p.close()
paul@203 1614
paul@114 1615
        freebusy = self.store.get_freebusy(self.user)
paul@114 1616
paul@114 1617
        if not freebusy:
paul@114 1618
            page.p("No events scheduled.")
paul@114 1619
            return
paul@114 1620
paul@154 1621
        # Obtain the user's timezone.
paul@147 1622
paul@244 1623
        tzid = self.get_tzid()
paul@147 1624
paul@114 1625
        # Day view: start at the earliest known day and produce days until the
paul@114 1626
        # latest known day, perhaps with expandable sections of empty days.
paul@114 1627
paul@114 1628
        # Month view: start at the earliest known month and produce months until
paul@114 1629
        # the latest known month, perhaps with expandable sections of empty
paul@114 1630
        # months.
paul@114 1631
paul@114 1632
        # Details of users to invite to new events could be superimposed on the
paul@114 1633
        # calendar.
paul@114 1634
paul@185 1635
        # Requests are listed and linked to their tentative positions in the
paul@185 1636
        # calendar. Other participants are also shown.
paul@185 1637
paul@185 1638
        request_summary = self._get_request_summary()
paul@185 1639
paul@185 1640
        period_groups = [request_summary, freebusy]
paul@185 1641
        period_group_types = ["request", "freebusy"]
paul@185 1642
        period_group_sources = ["Pending requests", "Your schedule"]
paul@185 1643
paul@187 1644
        for i, participant in enumerate(participants):
paul@185 1645
            period_groups.append(self.store.get_freebusy_for_other(self.user, get_uri(participant)))
paul@187 1646
            period_group_types.append("freebusy-part%d" % i)
paul@185 1647
            period_group_sources.append(participant)
paul@114 1648
paul@162 1649
        groups = []
paul@162 1650
        group_columns = []
paul@185 1651
        group_types = period_group_types
paul@185 1652
        group_sources = period_group_sources
paul@162 1653
        all_points = set()
paul@162 1654
paul@162 1655
        # Obtain time point information for each group of periods.
paul@162 1656
paul@185 1657
        for periods in period_groups:
paul@162 1658
            periods = convert_periods(periods, tzid)
paul@162 1659
paul@162 1660
            # Get the time scale with start and end points.
paul@162 1661
paul@162 1662
            scale = get_scale(periods)
paul@162 1663
paul@162 1664
            # Get the time slots for the periods.
paul@162 1665
paul@162 1666
            slots = get_slots(scale)
paul@162 1667
paul@162 1668
            # Add start of day time points for multi-day periods.
paul@162 1669
paul@244 1670
            add_day_start_points(slots, tzid)
paul@162 1671
paul@162 1672
            # Record the slots and all time points employed.
paul@162 1673
paul@162 1674
            groups.append(slots)
paul@201 1675
            all_points.update([point for point, active in slots])
paul@162 1676
paul@162 1677
        # Partition the groups into days.
paul@162 1678
paul@162 1679
        days = {}
paul@162 1680
        partitioned_groups = []
paul@171 1681
        partitioned_group_types = []
paul@185 1682
        partitioned_group_sources = []
paul@162 1683
paul@185 1684
        for slots, group_type, group_source in zip(groups, group_types, group_sources):
paul@162 1685
paul@162 1686
            # Propagate time points to all groups of time slots.
paul@162 1687
paul@162 1688
            add_slots(slots, all_points)
paul@162 1689
paul@162 1690
            # Count the number of columns employed by the group.
paul@162 1691
paul@162 1692
            columns = 0
paul@162 1693
paul@162 1694
            # Partition the time slots by day.
paul@162 1695
paul@162 1696
            partitioned = {}
paul@162 1697
paul@162 1698
            for day, day_slots in partition_by_day(slots).items():
paul@398 1699
paul@398 1700
                # Construct a list of time intervals within the day.
paul@398 1701
paul@201 1702
                intervals = []
paul@201 1703
                last = None
paul@201 1704
paul@201 1705
                for point, active in day_slots:
paul@201 1706
                    columns = max(columns, len(active))
paul@201 1707
                    if last:
paul@201 1708
                        intervals.append((last, point))
paul@201 1709
                    last = point
paul@201 1710
paul@201 1711
                if last:
paul@201 1712
                    intervals.append((last, None))
paul@162 1713
paul@162 1714
                if not days.has_key(day):
paul@162 1715
                    days[day] = set()
paul@162 1716
paul@162 1717
                # Convert each partition to a mapping from points to active
paul@162 1718
                # periods.
paul@162 1719
paul@201 1720
                partitioned[day] = dict(day_slots)
paul@201 1721
paul@201 1722
                # Record the divisions or intervals within each day.
paul@201 1723
paul@201 1724
                days[day].update(intervals)
paul@162 1725
paul@398 1726
            # Only include the requests column if it provides objects.
paul@398 1727
paul@194 1728
            if group_type != "request" or columns:
paul@194 1729
                group_columns.append(columns)
paul@194 1730
                partitioned_groups.append(partitioned)
paul@194 1731
                partitioned_group_types.append(group_type)
paul@194 1732
                partitioned_group_sources.append(group_source)
paul@114 1733
paul@279 1734
        # Add empty days.
paul@279 1735
paul@283 1736
        add_empty_days(days, tzid)
paul@279 1737
paul@279 1738
        # Show the controls permitting day selection.
paul@279 1739
paul@243 1740
        self.show_calendar_day_controls(days)
paul@243 1741
paul@279 1742
        # Show the calendar itself.
paul@279 1743
paul@230 1744
        page.table(cellspacing=5, cellpadding=5, class_="calendar")
paul@188 1745
        self.show_calendar_participant_headings(partitioned_group_types, partitioned_group_sources, group_columns)
paul@171 1746
        self.show_calendar_days(days, partitioned_groups, partitioned_group_types, group_columns)
paul@162 1747
        page.table.close()
paul@114 1748
paul@196 1749
        # End the form region.
paul@196 1750
paul@196 1751
        page.form.close()
paul@196 1752
paul@246 1753
    # More page fragment methods.
paul@246 1754
paul@243 1755
    def show_calendar_day_controls(self, days):
paul@243 1756
paul@243 1757
        "Show controls for the given 'days' in the calendar."
paul@243 1758
paul@243 1759
        page = self.page
paul@243 1760
        slots = self.env.get_args().get("slot", [])
paul@243 1761
paul@243 1762
        for day in days:
paul@243 1763
            value, identifier = self._day_value_and_identifier(day)
paul@243 1764
            self._slot_selector(value, identifier, slots)
paul@243 1765
paul@243 1766
        # Generate a dynamic stylesheet to allow day selections to colour
paul@243 1767
        # specific days.
paul@243 1768
        # NOTE: The style details need to be coordinated with the static
paul@243 1769
        # NOTE: stylesheet.
paul@243 1770
paul@243 1771
        page.style(type="text/css")
paul@243 1772
paul@243 1773
        for day in days:
paul@243 1774
            daystr = format_datetime(day)
paul@243 1775
            page.add("""\
paul@249 1776
input.newevent.selector#day-%s-:checked ~ table label.day.day-%s,
paul@249 1777
input.newevent.selector#day-%s-:checked ~ table label.timepoint.day-%s {
paul@243 1778
    background-color: #5f4;
paul@243 1779
    text-decoration: underline;
paul@243 1780
}
paul@243 1781
""" % (daystr, daystr, daystr, daystr))
paul@243 1782
paul@243 1783
        page.style.close()
paul@243 1784
paul@188 1785
    def show_calendar_participant_headings(self, group_types, group_sources, group_columns):
paul@186 1786
paul@186 1787
        """
paul@186 1788
        Show headings for the participants and other scheduling contributors,
paul@188 1789
        defined by 'group_types', 'group_sources' and 'group_columns'.
paul@186 1790
        """
paul@186 1791
paul@185 1792
        page = self.page
paul@185 1793
paul@188 1794
        page.colgroup(span=1, id="columns-timeslot")
paul@186 1795
paul@188 1796
        for group_type, columns in zip(group_types, group_columns):
paul@191 1797
            page.colgroup(span=max(columns, 1), id="columns-%s" % group_type)
paul@186 1798
paul@185 1799
        page.thead()
paul@185 1800
        page.tr()
paul@185 1801
        page.th("", class_="emptyheading")
paul@185 1802
paul@193 1803
        for group_type, source, columns in zip(group_types, group_sources, group_columns):
paul@193 1804
            page.th(source,
paul@193 1805
                class_=(group_type == "request" and "requestheading" or "participantheading"),
paul@193 1806
                colspan=max(columns, 1))
paul@185 1807
paul@185 1808
        page.tr.close()
paul@185 1809
        page.thead.close()
paul@185 1810
paul@171 1811
    def show_calendar_days(self, days, partitioned_groups, partitioned_group_types, group_columns):
paul@186 1812
paul@186 1813
        """
paul@186 1814
        Show calendar days, defined by a collection of 'days', the contributing
paul@186 1815
        period information as 'partitioned_groups' (partitioned by day), the
paul@186 1816
        'partitioned_group_types' indicating the kind of contribution involved,
paul@186 1817
        and the 'group_columns' defining the number of columns in each group.
paul@186 1818
        """
paul@186 1819
paul@162 1820
        page = self.page
paul@162 1821
paul@191 1822
        # Determine the number of columns required. Where participants provide
paul@191 1823
        # no columns for events, one still needs to be provided for the
paul@191 1824
        # participant itself.
paul@147 1825
paul@191 1826
        all_columns = sum([max(columns, 1) for columns in group_columns])
paul@191 1827
paul@191 1828
        # Determine the days providing time slots.
paul@191 1829
paul@162 1830
        all_days = days.items()
paul@162 1831
        all_days.sort()
paul@162 1832
paul@162 1833
        # Produce a heading and time points for each day.
paul@162 1834
paul@201 1835
        for day, intervals in all_days:
paul@279 1836
            groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups]
paul@279 1837
            is_empty = True
paul@279 1838
paul@279 1839
            for slots in groups_for_day:
paul@279 1840
                if not slots:
paul@279 1841
                    continue
paul@279 1842
paul@279 1843
                for active in slots.values():
paul@279 1844
                    if active:
paul@279 1845
                        is_empty = False
paul@279 1846
                        break
paul@279 1847
paul@282 1848
            page.thead(class_="separator%s" % (is_empty and " empty" or ""))
paul@282 1849
            page.tr()
paul@243 1850
            page.th(class_="dayheading container", colspan=all_columns+1)
paul@239 1851
            self._day_heading(day)
paul@114 1852
            page.th.close()
paul@153 1853
            page.tr.close()
paul@186 1854
            page.thead.close()
paul@114 1855
paul@282 1856
            page.tbody(class_="points%s" % (is_empty and " empty" or ""))
paul@280 1857
            self.show_calendar_points(intervals, groups_for_day, partitioned_group_types, group_columns)
paul@186 1858
            page.tbody.close()
paul@185 1859
paul@280 1860
    def show_calendar_points(self, intervals, groups, group_types, group_columns):
paul@186 1861
paul@186 1862
        """
paul@201 1863
        Show the time 'intervals' along with period information from the given
paul@186 1864
        'groups', having the indicated 'group_types', each with the number of
paul@186 1865
        columns given by 'group_columns'.
paul@186 1866
        """
paul@186 1867
paul@162 1868
        page = self.page
paul@162 1869
paul@244 1870
        # Obtain the user's timezone.
paul@244 1871
paul@244 1872
        tzid = self.get_tzid()
paul@244 1873
paul@203 1874
        # Produce a row for each interval.
paul@162 1875
paul@201 1876
        intervals = list(intervals)
paul@201 1877
        intervals.sort()
paul@162 1878
paul@201 1879
        for point, endpoint in intervals:
paul@244 1880
            continuation = point == get_start_of_day(point, tzid)
paul@153 1881
paul@203 1882
            # Some rows contain no period details and are marked as such.
paul@203 1883
paul@283 1884
            have_active = reduce(lambda x, y: x or y, [slots and slots.get(point) for slots in groups], None)
paul@203 1885
paul@397 1886
            css = " ".join([
paul@397 1887
                "slot",
paul@397 1888
                have_active and "busy" or "empty",
paul@397 1889
                continuation and "daystart" or ""
paul@397 1890
                ])
paul@203 1891
paul@203 1892
            page.tr(class_=css)
paul@162 1893
            page.th(class_="timeslot")
paul@201 1894
            self._time_point(point, endpoint)
paul@162 1895
            page.th.close()
paul@162 1896
paul@162 1897
            # Obtain slots for the time point from each group.
paul@162 1898
paul@171 1899
            for columns, slots, group_type in zip(group_columns, groups, group_types):
paul@162 1900
                active = slots and slots.get(point)
paul@162 1901
paul@191 1902
                # Where no periods exist for the given time interval, generate
paul@191 1903
                # an empty cell. Where a participant provides no periods at all,
paul@191 1904
                # the colspan is adjusted to be 1, not 0.
paul@191 1905
paul@162 1906
                if not active:
paul@196 1907
                    page.td(class_="empty container", colspan=max(columns, 1))
paul@201 1908
                    self._empty_slot(point, endpoint)
paul@196 1909
                    page.td.close()
paul@162 1910
                    continue
paul@162 1911
paul@162 1912
                slots = slots.items()
paul@162 1913
                slots.sort()
paul@162 1914
                spans = get_spans(slots)
paul@162 1915
paul@278 1916
                empty = 0
paul@278 1917
paul@162 1918
                # Show a column for each active period.
paul@117 1919
paul@153 1920
                for t in active:
paul@185 1921
                    if t and len(t) >= 2:
paul@278 1922
paul@278 1923
                        # Flush empty slots preceding this one.
paul@278 1924
paul@278 1925
                        if empty:
paul@278 1926
                            page.td(class_="empty container", colspan=empty)
paul@278 1927
                            self._empty_slot(point, endpoint)
paul@278 1928
                            page.td.close()
paul@278 1929
                            empty = 0
paul@278 1930
paul@395 1931
                        start, end, uid, recurrenceid, summary, organiser, key = get_freebusy_details(t)
paul@185 1932
                        span = spans[key]
paul@171 1933
paul@171 1934
                        # Produce a table cell only at the start of the period
paul@171 1935
                        # or when continued at the start of a day.
paul@171 1936
paul@153 1937
                        if point == start or continuation:
paul@153 1938
paul@195 1939
                            has_continued = continuation and point != start
paul@244 1940
                            will_continue = not ends_on_same_day(point, end, tzid)
paul@395 1941
                            is_organiser = organiser == self.user
paul@275 1942
paul@397 1943
                            css = " ".join([
paul@397 1944
                                "event",
paul@397 1945
                                has_continued and "continued" or "",
paul@397 1946
                                will_continue and "continues" or "",
paul@397 1947
                                is_organiser and "organising" or "attending"
paul@397 1948
                                ])
paul@195 1949
paul@189 1950
                            # Only anchor the first cell of events.
paul@427 1951
                            # Need to only anchor the first period for a recurring
paul@427 1952
                            # event.
paul@189 1953
paul@398 1954
                            html_id = "%s-%s-%s" % (group_type, uid, recurrenceid or "")
paul@398 1955
paul@398 1956
                            if point == start and html_id not in self.html_ids:
paul@398 1957
                                page.td(class_=css, rowspan=span, id=html_id)
paul@398 1958
                                self.html_ids.add(html_id)
paul@189 1959
                            else:
paul@195 1960
                                page.td(class_=css, rowspan=span)
paul@171 1961
paul@395 1962
                            # Only link to events if they are not being
paul@395 1963
                            # updated by requests.
paul@395 1964
paul@395 1965
                            if not summary or (uid, recurrenceid) in self._get_requests() and group_type != "request":
paul@395 1966
                                page.span(summary or "(Participant is busy)")
paul@185 1967
                            else:
paul@395 1968
                                page.a(summary, href=self.link_to(uid, recurrenceid))
paul@171 1969
paul@153 1970
                            page.td.close()
paul@153 1971
                    else:
paul@278 1972
                        empty += 1
paul@114 1973
paul@166 1974
                # Pad with empty columns.
paul@166 1975
paul@278 1976
                empty = columns - len(active)
paul@278 1977
paul@278 1978
                if empty:
paul@278 1979
                    page.td(class_="empty container", colspan=empty)
paul@201 1980
                    self._empty_slot(point, endpoint)
paul@196 1981
                    page.td.close()
paul@166 1982
paul@162 1983
            page.tr.close()
paul@114 1984
paul@239 1985
    def _day_heading(self, day):
paul@243 1986
paul@243 1987
        """
paul@243 1988
        Generate a heading for 'day' of the following form:
paul@243 1989
paul@243 1990
        <label class="day day-20150203" for="day-20150203">Tuesday, 3 February 2015</label>
paul@243 1991
        """
paul@243 1992
paul@239 1993
        page = self.page
paul@243 1994
        daystr = format_datetime(day)
paul@239 1995
        value, identifier = self._day_value_and_identifier(day)
paul@243 1996
        page.label(self.format_date(day, "full"), class_="day day-%s" % daystr, for_=identifier)
paul@239 1997
paul@201 1998
    def _time_point(self, point, endpoint):
paul@243 1999
paul@243 2000
        """
paul@243 2001
        Generate headings for the 'point' to 'endpoint' period of the following
paul@243 2002
        form:
paul@243 2003
paul@243 2004
        <label class="timepoint day-20150203" for="slot-20150203T090000-20150203T100000">09:00:00 CET</label>
paul@243 2005
        <span class="endpoint">10:00:00 CET</span>
paul@243 2006
        """
paul@243 2007
paul@201 2008
        page = self.page
paul@244 2009
        tzid = self.get_tzid()
paul@243 2010
        daystr = format_datetime(point.date())
paul@201 2011
        value, identifier = self._slot_value_and_identifier(point, endpoint)
paul@238 2012
        slots = self.env.get_args().get("slot", [])
paul@239 2013
        self._slot_selector(value, identifier, slots)
paul@243 2014
        page.label(self.format_time(point, "long"), class_="timepoint day-%s" % daystr, for_=identifier)
paul@244 2015
        page.span(self.format_time(endpoint or get_end_of_day(point, tzid), "long"), class_="endpoint")
paul@239 2016
paul@239 2017
    def _slot_selector(self, value, identifier, slots):
paul@400 2018
paul@400 2019
        """
paul@400 2020
        Provide a timeslot control having the given 'value', employing the
paul@400 2021
        indicated HTML 'identifier', and using the given 'slots' collection
paul@400 2022
        to select any control whose 'value' is in this collection, unless the
paul@400 2023
        "reset" request parameter has been asserted.
paul@400 2024
        """
paul@400 2025
paul@258 2026
        reset = self.env.get_args().has_key("reset")
paul@239 2027
        page = self.page
paul@258 2028
        if not reset and value in slots:
paul@249 2029
            page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector", checked="checked")
paul@202 2030
        else:
paul@249 2031
            page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector")
paul@201 2032
paul@201 2033
    def _empty_slot(self, point, endpoint):
paul@400 2034
paul@400 2035
        "Show an empty slot label for the given 'point' and 'endpoint'."
paul@400 2036
paul@197 2037
        page = self.page
paul@201 2038
        value, identifier = self._slot_value_and_identifier(point, endpoint)
paul@236 2039
        page.label("Select/deselect period", class_="newevent popup", for_=identifier)
paul@196 2040
paul@239 2041
    def _day_value_and_identifier(self, day):
paul@400 2042
paul@400 2043
        "Return a day value and HTML identifier for the given 'day'."
paul@400 2044
paul@239 2045
        value = "%s-" % format_datetime(day)
paul@239 2046
        identifier = "day-%s" % value
paul@239 2047
        return value, identifier
paul@239 2048
paul@201 2049
    def _slot_value_and_identifier(self, point, endpoint):
paul@400 2050
paul@400 2051
        """
paul@400 2052
        Return a slot value and HTML identifier for the given 'point' and
paul@400 2053
        'endpoint'.
paul@400 2054
        """
paul@400 2055
paul@202 2056
        value = "%s-%s" % (format_datetime(point), endpoint and format_datetime(endpoint) or "")
paul@201 2057
        identifier = "slot-%s" % value
paul@201 2058
        return value, identifier
paul@196 2059
paul@435 2060
    def _show_menu(self, name, default, items, class_="", index=None):
paul@400 2061
paul@400 2062
        """
paul@400 2063
        Show a select menu having the given 'name', set to the given 'default',
paul@400 2064
        providing the given (value, label) 'items', and employing the given CSS
paul@400 2065
        'class_' if specified.
paul@400 2066
        """
paul@400 2067
paul@257 2068
        page = self.page
paul@286 2069
        values = self.env.get_args().get(name, [default])
paul@435 2070
        if index is not None:
paul@435 2071
            values = values[index:]
paul@435 2072
            values = values and values[0:1] or [default]
paul@435 2073
paul@324 2074
        page.select(name=name, class_=class_)
paul@257 2075
        for v, label in items:
paul@355 2076
            if v is None:
paul@355 2077
                continue
paul@257 2078
            if v in values:
paul@324 2079
                page.option(label, value=v, selected="selected")
paul@257 2080
            else:
paul@324 2081
                page.option(label, value=v)
paul@257 2082
        page.select.close()
paul@257 2083
paul@435 2084
    def _show_date_controls(self, name, default, tzid, index=None):
paul@286 2085
paul@286 2086
        """
paul@286 2087
        Show date controls for a field with the given 'name' and 'default' value
paul@427 2088
        and 'tzid'.
paul@286 2089
        """
paul@286 2090
paul@286 2091
        page = self.page
paul@286 2092
        args = self.env.get_args()
paul@286 2093
paul@423 2094
        event_tzid = tzid or self.get_tzid()
paul@286 2095
paul@286 2096
        # Show dates for up to one week around the current date.
paul@286 2097
paul@427 2098
        base = to_date(default)
paul@286 2099
        items = []
paul@286 2100
        for i in range(-7, 8):
paul@286 2101
            d = base + timedelta(i)
paul@286 2102
            items.append((format_datetime(d), self.format_date(d, "full")))
paul@286 2103
paul@435 2104
        self._show_menu("%s-date" % name, format_datetime(base), items, index=index)
paul@286 2105
paul@286 2106
        # Show time details.
paul@286 2107
paul@423 2108
        default_time = isinstance(default, datetime) and default or None
paul@435 2109
paul@435 2110
        hour = args.get("%s-hour" % name, [])[index or 0:]
paul@435 2111
        hour = hour and hour[0] or "%02d" % (default_time and default_time.hour or 0)
paul@435 2112
        minute = args.get("%s-minute" % name, [])[index or 0:]
paul@435 2113
        minute = minute and minute[0] or "%02d" % (default_time and default_time.minute or 0)
paul@435 2114
        second = args.get("%s-second" % name, [])[index or 0:]
paul@435 2115
        second = second and second[0] or "%02d" % (default_time and default_time.second or 0)
paul@300 2116
paul@300 2117
        page.span(class_="time enabled")
paul@300 2118
        page.input(name="%s-hour" % name, type="text", value=hour, maxlength=2, size=2)
paul@300 2119
        page.add(":")
paul@300 2120
        page.input(name="%s-minute" % name, type="text", value=minute, maxlength=2, size=2)
paul@300 2121
        page.add(":")
paul@300 2122
        page.input(name="%s-second" % name, type="text", value=second, maxlength=2, size=2)
paul@300 2123
        page.add(" ")
paul@435 2124
        self._show_timezone_menu("%s-tzid" % name, event_tzid, index)
paul@300 2125
        page.span.close()
paul@286 2126
paul@435 2127
    def _show_timezone_menu(self, name, default, index=None):
paul@401 2128
paul@401 2129
        """
paul@401 2130
        Show timezone controls using a menu with the given 'name', set to the
paul@401 2131
        given 'default' unless a field of the given 'name' provides a value.
paul@401 2132
        """
paul@401 2133
paul@401 2134
        entries = [(tzid, tzid) for tzid in pytz.all_timezones]
paul@435 2135
        self._show_menu(name, default, entries, index=index)
paul@401 2136
paul@246 2137
    # Incoming HTTP request direction.
paul@246 2138
paul@69 2139
    def select_action(self):
paul@69 2140
paul@69 2141
        "Select the desired action and show the result."
paul@69 2142
paul@121 2143
        path_info = self.env.get_path_info().strip("/")
paul@121 2144
paul@69 2145
        if not path_info:
paul@114 2146
            self.show_calendar()
paul@121 2147
        elif self.show_object(path_info):
paul@70 2148
            pass
paul@70 2149
        else:
paul@70 2150
            self.no_page()
paul@69 2151
paul@82 2152
    def __call__(self):
paul@69 2153
paul@69 2154
        "Interpret a request and show an appropriate response."
paul@69 2155
paul@69 2156
        if not self.user:
paul@69 2157
            self.no_user()
paul@69 2158
        else:
paul@69 2159
            self.select_action()
paul@69 2160
paul@70 2161
        # Write the headers and actual content.
paul@70 2162
paul@69 2163
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 2164
        print >>self.out
paul@69 2165
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 2166
paul@69 2167
if __name__ == "__main__":
paul@128 2168
    Manager()()
paul@69 2169
paul@69 2170
# vim: tabstop=4 expandtab shiftwidth=4