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