imip-agent

Annotated imip_manager.py

244:9d17bc8ffa19
2015-02-03 Paul Boddie Introduced explicit time regime (TZID) usage so that the start of each day is positioned in the appropriate time zone. This affects end of day and adjacent day calculations.
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@149 29
import cgi, os, sys
paul@69 30
paul@146 31
sys.path.append(LIBRARY_PATH)
paul@69 32
paul@213 33
from imiptools.content import Handler
paul@222 34
from imiptools.data import get_address, get_uri, make_freebusy, parse_object, \
paul@222 35
                           Object, to_part
paul@239 36
from imiptools.dates import format_datetime, get_datetime, get_datetime_item, \
paul@241 37
                            get_end_of_day, get_start_of_day, get_start_of_next_day, \
paul@241 38
                            get_timestamp, ends_on_same_day, to_timezone
paul@83 39
from imiptools.mail import Messenger
paul@162 40
from imiptools.period import add_day_start_points, add_slots, convert_periods, \
paul@185 41
                             get_freebusy_details, \
paul@162 42
                             get_scale, have_conflict, get_slots, get_spans, \
paul@162 43
                             partition_by_day
paul@147 44
from imiptools.profile import Preferences
paul@213 45
import imip_store
paul@69 46
import markup
paul@69 47
paul@69 48
getenv = os.environ.get
paul@69 49
setenv = os.environ.__setitem__
paul@69 50
paul@69 51
class CGIEnvironment:
paul@69 52
paul@69 53
    "A CGI-compatible environment."
paul@69 54
paul@212 55
    def __init__(self, charset=None):
paul@212 56
        self.charset = charset
paul@69 57
        self.args = None
paul@69 58
        self.method = None
paul@69 59
        self.path = None
paul@69 60
        self.path_info = None
paul@69 61
        self.user = None
paul@69 62
paul@69 63
    def get_args(self):
paul@69 64
        if self.args is None:
paul@69 65
            if self.get_method() != "POST":
paul@69 66
                setenv("QUERY_STRING", "")
paul@212 67
            args = cgi.parse(keep_blank_values=True)
paul@212 68
paul@212 69
            if not self.charset:
paul@212 70
                self.args = args
paul@212 71
            else:
paul@212 72
                self.args = {}
paul@212 73
                for key, values in args.items():
paul@212 74
                    self.args[key] = [unicode(value, self.charset) for value in values]
paul@212 75
paul@69 76
        return self.args
paul@69 77
paul@69 78
    def get_method(self):
paul@69 79
        if self.method is None:
paul@69 80
            self.method = getenv("REQUEST_METHOD") or "GET"
paul@69 81
        return self.method
paul@69 82
paul@69 83
    def get_path(self):
paul@69 84
        if self.path is None:
paul@69 85
            self.path = getenv("SCRIPT_NAME") or ""
paul@69 86
        return self.path
paul@69 87
paul@69 88
    def get_path_info(self):
paul@69 89
        if self.path_info is None:
paul@69 90
            self.path_info = getenv("PATH_INFO") or ""
paul@69 91
        return self.path_info
paul@69 92
paul@69 93
    def get_user(self):
paul@69 94
        if self.user is None:
paul@69 95
            self.user = getenv("REMOTE_USER") or ""
paul@69 96
        return self.user
paul@69 97
paul@69 98
    def get_output(self):
paul@69 99
        return sys.stdout
paul@69 100
paul@69 101
    def get_url(self):
paul@69 102
        path = self.get_path()
paul@69 103
        path_info = self.get_path_info()
paul@69 104
        return "%s%s" % (path.rstrip("/"), path_info)
paul@69 105
paul@154 106
    def new_url(self, path_info):
paul@154 107
        path = self.get_path()
paul@154 108
        return "%s/%s" % (path.rstrip("/"), path_info.lstrip("/"))
paul@154 109
paul@222 110
class ManagerHandler(Handler):
paul@79 111
paul@121 112
    """
paul@121 113
    A content handler for use by the manager, as opposed to operating within the
paul@121 114
    mail processing pipeline.
paul@121 115
    """
paul@79 116
paul@121 117
    def __init__(self, obj, user, messenger):
paul@224 118
        Handler.__init__(self, messenger=messenger)
paul@224 119
        self.set_object(obj)
paul@79 120
        self.user = user
paul@82 121
paul@213 122
        self.organiser = self.obj.get_value("ORGANIZER")
paul@213 123
        self.attendees = self.obj.get_values("ATTENDEE")
paul@79 124
paul@79 125
    # Communication methods.
paul@79 126
paul@207 127
    def send_message(self, method, sender):
paul@79 128
paul@79 129
        """
paul@207 130
        Create a full calendar object employing the given 'method', and send it
paul@207 131
        to the appropriate recipients, also sending a copy to the 'sender'.
paul@79 132
        """
paul@79 133
paul@219 134
        parts = [self.obj.to_part(method)]
paul@207 135
paul@222 136
        if self.user == self.organiser:
paul@207 137
            recipients = map(get_address, self.attendees)
paul@207 138
        else:
paul@207 139
            recipients = [get_address(self.organiser)]
paul@207 140
paul@219 141
        # Bundle free/busy information if appropriate.
paul@219 142
paul@219 143
        preferences = Preferences(self.user)
paul@219 144
paul@219 145
        if preferences.get("freebusy_sharing") == "share" and \
paul@219 146
           preferences.get("freebusy_bundling") == "always":
paul@219 147
paul@222 148
            # Invent a unique identifier.
paul@222 149
paul@222 150
            utcnow = get_timestamp()
paul@222 151
            uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
paul@222 152
paul@222 153
            freebusy = self.store.get_freebusy(self.user)
paul@222 154
            parts.append(to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user)]))
paul@219 155
paul@219 156
        message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)
paul@207 157
        self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)
paul@79 158
paul@79 159
    # Action methods.
paul@79 160
paul@207 161
    def process_received_request(self, accept, update=False):
paul@79 162
paul@79 163
        """
paul@79 164
        Process the current request for the given 'user', accepting any request
paul@79 165
        when 'accept' is true, declining requests otherwise. Return whether any
paul@79 166
        action was taken.
paul@155 167
paul@155 168
        If 'update' is given, the sequence number will be incremented in order
paul@155 169
        to override any previous response.
paul@79 170
        """
paul@79 171
paul@79 172
        # When accepting or declining, do so only on behalf of this user,
paul@79 173
        # preserving any other attributes set as an attendee.
