1 #!/usr/bin/env python 2 3 """ 4 A Web interface to a calendar event. 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 from imiptools.data import get_uri, uri_dict, uri_items, uri_values 23 from imiptools.dates import to_timezone 24 from imiptools.mail import Messenger 25 from imiptools.period import have_conflict 26 from imipweb.data import EventPeriod, event_period_from_period, FormPeriod, PeriodError 27 from imipweb.client import ManagerClient 28 from imipweb.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject 29 30 class EventPageFragment(ResourceClientForObject, DateTimeFormUtilities, FormUtilities): 31 32 "A resource presenting the details of an event." 33 34 def __init__(self, resource=None): 35 ResourceClientForObject.__init__(self, resource) 36 37 # Various property values and labels. 38 39 property_items = [ 40 ("SUMMARY", "Summary"), 41 ("DTSTART", "Start"), 42 ("DTEND", "End"), 43 ("ORGANIZER", "Organiser"), 44 ("ATTENDEE", "Attendee"), 45 ] 46 47 partstat_items = [ 48 ("NEEDS-ACTION", "Not confirmed"), 49 ("ACCEPTED", "Attending"), 50 ("TENTATIVE", "Tentatively attending"), 51 ("DECLINED", "Not attending"), 52 ("DELEGATED", "Delegated"), 53 (None, "Not indicated"), 54 ] 55 56 def is_organiser(self): 57 return get_uri(self.obj.get_value("ORGANIZER")) == self.user 58 59 def can_remove_attendee(self, attendee): 60 61 """ 62 Return whether 'attendee' can be removed from the current object without 63 notification. 64 """ 65 66 return self.can_edit_attendee(attendee) or attendee == self.user 67 68 def can_edit_attendee(self, attendee): 69 70 "Return whether 'attendee' can be edited by an organiser." 71 72 return self.attendee_is_new(attendee) or not self.obj.is_shared() 73 74 def attendee_is_new(self, attendee): 75 76 "Return whether 'attendee' is new to the current object." 77 78 return attendee not in self.get_stored_attendees() 79 80 # Access to stored object information. 81 82 def get_stored_attendees(self): 83 return uri_values(self.obj.get_values("ATTENDEE") or []) 84 85 def get_stored_main_period(self): 86 87 "Return the main event period for the current object." 88 89 (dtstart, dtstart_attr), (dtend, dtend_attr) = self.obj.get_main_period_items(self.get_tzid()) 90 return EventPeriod(dtstart, dtend, self.get_tzid(), None, dtstart_attr, dtend_attr) 91 92 def get_stored_recurrences(self): 93 94 "Return recurrences computed using the current object." 95 96 recurrences = [] 97 for period in self.get_periods(self.obj): 98 if period.origin != "DTSTART": 99 recurrences.append(period) 100 return recurrences 101 102 # Access to current object information. 103 104 def get_current_main_period(self): 105 return self.get_stored_main_period() 106 107 def get_current_recurrences(self): 108 return self.get_stored_recurrences() 109 110 def get_current_attendees(self): 111 return self.get_stored_attendees() 112 113 # Page fragment methods. 114 115 def show_request_controls(self): 116 117 "Show form controls for a request." 118 119 page = self.page 120 args = self.env.get_args() 121 122 attendees = self.get_current_attendees() 123 is_attendee = self.user in attendees 124 is_request = self._have_request(self.uid, self.recurrenceid) 125 126 # Show appropriate options depending on the role of the user. 127 128 if is_attendee and not self.is_organiser(): 129 page.p("An action is required for this request:") 130 131 page.p() 132 self.control("reply", "submit", "Send reply") 133 page.add(" ") 134 self.control("discard", "submit", "Discard event") 135 page.add(" ") 136 self.control("ignore", "submit", "Do nothing for now") 137 page.p.close() 138 139 if self.is_organiser(): 140 page.p("As organiser, you can perform the following:") 141 142 page.p() 143 self.control("create", "submit", not self.obj.is_shared() and "Create event" or "Update event") 144 page.add(" ") 145 146 if self.obj.is_shared() and not is_request: 147 self.control("cancel", "submit", "Cancel event") 148 else: 149 self.control("discard", "submit", "Discard event") 150 151 page.add(" ") 152 self.control("save", "submit", "Save without sending") 153 page.p.close() 154 155 def show_object_on_page(self, errors=None): 156 157 """ 158 Show the calendar object on the current page. If 'errors' is given, show 159 a suitable message for the different errors provided. 160 """ 161 162 page = self.page 163 page.form(method="POST") 164 165 # Add a hidden control to help determine whether editing has already begun. 166 167 self.control("editing", "hidden", "true") 168 169 args = self.env.get_args() 170 171 # Obtain basic event information, generating any necessary editing controls. 172 173 attendees = self.update_current_attendees() 174 period = self.get_current_main_period() 175 self.show_object_datetime_controls(period) 176 177 # Obtain any separate recurrences for this event. 178 179 recurrenceids = self._get_active_recurrences(self.uid) 180 replaced = not self.recurrenceid and period.is_replaced(recurrenceids) 181 182 # Provide a summary of the object. 183 184 page.table(class_="object", cellspacing=5, cellpadding=5) 185 page.thead() 186 page.tr() 187 page.th("Event", class_="mainheading", colspan=2) 188 page.tr.close() 189 page.thead.close() 190 page.tbody() 191 192 for name, label in self.property_items: 193 field = name.lower() 194 195 items = uri_items(self.obj.get_items(name) or []) 196 rowspan = len(items) 197 198 if name == "ATTENDEE": 199 rowspan = len(attendees) + 1 # for the add button 200 elif not items: 201 continue 202 203 page.tr() 204 page.th(label, class_="objectheading %s%s" % (field, errors and field in errors and " error" or ""), rowspan=rowspan) 205 206 # Handle datetimes specially. 207 208 if name in ["DTSTART", "DTEND"]: 209 if not replaced: 210 211 # Obtain the datetime. 212 213 is_start = name == "DTSTART" 214 215 # Where no end datetime exists, use the start datetime as the 216 # basis of any potential datetime specified if dt-control is 217 # set. 218 219 self.show_datetime_controls(is_start and period.get_form_start() or period.get_form_end(), is_start) 220 221 elif name == "DTSTART": 222 page.td(class_="objectvalue %s replaced" % field, rowspan=2) 223 page.a("First occurrence replaced by a separate event", href=self.link_to(self.uid, replaced)) 224 page.td.close() 225 226 page.tr.close() 227 228 # Handle the summary specially. 229 230 elif name == "SUMMARY": 231 value = args.get("summary", [self.obj.get_value(name)])[0] 232 233 page.td(class_="objectvalue summary") 234 if self.is_organiser(): 235 self.control("summary", "text", value, size=80) 236 else: 237 page.add(value) 238 page.td.close() 239 page.tr.close() 240 241 # Handle attendees specially. 242 243 elif name == "ATTENDEE": 244 attendee_map = dict(items) 245 first = True 246 247 for i, value in enumerate(attendees): 248 if not first: 249 page.tr() 250 else: 251 first = False 252 253 # Obtain details of attendees to supply attributes. 254 255 self.show_attendee(i, value, attendee_map.get(value)) 256 page.tr.close() 257 258 # Allow more attendees to be specified. 259 260 if self.is_organiser(): 261 if not first: 262 page.tr() 263 264 page.td() 265 self.control("add", "submit", "add", id="add", class_="add") 266 page.label("Add attendee", for_="add", class_="add") 267 page.td.close() 268 page.tr.close() 269 270 # Handle potentially many values of other kinds. 271 272 else: 273 first = True 274 275 for i, (value, attr) in enumerate(items): 276 if not first: 277 page.tr() 278 else: 279 first = False 280 281 page.td(class_="objectvalue %s" % field) 282 page.add(value) 283 page.td.close() 284 page.tr.close() 285 286 page.tbody.close() 287 page.table.close() 288 289 self.show_recurrences(errors) 290 self.show_conflicting_events() 291 self.show_request_controls() 292 293 page.form.close() 294 295 def show_attendee(self, i, attendee, attendee_attr): 296 297 """ 298 For the current object, show the attendee in position 'i' with the given 299 'attendee' value, having 'attendee_attr' as any stored attributes. 300 """ 301 302 page = self.page 303 args = self.env.get_args() 304 305 partstat = attendee_attr and attendee_attr.get("PARTSTAT") 306 307 page.td(class_="objectvalue") 308 309 # Show a form control as organiser for new attendees. 310 311 if self.is_organiser() and self.can_edit_attendee(attendee): 312 self.control("attendee", "value", attendee, size="40") 313 else: 314 self.control("attendee", "hidden", attendee) 315 page.add(attendee) 316 page.add(" ") 317 318 # Show participation status, editable for the current user. 319 320 if attendee == self.user: 321 self.menu("partstat", partstat, self.partstat_items, "partstat") 322 323 # Allow the participation indicator to act as a submit 324 # button in order to refresh the page and show a control for 325 # the current user, if indicated. 326 327 elif self.is_organiser() and self.attendee_is_new(attendee): 328 self.control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh") 329 page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat") 330 331 # Otherwise, just show a label with the participation status. 332 333 else: 334 page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat") 335 336 # Permit organisers to remove attendees. 337 338 if self.is_organiser(): 339 340 # Permit the removal of newly-added attendees. 341 342 remove_type = self.can_remove_attendee(attendee) and "submit" or "checkbox" 343 self.control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove") 344 345 page.label("Remove", for_="remove-%d" % i, class_="remove") 346 page.label(for_="remove-%d" % i, class_="removed") 347 page.add("(Uninvited)") 348 page.span("Re-invite", class_="action") 349 page.label.close() 350 351 page.td.close() 352 353 def show_recurrences(self, errors=None): 354 355 """ 356 Show recurrences for the current object. If 'errors' is given, show a 357 suitable message for the different errors provided. 358 """ 359 360 page = self.page 361 362 # Obtain any parent object if this object is a specific recurrence. 363 364 if self.recurrenceid: 365 parent = self.get_stored_object(self.uid, None) 366 if not parent: 367 return 368 369 page.p() 370 page.a("This event modifies a recurring event.", href=self.link_to(self.uid)) 371 page.p.close() 372 373 # Obtain the periods associated with the event. 374 # NOTE: Add a control to add recurrences here. 375 376 recurrences = self.get_current_recurrences() 377 378 if len(recurrences) < 1: 379 return 380 381 recurrenceids = self._get_recurrences(self.uid) 382 383 page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) 384 385 # Show each recurrence in a separate table if editable. 386 387 if self.is_organiser() and recurrences: 388 389 for index, period in enumerate(recurrences): 390 self.show_recurrence(index, period, self.recurrenceid, recurrenceids, errors) 391 392 # Otherwise, use a compact single table. 393 394 else: 395 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 396 page.caption("Occurrences") 397 page.thead() 398 page.tr() 399 page.th("Start", class_="objectheading start") 400 page.th("End", class_="objectheading end") 401 page.tr.close() 402 page.thead.close() 403 page.tbody() 404 405 for index, period in enumerate(recurrences): 406 page.tr() 407 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, True) 408 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, False) 409 page.tr.close() 410 411 page.tbody.close() 412 page.table.close() 413 414 def show_recurrence(self, index, period, recurrenceid, recurrenceids, errors=None): 415 416 """ 417 Show recurrence controls for a recurrence provided by the current object 418 with the given 'index' position in the list of periods, the given 419 'period' details, where a 'recurrenceid' indicates any specific 420 recurrence, and where 'recurrenceids' indicates all known additional 421 recurrences for the object. 422 423 If 'errors' is given, show a suitable message for the different errors 424 provided. 425 """ 426 427 page = self.page 428 args = self.env.get_args() 429 430 p = event_period_from_period(period) 431 replaced = not recurrenceid and p.is_replaced(recurrenceids) 432 433 # Isolate the controls from neighbouring tables. 434 435 page.div() 436 437 self.show_object_datetime_controls(period, index) 438 439 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 440 page.caption(period.origin == "RRULE" and "Occurrence from rule" or "Occurrence") 441 page.tbody() 442 443 page.tr() 444 error = errors and ("dtstart", index) in errors and " error" or "" 445 page.th("Start", class_="objectheading start%s" % error) 446 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, True) 447 page.tr.close() 448 page.tr() 449 error = errors and ("dtend", index) in errors and " error" or "" 450 page.th("End", class_="objectheading end%s" % error) 451 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, False) 452 page.tr.close() 453 454 # Permit the removal of recurrences. 455 456 if not replaced: 457 page.tr() 458 page.th("") 459 page.td() 460 461 remove_type = not self.obj.is_shared() or not period.origin and "submit" or "checkbox" 462 self.control("recur-remove", remove_type, str(index), 463 str(index) in args.get("recur-remove", []), 464 id="recur-remove-%d" % index, class_="remove") 465 466 page.label("Remove", for_="recur-remove-%d" % index, class_="remove") 467 page.label(for_="recur-remove-%d" % index, class_="removed") 468 page.add("(Removed)") 469 page.span("Re-add", class_="action") 470 page.label.close() 471 472 page.td.close() 473 page.tr.close() 474 475 page.tbody.close() 476 page.table.close() 477 478 page.div.close() 479 480 def show_conflicting_events(self): 481 482 "Show conflicting events for the current object." 483 484 page = self.page 485 recurrenceids = self._get_active_recurrences(self.uid) 486 487 # Obtain the user's timezone. 488 489 tzid = self.get_tzid() 490 periods = self.get_periods(self.obj) 491 492 # Indicate whether there are conflicting events. 493 494 conflicts = [] 495 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 496 497 for participant in self.get_current_attendees(): 498 if participant == self.user: 499 freebusy = self.store.get_freebusy(participant) 500 else: 501 freebusy = self.store.get_freebusy_for_other(self.user, participant) 502 503 if not freebusy: 504 continue 505 506 # Obtain any time zone details from the suggested event. 507 508 _dtstart, attr = self.obj.get_item("DTSTART") 509 tzid = attr.get("TZID", tzid) 510 511 # Show any conflicts with periods of actual attendance. 512 513 participant_attr = attendee_map.get(participant) 514 partstat = participant_attr and participant_attr.get("PARTSTAT") 515 recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid) 516 517 for p in have_conflict(freebusy, periods, True): 518 if not self.recurrenceid and p.is_replaced(recurrences): 519 continue 520 521 if ( # Unidentified or different event 522 (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and 523 # Different period or unclear participation with the same period 524 (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and 525 # Participant not limited to organising 526 p.transp != "ORG" 527 ): 528 529 conflicts.append(p) 530 531 conflicts.sort() 532 533 # Show any conflicts with periods of actual attendance. 534 535 if conflicts: 536 page.p("This event conflicts with others:") 537 538 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 539 page.thead() 540 page.tr() 541 page.th("Event") 542 page.th("Start") 543 page.th("End") 544 page.tr.close() 545 page.thead.close() 546 page.tbody() 547 548 for p in conflicts: 549 550 # Provide details of any conflicting event. 551 552 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 553 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 554 555 page.tr() 556 557 # Show the event summary for the conflicting event. 558 559 page.td() 560 if p.summary: 561 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 562 else: 563 page.add("(Unspecified event)") 564 page.td.close() 565 566 page.td(start) 567 page.td(end) 568 569 page.tr.close() 570 571 page.tbody.close() 572 page.table.close() 573 574 class EventPage(EventPageFragment): 575 576 "A request handler for the event page." 577 578 def __init__(self, resource=None, messenger=None): 579 ResourceClientForObject.__init__(self, resource) 580 self.messenger = messenger or Messenger() 581 582 # Request logic methods. 583 584 def is_initial_load(self): 585 586 "Return whether the event is being loaded and shown for the first time." 587 588 return not self.env.get_args().has_key("editing") 589 590 def handle_request(self): 591 592 """ 593 Handle actions involving the current object, returning an error if one 594 occurred, or None if the request was successfully handled. 595 """ 596 597 # Handle a submitted form. 598 599 args = self.env.get_args() 600 601 # Get the possible actions. 602 603 reply = args.has_key("reply") 604 discard = args.has_key("discard") 605 create = args.has_key("create") 606 cancel = args.has_key("cancel") 607 ignore = args.has_key("ignore") 608 save = args.has_key("save") 609 610 have_action = reply or discard or create or cancel or ignore or save 611 612 if not have_action: 613 return ["action"] 614 615 # If ignoring the object, return to the calendar. 616 617 if ignore: 618 self.redirect(self.env.get_path()) 619 return None 620 621 # Update the object. 622 623 single_user = False 624 625 if reply or create or cancel or save: 626 627 # Update principal event details if organiser. 628 629 if self.is_organiser(): 630 631 # Update time periods (main and recurring). 632 633 try: 634 period = self.handle_main_period() 635 except PeriodError, exc: 636 return exc.args 637 638 try: 639 periods = self.handle_recurrence_periods() 640 except PeriodError, exc: 641 return exc.args 642 643 # Set the periods in the object, first obtaining removed and 644 # modified period information. 645 646 to_unschedule = self.get_removed_periods() 647 648 self.obj.set_period(period) 649 self.obj.set_periods(periods) 650 651 # Update summary. 652 653 if args.has_key("summary"): 654 self.obj["SUMMARY"] = [(args["summary"][0], {})] 655 656 # Obtain any participants and those to be removed. 657 658 attendees = self.get_attendees_from_page() 659 removed = [attendees[int(i)] for i in args.get("remove", [])] 660 to_cancel = self.update_attendees(self.obj, attendees, removed) 661 single_user = not attendees or attendees == [self.user] 662 663 # Update attendee participation for the current user. 664 665 if args.has_key("partstat"): 666 self.update_participation(self.obj, args["partstat"][0]) 667 668 # Process any action. 669 670 invite = not save and create and not single_user 671 save = save or create and single_user 672 673 handled = True 674 675 if reply or invite or cancel: 676 677 client = ManagerClient(self.obj, self.user, self.messenger) 678 679 # Process the object and remove it from the list of requests. 680 681 if reply and client.process_received_request(): 682 self.remove_request(self.uid, self.recurrenceid) 683 684 elif self.is_organiser() and (invite or cancel): 685 686 # Invitation, uninvitation and unscheduling... 687 688 if client.process_created_request( 689 invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule): 690 691 self.remove_request(self.uid, self.recurrenceid) 692 693 # Save single user events. 694 695 elif save: 696 self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node()) 697 self.update_event_in_freebusy() 698 self.remove_request(self.uid, self.recurrenceid) 699 700 # Remove the request and the object. 701 702 elif discard: 703 self.remove_event_from_freebusy() 704 self.remove_event(self.uid, self.recurrenceid) 705 self.remove_request(self.uid, self.recurrenceid) 706 707 else: 708 handled = False 709 710 # Upon handling an action, redirect to the main page. 711 712 if handled: 713 self.redirect(self.env.get_path()) 714 715 return None 716 717 def handle_main_period(self): 718 719 "Return period details for the main start/end period in an event." 720 721 return self.get_main_period_from_page().as_event_period() 722 723 def handle_recurrence_periods(self): 724 725 "Return period details for the recurrences specified for an event." 726 727 return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] 728 729 # Access to form-originating object information. 730 731 def get_main_period_from_page(self): 732 733 "Return the main period defined in the event form." 734 735 args = self.env.get_args() 736 737 dtend_enabled = args.get("dtend-control", [None])[0] 738 dttimes_enabled = args.get("dttimes-control", [None])[0] 739 start = self.get_date_control_values("dtstart") 740 end = self.get_date_control_values("dtend") 741 742 return FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid()) 743 744 def get_recurrences_from_page(self): 745 746 "Return the recurrences defined in the event form." 747 748 args = self.env.get_args() 749 750 all_dtend_enabled = args.get("dtend-control-recur", []) 751 all_dttimes_enabled = args.get("dttimes-control-recur", []) 752 all_starts = self.get_date_control_values("dtstart-recur", multiple=True) 753 all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") 754 all_origins = args.get("recur-origin", []) 755 756 periods = [] 757 758 for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \ 759 enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)): 760 761 dtend_enabled = str(index) in all_dtend_enabled 762 dttimes_enabled = str(index) in all_dttimes_enabled 763 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin) 764 periods.append(period) 765 766 return periods 767 768 def get_removed_periods(self): 769 770 "Return a list of recurrence periods to remove upon updating an event." 771 772 to_unschedule = [] 773 args = self.env.get_args() 774 for i in args.get("recur-remove", []): 775 to_unschedule.append(periods[int(i)]) 776 return to_unschedule 777 778 def get_attendees_from_page(self): 779 780 """ 781 Return attendees from the request, normalised for iCalendar purposes, 782 and without duplicates. 783 """ 784 785 args = self.env.get_args() 786 787 attendees = args.get("attendee", []) 788 unique_attendees = set() 789 ordered_attendees = [] 790 791 for attendee in attendees: 792 if not attendee.strip(): 793 continue 794 attendee = get_uri(attendee) 795 if attendee not in unique_attendees: 796 unique_attendees.add(attendee) 797 ordered_attendees.append(attendee) 798 799 return ordered_attendees 800 801 def update_attendees_from_page(self): 802 803 "Add or remove attendees. This does not affect the stored object." 804 805 args = self.env.get_args() 806 807 attendees = self.get_attendees_from_page() 808 809 if args.has_key("add"): 810 attendees.append("") 811 812 # Only actually remove attendees if the event is unsent, if the attendee 813 # is new, or if it is the current user being removed. 814 815 if args.has_key("remove"): 816 still_to_remove = [] 817 818 for i in args["remove"]: 819 try: 820 attendee = attendees[int(i)] 821 except IndexError: 822 continue 823 824 if self.can_remove_attendee(attendee): 825 attendees.remove(attendee) 826 else: 827 still_to_remove.append(i) 828 829 args["remove"] = still_to_remove 830 831 return attendees 832 833 # Access to current object information. 834 835 def get_current_main_period(self): 836 837 """ 838 Return the currently active main period for the current object depending 839 on whether editing has begun or whether the object has just been loaded. 840 """ 841 842 if self.is_initial_load() or not self.is_organiser(): 843 return self.get_stored_main_period() 844 else: 845 return self.get_main_period_from_page() 846 847 def get_current_recurrences(self): 848 849 """ 850 Return recurrences for the current object using the original object 851 details where no editing is in progress, using form data otherwise. 852 """ 853 854 if self.is_initial_load() or not self.is_organiser(): 855 return self.get_stored_recurrences() 856 else: 857 return self.get_recurrences_from_page() 858 859 def get_current_attendees(self): 860 861 """ 862 Return attendees for the current object depending on whether the object 863 has been edited or instead provides such information from its stored 864 form. 865 """ 866 867 if self.is_initial_load() or not self.is_organiser(): 868 return self.get_stored_attendees() 869 else: 870 return self.get_attendees_from_page() 871 872 def update_current_attendees(self): 873 874 "Return an updated collection of attendees for the current object." 875 876 if self.is_initial_load() or not self.is_organiser(): 877 return self.get_stored_attendees() 878 else: 879 return self.update_attendees_from_page() 880 881 # Full page output methods. 882 883 def show(self, path_info): 884 885 "Show an object request using the given 'path_info' for the current user." 886 887 uid, recurrenceid = self.get_identifiers(path_info) 888 obj = self.get_stored_object(uid, recurrenceid) 889 self.set_object(obj) 890 891 if not obj: 892 return False 893 894 errors = self.handle_request() 895 896 if not errors: 897 return True 898 899 self.new_page(title="Event") 900 self.show_object_on_page(errors) 901 902 return True 903 904 # vim: tabstop=4 expandtab shiftwidth=4