1 #!/usr/bin/env python 2 3 """ 4 A Web interface to a user's calendar. 5 6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 # Edit this path to refer to the location of the imiptools libraries, if 23 # necessary. 24 25 LIBRARY_PATH = "/var/lib/imip-agent" 26 27 from datetime import date, datetime, timedelta 28 import babel.dates 29 import cgi, os, sys 30 31 sys.path.append(LIBRARY_PATH) 32 33 from imiptools.content import Handler 34 from imiptools.data import get_address, get_uri, make_freebusy, Object, to_part 35 from imiptools.dates import format_datetime, format_time, get_date, get_datetime, \ 36 get_datetime_item, get_default_timezone, \ 37 get_end_of_day, get_start_of_day, get_start_of_next_day, \ 38 get_timestamp, ends_on_same_day, to_timezone 39 from imiptools.mail import Messenger 40 from imiptools.period import add_day_start_points, add_empty_days, add_slots, \ 41 convert_periods, get_freebusy_details, \ 42 get_scale, have_conflict, get_slots, get_spans, \ 43 partition_by_day, remove_from_freebusy, update_freebusy, \ 44 _update_freebusy 45 from imiptools.profile import Preferences 46 import imip_store 47 import markup 48 49 getenv = os.environ.get 50 setenv = os.environ.__setitem__ 51 52 class CGIEnvironment: 53 54 "A CGI-compatible environment." 55 56 def __init__(self, charset=None): 57 self.charset = charset 58 self.args = None 59 self.method = None 60 self.path = None 61 self.path_info = None 62 self.user = None 63 64 def get_args(self): 65 if self.args is None: 66 if self.get_method() != "POST": 67 setenv("QUERY_STRING", "") 68 args = cgi.parse(keep_blank_values=True) 69 70 if not self.charset: 71 self.args = args 72 else: 73 self.args = {} 74 for key, values in args.items(): 75 self.args[key] = [unicode(value, self.charset) for value in values] 76 77 return self.args 78 79 def get_method(self): 80 if self.method is None: 81 self.method = getenv("REQUEST_METHOD") or "GET" 82 return self.method 83 84 def get_path(self): 85 if self.path is None: 86 self.path = getenv("SCRIPT_NAME") or "" 87 return self.path 88 89 def get_path_info(self): 90 if self.path_info is None: 91 self.path_info = getenv("PATH_INFO") or "" 92 return self.path_info 93 94 def get_user(self): 95 if self.user is None: 96 self.user = getenv("REMOTE_USER") or "" 97 return self.user 98 99 def get_output(self): 100 return sys.stdout 101 102 def get_url(self): 103 path = self.get_path() 104 path_info = self.get_path_info() 105 return "%s%s" % (path.rstrip("/"), path_info) 106 107 def new_url(self, path_info): 108 path = self.get_path() 109 return "%s/%s" % (path.rstrip("/"), path_info.lstrip("/")) 110 111 class Common: 112 113 "Common handler and manager methods." 114 115 def __init__(self, user): 116 self.user = user 117 self.preferences = None 118 119 def get_preferences(self): 120 if not self.preferences: 121 self.preferences = Preferences(self.user) 122 return self.preferences 123 124 def get_tzid(self): 125 prefs = self.get_preferences() 126 return prefs.get("TZID") or get_default_timezone() 127 128 class ManagerHandler(Handler, Common): 129 130 """ 131 A content handler for use by the manager, as opposed to operating within the 132 mail processing pipeline. 133 """ 134 135 def __init__(self, obj, user, messenger): 136 Handler.__init__(self, messenger=messenger) 137 Common.__init__(self, user) 138 139 self.set_object(obj) 140 141 # Communication methods. 142 143 def send_message(self, method, sender, for_organiser): 144 145 """ 146 Create a full calendar object employing the given 'method', and send it 147 to the appropriate recipients, also sending a copy to the 'sender'. The 148 'for_organiser' value indicates whether the organiser is sending this 149 message. 150 """ 151 152 parts = [self.obj.to_part(method)] 153 154 # As organiser, send an invitation to attendees, excluding oneself if 155 # also attending. The updated event will be saved by the outgoing 156 # handler. 157 158 organiser = self.obj.get_value("ORGANIZER") 159 attendees = self.obj.get_values("ATTENDEE") 160 161 if for_organiser: 162 recipients = [get_address(attendee) for attendee in attendees if attendee != self.user] 163 else: 164 recipients = [get_address(organiser)] 165 166 # Bundle free/busy information if appropriate. 167 168 preferences = Preferences(self.user) 169 170 if preferences.get("freebusy_sharing") == "share" and \ 171 preferences.get("freebusy_bundling") == "always": 172 173 # Invent a unique identifier. 174 175 utcnow = get_timestamp() 176 uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) 177 178 freebusy = self.store.get_freebusy(self.user) 179 180 # Replace the non-updated free/busy details for this event with 181 # newer details (since the outgoing handler updates this user's 182 # free/busy details). 183 184 tzid = self.get_tzid() 185 186 _update_freebusy(freebusy, self.obj.get_periods_for_freebusy(tzid), 187 self.obj.get_value("TRANSP") or "OPAQUE", self.obj.get_value("UID")) 188 189 user_attr = self.messenger and self.messenger.sender != get_address(self.user) and \ 190 {"SENT-BY" : get_uri(self.messenger.sender)} or {} 191 192 parts.append(to_part("PUBLISH", [ 193 make_freebusy(freebusy, uid, self.user, user_attr) 194 ])) 195 196 message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) 197 self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) 198 199 # Action methods. 200 201 def process_received_request(self, update=False): 202 203 """ 204 Process the current request for the given 'user'. Return whether any 205 action was taken. 206 207 If 'update' is given, the sequence number will be incremented in order 208 to override any previous response. 209 """ 210 211 # Reply only on behalf of this user. 212 213 for attendee, attendee_attr in self.obj.get_items("ATTENDEE"): 214 215 if attendee == self.user: 216 if attendee_attr.has_key("RSVP"): 217 del attendee_attr["RSVP"] 218 if self.messenger and self.messenger.sender != get_address(attendee): 219 attendee_attr["SENT-BY"] = get_uri(self.messenger.sender) 220 self.obj["ATTENDEE"] = [(attendee, attendee_attr)] 221 222 self.update_dtstamp() 223 self.set_sequence(update) 224 225 self.send_message("REPLY", get_address(attendee), for_organiser=False) 226 227 return True 228 229 return False 230 231 def process_created_request(self, method, update=False, removed=None): 232 233 """ 234 Process the current request for the given 'user', sending a created 235 request of the given 'method' to attendees. Return whether any action 236 was taken. 237 238 If 'update' is given, the sequence number will be incremented in order 239 to override any previous message. 240 241 If 'removed' is specified, a list of participants to be removed is 242 provided. 243 """ 244 245 organiser, organiser_attr = self.obj.get_item("ORGANIZER") 246 247 if self.messenger and self.messenger.sender != get_address(organiser): 248 organiser_attr["SENT-BY"] = get_uri(self.messenger.sender) 249 250 if removed: 251 attendee_map = self.obj.get_value_map("ATTENDEE") 252 self.obj["ATTENDEE"] = [(attendee, attendee_attr) 253 for (attendee, attendee_attr) in attendee_map.items() 254 if attendee not in removed] 255 256 self.update_dtstamp() 257 self.set_sequence(update) 258 259 self.send_message(method, get_address(organiser), for_organiser=True) 260 261 return True 262 263 class Manager(Common): 264 265 "A simple manager application." 266 267 def __init__(self, messenger=None): 268 self.messenger = messenger or Messenger() 269 self.encoding = "utf-8" 270 self.env = CGIEnvironment(self.encoding) 271 272 user = self.env.get_user() 273 Common.__init__(self, user and get_uri(user) or None) 274 275 self.locale = None 276 self.requests = None 277 278 self.out = self.env.get_output() 279 self.page = markup.page() 280 281 self.store = imip_store.FileStore() 282 self.objects = {} 283 284 try: 285 self.publisher = imip_store.FilePublisher() 286 except OSError: 287 self.publisher = None 288 289 def _get_uid(self, path_info): 290 return path_info.lstrip("/").split("/", 1)[0] 291 292 def _get_object(self, uid): 293 if self.objects.has_key(uid): 294 return self.objects[uid] 295 296 fragment = uid and self.store.get_event(self.user, uid) or None 297 obj = self.objects[uid] = fragment and Object(fragment) 298 return obj 299 300 def _get_requests(self): 301 if self.requests is None: 302 self.requests = self.store.get_requests(self.user) 303 return self.requests 304 305 def _get_request_summary(self): 306 summary = [] 307 for uid in self._get_requests(): 308 obj = self._get_object(uid) 309 if obj: 310 summary.append(( 311 obj.get_value("DTSTART"), 312 obj.get_value("DTEND"), 313 uid 314 )) 315 return summary 316 317 # Preference methods. 318 319 def get_user_locale(self): 320 if not self.locale: 321 self.locale = self.get_preferences().get("LANG", "C") 322 return self.locale 323 324 # Prettyprinting of dates and times. 325 326 def format_date(self, dt, format): 327 return self._format_datetime(babel.dates.format_date, dt, format) 328 329 def format_time(self, dt, format): 330 return self._format_datetime(babel.dates.format_time, dt, format) 331 332 def format_datetime(self, dt, format): 333 return self._format_datetime( 334 isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date, 335 dt, format) 336 337 def _format_datetime(self, fn, dt, format): 338 return fn(dt, format=format, locale=self.get_user_locale()) 339 340 # Data management methods. 341 342 def remove_request(self, uid): 343 return self.store.dequeue_request(self.user, uid) 344 345 def remove_event(self, uid): 346 return self.store.remove_event(self.user, uid) 347 348 def update_freebusy(self, uid, obj): 349 tzid = self.get_tzid() 350 freebusy = self.store.get_freebusy(self.user) 351 update_freebusy(freebusy, self.user, obj.get_periods_for_freebusy(tzid), 352 obj.get_value("TRANSP"), uid, self.store) 353 354 def remove_from_freebusy(self, uid): 355 freebusy = self.store.get_freebusy(self.user) 356 remove_from_freebusy(freebusy, self.user, uid, self.store) 357 358 # Presentation methods. 359 360 def new_page(self, title): 361 self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css")) 362 363 def status(self, code, message): 364 self.header("Status", "%s %s" % (code, message)) 365 366 def header(self, header, value): 367 print >>self.out, "%s: %s" % (header, value) 368 369 def no_user(self): 370 self.status(403, "Forbidden") 371 self.new_page(title="Forbidden") 372 self.page.p("You are not logged in and thus cannot access scheduling requests.") 373 374 def no_page(self): 375 self.status(404, "Not Found") 376 self.new_page(title="Not Found") 377 self.page.p("No page is provided at the given address.") 378 379 def redirect(self, url): 380 self.status(302, "Redirect") 381 self.header("Location", url) 382 self.new_page(title="Redirect") 383 self.page.p("Redirecting to: %s" % url) 384 385 # Request logic methods. 386 387 def handle_newevent(self): 388 389 """ 390 Handle any new event operation, creating a new event and redirecting to 391 the event page for further activity. 392 """ 393 394 # Handle a submitted form. 395 396 args = self.env.get_args() 397 398 if not args.has_key("newevent"): 399 return 400 401 # Create a new event using the available information. 402 403 slots = args.get("slot", []) 404 participants = args.get("participants", []) 405 406 if not slots: 407 return 408 409 # Obtain the user's timezone. 410 411 tzid = self.get_tzid() 412 413 # Coalesce the selected slots. 414 415 slots.sort() 416 coalesced = [] 417 last = None 418 419 for slot in slots: 420 start, end = slot.split("-") 421 start = get_datetime(start, {"TZID" : tzid}) 422 end = end and get_datetime(end, {"TZID" : tzid}) or get_start_of_next_day(start, tzid) 423 424 if last: 425 last_start, last_end = last 426 427 # Merge adjacent dates and datetimes. 428 429 if start == last_end or get_start_of_day(last_end, tzid) == get_start_of_day(start, tzid): 430 last = last_start, end 431 continue 432 433 # Handle datetimes within dates. 434 # Datetime periods are within single days and are therefore 435 # discarded. 436 437 elif get_start_of_day(start, tzid) == get_start_of_day(last_start, tzid): 438 continue 439 440 # Add separate dates and datetimes. 441 442 else: 443 coalesced.append(last) 444 445 last = start, end 446 447 if last: 448 coalesced.append(last) 449 450 # Invent a unique identifier. 451 452 utcnow = get_timestamp() 453 uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) 454 455 # Define a single occurrence if only one coalesced slot exists. 456 # Otherwise, many occurrences are defined. 457 458 for i, (start, end) in enumerate(coalesced): 459 this_uid = "%s-%s" % (uid, i) 460 461 start_value, start_attr = get_datetime_item(start, tzid) 462 end_value, end_attr = get_datetime_item(end, tzid) 463 464 # Create a calendar object and store it as a request. 465 466 record = [] 467 rwrite = record.append 468 469 rwrite(("UID", {}, this_uid)) 470 rwrite(("SUMMARY", {}, "New event at %s" % utcnow)) 471 rwrite(("DTSTAMP", {}, utcnow)) 472 rwrite(("DTSTART", start_attr, start_value)) 473 rwrite(("DTEND", end_attr, end_value)) 474 rwrite(("ORGANIZER", {}, self.user)) 475 476 for participant in participants: 477 if not participant: 478 continue 479 participant = get_uri(participant) 480 rwrite(("ATTENDEE", {"RSVP" : "TRUE", "PARTSTAT" : "NEEDS-ACTION"}, participant)) 481 482 obj = ("VEVENT", {}, record) 483 484 self.store.set_event(self.user, this_uid, obj) 485 self.store.queue_request(self.user, this_uid) 486 487 # Redirect to the object (or the first of the objects), where instead of 488 # attendee controls, there will be organiser controls. 489 490 self.redirect(self.env.new_url("%s-0" % uid)) 491 492 def handle_request(self, uid, obj): 493 494 """ 495 Handle actions involving the given 'uid' and 'obj' object, returning an 496 error if one occurred, or None if the request was successfully handled. 497 """ 498 499 # Handle a submitted form. 500 501 args = self.env.get_args() 502 503 # Get the possible actions. 504 505 reply = args.has_key("reply") 506 discard = args.has_key("discard") 507 invite = args.has_key("invite") 508 cancel = args.has_key("cancel") 509 save = args.has_key("save") 510 511 have_action = reply or discard or invite or cancel or save 512 513 if not have_action: 514 return ["action"] 515 516 # Update the object. 517 518 if args.has_key("summary"): 519 obj["SUMMARY"] = [(args["summary"][0], {})] 520 521 organisers = obj.get_value_map("ORGANIZER") 522 attendees = obj.get_value_map("ATTENDEE") 523 524 if args.has_key("partstat"): 525 for d in attendees, organisers: 526 if d.has_key(self.user): 527 d[self.user]["PARTSTAT"] = args["partstat"][0] 528 if d[self.user].has_key("RSVP"): 529 del d[self.user]["RSVP"] 530 531 is_organiser = obj.get_value("ORGANIZER") == self.user 532 533 # Obtain any participants to be removed. 534 535 removed = args.get("remove") 536 537 # Obtain the user's timezone and process datetime values. 538 539 update = False 540 541 if is_organiser: 542 dtend_enabled = args.get("dtend-control", [None])[0] == "enable" 543 dttimes_enabled = args.get("dttimes-control", [None])[0] == "enable" 544 545 t = self.handle_date_controls("dtstart", dttimes_enabled) 546 if t: 547 dtstart, attr = t 548 update = self.set_datetime_in_object(dtstart, attr.get("TZID"), "DTSTART", obj) or update 549 else: 550 return ["dtstart"] 551 552 # Handle specified end datetimes. 553 554 if dtend_enabled: 555 t = self.handle_date_controls("dtend", dttimes_enabled) 556 if t: 557 dtend, attr = t 558 559 # Convert end dates to iCalendar "next day" dates. 560 561 if not isinstance(dtend, datetime): 562 dtend += timedelta(1) 563 update = self.set_datetime_in_object(dtend, attr.get("TZID"), "DTEND", obj) or update 564 else: 565 return ["dtend"] 566 567 # Otherwise, treat the end date as the start date. Datetimes are 568 # handled by making the event occupy the rest of the day. 569 570 else: 571 dtend = dtstart + timedelta(1) 572 if isinstance(dtstart, datetime): 573 dtend = get_start_of_day(dtend, attr["TZID"]) 574 update = self.set_datetime_in_object(dtend, attr.get("TZID"), "DTEND", obj) or update 575 576 if dtstart >= dtend: 577 return ["dtstart", "dtend"] 578 579 # Process any action. 580 581 handled = True 582 583 if reply or invite or cancel: 584 585 handler = ManagerHandler(obj, self.user, self.messenger) 586 587 # Process the object and remove it from the list of requests. 588 589 if reply and handler.process_received_request(update) or \ 590 is_organiser and (invite or cancel) and \ 591 handler.process_created_request(invite and "REQUEST" or "CANCEL", update, removed): 592 593 self.remove_request(uid) 594 595 # Save single user events. 596 597 elif save: 598 self.store.set_event(self.user, uid, obj.to_node()) 599 self.update_freebusy(uid, obj) 600 self.remove_request(uid) 601 602 # Remove the request and the object. 603 604 elif discard: 605 self.remove_from_freebusy(uid) 606 self.remove_event(uid) 607 self.remove_request(uid) 608 609 else: 610 handled = False 611 612 # Upon handling an action, redirect to the main page. 613 614 if handled: 615 self.redirect(self.env.get_path()) 616 617 return None 618 619 def handle_date_controls(self, name, with_time=True): 620 621 """ 622 Handle date control information for fields starting with 'name', 623 returning a (datetime, attr) tuple or None if the fields cannot be used 624 to construct a datetime object. 625 """ 626 627 args = self.env.get_args() 628 629 if args.has_key("%s-date" % name): 630 date = args["%s-date" % name][0] 631 632 if with_time: 633 hour = args.get("%s-hour" % name, [None])[0] 634 minute = args.get("%s-minute" % name, [None])[0] 635 second = args.get("%s-second" % name, [None])[0] 636 tzid = args.get("%s-tzid" % name, [self.get_tzid()])[0] 637 638 time = (hour or minute or second) and "T%s%s%s" % (hour, minute, second) or "" 639 value = "%s%s" % (date, time) 640 attr = {"TZID" : tzid, "VALUE" : "DATE-TIME"} 641 dt = get_datetime(value, attr) 642 else: 643 attr = {"VALUE" : "DATE"} 644 dt = get_datetime(date) 645 646 if dt: 647 return dt, attr 648 649 return None 650 651 def set_datetime_in_object(self, dt, tzid, property, obj): 652 653 """ 654 Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether 655 an update has occurred. 656 """ 657 658 if dt: 659 old_value = obj.get_value(property) 660 obj[property] = [get_datetime_item(dt, tzid)] 661 return format_datetime(dt) != old_value 662 663 return False 664 665 # Page fragment methods. 666 667 def show_request_controls(self, obj): 668 669 "Show form controls for a request concerning 'obj'." 670 671 page = self.page 672 673 is_organiser = obj.get_value("ORGANIZER") == self.user 674 675 attendees = obj.get_value_map("ATTENDEE") 676 is_attendee = attendees.has_key(self.user) 677 attendee_attr = attendees.get(self.user) 678 679 is_request = obj.get_value("UID") in self._get_requests() 680 681 have_other_attendees = len(attendees) > (is_attendee and 1 or 0) 682 683 # Show appropriate options depending on the role of the user. 684 685 if is_attendee and not is_organiser: 686 page.p("An action is required for this request:") 687 688 page.p() 689 page.input(name="reply", type="submit", value="Reply") 690 page.add(" ") 691 page.input(name="discard", type="submit", value="Discard") 692 page.p.close() 693 694 if is_organiser: 695 if have_other_attendees: 696 page.p("As organiser, you can perform the following:") 697 698 page.p() 699 page.input(name="invite", type="submit", value="Invite") 700 page.add(" ") 701 if is_request: 702 page.input(name="discard", type="submit", value="Discard") 703 else: 704 page.input(name="cancel", type="submit", value="Cancel") 705 page.p.close() 706 else: 707 page.p() 708 page.input(name="save", type="submit", value="Save") 709 page.add(" ") 710 page.input(name="discard", type="submit", value="Discard") 711 page.p.close() 712 713 property_items = [ 714 ("SUMMARY", "Summary"), 715 ("DTSTART", "Start"), 716 ("DTEND", "End"), 717 ("ORGANIZER", "Organiser"), 718 ("ATTENDEE", "Attendee"), 719 ] 720 721 partstat_items = [ 722 ("NEEDS-ACTION", "Not confirmed"), 723 ("ACCEPTED", "Attending"), 724 ("TENTATIVE", "Tentatively attending"), 725 ("DECLINED", "Not attending"), 726 ("DELEGATED", "Delegated"), 727 ] 728 729 def show_object_on_page(self, uid, obj, error=None): 730 731 """ 732 Show the calendar object with the given 'uid' and representation 'obj' 733 on the current page. If 'error' is given, show a suitable message. 734 """ 735 736 page = self.page 737 page.form(method="POST") 738 739 # Obtain the user's timezone. 740 741 tzid = self.get_tzid() 742 743 # Provide controls to change the displayed object. 744 745 args = self.env.get_args() 746 dtend_control = args.get("dtend-control", [None])[0] 747 dttimes_control = args.get("dttimes-control", [None])[0] 748 with_time = dttimes_control == "enable" 749 750 t = self.handle_date_controls("dtstart", with_time) 751 if t: 752 dtstart, dtstart_attr = t 753 else: 754 dtstart, dtstart_attr = obj.get_datetime_item("DTSTART") 755 756 if dtend_control == "enable": 757 t = self.handle_date_controls("dtend", with_time) 758 if t: 759 dtend, dtend_attr = t 760 else: 761 dtend, dtend_attr = None, {} 762 elif dtend_control == "disable": 763 dtend, dtend_attr = None, {} 764 else: 765 dtend, dtend_attr = obj.get_datetime_item("DTEND") 766 767 # Change end dates to refer to the actual dates, not the iCalendar 768 # "next day" dates. 769 770 if dtend and not isinstance(dtend, datetime): 771 dtend -= timedelta(1) 772 773 # Show the end datetime controls if already active or if an object needs 774 # them. 775 776 dtend_enabled = dtend_control == "enable" or isinstance(dtend, datetime) or dtstart != dtend 777 dttimes_enabled = dttimes_control == "enable" or isinstance(dtstart, datetime) or isinstance(dtend, datetime) 778 779 if dtend_enabled: 780 page.input(name="dtend-control", type="radio", value="enable", id="dtend-enable", checked="checked") 781 page.input(name="dtend-control", type="radio", value="disable", id="dtend-disable") 782 else: 783 page.input(name="dtend-control", type="radio", value="enable", id="dtend-enable") 784 page.input(name="dtend-control", type="radio", value="disable", id="dtend-disable", checked="checked") 785 786 if dttimes_enabled: 787 page.input(name="dttimes-control", type="radio", value="enable", id="dttimes-enable", checked="checked") 788 page.input(name="dttimes-control", type="radio", value="disable", id="dttimes-disable") 789 else: 790 page.input(name="dttimes-control", type="radio", value="enable", id="dttimes-enable") 791 page.input(name="dttimes-control", type="radio", value="disable", id="dttimes-disable", checked="checked") 792 793 # Provide a summary of the object. 794 795 page.table(class_="object", cellspacing=5, cellpadding=5) 796 page.thead() 797 page.tr() 798 page.th("Event", class_="mainheading", colspan=2) 799 page.tr.close() 800 page.thead.close() 801 page.tbody() 802 803 is_organiser = obj.get_value("ORGANIZER") == self.user 804 805 for name, label in self.property_items: 806 page.tr() 807 808 # Handle datetimes specially. 809 810 if name in ["DTSTART", "DTEND"]: 811 field = name.lower() 812 813 page.th(label, class_="objectheading %s%s" % (field, error and field in error and " error" or "")) 814 815 # Obtain the datetime. 816 817 if name == "DTSTART": 818 dt, attr, event_tzid = dtstart, dtstart_attr, dtstart_attr.get("TZID", tzid) 819 820 # Where no end datetime exists, use the start datetime as the 821 # basis of any potential datetime specified if dt-control is 822 # set. 823 824 else: 825 dt, attr, event_tzid = dtend or dtstart, dtend_attr or dtstart_attr, (dtend_attr or dtstart_attr).get("TZID", tzid) 826 827 # Show controls for editing as organiser. 828 829 if is_organiser: 830 value = format_datetime(dt) 831 832 page.td(class_="objectvalue %s" % field) 833 if name == "DTEND": 834 page.div(class_="dt disabled") 835 page.label("Specify end date", for_="dtend-enable", class_="enable") 836 page.div.close() 837 838 page.div(class_="dt enabled") 839 self._show_date_controls(field, value, attr, tzid) 840 if name == "DTSTART": 841 page.label("Specify times", for_="dttimes-enable", class_="time disabled enable") 842 page.label("Specify dates only", for_="dttimes-disable", class_="time enabled disable") 843 elif name == "DTEND": 844 page.label("End on same day", for_="dtend-disable", class_="disable") 845 page.div.close() 846 847 page.td.close() 848 849 # Show a label as attendee. 850 851 else: 852 page.td(self.format_datetime(dt, "full")) 853 854 page.tr.close() 855 856 # Handle the summary specially. 857 858 elif name == "SUMMARY": 859 value = args.get("summary", [obj.get_value(name)])[0] 860 861 page.th(label, class_="objectheading") 862 page.td() 863 if is_organiser: 864 page.input(name="summary", type="text", value=value, size=80) 865 else: 866 page.add(value) 867 page.td.close() 868 page.tr.close() 869 870 # Handle potentially many values. 871 872 else: 873 items = obj.get_items(name) 874 if not items: 875 continue 876 877 page.th(label, class_="objectheading", rowspan=len(items)) 878 879 first = True 880 881 for i, (value, attr) in enumerate(items): 882 if not first: 883 page.tr() 884 else: 885 first = False 886 887 if name in ("ATTENDEE", "ORGANIZER"): 888 page.td(class_="objectattribute") 889 page.add(value) 890 page.add(" ") 891 892 partstat = attr.get("PARTSTAT") 893 if value == self.user and (not is_organiser or name == "ORGANIZER"): 894 self._show_menu("partstat", partstat, self.partstat_items) 895 else: 896 page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat") 897 898 if is_organiser and name == "ATTENDEE": 899 page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove") 900 page.label("Remove", for_="remove-%d" % i, class_="remove") 901 page.label("Uninvited", for_="remove-%d" % i, class_="removed") 902 903 else: 904 page.td(class_="objectattribute") 905 page.add(value) 906 907 page.td.close() 908 page.tr.close() 909 910 page.tbody.close() 911 page.table.close() 912 913 self.show_conflicting_events(uid, obj) 914 self.show_request_controls(obj) 915 916 page.form.close() 917 918 def show_conflicting_events(self, uid, obj): 919 920 """ 921 Show conflicting events for the object having the given 'uid' and 922 representation 'obj'. 923 """ 924 925 page = self.page 926 927 # Obtain the user's timezone. 928 929 tzid = self.get_tzid() 930 931 dtstart = format_datetime(obj.get_utc_datetime("DTSTART")) 932 dtend = format_datetime(obj.get_utc_datetime("DTEND")) 933 934 # Indicate whether there are conflicting events. 935 936 freebusy = self.store.get_freebusy(self.user) 937 938 if freebusy: 939 940 # Obtain any time zone details from the suggested event. 941 942 _dtstart, attr = obj.get_item("DTSTART") 943 tzid = attr.get("TZID", tzid) 944 945 # Show any conflicts. 946 947 conflicts = [t for t in have_conflict(freebusy, [(dtstart, dtend)], True) if t[2] != uid] 948 949 if conflicts: 950 page.p("This event conflicts with others:") 951 952 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 953 page.thead() 954 page.tr() 955 page.th("Event") 956 page.th("Start") 957 page.th("End") 958 page.tr.close() 959 page.thead.close() 960 page.tbody() 961 962 for t in conflicts: 963 start, end, found_uid = t[:3] 964 965 # Provide details of any conflicting event. 966 967 start = self.format_datetime(to_timezone(get_datetime(start), tzid), "long") 968 end = self.format_datetime(to_timezone(get_datetime(end), tzid), "long") 969 970 page.tr() 971 972 # Show the event summary for the conflicting event. 973 974 page.td() 975 976 found_obj = self._get_object(found_uid) 977 if found_obj: 978 page.a(found_obj.get_value("SUMMARY"), href=self.env.new_url(found_uid)) 979 else: 980 page.add("No details available") 981 982 page.td.close() 983 984 page.td(start) 985 page.td(end) 986 987 page.tr.close() 988 989 page.tbody.close() 990 page.table.close() 991 992 def show_requests_on_page(self): 993 994 "Show requests for the current user." 995 996 # NOTE: This list could be more informative, but it is envisaged that 997 # NOTE: the requests would be visited directly anyway. 998 999 requests = self._get_requests() 1000 1001 self.page.div(id="pending-requests") 1002 1003 if requests: 1004 self.page.p("Pending requests:") 1005 1006 self.page.ul() 1007 1008 for request in requests: 1009 obj = self._get_object(request) 1010 if obj: 1011 self.page.li() 1012 self.page.a(obj.get_value("SUMMARY"), href="#request-%s" % request) 1013 self.page.li.close() 1014 1015 self.page.ul.close() 1016 1017 else: 1018 self.page.p("There are no pending requests.") 1019 1020 self.page.div.close() 1021 1022 def show_participants_on_page(self): 1023 1024 "Show participants for scheduling purposes." 1025 1026 args = self.env.get_args() 1027 participants = args.get("participants", []) 1028 1029 try: 1030 for name, value in args.items(): 1031 if name.startswith("remove-participant-"): 1032 i = int(name[len("remove-participant-"):]) 1033 del participants[i] 1034 break 1035 except ValueError: 1036 pass 1037 1038 # Trim empty participants. 1039 1040 while participants and not participants[-1].strip(): 1041 participants.pop() 1042 1043 # Show any specified participants together with controls to remove and 1044 # add participants. 1045 1046 self.page.div(id="participants") 1047 1048 self.page.p("Participants for scheduling:") 1049 1050 for i, participant in enumerate(participants): 1051 self.page.p() 1052 self.page.input(name="participants", type="text", value=participant) 1053 self.page.input(name="remove-participant-%d" % i, type="submit", value="Remove") 1054 self.page.p.close() 1055 1056 self.page.p() 1057 self.page.input(name="participants", type="text") 1058 self.page.input(name="add-participant", type="submit", value="Add") 1059 self.page.p.close() 1060 1061 self.page.div.close() 1062 1063 return participants 1064 1065 # Full page output methods. 1066 1067 def show_object(self, path_info): 1068 1069 "Show an object request using the given 'path_info' for the current user." 1070 1071 uid = self._get_uid(path_info) 1072 obj = self._get_object(uid) 1073 1074 if not obj: 1075 return False 1076 1077 error = self.handle_request(uid, obj) 1078 1079 if not error: 1080 return True 1081 1082 self.new_page(title="Event") 1083 self.show_object_on_page(uid, obj, error) 1084 1085 return True 1086 1087 def show_calendar(self): 1088 1089 "Show the calendar for the current user." 1090 1091 handled = self.handle_newevent() 1092 1093 self.new_page(title="Calendar") 1094 page = self.page 1095 1096 # Form controls are used in various places on the calendar page. 1097 1098 page.form(method="POST") 1099 1100 self.show_requests_on_page() 1101 participants = self.show_participants_on_page() 1102 1103 # Show a button for scheduling a new event. 1104 1105 page.p(class_="controls") 1106 page.input(name="newevent", type="submit", value="New event", id="newevent") 1107 page.input(name="reset", type="submit", value="Clear selections", id="reset") 1108 page.p.close() 1109 1110 # Show controls for hiding empty days and busy slots. 1111 # The positioning of the control, paragraph and table are important here. 1112 1113 page.input(name="showdays", type="checkbox", value="show", id="showdays", accesskey="D") 1114 page.input(name="hidebusy", type="checkbox", value="hide", id="hidebusy", accesskey="B") 1115 1116 page.p(class_="controls") 1117 page.label("Hide busy time periods", for_="hidebusy", class_="hidebusy enable") 1118 page.label("Show busy time periods", for_="hidebusy", class_="hidebusy disable") 1119 page.label("Show empty days", for_="showdays", class_="showdays disable") 1120 page.label("Hide empty days", for_="showdays", class_="showdays enable") 1121 page.p.close() 1122 1123 freebusy = self.store.get_freebusy(self.user) 1124 1125 if not freebusy: 1126 page.p("No events scheduled.") 1127 return 1128 1129 # Obtain the user's timezone. 1130 1131 tzid = self.get_tzid() 1132 1133 # Day view: start at the earliest known day and produce days until the 1134 # latest known day, perhaps with expandable sections of empty days. 1135 1136 # Month view: start at the earliest known month and produce months until 1137 # the latest known month, perhaps with expandable sections of empty 1138 # months. 1139 1140 # Details of users to invite to new events could be superimposed on the 1141 # calendar. 1142 1143 # Requests are listed and linked to their tentative positions in the 1144 # calendar. Other participants are also shown. 1145 1146 request_summary = self._get_request_summary() 1147 1148 period_groups = [request_summary, freebusy] 1149 period_group_types = ["request", "freebusy"] 1150 period_group_sources = ["Pending requests", "Your schedule"] 1151 1152 for i, participant in enumerate(participants): 1153 period_groups.append(self.store.get_freebusy_for_other(self.user, get_uri(participant))) 1154 period_group_types.append("freebusy-part%d" % i) 1155 period_group_sources.append(participant) 1156 1157 groups = [] 1158 group_columns = [] 1159 group_types = period_group_types 1160 group_sources = period_group_sources 1161 all_points = set() 1162 1163 # Obtain time point information for each group of periods. 1164 1165 for periods in period_groups: 1166 periods = convert_periods(periods, tzid) 1167 1168 # Get the time scale with start and end points. 1169 1170 scale = get_scale(periods) 1171 1172 # Get the time slots for the periods. 1173 1174 slots = get_slots(scale) 1175 1176 # Add start of day time points for multi-day periods. 1177 1178 add_day_start_points(slots, tzid) 1179 1180 # Record the slots and all time points employed. 1181 1182 groups.append(slots) 1183 all_points.update([point for point, active in slots]) 1184 1185 # Partition the groups into days. 1186 1187 days = {} 1188 partitioned_groups = [] 1189 partitioned_group_types = [] 1190 partitioned_group_sources = [] 1191 1192 for slots, group_type, group_source in zip(groups, group_types, group_sources): 1193 1194 # Propagate time points to all groups of time slots. 1195 1196 add_slots(slots, all_points) 1197 1198 # Count the number of columns employed by the group. 1199 1200 columns = 0 1201 1202 # Partition the time slots by day. 1203 1204 partitioned = {} 1205 1206 for day, day_slots in partition_by_day(slots).items(): 1207 intervals = [] 1208 last = None 1209 1210 for point, active in day_slots: 1211 columns = max(columns, len(active)) 1212 if last: 1213 intervals.append((last, point)) 1214 last = point 1215 1216 if last: 1217 intervals.append((last, None)) 1218 1219 if not days.has_key(day): 1220 days[day] = set() 1221 1222 # Convert each partition to a mapping from points to active 1223 # periods. 1224 1225 partitioned[day] = dict(day_slots) 1226 1227 # Record the divisions or intervals within each day. 1228 1229 days[day].update(intervals) 1230 1231 if group_type != "request" or columns: 1232 group_columns.append(columns) 1233 partitioned_groups.append(partitioned) 1234 partitioned_group_types.append(group_type) 1235 partitioned_group_sources.append(group_source) 1236 1237 # Add empty days. 1238 1239 add_empty_days(days, tzid) 1240 1241 # Show the controls permitting day selection. 1242 1243 self.show_calendar_day_controls(days) 1244 1245 # Show the calendar itself. 1246 1247 page.table(cellspacing=5, cellpadding=5, class_="calendar") 1248 self.show_calendar_participant_headings(partitioned_group_types, partitioned_group_sources, group_columns) 1249 self.show_calendar_days(days, partitioned_groups, partitioned_group_types, group_columns) 1250 page.table.close() 1251 1252 # End the form region. 1253 1254 page.form.close() 1255 1256 # More page fragment methods. 1257 1258 def show_calendar_day_controls(self, days): 1259 1260 "Show controls for the given 'days' in the calendar." 1261 1262 page = self.page 1263 slots = self.env.get_args().get("slot", []) 1264 1265 for day in days: 1266 value, identifier = self._day_value_and_identifier(day) 1267 self._slot_selector(value, identifier, slots) 1268 1269 # Generate a dynamic stylesheet to allow day selections to colour 1270 # specific days. 1271 # NOTE: The style details need to be coordinated with the static 1272 # NOTE: stylesheet. 1273 1274 page.style(type="text/css") 1275 1276 for day in days: 1277 daystr = format_datetime(day) 1278 page.add("""\ 1279 input.newevent.selector#day-%s-:checked ~ table label.day.day-%s, 1280 input.newevent.selector#day-%s-:checked ~ table label.timepoint.day-%s { 1281 background-color: #5f4; 1282 text-decoration: underline; 1283 } 1284 """ % (daystr, daystr, daystr, daystr)) 1285 1286 page.style.close() 1287 1288 def show_calendar_participant_headings(self, group_types, group_sources, group_columns): 1289 1290 """ 1291 Show headings for the participants and other scheduling contributors, 1292 defined by 'group_types', 'group_sources' and 'group_columns'. 1293 """ 1294 1295 page = self.page 1296 1297 page.colgroup(span=1, id="columns-timeslot") 1298 1299 for group_type, columns in zip(group_types, group_columns): 1300 page.colgroup(span=max(columns, 1), id="columns-%s" % group_type) 1301 1302 page.thead() 1303 page.tr() 1304 page.th("", class_="emptyheading") 1305 1306 for group_type, source, columns in zip(group_types, group_sources, group_columns): 1307 page.th(source, 1308 class_=(group_type == "request" and "requestheading" or "participantheading"), 1309 colspan=max(columns, 1)) 1310 1311 page.tr.close() 1312 page.thead.close() 1313 1314 def show_calendar_days(self, days, partitioned_groups, partitioned_group_types, group_columns): 1315 1316 """ 1317 Show calendar days, defined by a collection of 'days', the contributing 1318 period information as 'partitioned_groups' (partitioned by day), the 1319 'partitioned_group_types' indicating the kind of contribution involved, 1320 and the 'group_columns' defining the number of columns in each group. 1321 """ 1322 1323 page = self.page 1324 1325 # Determine the number of columns required. Where participants provide 1326 # no columns for events, one still needs to be provided for the 1327 # participant itself. 1328 1329 all_columns = sum([max(columns, 1) for columns in group_columns]) 1330 1331 # Determine the days providing time slots. 1332 1333 all_days = days.items() 1334 all_days.sort() 1335 1336 # Produce a heading and time points for each day. 1337 1338 for day, intervals in all_days: 1339 groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups] 1340 is_empty = True 1341 1342 for slots in groups_for_day: 1343 if not slots: 1344 continue 1345 1346 for active in slots.values(): 1347 if active: 1348 is_empty = False 1349 break 1350 1351 page.thead(class_="separator%s" % (is_empty and " empty" or "")) 1352 page.tr() 1353 page.th(class_="dayheading container", colspan=all_columns+1) 1354 self._day_heading(day) 1355 page.th.close() 1356 page.tr.close() 1357 page.thead.close() 1358 1359 page.tbody(class_="points%s" % (is_empty and " empty" or "")) 1360 self.show_calendar_points(intervals, groups_for_day, partitioned_group_types, group_columns) 1361 page.tbody.close() 1362 1363 def show_calendar_points(self, intervals, groups, group_types, group_columns): 1364 1365 """ 1366 Show the time 'intervals' along with period information from the given 1367 'groups', having the indicated 'group_types', each with the number of 1368 columns given by 'group_columns'. 1369 """ 1370 1371 page = self.page 1372 1373 # Obtain the user's timezone. 1374 1375 tzid = self.get_tzid() 1376 1377 # Produce a row for each interval. 1378 1379 intervals = list(intervals) 1380 intervals.sort() 1381 1382 for point, endpoint in intervals: 1383 continuation = point == get_start_of_day(point, tzid) 1384 1385 # Some rows contain no period details and are marked as such. 1386 1387 have_active = reduce(lambda x, y: x or y, [slots and slots.get(point) for slots in groups], None) 1388 1389 css = " ".join( 1390 ["slot"] + 1391 (have_active and ["busy"] or ["empty"]) + 1392 (continuation and ["daystart"] or []) 1393 ) 1394 1395 page.tr(class_=css) 1396 page.th(class_="timeslot") 1397 self._time_point(point, endpoint) 1398 page.th.close() 1399 1400 # Obtain slots for the time point from each group. 1401 1402 for columns, slots, group_type in zip(group_columns, groups, group_types): 1403 active = slots and slots.get(point) 1404 1405 # Where no periods exist for the given time interval, generate 1406 # an empty cell. Where a participant provides no periods at all, 1407 # the colspan is adjusted to be 1, not 0. 1408 1409 if not active: 1410 page.td(class_="empty container", colspan=max(columns, 1)) 1411 self._empty_slot(point, endpoint) 1412 page.td.close() 1413 continue 1414 1415 slots = slots.items() 1416 slots.sort() 1417 spans = get_spans(slots) 1418 1419 empty = 0 1420 1421 # Show a column for each active period. 1422 1423 for t in active: 1424 if t and len(t) >= 2: 1425 1426 # Flush empty slots preceding this one. 1427 1428 if empty: 1429 page.td(class_="empty container", colspan=empty) 1430 self._empty_slot(point, endpoint) 1431 page.td.close() 1432 empty = 0 1433 1434 start, end, uid, key = get_freebusy_details(t) 1435 span = spans[key] 1436 1437 # Produce a table cell only at the start of the period 1438 # or when continued at the start of a day. 1439 1440 if point == start or continuation: 1441 1442 obj = self._get_object(uid) 1443 1444 has_continued = continuation and point != start 1445 will_continue = not ends_on_same_day(point, end, tzid) 1446 is_organiser = obj and obj.get_value("ORGANIZER") == self.user 1447 1448 css = " ".join( 1449 ["event"] + 1450 (has_continued and ["continued"] or []) + 1451 (will_continue and ["continues"] or []) + 1452 (is_organiser and ["organising"] or ["attending"]) 1453 ) 1454 1455 # Only anchor the first cell of events. 1456 1457 if point == start: 1458 page.td(class_=css, rowspan=span, id="%s-%s" % (group_type, uid)) 1459 else: 1460 page.td(class_=css, rowspan=span) 1461 1462 if not obj: 1463 page.span("(Participant is busy)") 1464 else: 1465 summary = obj.get_value("SUMMARY") 1466 1467 # Only link to events if they are not being 1468 # updated by requests. 1469 1470 if uid in self._get_requests() and group_type != "request": 1471 page.span(summary) 1472 else: 1473 href = "%s/%s" % (self.env.get_url().rstrip("/"), uid) 1474 page.a(summary, href=href) 1475 1476 page.td.close() 1477 else: 1478 empty += 1 1479 1480 # Pad with empty columns. 1481 1482 empty = columns - len(active) 1483 1484 if empty: 1485 page.td(class_="empty container", colspan=empty) 1486 self._empty_slot(point, endpoint) 1487 page.td.close() 1488 1489 page.tr.close() 1490 1491 def _day_heading(self, day): 1492 1493 """ 1494 Generate a heading for 'day' of the following form: 1495 1496 <label class="day day-20150203" for="day-20150203">Tuesday, 3 February 2015</label> 1497 """ 1498 1499 page = self.page 1500 daystr = format_datetime(day) 1501 value, identifier = self._day_value_and_identifier(day) 1502 page.label(self.format_date(day, "full"), class_="day day-%s" % daystr, for_=identifier) 1503 1504 def _time_point(self, point, endpoint): 1505 1506 """ 1507 Generate headings for the 'point' to 'endpoint' period of the following 1508 form: 1509 1510 <label class="timepoint day-20150203" for="slot-20150203T090000-20150203T100000">09:00:00 CET</label> 1511 <span class="endpoint">10:00:00 CET</span> 1512 """ 1513 1514 page = self.page 1515 tzid = self.get_tzid() 1516 daystr = format_datetime(point.date()) 1517 value, identifier = self._slot_value_and_identifier(point, endpoint) 1518 slots = self.env.get_args().get("slot", []) 1519 self._slot_selector(value, identifier, slots) 1520 page.label(self.format_time(point, "long"), class_="timepoint day-%s" % daystr, for_=identifier) 1521 page.span(self.format_time(endpoint or get_end_of_day(point, tzid), "long"), class_="endpoint") 1522 1523 def _slot_selector(self, value, identifier, slots): 1524 reset = self.env.get_args().has_key("reset") 1525 page = self.page 1526 if not reset and value in slots: 1527 page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector", checked="checked") 1528 else: 1529 page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector") 1530 1531 def _empty_slot(self, point, endpoint): 1532 page = self.page 1533 value, identifier = self._slot_value_and_identifier(point, endpoint) 1534 page.label("Select/deselect period", class_="newevent popup", for_=identifier) 1535 1536 def _day_value_and_identifier(self, day): 1537 value = "%s-" % format_datetime(day) 1538 identifier = "day-%s" % value 1539 return value, identifier 1540 1541 def _slot_value_and_identifier(self, point, endpoint): 1542 value = "%s-%s" % (format_datetime(point), endpoint and format_datetime(endpoint) or "") 1543 identifier = "slot-%s" % value 1544 return value, identifier 1545 1546 def _show_menu(self, name, default, items): 1547 page = self.page 1548 values = self.env.get_args().get(name, [default]) 1549 page.select(name=name) 1550 for v, label in items: 1551 if v in values: 1552 page.option(label, value=v, selected="selected") 1553 else: 1554 page.option(label, value=v) 1555 page.select.close() 1556 1557 def _show_date_controls(self, name, default, attr, tzid): 1558 1559 """ 1560 Show date controls for a field with the given 'name' and 'default' value 1561 and 'attr', with the given 'tzid' being used if no other time regime 1562 information is provided. 1563 """ 1564 1565 page = self.page 1566 args = self.env.get_args() 1567 1568 event_tzid = attr.get("TZID", tzid) 1569 dt = get_datetime(default, attr) 1570 1571 # Show dates for up to one week around the current date. 1572 1573 base = get_date(dt) 1574 items = [] 1575 for i in range(-7, 8): 1576 d = base + timedelta(i) 1577 items.append((format_datetime(d), self.format_date(d, "full"))) 1578 1579 self._show_menu("%s-date" % name, format_datetime(base), items) 1580 1581 # Show time details. 1582 1583 dt_time = isinstance(dt, datetime) and dt or None 1584 hour = args.get("%s-hour" % name, "%02d" % (dt_time and dt_time.hour or 0)) 1585 minute = args.get("%s-minute" % name, "%02d" % (dt_time and dt_time.minute or 0)) 1586 second = args.get("%s-second" % name, "%02d" % (dt_time and dt_time.second or 0)) 1587 1588 page.span(class_="time enabled") 1589 page.input(name="%s-hour" % name, type="text", value=hour, maxlength=2, size=2) 1590 page.add(":") 1591 page.input(name="%s-minute" % name, type="text", value=minute, maxlength=2, size=2) 1592 page.add(":") 1593 page.input(name="%s-second" % name, type="text", value=second, maxlength=2, size=2) 1594 page.add(" ") 1595 self._show_menu("%s-tzid" % name, event_tzid, 1596 [(event_tzid, event_tzid)] + ( 1597 event_tzid != tzid and [(tzid, tzid)] or [] 1598 )) 1599 page.span.close() 1600 1601 # Incoming HTTP request direction. 1602 1603 def select_action(self): 1604 1605 "Select the desired action and show the result." 1606 1607 path_info = self.env.get_path_info().strip("/") 1608 1609 if not path_info: 1610 self.show_calendar() 1611 elif self.show_object(path_info): 1612 pass 1613 else: 1614 self.no_page() 1615 1616 def __call__(self): 1617 1618 "Interpret a request and show an appropriate response." 1619 1620 if not self.user: 1621 self.no_user() 1622 else: 1623 self.select_action() 1624 1625 # Write the headers and actual content. 1626 1627 print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding 1628 print >>self.out 1629 self.out.write(unicode(self.page).encode(self.encoding)) 1630 1631 if __name__ == "__main__": 1632 Manager()() 1633 1634 # vim: tabstop=4 expandtab shiftwidth=4