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