paul@79 174
paul@213 175
        for attendee, attendee_attr in self.obj.get_items("ATTENDEE"):
paul@79 176
paul@79 177
            if attendee == self.user:
paul@79 178
                attendee_attr["PARTSTAT"] = accept and "ACCEPTED" or "DECLINED"
paul@128 179
                if self.messenger and self.messenger.sender != get_address(attendee):
paul@128 180
                    attendee_attr["SENT-BY"] = get_uri(self.messenger.sender)
paul@213 181
                self.obj["ATTENDEE"] = [(attendee, attendee_attr)]
paul@155 182
                if update:
paul@213 183
                    sequence = self.obj.get_value("SEQUENCE") or "0"
paul@213 184
                    self.obj["SEQUENCE"] = [(str(int(sequence) + 1), {})]
paul@158 185
                self.update_dtstamp()
paul@155 186
paul@207 187
                self.send_message("REPLY", get_address(attendee))
paul@79 188
paul@79 189
                return True
paul@79 190
paul@79 191
        return False
paul@79 192
paul@207 193
    def process_created_request(self, update=False):
paul@207 194
paul@207 195
        """
paul@207 196
        Process the current request for the given 'user', sending a created
paul@207 197
        request to attendees. Return whether any action was taken.
paul@207 198
paul@207 199
        If 'update' is given, the sequence number will be incremented in order
paul@207 200
        to override any previous message.
paul@207 201
        """
paul@207 202
paul@213 203
        organiser, organiser_attr = self.obj.get_item("ORGANIZER")
paul@213 204
paul@213 205
        if self.messenger and self.messenger.sender != get_address(organiser):
paul@213 206
            organiser_attr["SENT-BY"] = get_uri(self.messenger.sender)
paul@207 207
        if update:
paul@213 208
            sequence = self.obj.get_value("SEQUENCE") or "0"
paul@213 209
            self.obj["SEQUENCE"] = [(str(int(sequence) + 1), {})]
paul@207 210
        self.update_dtstamp()
paul@207 211
paul@207 212
        self.send_message("REQUEST", get_address(self.organiser))
paul@207 213
paul@207 214
        return True
paul@207 215
paul@69 216
class Manager:
paul@69 217
paul@69 218
    "A simple manager application."
paul@69 219
paul@82 220
    def __init__(self, messenger=None):
paul@82 221
        self.messenger = messenger or Messenger()
paul@82 222
paul@212 223
        self.encoding = "utf-8"
paul@212 224
        self.env = CGIEnvironment(self.encoding)
paul@212 225
paul@69 226
        user = self.env.get_user()
paul@77 227
        self.user = user and get_uri(user) or None
paul@147 228
        self.preferences = None
paul@149 229
        self.locale = None
paul@121 230
        self.requests = None
paul@121 231
paul@69 232
        self.out = self.env.get_output()
paul@69 233
        self.page = markup.page()
paul@69 234
paul@77 235
        self.store = imip_store.FileStore()
paul@162 236
        self.objects = {}
paul@77 237
paul@77 238
        try:
paul@77 239
            self.publisher = imip_store.FilePublisher()
paul@77 240
        except OSError:
paul@77 241
            self.publisher = None
paul@77 242
paul@121 243
    def _get_uid(self, path_info):
paul@121 244
        return path_info.lstrip("/").split("/", 1)[0]
paul@121 245
paul@117 246
    def _get_object(self, uid):
paul@162 247
        if self.objects.has_key(uid):
paul@162 248
            return self.objects[uid]
paul@162 249
paul@117 250
        f = uid and self.store.get_event(self.user, uid) or None
paul@117 251
paul@117 252
        if not f:
paul@117 253
            return None
paul@117 254
paul@213 255
        fragment = parse_object(f, "utf-8")
paul@213 256
        obj = self.objects[uid] = fragment and Object(fragment)
paul@117 257
paul@121 258
        return obj
paul@121 259
paul@121 260
    def _get_requests(self):
paul@121 261
        if self.requests is None:
paul@121 262
            self.requests = self.store.get_requests(self.user)
paul@121 263
        return self.requests
paul@117 264
paul@162 265
    def _get_request_summary(self):
paul@162 266
        summary = []
paul@162 267
        for uid in self._get_requests():
paul@162 268
            obj = self._get_object(uid)
paul@162 269
            if obj:
paul@162 270
                summary.append((
paul@213 271
                    obj.get_value("DTSTART"),
paul@213 272
                    obj.get_value("DTEND"),
paul@162 273
                    uid
paul@162 274
                    ))
paul@162 275
        return summary
paul@162 276
paul@147 277
    # Preference methods.
paul@147 278
paul@149 279
    def get_user_locale(self):
paul@149 280
        if not self.locale:
paul@149 281
            self.locale = self.get_preferences().get("LANG", "C")
paul@149 282
        return self.locale
paul@147 283
paul@147 284
    def get_preferences(self):
paul@147 285
        if not self.preferences:
paul@147 286
            self.preferences = Preferences(self.user)
paul@147 287
        return self.preferences
paul@147 288
paul@244 289
    def get_tzid(self):
paul@244 290
        prefs = self.get_preferences()
paul@244 291
        return prefs.get("TZID", "UTC")
paul@244 292
paul@162 293
    # Prettyprinting of dates and times.
paul@162 294
paul@149 295
    def format_date(self, dt, format):
paul@149 296
        return self._format_datetime(babel.dates.format_date, dt, format)
paul@149 297
paul@149 298
    def format_time(self, dt, format):
paul@149 299
        return self._format_datetime(babel.dates.format_time, dt, format)
paul@149 300
paul@149 301
    def format_datetime(self, dt, format):
paul@232 302
        return self._format_datetime(
paul@232 303
            isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,
paul@232 304
            dt, format)
paul@232 305
paul@232 306
    def format_end_datetime(self, dt, format):
paul@235 307
        if isinstance(dt, date) and not isinstance(dt, datetime):
paul@232 308
            dt = dt - timedelta(1)
paul@232 309
        return self.format_datetime(dt, format)
paul@149 310
paul@149 311
    def _format_datetime(self, fn, dt, format):
paul@149 312
        return fn(dt, format=format, locale=self.get_user_locale())
paul@149 313
paul@78 314
    # Data management methods.
paul@78 315
paul@78 316
    def remove_request(self, uid):
paul@105 317
        return self.store.dequeue_request(self.user, uid)
paul@78 318
paul@234 319
    def remove_event(self, uid):
paul@234 320
        return self.store.remove_event(self.user, uid)
