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 # Decline a counter-proposal. 720 721 if decline: 722 for s in decline: 723 try: 724 i = int(s) 725 except (IndexError, ValueError): 726 pass 727 else: 728 attendee_uri = get_uri(args.get("decline", [])[i]) 729 self.process_declined_counter(attendee_uri) 730 731 # Update the counter-proposals synchronously instead of 732 # assuming that the outgoing handler will have done so 733 # before the form is refreshed. 734 735 self.remove_counter(attendee_uri) 736 737 have_action = reply or discard or create or cancel or ignore or save 738 739 if not have_action: 740 return ["action"] 741 742 # If ignoring the object, return to the calendar. 743 744 if ignore: 745 self.redirect(self.env.get_path()) 746 return None 747 748 # Update the object. 749 750 single_user = False 751 752 if reply or create or cancel or save: 753 754 # Update principal event details if organiser. 755 756 if self.is_organiser(): 757 758 # Update time periods (main and recurring). 759 760 try: 761 period = self.handle_main_period() 762 except PeriodError, exc: 763 return exc.args 764 765 try: 766 periods = self.handle_recurrence_periods() 767 except PeriodError, exc: 768 return exc.args 769 770 # Set the periods in the object, first obtaining removed and 771 # modified period information. 772 773 to_unschedule, to_exclude = self.get_removed_periods(periods) 774 775 self.obj.set_period(period) 776 self.obj.set_periods(periods) 777 self.obj.update_exceptions(to_exclude) 778 779 # Update summary. 780 781 if args.has_key("summary"): 782 self.obj["SUMMARY"] = [(args["summary"][0], {})] 783 784 # Obtain any participants and those to be removed. 785 786 attendees = map(lambda s: s and get_uri(s), self.get_attendees_from_page()) 787 removed = [attendees[int(i)] for i in args.get("remove", [])] 788 to_cancel = self.update_attendees(self.obj, attendees, removed) 789 single_user = not attendees or attendees == [self.user] 790 791 # Update attendee participation for the current user. 792 793 if args.has_key("partstat"): 794 self.update_participation(self.obj, args["partstat"][0]) 795 796 # Process any action. 797 798 invite = not save and create and not single_user 799 save = save or create and single_user 800 801 handled = True 802 803 if reply or invite or cancel: 804 805 # Process the object and remove it from the list of requests. 806 807 if reply and self.process_received_request(): 808 self.remove_request(self.uid, self.recurrenceid) 809 810 elif self.is_organiser() and (invite or cancel): 811 812 # Invitation, uninvitation and unscheduling... 813 814 if self.process_created_request( 815 invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule): 816 817 self.remove_request(self.uid, self.recurrenceid) 818 819 # Save single user events. 820 821 elif save: 822 self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node()) 823 self.update_event_in_freebusy() 824 self.remove_request(self.uid, self.recurrenceid) 825 826 # Remove the request and the object. 827 828 elif discard: 829 self.remove_event_from_freebusy() 830 self.remove_event(self.uid, self.recurrenceid) 831 self.remove_request(self.uid, self.recurrenceid) 832 833 else: 834 handled = False 835 836 # Upon handling an action, redirect to the main page. 837 838 if handled: 839 self.redirect(self.env.get_path()) 840 841 return None 842 843 def handle_main_period(self): 844 845 "Return period details for the main start/end period in an event." 846 847 return self.get_main_period_from_page().as_event_period() 848 849 def handle_recurrence_periods(self): 850 851 "Return period details for the recurrences specified for an event." 852 853 return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] 854 855 # Access to form-originating object information. 856 857 def get_main_period_from_page(self): 858 859 "Return the main period defined in the event form." 860 861 args = self.env.get_args() 862 863 dtend_enabled = args.get("dtend-control", [None])[0] 864 dttimes_enabled = args.get("dttimes-control", [None])[0] 865 start = self.get_date_control_values("dtstart") 866 end = self.get_date_control_values("dtend") 867 868 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid()) 869 870 # Handle absent main period details. 871 872 if not period.get_start(): 873 return self.get_stored_main_period() 874 else: 875 return period 876 877 def get_recurrences_from_page(self): 878 879 "Return the recurrences defined in the event form." 880 881 args = self.env.get_args() 882 883 all_dtend_enabled = args.get("dtend-control-recur", []) 884 all_dttimes_enabled = args.get("dttimes-control-recur", []) 885 all_starts = self.get_date_control_values("dtstart-recur", multiple=True) 886 all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") 887 all_origins = args.get("recur-origin", []) 888 889 periods = [] 890 891 for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \ 892 enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)): 893 894 dtend_enabled = str(index) in all_dtend_enabled 895 dttimes_enabled = str(index) in all_dttimes_enabled 896 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin) 897 periods.append(period) 898 899 return periods 900 901 def set_recurrences_in_page(self, recurrences): 902 903 "Set the recurrences defined in the event form." 904 905 args = self.env.get_args() 906 907 args["dtend-control-recur"] = [] 908 args["dttimes-control-recur"] = [] 909 args["recur-origin"] = [] 910 911 all_starts = [] 912 all_ends = [] 913 914 for index, period in enumerate(recurrences): 915 if period.end_enabled: 916 args["dtend-control-recur"].append(str(index)) 917 if period.times_enabled: 918 args["dttimes-control-recur"].append(str(index)) 919 args["recur-origin"].append(period.origin or "") 920 921 all_starts.append(period.get_form_start()) 922 all_ends.append(period.get_form_end()) 923 924 self.set_date_control_values("dtstart-recur", all_starts) 925 self.set_date_control_values("dtend-recur", all_ends, tzid_name="dtstart-recur") 926 927 def get_removed_periods(self, periods): 928 929 """ 930 Return those from the recurrence 'periods' to remove upon updating an 931 event along with those to exclude in a tuple of the form (unscheduled, 932 excluded). 933 """ 934 935 args = self.env.get_args() 936 to_unschedule = [] 937 to_exclude = [] 938 939 for i in args.get("recur-remove", []): 940 try: 941 period = periods[int(i)] 942 except (IndexError, ValueError): 943 continue 944 945 if not self.can_edit_recurrence(period): 946 to_unschedule.append(period) 947 else: 948 to_exclude.append(period) 949 950 return to_unschedule, to_exclude 951 952 def get_attendees_from_page(self): 953 954 """ 955 Return attendees from the request, using any stored attributes to obtain 956 verbose details. 957 """ 958 959 return self.get_verbose_attendees(self.env.get_args().get("attendee", [])) 960 961 def get_verbose_attendees(self, attendees): 962 963 """ 964 Use any stored attributes to obtain verbose details for the given 965 'attendees'. 966 """ 967 968 attendee_map = self.obj.get_value_map("ATTENDEE") 969 return [get_verbose_address(value, attendee_map.get(value)) for value in attendees] 970 971 def update_attendees_from_page(self): 972 973 "Add or remove attendees. This does not affect the stored object." 974 975 args = self.env.get_args() 976 977 attendees = self.get_attendees_from_page() 978 979 if args.has_key("add"): 980 attendees.append("") 981 982 # Only actually remove attendees if the event is unsent, if the attendee 983 # is new, or if it is the current user being removed. 984 985 if args.has_key("remove"): 986 still_to_remove = [] 987 correction = 0 988 989 for i in args["remove"]: 990 try: 991 i = int(i) - correction 992 attendee = attendees[i] 993 except (IndexError, ValueError): 994 continue 995 996 if self.can_remove_attendee(get_uri(attendee)): 997 del attendees[i] 998 correction += 1 999 else: 1000 still_to_remove.append(str(i)) 1001 1002 args["remove"] = still_to_remove 1003 1004 args["attendee"] = attendees 1005 return attendees 1006 1007 def update_recurrences_from_page(self): 1008 1009 "Add or remove recurrences. This does not affect the stored object." 1010 1011 args = self.env.get_args() 1012 1013 recurrences = self.get_recurrences_from_page() 1014 1015 # NOTE: Addition of recurrences to be supported. 1016 1017 # Only actually remove recurrences if the event is unsent, or if the 1018 # recurrence is new, but only for explicit recurrences. 1019 1020 if args.has_key("recur-remove"): 1021 still_to_remove = [] 1022 correction = 0 1023 1024 for i in args["recur-remove"]: 1025 try: 1026 i = int(i) - correction 1027 recurrence = recurrences[i] 1028 except (IndexError, ValueError): 1029 continue 1030 1031 if self.can_remove_recurrence(recurrence): 1032 del recurrences[i] 1033 correction += 1 1034 else: 1035 still_to_remove.append(str(i)) 1036 1037 args["recur-remove"] = still_to_remove 1038 1039 self.set_recurrences_in_page(recurrences) 1040 return recurrences 1041 1042 # Access to current object information. 1043 1044 def get_current_main_period(self): 1045 1046 """ 1047 Return the currently active main period for the current object depending 1048 on whether editing has begun or whether the object has just been loaded. 1049 """ 1050 1051 if self.is_initial_load() or not self.is_organiser(): 1052 return self.get_stored_main_period() 1053 else: 1054 return self.get_main_period_from_page() 1055 1056 def get_current_recurrences(self): 1057 1058 """ 1059 Return recurrences for the current object using the original object 1060 details where no editing is in progress, using form data otherwise. 1061 """ 1062 1063 if self.is_initial_load() or not self.is_organiser(): 1064 return self.get_stored_recurrences() 1065 else: 1066 return self.get_recurrences_from_page() 1067 1068 def update_current_recurrences(self): 1069 1070 "Return an updated collection of recurrences for the current object." 1071 1072 if self.is_initial_load() or not self.is_organiser(): 1073 return self.get_stored_recurrences() 1074 else: 1075 return self.update_recurrences_from_page() 1076 1077 def get_current_attendees(self): 1078 1079 """ 1080 Return attendees for the current object depending on whether the object 1081 has been edited or instead provides such information from its stored 1082 form. 1083 """ 1084 1085 if self.is_initial_load() or not self.is_organiser(): 1086 return self.get_stored_attendees() 1087 else: 1088 return self.get_attendees_from_page() 1089 1090 def update_current_attendees(self): 1091 1092 "Return an updated collection of attendees for the current object." 1093 1094 if self.is_initial_load() or not self.is_organiser(): 1095 return self.get_stored_attendees() 1096 else: 1097 return self.update_attendees_from_page() 1098 1099 # Full page output methods. 1100 1101 def show(self, path_info): 1102 1103 "Show an object request using the given 'path_info' for the current user." 1104 1105 uid, recurrenceid = self.get_identifiers(path_info) 1106 obj = self.get_stored_object(uid, recurrenceid) 1107 self.set_object(obj) 1108 1109 if not obj: 1110 return False 1111 1112 errors = self.handle_request() 1113 1114 if not errors: 1115 return True 1116 1117 self.update_current_attendees() 1118 self.update_current_recurrences() 1119 1120 self.new_page(title="Event") 1121 self.show_object_on_page(errors) 1122 1123 return True 1124 1125 # vim: tabstop=4 expandtab shiftwidth=4