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