paul@234 321
paul@78 322
    # Presentation methods.
paul@78 323
paul@69 324
    def new_page(self, title):
paul@192 325
        self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css"))
paul@69 326
paul@69 327
    def status(self, code, message):
paul@123 328
        self.header("Status", "%s %s" % (code, message))
paul@123 329
paul@123 330
    def header(self, header, value):
paul@123 331
        print >>self.out, "%s: %s" % (header, value)
paul@69 332
paul@69 333
    def no_user(self):
paul@69 334
        self.status(403, "Forbidden")
paul@69 335
        self.new_page(title="Forbidden")
paul@69 336
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@69 337
paul@70 338
    def no_page(self):
paul@70 339
        self.status(404, "Not Found")
paul@70 340
        self.new_page(title="Not Found")
paul@70 341
        self.page.p("No page is provided at the given address.")
paul@70 342
paul@123 343
    def redirect(self, url):
paul@123 344
        self.status(302, "Redirect")
paul@123 345
        self.header("Location", url)
paul@123 346
        self.new_page(title="Redirect")
paul@123 347
        self.page.p("Redirecting to: %s" % url)
paul@123 348
paul@121 349
    # Request logic and page fragment methods.
paul@121 350
paul@202 351
    def handle_newevent(self):
paul@202 352
paul@207 353
        """
paul@207 354
        Handle any new event operation, creating a new event and redirecting to
paul@207 355
        the event page for further activity.
paul@207 356
        """
paul@202 357
paul@202 358
        # Handle a submitted form.
paul@202 359
paul@202 360
        args = self.env.get_args()
paul@202 361
paul@202 362
        if not args.has_key("newevent"):
paul@202 363
            return
paul@202 364
paul@202 365
        # Create a new event using the available information.
paul@202 366
paul@236 367
        slots = args.get("slot", [])
paul@202 368
        participants = args.get("participants", [])
paul@202 369
paul@236 370
        if not slots:
paul@202 371
            return
paul@202 372
paul@236 373
        # Coalesce the selected slots.
paul@236 374
paul@236 375
        slots.sort()
paul@236 376
        coalesced = []
paul@236 377
        last = None
paul@236 378
paul@236 379
        for slot in slots:
paul@236 380
            start, end = slot.split("-")
paul@236 381
            if last:
paul@236 382
                if start == last[1]:
paul@236 383
                    last = last[0], end
paul@236 384
                    continue
paul@236 385
                else:
paul@236 386
                    coalesced.append(last)
paul@236 387
            last = start, end
paul@236 388
paul@236 389
        if last:
paul@236 390
            coalesced.append(last)
paul@202 391
paul@202 392
        # Obtain the user's timezone.
paul@202 393
paul@244 394
        tzid = self.get_tzid()
paul@202 395
paul@202 396
        # Invent a unique identifier.
paul@202 397
paul@222 398
        utcnow = get_timestamp()
paul@202 399
        uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
paul@202 400
paul@236 401
        # Define a single occurrence if only one coalesced slot exists.
paul@236 402
        # Otherwise, many occurrences are defined.
paul@202 403
paul@236 404
        for i, (start, end) in enumerate(coalesced):
paul@236 405
            this_uid = "%s-%s" % (uid, i)
paul@236 406
paul@239 407
            start = get_datetime(start, {"TZID" : tzid})
paul@239 408
            end = end and get_datetime(end, {"TZID" : tzid}) or get_start_of_next_day(start, tzid)
paul@239 409
paul@239 410
            start_value, start_attr = get_datetime_item(start)
paul@239 411
            end_value, end_attr = get_datetime_item(end)
paul@239 412
paul@236 413
            # Create a calendar object and store it as a request.
paul@236 414
paul@236 415
            record = []
paul@236 416
            rwrite = record.append
paul@202 417
paul@236 418
            rwrite(("UID", {}, this_uid))
paul@236 419
            rwrite(("SUMMARY", {}, "New event at %s" % utcnow))
paul@236 420
            rwrite(("DTSTAMP", {}, utcnow))
paul@239 421
            rwrite(("DTSTART", start_attr, start_value))
paul@239 422
            rwrite(("DTEND", end_attr, end_value))
paul@236 423
            rwrite(("ORGANIZER", {}, self.user))
paul@202 424
paul@236 425
            for participant in participants:
paul@236 426
                if not participant:
paul@236 427
                    continue
paul@236 428
                participant = get_uri(participant)
paul@236 429
                if participant != self.user:
paul@236 430
                    rwrite(("ATTENDEE", {}, participant))
paul@202 431
paul@236 432
            obj = ("VEVENT", {}, record)
paul@236 433
paul@236 434
            self.store.set_event(self.user, this_uid, obj)
paul@236 435
            self.store.queue_request(self.user, this_uid)
paul@202 436
paul@236 437
        # Redirect to the object (or the first of the objects), where instead of
paul@236 438
        # attendee controls, there will be organiser controls.
paul@236 439
paul@236 440
        self.redirect(self.env.new_url("%s-0" % uid))
paul@202 441
paul@212 442
    def handle_request(self, uid, obj, queued):
paul@121 443
paul@155 444
        """
paul@212 445
        Handle actions involving the given 'uid' and 'obj' object, where
paul@155 446
        'queued' indicates that the object has not yet been handled.
paul@155 447
        """
paul@121 448
paul@121 449
        # Handle a submitted form.
paul@121 450
paul@121 451
        args = self.env.get_args()
paul@123 452
        handled = True
paul@121 453
paul@212 454
        # Update the object.
paul@212 455
paul@212 456
        if args.has_key("summary"):
paul@213 457
            obj["SUMMARY"] = [(args["summary"][0], {})]
paul@212 458
paul@212 459
        # Process any action.
paul@212 460
paul@121 461
        accept = args.has_key("accept")
paul@121 462
        decline = args.has_key("decline")
paul@207 463
        invite = args.has_key("invite")
paul@155 464
        update = not queued and args.has_key("update")
paul@121 465
paul@207 466
        if accept or decline or invite:
paul@121 467
paul@212 468
            handler = ManagerHandler(obj, self.user, self.messenger)
paul@121 469
paul@212 470
            # Process the object and remove it from the list of requests.
paul@121 471
paul@207 472
            if (accept or decline) and handler.process_received_request(accept, update) or \
paul@207 473
                invite and handler.process_created_request(update):
paul@121 474
paul@121 475
                self.remove_request(uid)
paul@121 476
paul@207 477
        elif args.has_key("discard"):
