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