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 format_datetime, 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 query = self.env.get_query() 487 counter = query.get("counter", [None])[0] 488 489 attendees = self._get_counters(self.uid, self.recurrenceid) 490 tzid = self.get_tzid() 491 492 if not attendees: 493 return 494 495 page.p("The following counter-proposals have been received for this event:") 496 497 page.table(cellspacing=5, cellpadding=5, class_="counters") 498 page.thead() 499 page.tr() 500 page.th("Attendee", rowspan=2) 501 page.th("Periods", colspan=2) 502 page.tr.close() 503 page.tr() 504 page.th("Start") 505 page.th("End") 506 page.tr.close() 507 page.thead.close() 508 page.tbody() 509 510 for attendee in attendees: 511 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee) 512 periods = self.get_periods(obj) 513 514 first = True 515 for p in periods: 516 identifier = "%s-%s" % (format_datetime(p.get_start_point()), format_datetime(p.get_end_point())) 517 css = identifier == counter and "selected" or "" 518 519 if first: 520 page.tr(rowspan=len(periods), class_=css) 521 page.td(attendee) 522 first = False 523 else: 524 page.tr(class_=css) 525 526 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 527 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 528 529 page.td(start) 530 page.td(end) 531 532 page.tr.close() 533 534 page.tbody.close() 535 page.table.close() 536 537 def show_conflicting_events(self): 538 539 "Show conflicting events for the current object." 540 541 page = self.page 542 recurrenceids = self._get_active_recurrences(self.uid) 543 544 # Obtain the user's timezone. 545 546 tzid = self.get_tzid() 547 periods = self.get_periods(self.obj) 548 549 # Indicate whether there are conflicting events. 550 551 conflicts = [] 552 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 553 554 for participant in self.get_current_attendees(): 555 if participant == self.user: 556 freebusy = self.store.get_freebusy(participant) 557 else: 558 freebusy = self.store.get_freebusy_for_other(self.user, participant) 559 560 if not freebusy: 561 continue 562 563 # Obtain any time zone details from the suggested event. 564 565 _dtstart, attr = self.obj.get_item("DTSTART") 566 tzid = attr.get("TZID", tzid) 567 568 # Show any conflicts with periods of actual attendance. 569 570 participant_attr = attendee_map.get(participant) 571 partstat = participant_attr and participant_attr.get("PARTSTAT") 572 recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid) 573 574 for p in have_conflict(freebusy, periods, True): 575 if not self.recurrenceid and p.is_replaced(recurrences): 576 continue 577 578 if ( # Unidentified or different event 579 (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and 580 # Different period or unclear participation with the same period 581 (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and 582 # Participant not limited to organising 583 p.transp != "ORG" 584 ): 585 586 conflicts.append(p) 587 588 conflicts.sort() 589 590 # Show any conflicts with periods of actual attendance. 591 592 if conflicts: 593 page.p("This event conflicts with others:") 594 595 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 596 page.thead() 597 page.tr() 598 page.th("Event") 599 page.th("Start") 600 page.th("End") 601 page.tr.close() 602 page.thead.close() 603 page.tbody() 604 605 for p in conflicts: 606 607 # Provide details of any conflicting event. 608 609 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 610 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 611 612 page.tr() 613 614 # Show the event summary for the conflicting event. 615 616 page.td() 617 if p.summary: 618 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 619 else: 620 page.add("(Unspecified event)") 621 page.td.close() 622 623 page.td(start) 624 page.td(end) 625 626 page.tr.close() 627 628 page.tbody.close() 629 page.table.close() 630 631 class EventPage(EventPageFragment): 632 633 "A request handler for the event page." 634 635 def __init__(self, resource=None, messenger=None): 636 ResourceClientForObject.__init__(self, resource) 637 self.messenger = messenger or Messenger() 638 639 # Request logic methods. 640 641 def is_initial_load(self): 642 643 "Return whether the event is being loaded and shown for the first time." 644 645 return not self.env.get_args().has_key("editing") 646 647 def handle_request(self): 648 649 """ 650 Handle actions involving the current object, returning an error if one 651 occurred, or None if the request was successfully handled. 652 """ 653 654 # Handle a submitted form. 655 656 args = self.env.get_args() 657 658 # Get the possible actions. 659 660 reply = args.has_key("reply") 661 discard = args.has_key("discard") 662 create = args.has_key("create") 663 cancel = args.has_key("cancel") 664 ignore = args.has_key("ignore") 665 save = args.has_key("save") 666 667 have_action = reply or discard or create or cancel or ignore or save 668 669 if not have_action: 670 return ["action"] 671 672 # If ignoring the object, return to the calendar. 673 674 if ignore: 675 self.redirect(self.env.get_path()) 676 return None 677 678 # Update the object. 679 680 single_user = False 681 682 if reply or create or cancel or save: 683 684 # Update principal event details if organiser. 685 686 if self.is_organiser(): 687 688 # Update time periods (main and recurring). 689 690 try: 691 period = self.handle_main_period() 692 except PeriodError, exc: 693 return exc.args 694 695 try: 696 periods = self.handle_recurrence_periods() 697 except PeriodError, exc: 698 return exc.args 699 700 # Set the periods in the object, first obtaining removed and 701 # modified period information. 702 703 to_unschedule = self.get_removed_periods() 704 705 self.obj.set_period(period) 706 self.obj.set_periods(periods) 707 708 # Update summary. 709 710 if args.has_key("summary"): 711 self.obj["SUMMARY"] = [(args["summary"][0], {})] 712 713 # Obtain any participants and those to be removed. 714 715 attendees = self.get_attendees_from_page() 716 removed = [attendees[int(i)] for i in args.get("remove", [])] 717 to_cancel = self.update_attendees(self.obj, attendees, removed) 718 single_user = not attendees or attendees == [self.user] 719 720 # Update attendee participation for the current user. 721 722 if args.has_key("partstat"): 723 self.update_participation(self.obj, args["partstat"][0]) 724 725 # Process any action. 726 727 invite = not save and create and not single_user 728 save = save or create and single_user 729 730 handled = True 731 732 if reply or invite or cancel: 733 734 client = ManagerClient(self.obj, self.user, self.messenger) 735 736 # Process the object and remove it from the list of requests. 737 738 if reply and client.process_received_request(): 739 self.remove_request(self.uid, self.recurrenceid) 740 741 elif self.is_organiser() and (invite or cancel): 742 743 # Invitation, uninvitation and unscheduling... 744 745 if client.process_created_request( 746 invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule): 747 748 self.remove_request(self.uid, self.recurrenceid) 749 750 # Save single user events. 751 752 elif save: 753 self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node()) 754 self.update_event_in_freebusy() 755 self.remove_request(self.uid, self.recurrenceid) 756 757 # Remove the request and the object. 758 759 elif discard: 760 self.remove_event_from_freebusy() 761 self.remove_event(self.uid, self.recurrenceid) 762 self.remove_request(self.uid, self.recurrenceid) 763 764 else: 765 handled = False 766 767 # Upon handling an action, redirect to the main page. 768 769 if handled: 770 self.redirect(self.env.get_path()) 771 772 return None 773 774 def handle_main_period(self): 775 776 "Return period details for the main start/end period in an event." 777 778 return self.get_main_period_from_page().as_event_period() 779 780 def handle_recurrence_periods(self): 781 782 "Return period details for the recurrences specified for an event." 783 784 return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] 785 786 # Access to form-originating object information. 787 788 def get_main_period_from_page(self): 789 790 "Return the main period defined in the event form." 791 792 args = self.env.get_args() 793 794 dtend_enabled = args.get("dtend-control", [None])[0] 795 dttimes_enabled = args.get("dttimes-control", [None])[0] 796 start = self.get_date_control_values("dtstart") 797 end = self.get_date_control_values("dtend") 798 799 return FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid()) 800 801 def get_recurrences_from_page(self): 802 803 "Return the recurrences defined in the event form." 804 805 args = self.env.get_args() 806 807 all_dtend_enabled = args.get("dtend-control-recur", []) 808 all_dttimes_enabled = args.get("dttimes-control-recur", []) 809 all_starts = self.get_date_control_values("dtstart-recur", multiple=True) 810 all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") 811 all_origins = args.get("recur-origin", []) 812 813 periods = [] 814 815 for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \ 816 enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)): 817 818 dtend_enabled = str(index) in all_dtend_enabled 819 dttimes_enabled = str(index) in all_dttimes_enabled 820 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin) 821 periods.append(period) 822 823 return periods 824 825 def get_removed_periods(self): 826 827 "Return a list of recurrence periods to remove upon updating an event." 828 829 to_unschedule = [] 830 args = self.env.get_args() 831 for i in args.get("recur-remove", []): 832 to_unschedule.append(periods[int(i)]) 833 return to_unschedule 834 835 def get_attendees_from_page(self): 836 837 """ 838 Return attendees from the request, normalised for iCalendar purposes, 839 and without duplicates. 840 """ 841 842 args = self.env.get_args() 843 844 attendees = args.get("attendee", []) 845 unique_attendees = set() 846 ordered_attendees = [] 847 848 for attendee in attendees: 849 if not attendee.strip(): 850 continue 851 attendee = get_uri(attendee) 852 if attendee not in unique_attendees: 853 unique_attendees.add(attendee) 854 ordered_attendees.append(attendee) 855 856 return ordered_attendees 857 858 def update_attendees_from_page(self): 859 860 "Add or remove attendees. This does not affect the stored object." 861 862 args = self.env.get_args() 863 864 attendees = self.get_attendees_from_page() 865 866 if args.has_key("add"): 867 attendees.append("") 868 869 # Only actually remove attendees if the event is unsent, if the attendee 870 # is new, or if it is the current user being removed. 871 872 if args.has_key("remove"): 873 still_to_remove = [] 874 875 for i in args["remove"]: 876 try: 877 attendee = attendees[int(i)] 878 except IndexError: 879 continue 880 881 if self.can_remove_attendee(attendee): 882 attendees.remove(attendee) 883 else: 884 still_to_remove.append(i) 885 886 args["remove"] = still_to_remove 887 888 return attendees 889 890 # Access to current object information. 891 892 def get_current_main_period(self): 893 894 """ 895 Return the currently active main period for the current object depending 896 on whether editing has begun or whether the object has just been loaded. 897 """ 898 899 if self.is_initial_load() or not self.is_organiser(): 900 return self.get_stored_main_period() 901 else: 902 return self.get_main_period_from_page() 903 904 def get_current_recurrences(self): 905 906 """ 907 Return recurrences for the current object using the original object 908 details where no editing is in progress, using form data otherwise. 909 """ 910 911 if self.is_initial_load() or not self.is_organiser(): 912 return self.get_stored_recurrences() 913 else: 914 return self.get_recurrences_from_page() 915 916 def get_current_attendees(self): 917 918 """ 919 Return attendees for the current object depending on whether the object 920 has been edited or instead provides such information from its stored 921 form. 922 """ 923 924 if self.is_initial_load() or not self.is_organiser(): 925 return self.get_stored_attendees() 926 else: 927 return self.get_attendees_from_page() 928 929 def update_current_attendees(self): 930 931 "Return an updated collection of attendees for the current object." 932 933 if self.is_initial_load() or not self.is_organiser(): 934 return self.get_stored_attendees() 935 else: 936 return self.update_attendees_from_page() 937 938 # Full page output methods. 939 940 def show(self, path_info): 941 942 "Show an object request using the given 'path_info' for the current user." 943 944 uid, recurrenceid = self.get_identifiers(path_info) 945 obj = self.get_stored_object(uid, recurrenceid) 946 self.set_object(obj) 947 948 if not obj: 949 return False 950 951 errors = self.handle_request() 952 953 if not errors: 954 return True 955 956 self.new_page(title="Event") 957 self.show_object_on_page(errors) 958 959 return True 960 961 # vim: tabstop=4 expandtab shiftwidth=4