paul@121 478
paul@234 479
            # Remove the request and the object.
paul@121 480
paul@234 481
            self.remove_event(uid)
paul@121 482
            self.remove_request(uid)
paul@121 483
paul@121 484
        else:
paul@123 485
            handled = False
paul@121 486
paul@212 487
        # Upon handling an action, redirect to the main page.
paul@212 488
paul@123 489
        if handled:
paul@123 490
            self.redirect(self.env.get_path())
paul@123 491
paul@123 492
        return handled
paul@121 493
paul@212 494
    def show_request_controls(self, obj, needs_action):
paul@155 495
paul@155 496
        """
paul@212 497
        Show form controls for a request concerning 'obj', indicating whether
paul@212 498
        action is needed if 'needs_action' is specified as a true value.
paul@155 499
        """
paul@155 500
paul@212 501
        page = self.page
paul@212 502
paul@213 503
        is_organiser = obj.get_value("ORGANIZER") == self.user
paul@207 504
paul@207 505
        if not is_organiser:
paul@213 506
            attendees = obj.get_value_map("ATTENDEE")
paul@207 507
            attendee_attr = attendees.get(self.user)
paul@121 508
paul@207 509
            if attendee_attr:
paul@207 510
                partstat = attendee_attr.get("PARTSTAT")
paul@207 511
                if partstat == "ACCEPTED":
paul@212 512
                    page.p("This request has been accepted.")
paul@207 513
                elif partstat == "DECLINED":
paul@212 514
                    page.p("This request has been declined.")
paul@207 515
                else:
paul@212 516
                    page.p("This request has not yet been dealt with.")
paul@121 517
paul@155 518
        if needs_action:
paul@212 519
            page.p("An action is required for this request:")
paul@155 520
        else:
paul@212 521
            page.p("This request can be updated as follows:")
paul@155 522
paul@212 523
        page.p()
paul@207 524
paul@207 525
        # Show appropriate options depending on the role of the user.
paul@207 526
paul@207 527
        if is_organiser:
paul@212 528
            page.input(name="invite", type="submit", value="Invite")
paul@207 529
        else:
paul@212 530
            page.input(name="accept", type="submit", value="Accept")
paul@212 531
            page.add(" ")
paul@212 532
            page.input(name="decline", type="submit", value="Decline")
paul@207 533
paul@212 534
        page.add(" ")
paul@212 535
        page.input(name="discard", type="submit", value="Discard")
paul@207 536
paul@207 537
        # Updated objects need to have details updated upon sending.
paul@207 538
paul@155 539
        if not needs_action:
paul@212 540
            page.input(name="update", type="hidden", value="true")
paul@207 541
paul@212 542
        page.p.close()
paul@121 543
paul@210 544
    object_labels = {
paul@210 545
        "SUMMARY"   : "Summary",
paul@210 546
        "DTSTART"   : "Start",
paul@210 547
        "DTEND"     : "End",
paul@210 548
        "ORGANIZER" : "Organiser",
paul@210 549
        "ATTENDEE"  : "Attendee",
paul@210 550
        }
paul@210 551
paul@212 552
    def show_object_on_page(self, uid, obj, needs_action):
paul@121 553
paul@121 554
        """
paul@121 555
        Show the calendar object with the given 'uid' and representation 'obj'
paul@121 556
        on the current page.
paul@121 557
        """
paul@121 558
paul@210 559
        page = self.page
paul@212 560
        page.form(method="POST")
paul@210 561
paul@154 562
        # Obtain the user's timezone.
paul@154 563
paul@244 564
        tzid = self.get_tzid()
paul@121 565
paul@121 566
        # Provide a summary of the object.
paul@121 567
paul@230 568
        page.table(class_="object", cellspacing=5, cellpadding=5)
paul@212 569
        page.thead()
paul@212 570
        page.tr()
paul@212 571
        page.th("Event", class_="mainheading", colspan=2)
paul@212 572
        page.tr.close()
paul@212 573
        page.thead.close()
paul@212 574
        page.tbody()
paul@121 575
paul@121 576
        for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]:
paul@210 577
            page.tr()
paul@210 578
paul@210 579
            label = self.object_labels.get(name, name)
paul@210 580
paul@210 581
            # Handle datetimes specially.
paul@210 582
paul@147 583
            if name in ["DTSTART", "DTEND"]:
paul@213 584
                value, attr = obj.get_item(name)
paul@154 585
                tzid = attr.get("TZID", tzid)
paul@232 586
                value = (
paul@232 587
                    name == "DTSTART" and self.format_datetime or self.format_end_datetime
paul@232 588
                    )(to_timezone(get_datetime(value), tzid), "full")
paul@210 589
                page.th(label, class_="objectheading")
paul@210 590
                page.td(value)
paul@210 591
                page.tr.close()
paul@210 592
paul@212 593
            # Handle the summary specially.
paul@212 594
paul@212 595
            elif name == "SUMMARY":
paul@213 596
                value = obj.get_value(name)
paul@212 597
                page.th(label, class_="objectheading")
paul@212 598
                page.td()
paul@212 599
                page.input(name="summary", type="text", value=value, size=80)
paul@212 600
                page.td.close()
paul@212 601
                page.tr.close()
paul@212 602
paul@210 603
            # Handle potentially many values.
paul@210 604
paul@147 605
            else:
paul@213 606
                items = obj.get_items(name)
paul@233 607
                if not items:
paul@233 608
                    continue
paul@233 609
paul@210 610
                page.th(label, class_="objectheading", rowspan=len(items))
paul@210 611
paul@210 612
                first = True
paul@210 613
paul@210 614
                for value, attr in items:
paul@210 615
                    if not first:
paul@210 616
                        page.tr()
paul@210 617
                    else:
paul@210 618
                        first = False
paul@121 619
paul@210 620
                    page.td()
paul@210 621
                    page.add(value)
paul@210 622
paul@210 623
                    if name == "ATTENDEE":
paul@210 624
                        partstat = attr.get("PARTSTAT")
paul@210 625
                        if partstat:
paul@210 626
                            page.add(" (%s)" % partstat)
paul@210 627
paul@210 628
                    page.td.close()
paul@210 629
                    page.tr.close()
paul@210 630
paul@212 631
        page.tbody.close()
paul@210 632
        page.table.close()
paul@121 633
paul@213 634
        dtstart = format_datetime(obj.get_utc_datetime("DTSTART"))
paul@213 635
        dtend = format_datetime(obj.get_utc_datetime("DTEND"))
