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