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, 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 uri_values(self.obj.get_values("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 = 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(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 page.add(value) 318 page.td.close() 319 page.tr.close() 320 321 page.tbody.close() 322 page.table.close() 323 324 self.show_recurrences(errors) 325 self.show_counters() 326 self.show_conflicting_events() 327 self.show_request_controls() 328 329 page.form.close() 330 331 def show_attendee(self, i, attendee, attendee_attr): 332 333 """ 334 For the current object, show the attendee in position 'i' with the given 335 'attendee' value, having 'attendee_attr' as any stored attributes. 336 """ 337 338 page = self.page 339 args = self.env.get_args() 340 341 partstat = attendee_attr and attendee_attr.get("PARTSTAT") 342 343 page.td(class_="objectvalue") 344 345 # Show a form control as organiser for new attendees. 346 347 if self.is_organiser() and self.can_edit_attendee(attendee): 348 self.control("attendee", "value", attendee, size="40") 349 else: 350 self.control("attendee", "hidden", attendee) 351 page.add(attendee) 352 page.add(" ") 353 354 # Show participation status, editable for the current user. 355 356 if attendee == self.user: 357 self.menu("partstat", partstat, self.partstat_items, "partstat") 358 359 # Allow the participation indicator to act as a submit 360 # button in order to refresh the page and show a control for 361 # the current user, if indicated. 362 363 elif self.is_organiser() and self.attendee_is_new(attendee): 364 self.control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh") 365 page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat") 366 367 # Otherwise, just show a label with the participation status. 368 369 else: 370 page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat") 371 372 # Permit organisers to remove attendees. 373 374 if self.is_organiser(): 375 376 # Permit the removal of newly-added attendees. 377 378 remove_type = self.can_remove_attendee(attendee) and "submit" or "checkbox" 379 self.control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove") 380 381 page.label("Remove", for_="remove-%d" % i, class_="remove") 382 page.label(for_="remove-%d" % i, class_="removed") 383 page.add("(Uninvited)") 384 page.span("Re-invite", class_="action") 385 page.label.close() 386 387 page.td.close() 388 389 def show_recurrences(self, errors=None): 390 391 """ 392 Show recurrences for the current object. If 'errors' is given, show a 393 suitable message for the different errors provided. 394 """ 395 396 page = self.page 397 398 # Obtain any parent object if this object is a specific recurrence. 399 400 if self.recurrenceid: 401 parent = self.get_stored_object(self.uid, None) 402 if not parent: 403 return 404 405 page.p() 406 page.a("This event modifies a recurring event.", href=self.link_to(self.uid)) 407 page.p.close() 408 409 # Obtain the periods associated with the event. 410 # NOTE: Add a control to add recurrences here. 411 412 recurrences = self.get_current_recurrences() 413 414 if len(recurrences) < 1: 415 return 416 417 recurrenceids = self._get_recurrences(self.uid) 418 419 page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) 420 421 # Show each recurrence in a separate table if editable. 422 423 if self.is_organiser() and recurrences: 424 425 for index, period in enumerate(recurrences): 426 self.show_recurrence(index, period, self.recurrenceid, recurrenceids, errors) 427 428 # Otherwise, use a compact single table. 429 430 else: 431 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 432 page.caption("Occurrences") 433 page.thead() 434 page.tr() 435 page.th("Start", class_="objectheading start") 436 page.th("End", class_="objectheading end") 437 page.tr.close() 438 page.thead.close() 439 page.tbody() 440 441 for index, period in enumerate(recurrences): 442 page.tr() 443 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, True) 444 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, False) 445 page.tr.close() 446 447 page.tbody.close() 448 page.table.close() 449 450 def show_recurrence(self, index, period, recurrenceid, recurrenceids, errors=None): 451 452 """ 453 Show recurrence controls for a recurrence provided by the current object 454 with the given 'index' position in the list of periods, the given 455 'period' details, where a 'recurrenceid' indicates any specific 456 recurrence, and where 'recurrenceids' indicates all known additional 457 recurrences for the object. 458 459 If 'errors' is given, show a suitable message for the different errors 460 provided. 461 """ 462 463 page = self.page 464 args = self.env.get_args() 465 466 p = event_period_from_period(period) 467 replaced = not recurrenceid and p.is_replaced(recurrenceids) 468 469 # Isolate the controls from neighbouring tables. 470 471 page.div() 472 473 self.show_object_datetime_controls(period, index) 474 475 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 476 page.caption(period.origin == "RRULE" and "Occurrence from rule" or "Occurrence") 477 page.tbody() 478 479 page.tr() 480 error = errors and ("dtstart", index) in errors and " error" or "" 481 page.th("Start", class_="objectheading start%s" % error) 482 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, True) 483 page.tr.close() 484 page.tr() 485 error = errors and ("dtend", index) in errors and " error" or "" 486 page.th("End", class_="objectheading end%s" % error) 487 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, False) 488 page.tr.close() 489 490 # Permit the removal of recurrences. 491 492 if not replaced: 493 page.tr() 494 page.th("") 495 page.td() 496 497 remove_type = self.can_remove_recurrence(period) and "submit" or "checkbox" 498 499 self.control("recur-remove", remove_type, str(index), 500 str(index) in args.get("recur-remove", []), 501 id="recur-remove-%d" % index, class_="remove") 502 503 page.label("Remove", for_="recur-remove-%d" % index, class_="remove") 504 page.label(for_="recur-remove-%d" % index, class_="removed") 505 page.add("(Removed)") 506 page.span("Re-add", class_="action") 507 page.label.close() 508 509 page.td.close() 510 page.tr.close() 511 512 page.tbody.close() 513 page.table.close() 514 515 page.div.close() 516 517 def show_counters(self): 518 519 "Show any counter-proposals for the current object." 520 521 page = self.page 522 query = self.env.get_query() 523 counter = query.get("counter", [None])[0] 524 525 attendees = self._get_counters(self.uid, self.recurrenceid) 526 tzid = self.get_tzid() 527 528 if not attendees: 529 return 530 531 page.p("The following counter-proposals have been received for this event:") 532 533 page.table(cellspacing=5, cellpadding=5, class_="counters") 534 page.thead() 535 page.tr() 536 page.th("Attendee", rowspan=2) 537 page.th("Periods", colspan=2) 538 page.tr.close() 539 page.tr() 540 page.th("Start") 541 page.th("End") 542 page.tr.close() 543 page.thead.close() 544 page.tbody() 545 546 for attendee in attendees: 547 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee) 548 periods = self.get_periods(obj) 549 550 first = True 551 for p in periods: 552 identifier = "%s-%s" % (format_datetime(p.get_start_point()), format_datetime(p.get_end_point())) 553 css = identifier == counter and "selected" or "" 554 555 if first: 556 page.tr(rowspan=len(periods), class_=css) 557 page.td(attendee) 558 first = False 559 else: 560 page.tr(class_=css) 561 562 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 563 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 564 565 page.td(start) 566 page.td(end) 567 568 page.tr.close() 569 570 page.tbody.close() 571 page.table.close() 572 573 def show_conflicting_events(self): 574 575 "Show conflicting events for the current object." 576 577 page = self.page 578 recurrenceids = self._get_active_recurrences(self.uid) 579 580 # Obtain the user's timezone. 581 582 tzid = self.get_tzid() 583 periods = self.get_periods(self.obj) 584 585 # Indicate whether there are conflicting events. 586 587 conflicts = [] 588 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 589 590 for participant in self.get_current_attendees(): 591 if participant == self.user: 592 freebusy = self.store.get_freebusy(participant) 593 elif participant: 594 freebusy = self.store.get_freebusy_for_other(self.user, participant) 595 else: 596 continue 597 598 if not freebusy: 599 continue 600 601 # Obtain any time zone details from the suggested event. 602 603 _dtstart, attr = self.obj.get_item("DTSTART") 604 tzid = attr.get("TZID", tzid) 605 606 # Show any conflicts with periods of actual attendance. 607 608 participant_attr = attendee_map.get(participant) 609 partstat = participant_attr and participant_attr.get("PARTSTAT") 610 recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid) 611 612 for p in have_conflict(freebusy, periods, True): 613 if not self.recurrenceid and p.is_replaced(recurrences): 614 continue 615 616 if ( # Unidentified or different event 617 (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and 618 # Different period or unclear participation with the same period 619 (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and 620 # Participant not limited to organising 621 p.transp != "ORG" 622 ): 623 624 conflicts.append(p) 625 626 conflicts.sort() 627 628 # Show any conflicts with periods of actual attendance. 629 630 if conflicts: 631 page.p("This event conflicts with others:") 632 633 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 634 page.thead() 635 page.tr() 636 page.th("Event") 637 page.th("Start") 638 page.th("End") 639 page.tr.close() 640 page.thead.close() 641 page.tbody() 642 643 for p in conflicts: 644 645 # Provide details of any conflicting event. 646 647 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 648 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 649 650 page.tr() 651 652 # Show the event summary for the conflicting event. 653 654 page.td() 655 if p.summary: 656 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 657 else: 658 page.add("(Unspecified event)") 659 page.td.close() 660 661 page.td(start) 662 page.td(end) 663 664 page.tr.close() 665 666 page.tbody.close() 667 page.table.close() 668 669 class EventPage(EventPageFragment): 670 671 "A request handler for the event page." 672 673 def __init__(self, resource=None, messenger=None): 674 ResourceClientForObject.__init__(self, resource) 675 self.messenger = messenger or Messenger() 676 677 # Request logic methods. 678 679 def is_initial_load(self): 680 681 "Return whether the event is being loaded and shown for the first time." 682 683 return not self.env.get_args().has_key("editing") 684 685 def handle_request(self): 686 687 """ 688 Handle actions involving the current object, returning an error if one 689 occurred, or None if the request was successfully handled. 690 """ 691 692 # Handle a submitted form. 693 694 args = self.env.get_args() 695 696 # Get the possible actions. 697 698 reply = args.has_key("reply") 699 discard = args.has_key("discard") 700 create = args.has_key("create") 701 cancel = args.has_key("cancel") 702 ignore = args.has_key("ignore") 703 save = args.has_key("save") 704 705 have_action = reply or discard or create or cancel or ignore or save 706 707 if not have_action: 708 return ["action"] 709 710 # If ignoring the object, return to the calendar. 711 712 if ignore: 713 self.redirect(self.env.get_path()) 714 return None 715 716 # Update the object. 717 718 single_user = False 719 720 if reply or create or cancel or save: 721 722 # Update principal event details if organiser. 723 724 if self.is_organiser(): 725 726 # Update time periods (main and recurring). 727 728 try: 729 period = self.handle_main_period() 730 except PeriodError, exc: 731 return exc.args 732 733 try: 734 periods = self.handle_recurrence_periods() 735 except PeriodError, exc: 736 return exc.args 737 738 # Set the periods in the object, first obtaining removed and 739 # modified period information. 740 741 to_unschedule, to_exclude = self.get_removed_periods(periods) 742 743 self.obj.set_period(period) 744 self.obj.set_periods(periods) 745 self.obj.update_exceptions(to_exclude) 746 747 # Update summary. 748 749 if args.has_key("summary"): 750 self.obj["SUMMARY"] = [(args["summary"][0], {})] 751 752 # Obtain any participants and those to be removed. 753 754 attendees = self.get_attendees_from_page() 755 removed = [attendees[int(i)] for i in args.get("remove", [])] 756 to_cancel = self.update_attendees(self.obj, attendees, removed) 757 single_user = not attendees or attendees == [self.user] 758 759 # Update attendee participation for the current user. 760 761 if args.has_key("partstat"): 762 self.update_participation(self.obj, args["partstat"][0]) 763 764 # Process any action. 765 766 invite = not save and create and not single_user 767 save = save or create and single_user 768 769 handled = True 770 771 if reply or invite or cancel: 772 773 client = ManagerClient(self.obj, self.user, self.messenger) 774 775 # Process the object and remove it from the list of requests. 776 777 if reply and client.process_received_request(): 778 self.remove_request(self.uid, self.recurrenceid) 779 780 elif self.is_organiser() and (invite or cancel): 781 782 # Invitation, uninvitation and unscheduling... 783 784 if client.process_created_request( 785 invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule): 786 787 self.remove_request(self.uid, self.recurrenceid) 788 789 # Save single user events. 790 791 elif save: 792 self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node()) 793 self.update_event_in_freebusy() 794 self.remove_request(self.uid, self.recurrenceid) 795 796 # Remove the request and the object. 797 798 elif discard: 799 self.remove_event_from_freebusy() 800 self.remove_event(self.uid, self.recurrenceid) 801 self.remove_request(self.uid, self.recurrenceid) 802 803 else: 804 handled = False 805 806 # Upon handling an action, redirect to the main page. 807 808 if handled: 809 self.redirect(self.env.get_path()) 810 811 return None 812 813 def handle_main_period(self): 814 815 "Return period details for the main start/end period in an event." 816 817 return self.get_main_period_from_page().as_event_period() 818 819 def handle_recurrence_periods(self): 820 821 "Return period details for the recurrences specified for an event." 822 823 return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] 824 825 # Access to form-originating object information. 826 827 def get_main_period_from_page(self): 828 829 "Return the main period defined in the event form." 830 831 args = self.env.get_args() 832 833 dtend_enabled = args.get("dtend-control", [None])[0] 834 dttimes_enabled = args.get("dttimes-control", [None])[0] 835 start = self.get_date_control_values("dtstart") 836 end = self.get_date_control_values("dtend") 837 838 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid()) 839 840 # Handle absent main period details. 841 842 if not period.get_start(): 843 return self.get_stored_main_period() 844 else: 845 return period 846 847 def get_recurrences_from_page(self): 848 849 "Return the recurrences defined in the event form." 850 851 args = self.env.get_args() 852 853 all_dtend_enabled = args.get("dtend-control-recur", []) 854 all_dttimes_enabled = args.get("dttimes-control-recur", []) 855 all_starts = self.get_date_control_values("dtstart-recur", multiple=True) 856 all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") 857 all_origins = args.get("recur-origin", []) 858 859 periods = [] 860 861 for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \ 862 enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)): 863 864 dtend_enabled = str(index) in all_dtend_enabled 865 dttimes_enabled = str(index) in all_dttimes_enabled 866 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin) 867 periods.append(period) 868 869 return periods 870 871 def set_recurrences_in_page(self, recurrences): 872 873 "Set the recurrences defined in the event form." 874 875 args = self.env.get_args() 876 877 args["dtend-control-recur"] = [] 878 args["dttimes-control-recur"] = [] 879 args["recur-origin"] = [] 880 881 all_starts = [] 882 all_ends = [] 883 884 for index, period in enumerate(recurrences): 885 if period.end_enabled: 886 args["dtend-control-recur"].append(str(index)) 887 if period.times_enabled: 888 args["dttimes-control-recur"].append(str(index)) 889 args["recur-origin"].append(period.origin or "") 890 891 all_starts.append(period.get_form_start()) 892 all_ends.append(period.get_form_end()) 893 894 self.set_date_control_values("dtstart-recur", all_starts) 895 self.set_date_control_values("dtend-recur", all_ends, tzid_name="dtstart-recur") 896 897 def get_removed_periods(self, periods): 898 899 """ 900 Return those from the recurrence 'periods' to remove upon updating an 901 event along with those to exclude in a tuple of the form (unscheduled, 902 excluded). 903 """ 904 905 args = self.env.get_args() 906 to_unschedule = [] 907 to_exclude = [] 908 909 for i in args.get("recur-remove", []): 910 try: 911 period = periods[int(i)] 912 except (IndexError, ValueError): 913 continue 914 915 if not self.can_edit_recurrence(period): 916 to_unschedule.append(period) 917 else: 918 to_exclude.append(period) 919 920 return to_unschedule, to_exclude 921 922 def get_attendees_from_page(self): 923 924 """ 925 Return attendees from the request, normalised for iCalendar purposes. 926 """ 927 928 args = self.env.get_args() 929 attendees = [] 930 931 for attendee in args.get("attendee", []): 932 if attendee.strip(): 933 attendee = get_uri(attendee) 934 attendees.append(attendee) 935 936 return attendees 937 938 def update_attendees_from_page(self): 939 940 "Add or remove attendees. This does not affect the stored object." 941 942 args = self.env.get_args() 943 944 attendees = self.get_attendees_from_page() 945 946 if args.has_key("add"): 947 attendees.append("") 948 949 # Only actually remove attendees if the event is unsent, if the attendee 950 # is new, or if it is the current user being removed. 951 952 if args.has_key("remove"): 953 still_to_remove = [] 954 955 for i in args["remove"]: 956 try: 957 attendee = attendees[int(i)] 958 except IndexError: 959 continue 960 961 if self.can_remove_attendee(attendee): 962 attendees.remove(attendee) 963 else: 964 still_to_remove.append(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 987 for i in args["recur-remove"]: 988 try: 989 recurrence = recurrences[int(i)] 990 except IndexError: 991 continue 992 993 if self.can_remove_recurrence(recurrence): 994 recurrences.remove(recurrence) 995 else: 996 still_to_remove.append(i) 997 998 args["recur-remove"] = still_to_remove 999 1000 self.set_recurrences_in_page(recurrences) 1001 return recurrences 1002 1003 # Access to current object information. 1004 1005 def get_current_main_period(self): 1006 1007 """ 1008 Return the currently active main period for the current object depending 1009 on whether editing has begun or whether the object has just been loaded. 1010 """ 1011 1012 if self.is_initial_load() or not self.is_organiser(): 1013 return self.get_stored_main_period() 1014 else: 1015 return self.get_main_period_from_page() 1016 1017 def get_current_recurrences(self): 1018 1019 """ 1020 Return recurrences for the current object using the original object 1021 details where no editing is in progress, using form data otherwise. 1022 """ 1023 1024 if self.is_initial_load() or not self.is_organiser(): 1025 return self.get_stored_recurrences() 1026 else: 1027 return self.get_recurrences_from_page() 1028 1029 def update_current_recurrences(self): 1030 1031 "Return an updated collection of recurrences for the current object." 1032 1033 if self.is_initial_load() or not self.is_organiser(): 1034 return self.get_stored_recurrences() 1035 else: 1036 return self.update_recurrences_from_page() 1037 1038 def get_current_attendees(self): 1039 1040 """ 1041 Return attendees for the current object depending on whether the object 1042 has been edited or instead provides such information from its stored 1043 form. 1044 """ 1045 1046 if self.is_initial_load() or not self.is_organiser(): 1047 return self.get_stored_attendees() 1048 else: 1049 return self.get_attendees_from_page() 1050 1051 def update_current_attendees(self): 1052 1053 "Return an updated collection of attendees for the current object." 1054 1055 if self.is_initial_load() or not self.is_organiser(): 1056 return self.get_stored_attendees() 1057 else: 1058 return self.update_attendees_from_page() 1059 1060 # Full page output methods. 1061 1062 def show(self, path_info): 1063 1064 "Show an object request using the given 'path_info' for the current user." 1065 1066 uid, recurrenceid = self.get_identifiers(path_info) 1067 obj = self.get_stored_object(uid, recurrenceid) 1068 self.set_object(obj) 1069 1070 if not obj: 1071 return False 1072 1073 errors = self.handle_request() 1074 1075 if not errors: 1076 return True 1077 1078 self.update_current_attendees() 1079 self.update_current_recurrences() 1080 1081 self.new_page(title="Event") 1082 self.show_object_on_page(errors) 1083 1084 return True 1085 1086 # vim: tabstop=4 expandtab shiftwidth=4