paul@121 636
paul@121 637
        # Indicate whether there are conflicting events.
paul@121 638
paul@121 639
        freebusy = self.store.get_freebusy(self.user)
paul@121 640
paul@121 641
        if freebusy:
paul@121 642
paul@121 643
            # Obtain any time zone details from the suggested event.
paul@121 644
paul@213 645
            _dtstart, attr = obj.get_item("DTSTART")
paul@154 646
            tzid = attr.get("TZID", tzid)
paul@121 647
paul@121 648
            # Show any conflicts.
paul@121 649
paul@121 650
            for t in have_conflict(freebusy, [(dtstart, dtend)], True):
paul@121 651
                start, end, found_uid = t[:3]
paul@154 652
paul@154 653
                # Provide details of any conflicting event.
paul@154 654
paul@121 655
                if uid != found_uid:
paul@149 656
                    start = self.format_datetime(to_timezone(get_datetime(start), tzid), "full")
paul@149 657
                    end = self.format_datetime(to_timezone(get_datetime(end), tzid), "full")
paul@210 658
                    page.p("Event conflicts with another from %s to %s: " % (start, end))
paul@154 659
paul@154 660
                    # Show the event summary for the conflicting event.
paul@154 661
paul@154 662
                    found_obj = self._get_object(found_uid)
paul@154 663
                    if found_obj:
paul@213 664
                        page.a(found_obj.get_value("SUMMARY"), href=self.env.new_url(found_uid))
paul@121 665
paul@212 666
        self.show_request_controls(obj, needs_action)
paul@212 667
        page.form.close()
paul@212 668
paul@121 669
    def show_requests_on_page(self):
paul@69 670
paul@69 671
        "Show requests for the current user."
paul@69 672
paul@69 673
        # NOTE: This list could be more informative, but it is envisaged that
paul@69 674
        # NOTE: the requests would be visited directly anyway.
paul@69 675
paul@121 676
        requests = self._get_requests()
paul@70 677
paul@185 678
        self.page.div(id="pending-requests")
paul@185 679
paul@80 680
        if requests:
paul@114 681
            self.page.p("Pending requests:")
paul@114 682
paul@80 683
            self.page.ul()
paul@69 684
paul@80 685
            for request in requests:
paul@165 686
                obj = self._get_object(request)
paul@165 687
                if obj:
paul@165 688
                    self.page.li()
paul@213 689
                    self.page.a(obj.get_value("SUMMARY"), href="#request-%s" % request)
paul@165 690
                    self.page.li.close()
paul@80 691
paul@80 692
            self.page.ul.close()
paul@80 693
paul@80 694
        else:
paul@80 695
            self.page.p("There are no pending requests.")
paul@69 696
paul@185 697
        self.page.div.close()
paul@185 698
paul@185 699
    def show_participants_on_page(self):
paul@185 700
paul@185 701
        "Show participants for scheduling purposes."
paul@185 702
paul@185 703
        args = self.env.get_args()
paul@185 704
        participants = args.get("participants", [])
paul@185 705
paul@185 706
        try:
paul@185 707
            for name, value in args.items():
paul@185 708
                if name.startswith("remove-participant-"):
paul@185 709
                    i = int(name[len("remove-participant-"):])
paul@185 710
                    del participants[i]
paul@185 711
                    break
paul@185 712
        except ValueError:
paul@185 713
            pass
paul@185 714
paul@185 715
        # Trim empty participants.
paul@185 716
paul@185 717
        while participants and not participants[-1].strip():
paul@185 718
            participants.pop()
paul@185 719
paul@185 720
        # Show any specified participants together with controls to remove and
paul@185 721
        # add participants.
paul@185 722
paul@185 723
        self.page.div(id="participants")
paul@185 724
paul@185 725
        self.page.p("Participants for scheduling:")
paul@185 726
paul@185 727
        for i, participant in enumerate(participants):
paul@185 728
            self.page.p()
paul@185 729
            self.page.input(name="participants", type="text", value=participant)
paul@185 730
            self.page.input(name="remove-participant-%d" % i, type="submit", value="Remove")
paul@185 731
            self.page.p.close()
paul@185 732
paul@185 733
        self.page.p()
paul@185 734
        self.page.input(name="participants", type="text")
paul@185 735
        self.page.input(name="add-participant", type="submit", value="Add")
paul@185 736
        self.page.p.close()
paul@185 737
paul@185 738
        self.page.div.close()
paul@185 739
paul@185 740
        return participants
paul@185 741
paul@121 742
    # Full page output methods.
paul@70 743
paul@121 744
    def show_object(self, path_info):
paul@70 745
paul@121 746
        "Show an object request using the given 'path_info' for the current user."
paul@70 747
paul@121 748
        uid = self._get_uid(path_info)
paul@121 749
        obj = self._get_object(uid)
paul@121 750
paul@121 751
        if not obj:
paul@70 752
            return False
paul@70 753
paul@123 754
        is_request = uid in self._get_requests()
paul@155 755
        handled = self.handle_request(uid, obj, is_request)
paul@77 756
paul@123 757
        if handled:
paul@123 758
            return True
paul@73 759
paul@123 760
        self.new_page(title="Event")
paul@212 761
        self.show_object_on_page(uid, obj, is_request and not handled)
paul@73 762
paul@70 763
        return True
paul@70 764
paul@114 765
    def show_calendar(self):
paul@114 766
paul@114 767
        "Show the calendar for the current user."
paul@114 768
paul@202 769
        handled = self.handle_newevent()
paul@202 770
paul@114 771
        self.new_page(title="Calendar")
paul@162 772
        page = self.page
paul@162 773
paul@196 774
        # Form controls are used in various places on the calendar page.
paul@196 775
paul@196 776
        page.form(method="POST")
paul@196 777
paul@121 778
        self.show_requests_on_page()
paul@185 779
        participants = self.show_participants_on_page()
paul@114 780
paul@196 781
        # Show a button for scheduling a new event.
paul@196 782
paul@230 783
        page.p(class_="controls")
paul@196 784
        page.input(name="newevent", type="submit", value="New event", id="newevent")
paul@196 785
        page.p.close()
paul@196 786
paul@231 787
        # Show controls for hiding empty and busy slots.
paul@203 788
        # The positioning of the control, paragraph and table are important here.
paul@203 789
paul@203 790
        page.input(name="hideslots", type="checkbox", value="hide", id="hideslots")
paul@231 791
        page.input(name="hidebusy", type="checkbox", value="hide", id="hidebusy")
