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