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