paul@203 792
paul@230 793
        page.p(class_="controls")
paul@237 794
        page.label("Hide busy time periods", for_="hidebusy", class_="hidebusy enable")
paul@237 795
        page.label("Show busy time periods", for_="hidebusy", class_="hidebusy disable")
paul@237 796
        page.label("Hide unused time periods", for_="hideslots", class_="hideslots enable")
paul@237 797
        page.label("Show unused time periods", for_="hideslots", class_="hideslots disable")
paul@203 798
        page.p.close()
paul@203 799
paul@114 800
        freebusy = self.store.get_freebusy(self.user)
paul@114 801
paul@114 802
        if not freebusy:
paul@114 803
            page.p("No events scheduled.")
paul@114 804
            return
paul@114 805
paul@154 806
        # Obtain the user's timezone.
paul@147 807
paul@244 808
        tzid = self.get_tzid()
paul@147 809
paul@114 810
        # Day view: start at the earliest known day and produce days until the
paul@114 811
        # latest known day, perhaps with expandable sections of empty days.
paul@114 812
paul@114 813
        # Month view: start at the earliest known month and produce months until
paul@114 814
        # the latest known month, perhaps with expandable sections of empty
paul@114 815
        # months.
paul@114 816
paul@114 817
        # Details of users to invite to new events could be superimposed on the
paul@114 818
        # calendar.
paul@114 819
paul@185 820
        # Requests are listed and linked to their tentative positions in the
paul@185 821
        # calendar. Other participants are also shown.
paul@185 822
paul@185 823
        request_summary = self._get_request_summary()
paul@185 824
paul@185 825
        period_groups = [request_summary, freebusy]
paul@185 826
        period_group_types = ["request", "freebusy"]
paul@185 827
        period_group_sources = ["Pending requests", "Your schedule"]
paul@185 828
paul@187 829
        for i, participant in enumerate(participants):
paul@185 830
            period_groups.append(self.store.get_freebusy_for_other(self.user, get_uri(participant)))
paul@187 831
            period_group_types.append("freebusy-part%d" % i)
paul@185 832
            period_group_sources.append(participant)
paul@114 833
paul@162 834
        groups = []
paul@162 835
        group_columns = []
paul@185 836
        group_types = period_group_types
paul@185 837
        group_sources = period_group_sources
paul@162 838
        all_points = set()
paul@162 839
paul@162 840
        # Obtain time point information for each group of periods.
paul@162 841
paul@185 842
        for periods in period_groups:
paul@162 843
            periods = convert_periods(periods, tzid)
paul@162 844
paul@162 845
            # Get the time scale with start and end points.
paul@162 846
paul@162 847
            scale = get_scale(periods)
paul@162 848
paul@162 849
            # Get the time slots for the periods.
paul@162 850
paul@162 851
            slots = get_slots(scale)
paul@162 852
paul@162 853
            # Add start of day time points for multi-day periods.
paul@162 854
paul@244 855
            add_day_start_points(slots, tzid)
paul@162 856
paul@162 857
            # Record the slots and all time points employed.
paul@162 858
paul@162 859
            groups.append(slots)
paul@201 860
            all_points.update([point for point, active in slots])
paul@162 861
paul@162 862
        # Partition the groups into days.
paul@162 863
paul@162 864
        days = {}
paul@162 865
        partitioned_groups = []
paul@171 866
        partitioned_group_types = []
paul@185 867
        partitioned_group_sources = []
paul@162 868
paul@185 869
        for slots, group_type, group_source in zip(groups, group_types, group_sources):
paul@162 870
paul@162 871
            # Propagate time points to all groups of time slots.
paul@162 872
paul@162 873
            add_slots(slots, all_points)
paul@162 874
paul@162 875
            # Count the number of columns employed by the group.
paul@162 876
paul@162 877
            columns = 0
paul@162 878
paul@162 879
            # Partition the time slots by day.
paul@162 880
paul@162 881
            partitioned = {}
paul@162 882
paul@162 883
            for day, day_slots in partition_by_day(slots).items():
paul@201 884
                intervals = []
paul@201 885
                last = None
paul@201 886
paul@201 887
                for point, active in day_slots:
paul@201 888
                    columns = max(columns, len(active))
paul@201 889
                    if last:
paul@201 890
                        intervals.append((last, point))
paul@201 891
                    last = point
paul@201 892
paul@201 893
                if last:
paul@201 894
                    intervals.append((last, None))
paul@162 895
paul@162 896
                if not days.has_key(day):
paul@162 897
                    days[day] = set()
paul@162 898
paul@162 899
                # Convert each partition to a mapping from points to active
paul@162 900
                # periods.
paul@162 901
paul@201 902
                partitioned[day] = dict(day_slots)
paul@201 903
paul@201 904
                # Record the divisions or intervals within each day.
paul@201 905
paul@201 906
                days[day].update(intervals)
paul@162 907
paul@194 908
            if group_type != "request" or columns:
paul@194 909
                group_columns.append(columns)
paul@194 910
                partitioned_groups.append(partitioned)
paul@194 911
                partitioned_group_types.append(group_type)
paul@194 912
                partitioned_group_sources.append(group_source)
paul@114 913
paul@243 914
        self.show_calendar_day_controls(days)
paul@243 915
paul@230 916
        page.table(cellspacing=5, cellpadding=5, class_="calendar")
paul@188 917
        self.show_calendar_participant_headings(partitioned_group_types, partitioned_group_sources, group_columns)
paul@171 918
        self.show_calendar_days(days, partitioned_groups, partitioned_group_types, group_columns)
paul@162 919
        page.table.close()
paul@114 920
paul@196 921
        # End the form region.
paul@196 922
paul@196 923
        page.form.close()
paul@196 924
paul@243 925
    def show_calendar_day_controls(self, days):
paul@243 926
paul@243 927
        "Show controls for the given 'days' in the calendar."
paul@243 928
paul@243 929
        page = self.page
paul@243 930
        slots = self.env.get_args().get("slot", [])
paul@243 931
paul@243 932
        for day in days:
paul@243 933
            value, identifier = self._day_value_and_identifier(day)
paul@243 934
            self._slot_selector(value, identifier, slots)
paul@243 935
paul@243 936
        # Generate a dynamic stylesheet to allow day selections to colour
paul@243 937
        # specific days.
paul@243 938
        # NOTE: The style details need to be coordinated with the static
paul@243 939
        # NOTE: stylesheet.
