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 page.p("The following counter-proposals have been received for this event:") 544 545 page.table(cellspacing=5, cellpadding=5, class_="counters") 546 page.thead() 547 page.tr() 548 page.th("Attendee", rowspan=2) 549 page.th("Periods", colspan=2) 550 page.tr.close() 551 page.tr() 552 page.th("Start") 553 page.th("End") 554 page.tr.close() 555 page.thead.close() 556 page.tbody() 557 558 for i, attendee in enumerate(attendees): 559 attendee_uri = get_uri(attendee) 560 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee_uri) 561 periods = self.get_periods(obj) 562 563 first = True 564 for p in periods: 565 identifier = "%s-%s" % (format_datetime(p.get_start_point()), format_datetime(p.get_end_point())) 566 css = identifier == counter and "selected" or "" 567 568 page.tr(class_=css) 569 570 if first: 571 page.td(attendee, rowspan=len(periods)) 572 573 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 574 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 575 576 page.td(start) 577 page.td(end) 578 579 if first: 580 page.td(rowspan=len(periods)) 581 self.control("accept-%d" % i, "submit", "Accept") 582 self.control("decline-%d" % i, "submit", "Decline") 583 self.control("counter", "hidden", attendee) 584 page.td.close() 585 586 page.tr.close() 587 first = False 588 589 page.tbody.close() 590 page.table.close() 591 592 def show_conflicting_events(self): 593 594 "Show conflicting events for the current object." 595 596 page = self.page 597 recurrenceids = self._get_active_recurrences(self.uid) 598 599 # Obtain the user's timezone. 600 601 tzid = self.get_tzid() 602 periods = self.get_periods(self.obj) 603 604 # Indicate whether there are conflicting events. 605 606 conflicts = [] 607 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 608 609 for participant in self.get_current_attendees(): 610 if participant == self.user: 611 freebusy = self.store.get_freebusy(participant) 612 elif participant: 613 freebusy = self.store.get_freebusy_for_other(self.user, participant) 614 else: 615 continue 616 617 if not freebusy: 618 continue 619 620 # Obtain any time zone details from the suggested event. 621 622 _dtstart, attr = self.obj.get_item("DTSTART") 623 tzid = attr.get("TZID", tzid) 624 625 # Show any conflicts with periods of actual attendance. 626 627 participant_attr = attendee_map.get(participant) 628 partstat = participant_attr and participant_attr.get("PARTSTAT") 629 recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid) 630 631 for p in have_conflict(freebusy, periods, True): 632 if not self.recurrenceid and p.is_replaced(recurrences): 633 continue 634 635 if ( # Unidentified or different event 636 (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and 637 # Different period or unclear participation with the same period 638 (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and 639 # Participant not limited to organising 640 p.transp != "ORG" 641 ): 642 643 conflicts.append(p) 644 645 conflicts.sort() 646 647 # Show any conflicts with periods of actual attendance. 648 649 if conflicts: 650 page.p("This event conflicts with others:") 651 652 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 653 page.thead() 654 page.tr() 655 page.th("Event") 656 page.th("Start") 657 page.th("End") 658 page.tr.close() 659 page.thead.close() 660 page.tbody() 661 662 for p in conflicts: 663 664 # Provide details of any conflicting event. 665 666 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 667 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 668 669 page.tr() 670 671 # Show the event summary for the conflicting event. 672 673 page.td() 674 if p.summary: 675 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 676 else: 677 page.add("(Unspecified event)") 678 page.td.close() 679 680 page.td(start) 681 page.td(end) 682 683 page.tr.close() 684 685 page.tbody.close() 686 page.table.close() 687 688 class EventPage(EventPageFragment): 689 690 "A request handler for the event page." 691 692 def __init__(self, resource=None, messenger=None): 693 ResourceClientForObject.__init__(self, resource, messenger or Messenger()) 694 695 # Request logic methods. 696 697 def is_initial_load(self): 698 699 "Return whether the event is being loaded and shown for the first time." 700 701 return not self.env.get_args().has_key("editing") 702 703 def handle_request(self): 704 705 """ 706 Handle actions involving the current object, returning an error if one 707 occurred, or None if the request was successfully handled. 708 """ 709 710 # Handle a submitted form. 711 712 args = self.env.get_args() 713 714 # Get the possible actions. 715 716 reply = args.has_key("reply") 717 discard = args.has_key("discard") 718 create = args.has_key("create") 719 cancel = args.has_key("cancel") 720 ignore = args.has_key("ignore") 721 save = args.has_key("save") 722 accept = self.prefixed_args("accept-", int) 723 decline = self.prefixed_args("decline-", int) 724 725 have_action = reply or discard or create or cancel or ignore or save or accept or decline 726 727 if not have_action: 728 return ["action"] 729 730 # If ignoring the object, return to the calendar. 731 732 if ignore: 733 self.redirect(self.env.get_path()) 734 return None 735 736 # Update the object. 737 738 single_user = False 739 changed = False 740 741 if reply or create or cancel or save: 742 743 # Update principal event details if organiser. 744 # NOTE: Handle edited details for counter-proposals. 745 746 if self.can_change_object(): 747 748 # Update time periods (main and recurring). 749 750 try: 751 period = self.handle_main_period() 752 except PeriodError, exc: 753 return exc.args 754 755 try: 756 periods = self.handle_recurrence_periods() 757 except PeriodError, exc: 758 return exc.args 759 760 # Set the periods in the object, first obtaining removed and 761 # modified period information. 762 763 to_unschedule, to_exclude = self.get_removed_periods(periods) 764 765 changed = self.obj.set_period(period) or changed 766 changed = self.obj.set_periods(periods) or changed 767 changed = self.obj.update_exceptions(to_exclude) or changed 768 769 # Organiser-only changes... 770 771 if self.is_organiser(): 772 773 # Update summary. 774 775 if args.has_key("summary"): 776 self.obj["SUMMARY"] = [(args["summary"][0], {})] 777 778 # Obtain any new participants and those to be removed. 779 780 if self.can_change_object(): 781 attendees = self.get_attendees_from_page() 782 removed = [attendees[int(i)] for i in args.get("remove", [])] 783 added, to_cancel = self.update_attendees(attendees, removed) 784 single_user = not attendees or attendees == [self.user] 785 changed = added or changed 786 787 # Update attendee participation for the current user. 788 789 if args.has_key("partstat"): 790 self.update_participation(args["partstat"][0]) 791 792 # Process any action. 793 794 invite = not save and create and not single_user 795 save = save or create and single_user 796 797 handled = True 798 799 if reply or invite or cancel: 800 801 # Process the object and remove it from the list of requests. 802 803 if reply and self.process_received_request(changed): 804 self.remove_request() 805 806 elif self.is_organiser() and (invite or cancel): 807 808 # Invitation, uninvitation and unscheduling... 809 810 if self.process_created_request( 811 invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule): 812 813 self.remove_request() 814 815 # Save single user events. 816 817 elif save: 818 self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node()) 819 self.update_event_in_freebusy() 820 self.remove_request() 821 822 # Remove the request and the object. 823 824 elif discard: 825 self.remove_event_from_freebusy() 826 self.remove_event() 827 self.remove_request() 828 829 # Update counter-proposal records synchronously instead of assuming 830 # that the outgoing handler will have done so before the form is 831 # refreshed. 832 833 # Accept a counter-proposal and decline all others, sending a new 834 # request to all attendees. 835 836 elif accept: 837 838 # Take the first accepted proposal, although there should be only 839 # one anyway. 840 841 for i in accept: 842 attendee_uri = get_uri(args.get("counter", [])[i]) 843 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee_uri) 844 self.obj.set_periods(self.get_periods(obj)) 845 break 846 847 # Remove counter-proposals and issue a new invitation. 848 849 attendees = uri_values(args.get("counter", [])) 850 self.remove_counters(attendees) 851 self.process_created_request("REQUEST") 852 853 # Decline a counter-proposal individually. 854 855 elif decline: 856 for i in decline: 857 attendee_uri = get_uri(args.get("counter", [])[i]) 858 self.process_declined_counter(attendee_uri) 859 self.remove_counter(attendee_uri) 860 861 # Redirect to the event. 862 863 self.redirect(self.env.get_url()) 864 handled = False 865 866 else: 867 handled = False 868 869 # Upon handling an action, redirect to the main page. 870 871 if handled: 872 self.redirect(self.env.get_path()) 873 874 return None 875 876 def handle_main_period(self): 877 878 "Return period details for the main start/end period in an event." 879 880 return self.get_main_period_from_page().as_event_period() 881 882 def handle_recurrence_periods(self): 883 884 "Return period details for the recurrences specified for an event." 885 886 return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] 887 888 # Access to form-originating object information. 889 890 def get_main_period_from_page(self): 891 892 "Return the main period defined in the event form." 893 894 args = self.env.get_args() 895 896 dtend_enabled = args.get("dtend-control", [None])[0] 897 dttimes_enabled = args.get("dttimes-control", [None])[0] 898 start = self.get_date_control_values("dtstart") 899 end = self.get_date_control_values("dtend") 900 901 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), "DTSTART") 902 903 # Handle absent main period details. 904 905 if not period.get_start(): 906 return self.get_stored_main_period() 907 else: 908 return period 909 910 def get_recurrences_from_page(self): 911 912 "Return the recurrences defined in the event form." 913 914 args = self.env.get_args() 915 916 all_dtend_enabled = args.get("dtend-control-recur", []) 917 all_dttimes_enabled = args.get("dttimes-control-recur", []) 918 all_starts = self.get_date_control_values("dtstart-recur", multiple=True) 919 all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") 920 all_origins = args.get("recur-origin", []) 921 922 periods = [] 923 924 for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \ 925 enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)): 926 927 dtend_enabled = str(index) in all_dtend_enabled 928 dttimes_enabled = str(index) in all_dttimes_enabled 929 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin) 930 periods.append(period) 931 932 return periods 933 934 def set_recurrences_in_page(self, recurrences): 935 936 "Set the recurrences defined in the event form." 937 938 args = self.env.get_args() 939 940 args["dtend-control-recur"] = [] 941 args["dttimes-control-recur"] = [] 942 args["recur-origin"] = [] 943 944 all_starts = [] 945 all_ends = [] 946 947 for index, period in enumerate(recurrences): 948 if period.end_enabled: 949 args["dtend-control-recur"].append(str(index)) 950 if period.times_enabled: 951 args["dttimes-control-recur"].append(str(index)) 952 args["recur-origin"].append(period.origin or "") 953 954 all_starts.append(period.get_form_start()) 955 all_ends.append(period.get_form_end()) 956 957 self.set_date_control_values("dtstart-recur", all_starts) 958 self.set_date_control_values("dtend-recur", all_ends, tzid_name="dtstart-recur") 959 960 def get_removed_periods(self, periods): 961 962 """ 963 Return those from the recurrence 'periods' to remove upon updating an 964 event along with those to exclude in a tuple of the form (unscheduled, 965 excluded). 966 """ 967 968 args = self.env.get_args() 969 to_unschedule = [] 970 to_exclude = [] 971 972 for i in args.get("recur-remove", []): 973 try: 974 period = periods[int(i)] 975 except (IndexError, ValueError): 976 continue 977 978 if not self.can_edit_recurrence(period): 979 to_unschedule.append(period) 980 else: 981 to_exclude.append(period) 982 983 return to_unschedule, to_exclude 984 985 def get_attendees_from_page(self): 986 987 """ 988 Return attendees from the request, using any stored attributes to obtain 989 verbose details. 990 """ 991 992 return self.get_verbose_attendees(self.env.get_args().get("attendee", [])) 993 994 def get_verbose_attendees(self, attendees): 995 996 """ 997 Use any stored attributes to obtain verbose details for the given 998 'attendees'. 999 """ 1000 1001 attendee_map = self.obj.get_value_map("ATTENDEE") 1002 return [get_verbose_address(value, attendee_map.get(value)) for value in attendees] 1003 1004 def update_attendees_from_page(self): 1005 1006 "Add or remove attendees. This does not affect the stored object." 1007 1008 args = self.env.get_args() 1009 1010 attendees = self.get_attendees_from_page() 1011 1012 if args.has_key("add"): 1013 attendees.append("") 1014 1015 # Only actually remove attendees if the event is unsent, if the attendee 1016 # is new, or if it is the current user being removed. 1017 1018 if args.has_key("remove"): 1019 still_to_remove = [] 1020 correction = 0 1021 1022 for i in args["remove"]: 1023 try: 1024 i = int(i) - correction 1025 attendee = attendees[i] 1026 except (IndexError, ValueError): 1027 continue 1028 1029 if self.can_remove_attendee(get_uri(attendee)): 1030 del attendees[i] 1031 correction += 1 1032 else: 1033 still_to_remove.append(str(i)) 1034 1035 args["remove"] = still_to_remove 1036 1037 args["attendee"] = attendees 1038 return attendees 1039 1040 def update_recurrences_from_page(self): 1041 1042 "Add or remove recurrences. This does not affect the stored object." 1043 1044 args = self.env.get_args() 1045 1046 recurrences = self.get_recurrences_from_page() 1047 1048 # NOTE: Addition of recurrences to be supported. 1049 1050 # Only actually remove recurrences if the event is unsent, or if the 1051 # recurrence is new, but only for explicit recurrences. 1052 1053 if args.has_key("recur-remove"): 1054 still_to_remove = [] 1055 correction = 0 1056 1057 for i in args["recur-remove"]: 1058 try: 1059 i = int(i) - correction 1060 recurrence = recurrences[i] 1061 except (IndexError, ValueError): 1062 continue 1063 1064 if self.can_remove_recurrence(recurrence): 1065 del recurrences[i] 1066 correction += 1 1067 else: 1068 still_to_remove.append(str(i)) 1069 1070 args["recur-remove"] = still_to_remove 1071 1072 self.set_recurrences_in_page(recurrences) 1073 return recurrences 1074 1075 # Access to current object information. 1076 1077 def get_current_main_period(self): 1078 1079 """ 1080 Return the currently active main period for the current object depending 1081 on whether editing has begun or whether the object has just been loaded. 1082 """ 1083 1084 if self.is_initial_load() or not self.can_change_object(): 1085 return self.get_stored_main_period() 1086 else: 1087 return self.get_main_period_from_page() 1088 1089 def get_current_recurrences(self): 1090 1091 """ 1092 Return recurrences for the current object using the original object 1093 details where no editing is in progress, using form data otherwise. 1094 """ 1095 1096 if self.is_initial_load() or not self.can_change_object(): 1097 return self.get_stored_recurrences() 1098 else: 1099 return self.get_recurrences_from_page() 1100 1101 def update_current_recurrences(self): 1102 1103 "Return an updated collection of recurrences for the current object." 1104 1105 if self.is_initial_load() or not self.can_change_object(): 1106 return self.get_stored_recurrences() 1107 else: 1108 return self.update_recurrences_from_page() 1109 1110 def get_current_attendees(self): 1111 1112 """ 1113 Return attendees for the current object depending on whether the object 1114 has been edited or instead provides such information from its stored 1115 form. 1116 """ 1117 1118 if self.is_initial_load() or not self.can_change_object(): 1119 return self.get_stored_attendees() 1120 else: 1121 return self.get_attendees_from_page() 1122 1123 def update_current_attendees(self): 1124 1125 "Return an updated collection of attendees for the current object." 1126 1127 if self.is_initial_load() or not self.can_change_object(): 1128 return self.get_stored_attendees() 1129 else: 1130 return self.update_attendees_from_page() 1131 1132 # Full page output methods. 1133 1134 def show(self, path_info): 1135 1136 "Show an object request using the given 'path_info' for the current user." 1137 1138 uid, recurrenceid = self.get_identifiers(path_info) 1139 obj = self.get_stored_object(uid, recurrenceid) 1140 self.set_object(obj) 1141 1142 if not obj: 1143 return False 1144 1145 errors = self.handle_request() 1146 1147 if not errors: 1148 return True 1149 1150 self.update_current_attendees() 1151 self.update_current_recurrences() 1152 1153 self.new_page(title="Event") 1154 self.show_object_on_page(errors) 1155 1156 return True 1157 1158 # vim: tabstop=4 expandtab shiftwidth=4