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_counters() 291 self.show_conflicting_events() 292 self.show_request_controls() 293 294 page.form.close() 295 296 def show_attendee(self, i, attendee, attendee_attr): 297 298 """ 299 For the current object, show the attendee in position 'i' with the given 300 'attendee' value, having 'attendee_attr' as any stored attributes. 301 """ 302 303 page = self.page 304 args = self.env.get_args() 305 306 partstat = attendee_attr and attendee_attr.get("PARTSTAT") 307 308 page.td(class_="objectvalue") 309 310 # Show a form control as organiser for new attendees. 311 312 if self.is_organiser() and self.can_edit_attendee(attendee): 313 self.control("attendee", "value", attendee, size="40") 314 else: 315 self.control("attendee", "hidden", attendee) 316 page.add(attendee) 317 page.add(" ") 318 319 # Show participation status, editable for the current user. 320 321 if attendee == self.user: 322 self.menu("partstat", partstat, self.partstat_items, "partstat") 323 324 # Allow the participation indicator to act as a submit 325 # button in order to refresh the page and show a control for 326 # the current user, if indicated. 327 328 elif self.is_organiser() and self.attendee_is_new(attendee): 329 self.control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh") 330 page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat") 331 332 # Otherwise, just show a label with the participation status. 333 334 else: 335 page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat") 336 337 # Permit organisers to remove attendees. 338 339 if self.is_organiser(): 340 341 # Permit the removal of newly-added attendees. 342 343 remove_type = self.can_remove_attendee(attendee) and "submit" or "checkbox" 344 self.control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove") 345 346 page.label("Remove", for_="remove-%d" % i, class_="remove") 347 page.label(for_="remove-%d" % i, class_="removed") 348 page.add("(Uninvited)") 349 page.span("Re-invite", class_="action") 350 page.label.close() 351 352 page.td.close() 353 354 def show_recurrences(self, errors=None): 355 356 """ 357 Show recurrences for the current object. If 'errors' is given, show a 358 suitable message for the different errors provided. 359 """ 360 361 page = self.page 362 363 # Obtain any parent object if this object is a specific recurrence. 364 365 if self.recurrenceid: 366 parent = self.get_stored_object(self.uid, None) 367 if not parent: 368 return 369 370 page.p() 371 page.a("This event modifies a recurring event.", href=self.link_to(self.uid)) 372 page.p.close() 373 374 # Obtain the periods associated with the event. 375 # NOTE: Add a control to add recurrences here. 376 377 recurrences = self.get_current_recurrences() 378 379 if len(recurrences) < 1: 380 return 381 382 recurrenceids = self._get_recurrences(self.uid) 383 384 page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) 385 386 # Show each recurrence in a separate table if editable. 387 388 if self.is_organiser() and recurrences: 389 390 for index, period in enumerate(recurrences): 391 self.show_recurrence(index, period, self.recurrenceid, recurrenceids, errors) 392 393 # Otherwise, use a compact single table. 394 395 else: 396 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 397 page.caption("Occurrences") 398 page.thead() 399 page.tr() 400 page.th("Start", class_="objectheading start") 401 page.th("End", class_="objectheading end") 402 page.tr.close() 403 page.thead.close() 404 page.tbody() 405 406 for index, period in enumerate(recurrences): 407 page.tr() 408 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, True) 409 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, False) 410 page.tr.close() 411 412 page.tbody.close() 413 page.table.close() 414 415 def show_recurrence(self, index, period, recurrenceid, recurrenceids, errors=None): 416 417 """ 418 Show recurrence controls for a recurrence provided by the current object 419 with the given 'index' position in the list of periods, the given 420 'period' details, where a 'recurrenceid' indicates any specific 421 recurrence, and where 'recurrenceids' indicates all known additional 422 recurrences for the object. 423 424 If 'errors' is given, show a suitable message for the different errors 425 provided. 426 """ 427 428 page = self.page 429 args = self.env.get_args() 430 431 p = event_period_from_period(period) 432 replaced = not recurrenceid and p.is_replaced(recurrenceids) 433 434 # Isolate the controls from neighbouring tables. 435 436 page.div() 437 438 self.show_object_datetime_controls(period, index) 439 440 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 441 page.caption(period.origin == "RRULE" and "Occurrence from rule" or "Occurrence") 442 page.tbody() 443 444 page.tr() 445 error = errors and ("dtstart", index) in errors and " error" or "" 446 page.th("Start", class_="objectheading start%s" % error) 447 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, True) 448 page.tr.close() 449 page.tr() 450 error = errors and ("dtend", index) in errors and " error" or "" 451 page.th("End", class_="objectheading end%s" % error) 452 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, False) 453 page.tr.close() 454 455 # Permit the removal of recurrences. 456 457 if not replaced: 458 page.tr() 459 page.th("") 460 page.td() 461 462 remove_type = not self.obj.is_shared() or not period.origin and "submit" or "checkbox" 463 self.control("recur-remove", remove_type, str(index), 464 str(index) in args.get("recur-remove", []), 465 id="recur-remove-%d" % index, class_="remove") 466 467 page.label("Remove", for_="recur-remove-%d" % index, class_="remove") 468 page.label(for_="recur-remove-%d" % index, class_="removed") 469 page.add("(Removed)") 470 page.span("Re-add", class_="action") 471 page.label.close() 472 473 page.td.close() 474 page.tr.close() 475 476 page.tbody.close() 477 page.table.close() 478 479 page.div.close() 480 481 def show_counters(self): 482 483 "Show any counter-proposals for the current object." 484 485 page = self.page 486 attendees = self._get_counters(self.uid, self.recurrenceid) 487 tzid = self.get_tzid() 488 489 if not attendees: 490 return 491 492 page.p("The following counter-proposals have been received for this event:") 493 494 page.table(cellspacing=5, cellpadding=5, class_="counters") 495 page.thead() 496 page.tr() 497 page.th("Attendee", rowspan=2) 498 page.th("Periods", colspan=2) 499 page.tr.close() 500 page.tr() 501 page.th("Start") 502 page.th("End") 503 page.tr.close() 504 page.thead.close() 505 page.tbody() 506 507 for attendee in attendees: 508 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee) 509 periods = self.get_periods(obj) 510 511 page.tr(rowspan=len(periods)) 512 page.td(attendee) 513 514 first = True 515 for p in periods: 516 if not first: 517 page.tr() 518 else: 519 first = False 520 521 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 522 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 523 524 page.td(start) 525 page.td(end) 526 527 page.tr.close() 528 529 page.tbody.close() 530 page.table.close() 531 532 def show_conflicting_events(self): 533 534 "Show conflicting events for the current object." 535 536 page = self.page 537 recurrenceids = self._get_active_recurrences(self.uid) 538 539 # Obtain the user's timezone. 540 541 tzid = self.get_tzid() 542 periods = self.get_periods(self.obj) 543 544 # Indicate whether there are conflicting events. 545 546 conflicts = [] 547 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 548 549 for participant in self.get_current_attendees(): 550 if participant == self.user: 551 freebusy = self.store.get_freebusy(participant) 552 else: 553 freebusy = self.store.get_freebusy_for_other(self.user, participant) 554 555 if not freebusy: 556 continue 557 558 # Obtain any time zone details from the suggested event. 559 560 _dtstart, attr = self.obj.get_item("DTSTART") 561 tzid = attr.get("TZID", tzid) 562 563 # Show any conflicts with periods of actual attendance. 564 565 participant_attr = attendee_map.get(participant) 566 partstat = participant_attr and participant_attr.get("PARTSTAT") 567 recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid) 568 569 for p in have_conflict(freebusy, periods, True): 570 if not self.recurrenceid and p.is_replaced(recurrences): 571 continue 572 573 if ( # Unidentified or different event 574 (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and 575 # Different period or unclear participation with the same period 576 (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and 577 # Participant not limited to organising 578 p.transp != "ORG" 579 ): 580 581 conflicts.append(p) 582 583 conflicts.sort() 584 585 # Show any conflicts with periods of actual attendance. 586 587 if conflicts: 588 page.p("This event conflicts with others:") 589 590 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 591 page.thead() 592 page.tr() 593 page.th("Event") 594 page.th("Start") 595 page.th("End") 596 page.tr.close() 597 page.thead.close() 598 page.tbody() 599 600 for p in conflicts: 601 602 # Provide details of any conflicting event. 603 604 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 605 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 606 607 page.tr() 608 609 # Show the event summary for the conflicting event. 610 611 page.td() 612 if p.summary: 613 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 614 else: 615 page.add("(Unspecified event)") 616 page.td.close() 617 618 page.td(start) 619 page.td(end) 620 621 page.tr.close() 622 623 page.tbody.close() 624 page.table.close() 625 626 class EventPage(EventPageFragment): 627 628 "A request handler for the event page." 629 630 def __init__(self, resource=None, messenger=None): 631 ResourceClientForObject.__init__(self, resource) 632 self.messenger = messenger or Messenger() 633 634 # Request logic methods. 635 636 def is_initial_load(self): 637 638 "Return whether the event is being loaded and shown for the first time." 639 640 return not self.env.get_args().has_key("editing") 641 642 def handle_request(self): 643 644 """ 645 Handle actions involving the current object, returning an error if one 646 occurred, or None if the request was successfully handled. 647 """ 648 649 # Handle a submitted form. 650 651 args = self.env.get_args() 652 653 # Get the possible actions. 654 655 reply = args.has_key("reply") 656 discard = args.has_key("discard") 657 create = args.has_key("create") 658 cancel = args.has_key("cancel") 659 ignore = args.has_key("ignore") 660 save = args.has_key("save") 661 662 have_action = reply or discard or create or cancel or ignore or save 663 664 if not have_action: 665 return ["action"] 666 667 # If ignoring the object, return to the calendar. 668 669 if ignore: 670 self.redirect(self.env.get_path()) 671 return None 672 673 # Update the object. 674 675 single_user = False 676 677 if reply or create or cancel or save: 678 679 # Update principal event details if organiser. 680 681 if self.is_organiser(): 682 683 # Update time periods (main and recurring). 684 685 try: 686 period = self.handle_main_period() 687 except PeriodError, exc: 688 return exc.args 689 690 try: 691 periods = self.handle_recurrence_periods() 692 except PeriodError, exc: 693 return exc.args 694 695 # Set the periods in the object, first obtaining removed and 696 # modified period information. 697 698 to_unschedule = self.get_removed_periods() 699 700 self.obj.set_period(period) 701 self.obj.set_periods(periods) 702 703 # Update summary. 704 705 if args.has_key("summary"): 706 self.obj["SUMMARY"] = [(args["summary"][0], {})] 707 708 # Obtain any participants and those to be removed. 709 710 attendees = self.get_attendees_from_page() 711 removed = [attendees[int(i)] for i in args.get("remove", [])] 712 to_cancel = self.update_attendees(self.obj, attendees, removed) 713 single_user = not attendees or attendees == [self.user] 714 715 # Update attendee participation for the current user. 716 717 if args.has_key("partstat"): 718 self.update_participation(self.obj, args["partstat"][0]) 719 720 # Process any action. 721 722 invite = not save and create and not single_user 723 save = save or create and single_user 724 725 handled = True 726 727 if reply or invite or cancel: 728 729 client = ManagerClient(self.obj, self.user, self.messenger) 730 731 # Process the object and remove it from the list of requests. 732 733 if reply and client.process_received_request(): 734 self.remove_request(self.uid, self.recurrenceid) 735 736 elif self.is_organiser() and (invite or cancel): 737 738 # Invitation, uninvitation and unscheduling... 739 740 if client.process_created_request( 741 invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule): 742 743 self.remove_request(self.uid, self.recurrenceid) 744 745 # Save single user events. 746 747 elif save: 748 self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node()) 749 self.update_event_in_freebusy() 750 self.remove_request(self.uid, self.recurrenceid) 751 752 # Remove the request and the object. 753 754 elif discard: 755 self.remove_event_from_freebusy() 756 self.remove_event(self.uid, self.recurrenceid) 757 self.remove_request(self.uid, self.recurrenceid) 758 759 else: 760 handled = False 761 762 # Upon handling an action, redirect to the main page. 763 764 if handled: 765 self.redirect(self.env.get_path()) 766 767 return None 768 769 def handle_main_period(self): 770 771 "Return period details for the main start/end period in an event." 772 773 return self.get_main_period_from_page().as_event_period() 774 775 def handle_recurrence_periods(self): 776 777 "Return period details for the recurrences specified for an event." 778 779 return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] 780 781 # Access to form-originating object information. 782 783 def get_main_period_from_page(self): 784 785 "Return the main period defined in the event form." 786 787 args = self.env.get_args() 788 789 dtend_enabled = args.get("dtend-control", [None])[0] 790 dttimes_enabled = args.get("dttimes-control", [None])[0] 791 start = self.get_date_control_values("dtstart") 792 end = self.get_date_control_values("dtend") 793 794 return FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid()) 795 796 def get_recurrences_from_page(self): 797 798 "Return the recurrences defined in the event form." 799 800 args = self.env.get_args() 801 802 all_dtend_enabled = args.get("dtend-control-recur", []) 803 all_dttimes_enabled = args.get("dttimes-control-recur", []) 804 all_starts = self.get_date_control_values("dtstart-recur", multiple=True) 805 all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") 806 all_origins = args.get("recur-origin", []) 807 808 periods = [] 809 810 for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \ 811 enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)): 812 813 dtend_enabled = str(index) in all_dtend_enabled 814 dttimes_enabled = str(index) in all_dttimes_enabled 815 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin) 816 periods.append(period) 817 818 return periods 819 820 def get_removed_periods(self): 821 822 "Return a list of recurrence periods to remove upon updating an event." 823 824 to_unschedule = [] 825 args = self.env.get_args() 826 for i in args.get("recur-remove", []): 827 to_unschedule.append(periods[int(i)]) 828 return to_unschedule 829 830 def get_attendees_from_page(self): 831 832 """ 833 Return attendees from the request, normalised for iCalendar purposes, 834 and without duplicates. 835 """ 836 837 args = self.env.get_args() 838 839 attendees = args.get("attendee", []) 840 unique_attendees = set() 841 ordered_attendees = [] 842 843 for attendee in attendees: 844 if not attendee.strip(): 845 continue 846 attendee = get_uri(attendee) 847 if attendee not in unique_attendees: 848 unique_attendees.add(attendee) 849 ordered_attendees.append(attendee) 850 851 return ordered_attendees 852 853 def update_attendees_from_page(self): 854 855 "Add or remove attendees. This does not affect the stored object." 856 857 args = self.env.get_args() 858 859 attendees = self.get_attendees_from_page() 860 861 if args.has_key("add"): 862 attendees.append("") 863 864 # Only actually remove attendees if the event is unsent, if the attendee 865 # is new, or if it is the current user being removed. 866 867 if args.has_key("remove"): 868 still_to_remove = [] 869 870 for i in args["remove"]: 871 try: 872 attendee = attendees[int(i)] 873 except IndexError: 874 continue 875 876 if self.can_remove_attendee(attendee): 877 attendees.remove(attendee) 878 else: 879 still_to_remove.append(i) 880 881 args["remove"] = still_to_remove 882 883 return attendees 884 885 # Access to current object information. 886 887 def get_current_main_period(self): 888 889 """ 890 Return the currently active main period for the current object depending 891 on whether editing has begun or whether the object has just been loaded. 892 """ 893 894 if self.is_initial_load() or not self.is_organiser(): 895 return self.get_stored_main_period() 896 else: 897 return self.get_main_period_from_page() 898 899 def get_current_recurrences(self): 900 901 """ 902 Return recurrences for the current object using the original object 903 details where no editing is in progress, using form data otherwise. 904 """ 905 906 if self.is_initial_load() or not self.is_organiser(): 907 return self.get_stored_recurrences() 908 else: 909 return self.get_recurrences_from_page() 910 911 def get_current_attendees(self): 912 913 """ 914 Return attendees for the current object depending on whether the object 915 has been edited or instead provides such information from its stored 916 form. 917 """ 918 919 if self.is_initial_load() or not self.is_organiser(): 920 return self.get_stored_attendees() 921 else: 922 return self.get_attendees_from_page() 923 924 def update_current_attendees(self): 925 926 "Return an updated collection of attendees for the current object." 927 928 if self.is_initial_load() or not self.is_organiser(): 929 return self.get_stored_attendees() 930 else: 931 return self.update_attendees_from_page() 932 933 # Full page output methods. 934 935 def show(self, path_info): 936 937 "Show an object request using the given 'path_info' for the current user." 938 939 uid, recurrenceid = self.get_identifiers(path_info) 940 obj = self.get_stored_object(uid, recurrenceid) 941 self.set_object(obj) 942 943 if not obj: 944 return False 945 946 errors = self.handle_request() 947 948 if not errors: 949 return True 950 951 self.new_page(title="Event") 952 self.show_object_on_page(errors) 953 954 return True 955 956 # vim: tabstop=4 expandtab shiftwidth=4