paul@243 940
paul@243 941
        page.style(type="text/css")
paul@243 942
paul@243 943
        for day in days:
paul@243 944
            daystr = format_datetime(day)
paul@243 945
            page.add("""\
paul@243 946
input.newevent#day-%s-:checked ~ table label.day.day-%s,
paul@243 947
input.newevent#day-%s-:checked ~ table label.timepoint.day-%s {
paul@243 948
    background-color: #5f4;
paul@243 949
    text-decoration: underline;
paul@243 950
}
paul@243 951
""" % (daystr, daystr, daystr, daystr))
paul@243 952
paul@243 953
        page.style.close()
paul@243 954
paul@188 955
    def show_calendar_participant_headings(self, group_types, group_sources, group_columns):
paul@186 956
paul@186 957
        """
paul@186 958
        Show headings for the participants and other scheduling contributors,
paul@188 959
        defined by 'group_types', 'group_sources' and 'group_columns'.
paul@186 960
        """
paul@186 961
paul@185 962
        page = self.page
paul@185 963
paul@188 964
        page.colgroup(span=1, id="columns-timeslot")
paul@186 965
paul@188 966
        for group_type, columns in zip(group_types, group_columns):
paul@191 967
            page.colgroup(span=max(columns, 1), id="columns-%s" % group_type)
paul@186 968
paul@185 969
        page.thead()
paul@185 970
        page.tr()
paul@185 971
        page.th("", class_="emptyheading")
paul@185 972
paul@193 973
        for group_type, source, columns in zip(group_types, group_sources, group_columns):
paul@193 974
            page.th(source,
paul@193 975
                class_=(group_type == "request" and "requestheading" or "participantheading"),
paul@193 976
                colspan=max(columns, 1))
paul@185 977
paul@185 978
        page.tr.close()
paul@185 979
        page.thead.close()
paul@185 980
paul@171 981
    def show_calendar_days(self, days, partitioned_groups, partitioned_group_types, group_columns):
paul@186 982
paul@186 983
        """
paul@186 984
        Show calendar days, defined by a collection of 'days', the contributing
paul@186 985
        period information as 'partitioned_groups' (partitioned by day), the
paul@186 986
        'partitioned_group_types' indicating the kind of contribution involved,
paul@186 987
        and the 'group_columns' defining the number of columns in each group.
paul@186 988
        """
paul@186 989
paul@162 990
        page = self.page
paul@162 991
paul@191 992
        # Determine the number of columns required. Where participants provide
paul@191 993
        # no columns for events, one still needs to be provided for the
paul@191 994
        # participant itself.
paul@147 995
paul@191 996
        all_columns = sum([max(columns, 1) for columns in group_columns])
paul@191 997
paul@191 998
        # Determine the days providing time slots.
paul@191 999
paul@162 1000
        all_days = days.items()
paul@162 1001
        all_days.sort()
paul@162 1002
paul@162 1003
        # Produce a heading and time points for each day.
paul@162 1004
paul@201 1005
        for day, intervals in all_days:
paul@186 1006
            page.thead()
paul@114 1007
            page.tr()
paul@243 1008
            page.th(class_="dayheading container", colspan=all_columns+1)
paul@239 1009
            self._day_heading(day)
paul@114 1010
            page.th.close()
paul@153 1011
            page.tr.close()
paul@186 1012
            page.thead.close()
paul@114 1013
paul@162 1014
            groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups]
paul@162 1015
paul@186 1016
            page.tbody()
paul@201 1017
            self.show_calendar_points(intervals, groups_for_day, partitioned_group_types, group_columns)
paul@186 1018
            page.tbody.close()
paul@185 1019
paul@201 1020
    def show_calendar_points(self, intervals, groups, group_types, group_columns):
paul@186 1021
paul@186 1022
        """
paul@201 1023
        Show the time 'intervals' along with period information from the given
paul@186 1024
        'groups', having the indicated 'group_types', each with the number of
paul@186 1025
        columns given by 'group_columns'.
paul@186 1026
        """
paul@186 1027
paul@162 1028
        page = self.page
paul@162 1029
paul@244 1030
        # Obtain the user's timezone.
paul@244 1031
paul@244 1032
        tzid = self.get_tzid()
paul@244 1033
paul@203 1034
        # Produce a row for each interval.
paul@162 1035
paul@201 1036
        intervals = list(intervals)
paul@201 1037
        intervals.sort()
paul@162 1038
paul@201 1039
        for point, endpoint in intervals:
paul@244 1040
            continuation = point == get_start_of_day(point, tzid)
paul@153 1041
paul@203 1042
            # Some rows contain no period details and are marked as such.
paul@203 1043
paul@203 1044
            have_active = reduce(lambda x, y: x or y, [slots.get(point) for slots in groups], None)
paul@203 1045
paul@203 1046
            css = " ".join(
paul@203 1047
                ["slot"] +
paul@231 1048
                (have_active and ["busy"] or ["empty"]) +
paul@203 1049
                (continuation and ["daystart"] or [])
paul@203 1050
                )
paul@203 1051
paul@203 1052
            page.tr(class_=css)
paul@162 1053
            page.th(class_="timeslot")
paul@201 1054
            self._time_point(point, endpoint)
paul@162 1055
            page.th.close()
paul@162 1056
paul@162 1057
            # Obtain slots for the time point from each group.
paul@162 1058
paul@171 1059
            for columns, slots, group_type in zip(group_columns, groups, group_types):
paul@162 1060
                active = slots and slots.get(point)
paul@162 1061
paul@191 1062
                # Where no periods exist for the given time interval, generate
paul@191 1063
                # an empty cell. Where a participant provides no periods at all,
paul@191 1064
                # the colspan is adjusted to be 1, not 0.
paul@191 1065
paul@162 1066
                if not active:
paul@196 1067
                    page.td(class_="empty container", colspan=max(columns, 1))
paul@201 1068
                    self._empty_slot(point, endpoint)
paul@196 1069
                    page.td.close()
paul@162 1070
                    continue
paul@162 1071
paul@162 1072
                slots = slots.items()
paul@162 1073
                slots.sort()
paul@162 1074
                spans = get_spans(slots)
paul@162 1075
paul@162 1076
                # Show a column for each active period.
paul@117 1077
paul@153 1078
                for t in active:
paul@185 1079
                    if t and len(t) >= 2:
paul@185 1080
                        start, end, uid, key = get_freebusy_details(t)
paul@185 1081
                        span = spans[key]
paul@171 1082
paul@171 1083
                        # Produce a table cell only at the start of the period
