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