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, get_verbose_address, 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.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject 28 29 class EventPageFragment(ResourceClientForObject, DateTimeFormUtilities, FormUtilities): 30 31 "A resource presenting the details of an event." 32 33 def __init__(self, resource=None): 34 ResourceClientForObject.__init__(self, resource) 35 36 # Various property values and labels. 37 38 property_items = [ 39 ("SUMMARY", "Summary"), 40 ("DTSTART", "Start"), 41 ("DTEND", "End"), 42 ("ORGANIZER", "Organiser"), 43 ("ATTENDEE", "Attendee"), 44 ] 45 46 partstat_items = [ 47 ("NEEDS-ACTION", "Not confirmed"), 48 ("ACCEPTED", "Attending"), 49 ("TENTATIVE", "Tentatively attending"), 50 ("DECLINED", "Not attending"), 51 ("DELEGATED", "Delegated"), 52 (None, "Not indicated"), 53 ] 54 55 def can_change_object(self): 56 return self.is_organiser() or self._is_request() 57 58 def can_remove_recurrence(self, recurrence): 59 60 """ 61 Return whether the 'recurrence' can be removed from the current object 62 without notification. 63 """ 64 65 return self.can_edit_recurrence(recurrence) and recurrence.origin != "RRULE" 66 67 def can_edit_recurrence(self, recurrence): 68 69 "Return whether 'recurrence' can be edited." 70 71 return self.recurrence_is_new(recurrence) or not self.obj.is_shared() 72 73 def recurrence_is_new(self, recurrence): 74 75 "Return whether 'recurrence' is new to the current object." 76 77 return recurrence not in self.get_stored_recurrences() 78 79 def can_remove_attendee(self, attendee): 80 81 """ 82 Return whether 'attendee' can be removed from the current object without 83 notification. 84 """ 85 86 return self.can_edit_attendee(attendee) or attendee == self.user and self.is_organiser() 87 88 def can_edit_attendee(self, attendee): 89 90 "Return whether 'attendee' can be edited by an organiser." 91 92 return self.attendee_is_new(attendee) or not self.obj.is_shared() 93 94 def attendee_is_new(self, attendee): 95 96 "Return whether 'attendee' is new to the current object." 97 98 return attendee not in uri_values(self.get_stored_attendees()) 99 100 # Access to stored object information. 101 102 def get_stored_attendees(self): 103 return [get_verbose_address(value, attr) for value, attr in self.obj.get_items("ATTENDEE") or []] 104 105 def get_stored_main_period(self): 106 107 "Return the main event period for the current object." 108 109 (dtstart, dtstart_attr), (dtend, dtend_attr) = self.obj.get_main_period_items(self.get_tzid()) 110 return EventPeriod(dtstart, dtend, self.get_tzid(), None, dtstart_attr, dtend_attr) 111 112 def get_stored_recurrences(self): 113 114 "Return recurrences computed using the current object." 115 116 recurrences = [] 117 for period in self.get_periods(self.obj): 118 if period.origin != "DTSTART": 119 recurrences.append(period) 120 return recurrences 121 122 # Access to current object information. 123 124 def get_current_main_period(self): 125 return self.get_stored_main_period() 126 127 def get_current_recurrences(self): 128 return self.get_stored_recurrences() 129 130 def get_current_attendees(self): 131 return self.get_stored_attendees() 132 133 # Page fragment methods. 134 135 def show_request_controls(self): 136 137 "Show form controls for a request." 138 139 page = self.page 140 args = self.env.get_args() 141 142 attendees = uri_values(self.get_current_attendees()) 143 is_attendee = self.user in attendees 144 145 if not self.obj.is_shared(): 146 page.p("This event has not been shared.") 147 148 # Show appropriate options depending on the role of the user. 149 150 if is_attendee and not self.is_organiser(): 151 page.p("An action is required for this request:") 152 153 page.p() 154 self.control("reply", "submit", "Send reply") 155 page.add(" ") 156 self.control("discard", "submit", "Discard event") 157 page.add(" ") 158 self.control("ignore", "submit", "Do nothing for now") 159 page.p.close() 160 161 if self.is_organiser(): 162 page.p("As organiser, you can perform the following:") 163 164 page.p() 165 self.control("create", "submit", "Update event") 166 page.add(" ") 167 168 if self.obj.is_shared() and not self._is_request(): 169 self.control("cancel", "submit", "Cancel event") 170 else: 171 self.control("discard", "submit", "Discard event") 172 173 page.add(" ") 174 self.control("save", "submit", "Save without sending") 175 page.p.close() 176 177 def show_object_on_page(self, errors=None): 178 179 """ 180 Show the calendar object on the current page. If 'errors' is given, show 181 a suitable message for the different errors provided. 182 """ 183 184 page = self.page 185 page.form(method="POST") 186 187 # Add a hidden control to help determine whether editing has already begun. 188 189 self.control("editing", "hidden", "true") 190 191 args = self.env.get_args() 192 193 # Obtain basic event information, generating any necessary editing controls. 194 195 attendees = self.get_current_attendees() 196 period = self.get_current_main_period() 197 self.show_object_datetime_controls(period) 198 199 # Obtain any separate recurrences for this event. 200 201 recurrenceids = self._get_active_recurrences(self.uid) 202 replaced = not self.recurrenceid and period.is_replaced(recurrenceids) 203 excluded = period not in self.get_periods(self.obj) 204 205 # Provide a summary of the object. 206 207 page.table(class_="object", cellspacing=5, cellpadding=5) 208 page.thead() 209 page.tr() 210 page.th("Event", class_="mainheading", colspan=2) 211 page.tr.close() 212 page.thead.close() 213 page.tbody() 214 215 for name, label in self.property_items: 216 field = name.lower() 217 218 items = uri_items(self.obj.get_items(name) or []) 219 rowspan = len(items) 220 221 if name == "ATTENDEE": 222 rowspan = len(attendees) + 1 # for the add button 223 elif not items: 224 continue 225 226 page.tr() 227 page.th(label, class_="objectheading %s%s" % (field, errors and field in errors and " error" or ""), rowspan=rowspan) 228 229 # Handle datetimes specially. 230 231 if name in ["DTSTART", "DTEND"]: 232 if not replaced and not excluded: 233 234 # Obtain the datetime. 235 236 is_start = name == "DTSTART" 237 238 # Where no end datetime exists, use the start datetime as the 239 # basis of any potential datetime specified if dt-control is 240 # set. 241 242 self.show_datetime_controls(is_start and period.get_form_start() or period.get_form_end(), is_start) 243 244 elif name == "DTSTART": 245 246 # Replaced occurrences link to their replacements. 247 248 if replaced: 249 page.td(class_="objectvalue %s replaced" % field, rowspan=2) 250 page.a("First occurrence replaced by a separate event", href=self.link_to(self.uid, replaced)) 251 page.td.close() 252 253 # NOTE: Should provide a way of editing recurrences when the 254 # NOTE: first occurrence is excluded, plus a way of 255 # NOTE: reinstating the occurrence. 256 257 elif excluded: 258 page.td(class_="objectvalue %s excluded" % field, rowspan=2) 259 page.add("First occurrence excluded") 260 page.td.close() 261 262 page.tr.close() 263 264 # Handle the summary specially. 265 266 elif name == "SUMMARY": 267 value = args.get("summary", [self.obj.get_value(name)])[0] 268 269 page.td(class_="objectvalue summary") 270 if self.is_organiser(): 271 self.control("summary", "text", value, size=80) 272 else: 273 page.add(value) 274 page.td.close() 275 page.tr.close() 276 277 # Handle attendees specially. 278 279 elif name == "ATTENDEE": 280 attendee_map = dict(items) 281 first = True 282 283 for i, value in enumerate(attendees): 284 if not first: 285 page.tr() 286 else: 287 first = False 288 289 # Obtain details of attendees to supply attributes. 290 291 self.show_attendee(i, value, attendee_map.get(get_uri(value))) 292 page.tr.close() 293 294 # Allow more attendees to be specified. 295 296 if self.can_change_object(): 297 if not first: 298 page.tr() 299 300 page.td() 301 self.control("add", "submit", "add", id="add", class_="add") 302 page.label("Add attendee", for_="add", class_="add") 303 page.td.close() 304 page.tr.close() 305 306 # NOTE: Permit attendees to suggest others for counter-proposals. 307 308 # Handle potentially many values of other kinds. 309 310 else: 311 first = True 312 313 for i, (value, attr) in enumerate(items): 314 if not first: 315 page.tr() 316 else: 317 first = False 318 319 page.td(class_="objectvalue %s" % field) 320 if name == "ORGANIZER": 321 page.add(get_verbose_address(value, attr)) 322 else: 323 page.add(value) 324 page.td.close() 325 page.tr.close() 326 327 page.tbody.close() 328 page.table.close() 329 330 self.show_recurrences(errors) 331 self.show_counters() 332 self.show_conflicting_events() 333 self.show_request_controls() 334 335 page.form.close() 336 337 def show_attendee(self, i, attendee, attendee_attr): 338 339 """ 340 For the current object, show the attendee in position 'i' with the given 341 'attendee' value, having 'attendee_attr' as any stored attributes. 342 """ 343 344 page = self.page 345 args = self.env.get_args() 346 347 attendee_uri = get_uri(attendee) 348 partstat = attendee_attr and attendee_attr.get("PARTSTAT") 349 350 page.td(class_="objectvalue") 351 352 # Show a form control as organiser for new attendees. 353 # NOTE: Permit suggested attendee editing for counter-proposals. 354 355 if self.can_change_object() and self.can_edit_attendee(attendee_uri): 356 self.control("attendee", "value", attendee, size="40") 357 else: 358 self.control("attendee", "hidden", attendee) 359 page.add(attendee) 360 page.add(" ") 361 362 # Show participation status, editable for the current user. 363 364 if attendee_uri == self.user: 365 self.menu("partstat", partstat, self.partstat_items, "partstat") 366 367 # Allow the participation indicator to act as a submit 368 # button in order to refresh the page and show a control for 369 # the current user, if indicated. 370 371 elif self.is_organiser() and self.attendee_is_new(attendee_uri): 372 self.control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh") 373 page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat") 374 375 # Otherwise, just show a label with the participation status. 376 377 else: 378 page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat") 379 380 # Permit organisers to remove attendees. 381 # NOTE: Permit the removal of suggested attendees for counter-proposals. 382 383 if self.can_change_object() and (self.can_remove_attendee(attendee_uri) or self.is_organiser()): 384 385 # Permit the removal of newly-added attendees. 386 387 remove_type = self.can_remove_attendee(attendee_uri) and "submit" or "checkbox" 388 self.control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove") 389 390 page.label("Remove", for_="remove-%d" % i, class_="remove") 391 page.label(for_="remove-%d" % i, class_="removed") 392 page.add("(Uninvited)") 393 page.span("Re-invite", class_="action") 394 page.label.close() 395 396 page.td.close() 397 398 def show_recurrences(self, errors=None): 399 400 """ 401 Show recurrences for the current object. If 'errors' is given, show a 402 suitable message for the different errors provided. 403 """ 404 405 page = self.page 406 407 # Obtain any parent object if this object is a specific recurrence. 408 409 if self.recurrenceid: 410 parent = self.get_stored_object(self.uid, None) 411 if not parent: 412 return 413 414 page.p() 415 page.a("This event modifies a recurring event.", href=self.link_to(self.uid)) 416 page.p.close() 417 418 # Obtain the periods associated with the event. 419 # NOTE: Add a control to add recurrences here. 420 421 recurrences = self.get_current_recurrences() 422 423 if len(recurrences) < 1: 424 return 425 426 recurrenceids = self._get_recurrences(self.uid) 427 428 page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) 429 430 # Show each recurrence in a separate table if editable. 431 # NOTE: Allow recurrence editing for counter-proposals. 432 433 if self.can_change_object() and recurrences: 434 435 for index, period in enumerate(recurrences): 436 self.show_recurrence(index, period, self.recurrenceid, recurrenceids, errors) 437 438 # Otherwise, use a compact single table. 439 440 else: 441 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 442 page.caption("Occurrences") 443 page.thead() 444 page.tr() 445 page.th("Start", class_="objectheading start") 446 page.th("End", class_="objectheading end") 447 page.tr.close() 448 page.thead.close() 449 page.tbody() 450 451 for index, period in enumerate(recurrences): 452 page.tr() 453 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, True) 454 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, False) 455 page.tr.close() 456 457 page.tbody.close() 458 page.table.close() 459 460 def show_recurrence(self, index, period, recurrenceid, recurrenceids, errors=None): 461 462 """ 463 Show recurrence controls for a recurrence provided by the current object 464 with the given 'index' position in the list of periods, the given 465 'period' details, where a 'recurrenceid' indicates any specific 466 recurrence, and where 'recurrenceids' indicates all known additional 467 recurrences for the object. 468 469 If 'errors' is given, show a suitable message for the different errors 470 provided. 471 """ 472 473 page = self.page 474 args = self.env.get_args() 475 476 p = event_period_from_period(period) 477 replaced = not recurrenceid and p.is_replaced(recurrenceids) 478 479 # Isolate the controls from neighbouring tables. 480 481 page.div() 482 483 self.show_object_datetime_controls(period, index) 484 485 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 486 page.caption(period.origin == "RRULE" and "Occurrence from rule" or "Occurrence") 487 page.tbody() 488 489 page.tr() 490 error = errors and ("dtstart", index) in errors and " error" or "" 491 page.th("Start", class_="objectheading start%s" % error) 492 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, True) 493 page.tr.close() 494 page.tr() 495 error = errors and ("dtend", index) in errors and " error" or "" 496 page.th("End", class_="objectheading end%s" % error) 497 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, False) 498 page.tr.close() 499 500 # Permit the removal of recurrences. 501 502 if not replaced: 503 page.tr() 504 page.th("") 505 page.td() 506 507 remove_type = self.can_remove_recurrence(period) and "submit" or "checkbox" 508 509 self.control("recur-remove", remove_type, str(index), 510 str(index) in args.get("recur-remove", []), 511 id="recur-remove-%d" % index, class_="remove") 512 513 page.label("Remove", for_="recur-remove-%d" % index, class_="remove") 514 page.label(for_="recur-remove-%d" % index, class_="removed") 515 page.add("(Removed)") 516 page.span("Re-add", class_="action") 517 page.label.close() 518 519 page.td.close() 520 page.tr.close() 521 522 page.tbody.close() 523 page.table.close() 524 525 page.div.close() 526 527 def show_counters(self): 528 529 "Show any counter-proposals for the current object." 530 531 page = self.page 532 query = self.env.get_query() 533 counter = query.get("counter", [None])[0] 534 535 attendees = self._get_counters(self.uid, self.recurrenceid) 536 tzid = self.get_tzid() 537 538 if not attendees: 539 return 540 541 attendees = self.get_verbose_attendees(attendees) 542 543 # Get suggestions. Attendees are aggregated and reference the existing 544 # attendees suggesting them. Periods are referenced by each existing 545 # attendee. 546 547 suggested_attendees = {} 548 suggested_periods = {} 549 550 for i, attendee in enumerate(attendees): 551 attendee_uri = get_uri(attendee) 552 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee_uri) 553 554 # Get suggested attendees. 555 556 for suggested_uri, suggested_attr in uri_dict(obj.get_value_map("ATTENDEE")).items(): 557 if suggested_uri == attendee_uri: 558 continue 559 suggested = get_verbose_address(suggested_uri, suggested_attr) 560 561 if not suggested_attendees.has_key(suggested): 562 suggested_attendees[suggested] = [] 563 suggested_attendees[suggested].append(attendee) 564 565 # Get suggested periods. 566 567 suggested_periods[attendee] = self.get_periods(obj) 568 569 # Present the suggested attendees. 570 571 page.p("The following attendees have been suggested for this event:") 572 573 page.table(cellspacing=5, cellpadding=5, class_="counters") 574 page.thead() 575 page.tr() 576 page.th("Attendee") 577 page.th("Suggested by...") 578 page.tr.close() 579 page.thead.close() 580 page.tbody() 581 582 suggested_attendees = list(suggested_attendees.items()) 583 suggested_attendees.sort() 584 585 for suggested, attendees in suggested_attendees: 586 page.tr() 587 page.td(suggested) 588 page.td(", ".join(attendees)) 589 page.tr.close() 590 591 page.tbody.close() 592 page.table.close() 593 594 # Present the suggested periods. 595 596 page.p("The following periods have been suggested for this event:") 597 598 page.table(cellspacing=5, cellpadding=5, class_="counters") 599 page.thead() 600 page.tr() 601 page.th("Periods", colspan=2) 602 page.th("Suggested by...", rowspan=2) 603 page.tr.close() 604 page.tr() 605 page.th("Start") 606 page.th("End") 607 page.tr.close() 608 page.thead.close() 609 page.tbody() 610 611 suggested_periods = list(suggested_periods.items()) 612 suggested_periods.sort() 613 614 for attendee, periods in suggested_periods: 615 first = True 616 for p in periods: 617 identifier = "%s-%s" % (format_datetime(p.get_start_point()), format_datetime(p.get_end_point())) 618 css = identifier == counter and "selected" or "" 619 620 page.tr(class_=css) 621 622 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 623 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 624 625 # Show each period. 626 627 page.td(start) 628 page.td(end) 629 630 # Show attendees and controls alongside the first period in each 631 # attendee's collection. 632 633 if first: 634 page.td(attendee, rowspan=len(periods)) 635 page.td(rowspan=len(periods)) 636 self.control("accept-%d" % i, "submit", "Accept") 637 self.control("decline-%d" % i, "submit", "Decline") 638 self.control("counter", "hidden", attendee) 639 page.td.close() 640 641 page.tr.close() 642 first = False 643 644 page.tbody.close() 645 page.table.close() 646 647 def show_conflicting_events(self): 648 649 "Show conflicting events for the current object." 650 651 page = self.page 652 recurrenceids = self._get_active_recurrences(self.uid) 653 654 # Obtain the user's timezone. 655 656 tzid = self.get_tzid() 657 periods = self.get_periods(self.obj) 658 659 # Indicate whether there are conflicting events. 660 661 conflicts = [] 662 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 663 664 for participant in self.get_current_attendees(): 665 if participant == self.user: 666 freebusy = self.store.get_freebusy(participant) 667 elif participant: 668 freebusy = self.store.get_freebusy_for_other(self.user, participant) 669 else: 670 continue 671 672 if not freebusy: 673 continue 674 675 # Obtain any time zone details from the suggested event. 676 677 _dtstart, attr = self.obj.get_item("DTSTART") 678 tzid = attr.get("TZID", tzid) 679 680 # Show any conflicts with periods of actual attendance. 681 682 participant_attr = attendee_map.get(participant) 683 partstat = participant_attr and participant_attr.get("PARTSTAT") 684 recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid) 685 686 for p in have_conflict(freebusy, periods, True): 687 if not self.recurrenceid and p.is_replaced(recurrences): 688 continue 689 690 if ( # Unidentified or different event 691 (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and 692 # Different period or unclear participation with the same period 693 (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and 694 # Participant not limited to organising 695 p.transp != "ORG" 696 ): 697 698 conflicts.append(p) 699 700 conflicts.sort() 701 702 # Show any conflicts with periods of actual attendance. 703 704 if conflicts: 705 page.p("This event conflicts with others:") 706 707 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 708 page.thead() 709 page.tr() 710 page.th("Event") 711 page.th("Start") 712 page.th("End") 713 page.tr.close() 714 page.thead.close() 715 page.tbody() 716 717 for p in conflicts: 718 719 # Provide details of any conflicting event. 720 721 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 722 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 723 724 page.tr() 725 726 # Show the event summary for the conflicting event. 727 728 page.td() 729 if p.summary: 730 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 731 else: 732 page.add("(Unspecified event)") 733 page.td.close() 734 735 page.td(start) 736 page.td(end) 737 738 page.tr.close() 739 740 page.tbody.close() 741 page.table.close() 742 743 class EventPage(EventPageFragment): 744 745 "A request handler for the event page." 746 747 def __init__(self, resource=None, messenger=None): 748 ResourceClientForObject.__init__(self, resource, messenger or Messenger()) 749 750 # Request logic methods. 751 752 def is_initial_load(self): 753 754 "Return whether the event is being loaded and shown for the first time." 755 756 return not self.env.get_args().has_key("editing") 757 758 def handle_request(self): 759 760 """ 761 Handle actions involving the current object, returning an error if one 762 occurred, or None if the request was successfully handled. 763 """ 764 765 # Handle a submitted form. 766 767 args = self.env.get_args() 768 769 # Get the possible actions. 770 771 reply = args.has_key("reply") 772 discard = args.has_key("discard") 773 create = args.has_key("create") 774 cancel = args.has_key("cancel") 775 ignore = args.has_key("ignore") 776 save = args.has_key("save") 777 accept = self.prefixed_args("accept-", int) 778 decline = self.prefixed_args("decline-", int) 779 780 have_action = reply or discard or create or cancel or ignore or save or accept or decline 781 782 if not have_action: 783 return ["action"] 784 785 # If ignoring the object, return to the calendar. 786 787 if ignore: 788 self.redirect(self.env.get_path()) 789 return None 790 791 # Update the object. 792 793 single_user = False 794 changed = False 795 796 if reply or create or cancel or save: 797 798 # Update principal event details if organiser. 799 # NOTE: Handle edited details for counter-proposals. 800 801 if self.can_change_object(): 802 803 # Update time periods (main and recurring). 804 805 try: 806 period = self.handle_main_period() 807 except PeriodError, exc: 808 return exc.args 809 810 try: 811 periods = self.handle_recurrence_periods() 812 except PeriodError, exc: 813 return exc.args 814 815 # Set the periods in the object, first obtaining removed and 816 # modified period information. 817 818 to_unschedule, to_exclude = self.get_removed_periods(periods) 819 820 changed = self.obj.set_period(period) or changed 821 changed = self.obj.set_periods(periods) or changed 822 changed = self.obj.update_exceptions(to_exclude) or changed 823 824 # Organiser-only changes... 825 826 if self.is_organiser(): 827 828 # Update summary. 829 830 if args.has_key("summary"): 831 self.obj["SUMMARY"] = [(args["summary"][0], {})] 832 833 # Obtain any new participants and those to be removed. 834 835 if self.can_change_object(): 836 attendees = self.get_attendees_from_page() 837 removed = [attendees[int(i)] for i in args.get("remove", [])] 838 added, to_cancel = self.update_attendees(attendees, removed) 839 single_user = not attendees or attendees == [self.user] 840 changed = added or changed 841 842 # Update attendee participation for the current user. 843 844 if args.has_key("partstat"): 845 self.update_participation(args["partstat"][0]) 846 847 # Process any action. 848 849 invite = not save and create and not single_user 850 save = save or create and single_user 851 852 handled = True 853 854 if reply or invite or cancel: 855 856 # Process the object and remove it from the list of requests. 857 858 if reply and self.process_received_request(changed): 859 self.remove_request() 860 861 elif self.is_organiser() and (invite or cancel): 862 863 # Invitation, uninvitation and unscheduling... 864 865 if self.process_created_request( 866 invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule): 867 868 self.remove_request() 869 870 # Save single user events. 871 872 elif save: 873 self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node()) 874 self.update_event_in_freebusy() 875 self.remove_request() 876 877 # Remove the request and the object. 878 879 elif discard: 880 self.remove_event_from_freebusy() 881 self.remove_event() 882 self.remove_request() 883 884 # Update counter-proposal records synchronously instead of assuming 885 # that the outgoing handler will have done so before the form is 886 # refreshed. 887 888 # Accept a counter-proposal and decline all others, sending a new 889 # request to all attendees. 890 891 elif accept: 892 893 # Take the first accepted proposal, although there should be only 894 # one anyway. 895 896 for i in accept: 897 attendee_uri = get_uri(args.get("counter", [])[i]) 898 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee_uri) 899 self.obj.set_periods(self.get_periods(obj)) 900 break 901 902 # Remove counter-proposals and issue a new invitation. 903 904 attendees = uri_values(args.get("counter", [])) 905 self.remove_counters(attendees) 906 self.process_created_request("REQUEST") 907 908 # Decline a counter-proposal individually. 909 910 elif decline: 911 for i in decline: 912 attendee_uri = get_uri(args.get("counter", [])[i]) 913 self.process_declined_counter(attendee_uri) 914 self.remove_counter(attendee_uri) 915 916 # Redirect to the event. 917 918 self.redirect(self.env.get_url()) 919 handled = False 920 921 else: 922 handled = False 923 924 # Upon handling an action, redirect to the main page. 925 926 if handled: 927 self.redirect(self.env.get_path()) 928 929 return None 930 931 def handle_main_period(self): 932 933 "Return period details for the main start/end period in an event." 934 935 return self.get_main_period_from_page().as_event_period() 936 937 def handle_recurrence_periods(self): 938 939 "Return period details for the recurrences specified for an event." 940 941 return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] 942 943 # Access to form-originating object information. 944 945 def get_main_period_from_page(self): 946 947 "Return the main period defined in the event form." 948 949 args = self.env.get_args() 950 951 dtend_enabled = args.get("dtend-control", [None])[0] 952 dttimes_enabled = args.get("dttimes-control", [None])[0] 953 start = self.get_date_control_values("dtstart") 954 end = self.get_date_control_values("dtend") 955 956 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), "DTSTART") 957 958 # Handle absent main period details. 959 960 if not period.get_start(): 961 return self.get_stored_main_period() 962 else: 963 return period 964 965 def get_recurrences_from_page(self): 966 967 "Return the recurrences defined in the event form." 968 969 args = self.env.get_args() 970 971 all_dtend_enabled = args.get("dtend-control-recur", []) 972 all_dttimes_enabled = args.get("dttimes-control-recur", []) 973 all_starts = self.get_date_control_values("dtstart-recur", multiple=True) 974 all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") 975 all_origins = args.get("recur-origin", []) 976 977 periods = [] 978 979 for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \ 980 enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)): 981 982 dtend_enabled = str(index) in all_dtend_enabled 983 dttimes_enabled = str(index) in all_dttimes_enabled 984 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin) 985 periods.append(period) 986 987 return periods 988 989 def set_recurrences_in_page(self, recurrences): 990 991 "Set the recurrences defined in the event form." 992 993 args = self.env.get_args() 994 995 args["dtend-control-recur"] = [] 996 args["dttimes-control-recur"] = [] 997 args["recur-origin"] = [] 998 999 all_starts = [] 1000 all_ends = [] 1001 1002 for index, period in enumerate(recurrences): 1003 if period.end_enabled: 1004 args["dtend-control-recur"].append(str(index)) 1005 if period.times_enabled: 1006 args["dttimes-control-recur"].append(str(index)) 1007 args["recur-origin"].append(period.origin or "") 1008 1009 all_starts.append(period.get_form_start()) 1010 all_ends.append(period.get_form_end()) 1011 1012 self.set_date_control_values("dtstart-recur", all_starts) 1013 self.set_date_control_values("dtend-recur", all_ends, tzid_name="dtstart-recur") 1014 1015 def get_removed_periods(self, periods): 1016 1017 """ 1018 Return those from the recurrence 'periods' to remove upon updating an 1019 event along with those to exclude in a tuple of the form (unscheduled, 1020 excluded). 1021 """ 1022 1023 args = self.env.get_args() 1024 to_unschedule = [] 1025 to_exclude = [] 1026 1027 for i in args.get("recur-remove", []): 1028 try: 1029 period = periods[int(i)] 1030 except (IndexError, ValueError): 1031 continue 1032 1033 if not self.can_edit_recurrence(period): 1034 to_unschedule.append(period) 1035 else: 1036 to_exclude.append(period) 1037 1038 return to_unschedule, to_exclude 1039 1040 def get_attendees_from_page(self): 1041 1042 """ 1043 Return attendees from the request, using any stored attributes to obtain 1044 verbose details. 1045 """ 1046 1047 return self.get_verbose_attendees(self.env.get_args().get("attendee", [])) 1048 1049 def get_verbose_attendees(self, attendees): 1050 1051 """ 1052 Use any stored attributes to obtain verbose details for the given 1053 'attendees'. 1054 """ 1055 1056 attendee_map = self.obj.get_value_map("ATTENDEE") 1057 return [get_verbose_address(value, attendee_map.get(value)) for value in attendees] 1058 1059 def update_attendees_from_page(self): 1060 1061 "Add or remove attendees. This does not affect the stored object." 1062 1063 args = self.env.get_args() 1064 1065 attendees = self.get_attendees_from_page() 1066 1067 if args.has_key("add"): 1068 attendees.append("") 1069 1070 # Only actually remove attendees if the event is unsent, if the attendee 1071 # is new, or if it is the current user being removed. 1072 1073 if args.has_key("remove"): 1074 still_to_remove = [] 1075 correction = 0 1076 1077 for i in args["remove"]: 1078 try: 1079 i = int(i) - correction 1080 attendee = attendees[i] 1081 except (IndexError, ValueError): 1082 continue 1083 1084 if self.can_remove_attendee(get_uri(attendee)): 1085 del attendees[i] 1086 correction += 1 1087 else: 1088 still_to_remove.append(str(i)) 1089 1090 args["remove"] = still_to_remove 1091 1092 args["attendee"] = attendees 1093 return attendees 1094 1095 def update_recurrences_from_page(self): 1096 1097 "Add or remove recurrences. This does not affect the stored object." 1098 1099 args = self.env.get_args() 1100 1101 recurrences = self.get_recurrences_from_page() 1102 1103 # NOTE: Addition of recurrences to be supported. 1104 1105 # Only actually remove recurrences if the event is unsent, or if the 1106 # recurrence is new, but only for explicit recurrences. 1107 1108 if args.has_key("recur-remove"): 1109 still_to_remove = [] 1110 correction = 0 1111 1112 for i in args["recur-remove"]: 1113 try: 1114 i = int(i) - correction 1115 recurrence = recurrences[i] 1116 except (IndexError, ValueError): 1117 continue 1118 1119 if self.can_remove_recurrence(recurrence): 1120 del recurrences[i] 1121 correction += 1 1122 else: 1123 still_to_remove.append(str(i)) 1124 1125 args["recur-remove"] = still_to_remove 1126 1127 self.set_recurrences_in_page(recurrences) 1128 return recurrences 1129 1130 # Access to current object information. 1131 1132 def get_current_main_period(self): 1133 1134 """ 1135 Return the currently active main period for the current object depending 1136 on whether editing has begun or whether the object has just been loaded. 1137 """ 1138 1139 if self.is_initial_load() or not self.can_change_object(): 1140 return self.get_stored_main_period() 1141 else: 1142 return self.get_main_period_from_page() 1143 1144 def get_current_recurrences(self): 1145 1146 """ 1147 Return recurrences for the current object using the original object 1148 details where no editing is in progress, using form data otherwise. 1149 """ 1150 1151 if self.is_initial_load() or not self.can_change_object(): 1152 return self.get_stored_recurrences() 1153 else: 1154 return self.get_recurrences_from_page() 1155 1156 def update_current_recurrences(self): 1157 1158 "Return an updated collection of recurrences for the current object." 1159 1160 if self.is_initial_load() or not self.can_change_object(): 1161 return self.get_stored_recurrences() 1162 else: 1163 return self.update_recurrences_from_page() 1164 1165 def get_current_attendees(self): 1166 1167 """ 1168 Return attendees for the current object depending on whether the object 1169 has been edited or instead provides such information from its stored 1170 form. 1171 """ 1172 1173 if self.is_initial_load() or not self.can_change_object(): 1174 return self.get_stored_attendees() 1175 else: 1176 return self.get_attendees_from_page() 1177 1178 def update_current_attendees(self): 1179 1180 "Return an updated collection of attendees for the current object." 1181 1182 if self.is_initial_load() or not self.can_change_object(): 1183 return self.get_stored_attendees() 1184 else: 1185 return self.update_attendees_from_page() 1186 1187 # Full page output methods. 1188 1189 def show(self, path_info): 1190 1191 "Show an object request using the given 'path_info' for the current user." 1192 1193 uid, recurrenceid = self.get_identifiers(path_info) 1194 obj = self.get_stored_object(uid, recurrenceid) 1195 self.set_object(obj) 1196 1197 if not obj: 1198 return False 1199 1200 errors = self.handle_request() 1201 1202 if not errors: 1203 return True 1204 1205 self.update_current_attendees() 1206 self.update_current_recurrences() 1207 1208 self.new_page(title="Event") 1209 self.show_object_on_page(errors) 1210 1211 return True 1212 1213 # vim: tabstop=4 expandtab shiftwidth=4