paul@171 1084
                        # or when continued at the start of a day.
paul@171 1085
paul@153 1086
                        if point == start or continuation:
paul@153 1087
paul@195 1088
                            has_continued = continuation and point != start
paul@244 1089
                            will_continue = not ends_on_same_day(point, end, tzid)
paul@195 1090
                            css = " ".join(
paul@195 1091
                                ["event"] +
paul@195 1092
                                (has_continued and ["continued"] or []) +
paul@195 1093
                                (will_continue and ["continues"] or [])
paul@195 1094
                                )
paul@195 1095
paul@189 1096
                            # Only anchor the first cell of events.
paul@189 1097
paul@189 1098
                            if point == start:
paul@195 1099
                                page.td(class_=css, rowspan=span, id="%s-%s" % (group_type, uid))
paul@189 1100
                            else:
paul@195 1101
                                page.td(class_=css, rowspan=span)
paul@171 1102
paul@153 1103
                            obj = self._get_object(uid)
paul@185 1104
paul@185 1105
                            if not obj:
paul@185 1106
                                page.span("")
paul@185 1107
                            else:
paul@213 1108
                                summary = obj.get_value("SUMMARY")
paul@171 1109
paul@171 1110
                                # Only link to events if they are not being
paul@171 1111
                                # updated by requests.
paul@171 1112
paul@171 1113
                                if uid in self._get_requests() and group_type != "request":
paul@189 1114
                                    page.span(summary)
paul@164 1115
                                else:
paul@171 1116
                                    href = "%s/%s" % (self.env.get_url().rstrip("/"), uid)
paul@189 1117
                                    page.a(summary, href=href)
paul@171 1118
paul@153 1119
                            page.td.close()
paul@153 1120
                    else:
paul@196 1121
                        page.td(class_="empty container")
paul@201 1122
                        self._empty_slot(point, endpoint)
paul@196 1123
                        page.td.close()
paul@114 1124
paul@166 1125
                # Pad with empty columns.
paul@166 1126
paul@166 1127
                i = columns - len(active)
paul@166 1128
                while i > 0:
paul@166 1129
                    i -= 1
paul@196 1130
                    page.td(class_="empty container")
paul@201 1131
                    self._empty_slot(point, endpoint)
paul@196 1132
                    page.td.close()
paul@166 1133
paul@162 1134
            page.tr.close()
paul@114 1135
paul@239 1136
    def _day_heading(self, day):
paul@243 1137
paul@243 1138
        """
paul@243 1139
        Generate a heading for 'day' of the following form:
paul@243 1140
paul@243 1141
        <label class="day day-20150203" for="day-20150203">Tuesday, 3 February 2015</label>
paul@243 1142
        """
paul@243 1143
paul@239 1144
        page = self.page
paul@243 1145
        daystr = format_datetime(day)
paul@239 1146
        value, identifier = self._day_value_and_identifier(day)
paul@243 1147
        page.label(self.format_date(day, "full"), class_="day day-%s" % daystr, for_=identifier)
paul@239 1148
paul@201 1149
    def _time_point(self, point, endpoint):
paul@243 1150
paul@243 1151
        """
paul@243 1152
        Generate headings for the 'point' to 'endpoint' period of the following
paul@243 1153
        form:
paul@243 1154
paul@243 1155
        <label class="timepoint day-20150203" for="slot-20150203T090000-20150203T100000">09:00:00 CET</label>
paul@243 1156
        <span class="endpoint">10:00:00 CET</span>
paul@243 1157
        """
paul@243 1158
paul@201 1159
        page = self.page
paul@244 1160
        tzid = self.get_tzid()
paul@243 1161
        daystr = format_datetime(point.date())
paul@201 1162
        value, identifier = self._slot_value_and_identifier(point, endpoint)
paul@238 1163
        slots = self.env.get_args().get("slot", [])
paul@239 1164
        self._slot_selector(value, identifier, slots)
paul@243 1165
        page.label(self.format_time(point, "long"), class_="timepoint day-%s" % daystr, for_=identifier)
paul@244 1166
        page.span(self.format_time(endpoint or get_end_of_day(point, tzid), "long"), class_="endpoint")
paul@239 1167
paul@239 1168
    def _slot_selector(self, value, identifier, slots):
paul@239 1169
        page = self.page
paul@238 1170
        if value in slots:
paul@236 1171
            page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent", checked="checked")
paul@202 1172
        else:
paul@236 1173
            page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent")
paul@201 1174
paul@201 1175
    def _empty_slot(self, point, endpoint):
paul@197 1176
        page = self.page
paul@201 1177
        value, identifier = self._slot_value_and_identifier(point, endpoint)
paul@236 1178
        page.label("Select/deselect period", class_="newevent popup", for_=identifier)
paul@196 1179
paul@239 1180
    def _day_value_and_identifier(self, day):
paul@239 1181
        value = "%s-" % format_datetime(day)
paul@239 1182
        identifier = "day-%s" % value
paul@239 1183
        return value, identifier
paul@239 1184
paul@201 1185
    def _slot_value_and_identifier(self, point, endpoint):
paul@202 1186
        value = "%s-%s" % (format_datetime(point), endpoint and format_datetime(endpoint) or "")
paul@201 1187
        identifier = "slot-%s" % value
paul@201 1188
        return value, identifier
paul@196 1189
paul@69 1190
    def select_action(self):
paul@69 1191
paul@69 1192
        "Select the desired action and show the result."
paul@69 1193
paul@121 1194
        path_info = self.env.get_path_info().strip("/")
paul@121 1195
paul@69 1196
        if not path_info:
paul@114 1197
            self.show_calendar()
paul@121 1198
        elif self.show_object(path_info):
paul@70 1199
            pass
paul@70 1200
        else:
paul@70 1201
            self.no_page()
paul@69 1202
paul@82 1203
    def __call__(self):
paul@69 1204
paul@69 1205
        "Interpret a request and show an appropriate response."
paul@69 1206
paul@69 1207
        if not self.user:
paul@69 1208
            self.no_user()
paul@69 1209
        else:
paul@69 1210
            self.select_action()
paul@69 1211
paul@70 1212
        # Write the headers and actual content.
paul@70 1213
paul@69 1214
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 1215
        print >>self.out
paul@69 1216
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 1217
paul@69 1218
if __name__ == "__main__":
paul@128 1219
    Manager()()
paul@69 1220
paul@69 1221
# vim: tabstop=4 expandtab shiftwidth=4