1 #!/usr/bin/env python 2 3 """ 4 A Web interface to a calendar event. 5 6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from imiptools.data import get_uri, get_verbose_address, uri_dict, uri_items, uri_values 23 from imiptools.dates import format_datetime, to_timezone 24 from imiptools.mail import Messenger 25 from imiptools.period import have_conflict 26 from imipweb.data import EventPeriod, event_period_from_period, FormPeriod, PeriodError 27 from imipweb.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject 28 29 class EventPageFragment(ResourceClientForObject, DateTimeFormUtilities, FormUtilities): 30 31 "A resource presenting the details of an event." 32 33 def __init__(self, resource=None): 34 ResourceClientForObject.__init__(self, resource) 35 36 # Various property values and labels. 37 38 property_items = [ 39 ("SUMMARY", "Summary"), 40 ("DTSTART", "Start"), 41 ("DTEND", "End"), 42 ("ORGANIZER", "Organiser"), 43 ("ATTENDEE", "Attendee"), 44 ] 45 46 partstat_items = [ 47 ("NEEDS-ACTION", "Not confirmed"), 48 ("ACCEPTED", "Attending"), 49 ("TENTATIVE", "Tentatively attending"), 50 ("DECLINED", "Not attending"), 51 ("DELEGATED", "Delegated"), 52 (None, "Not indicated"), 53 ] 54 55 def can_change_object(self): 56 return self.is_organiser() or self._is_request() 57 58 def can_remove_recurrence(self, recurrence): 59 60 """ 61 Return whether the 'recurrence' can be removed from the current object 62 without notification. 63 """ 64 65 return self.can_edit_recurrence(recurrence) and recurrence.origin != "RRULE" 66 67 def can_edit_recurrence(self, recurrence): 68 69 "Return whether 'recurrence' can be edited." 70 71 return self.recurrence_is_new(recurrence) or not self.obj.is_shared() 72 73 def recurrence_is_new(self, recurrence): 74 75 "Return whether 'recurrence' is new to the current object." 76 77 return recurrence not in self.get_stored_recurrences() 78 79 def can_remove_attendee(self, attendee): 80 81 """ 82 Return whether 'attendee' can be removed from the current object without 83 notification. 84 """ 85 86 return self.can_edit_attendee(attendee) or attendee == self.user and self.is_organiser() 87 88 def can_edit_attendee(self, attendee): 89 90 "Return whether 'attendee' can be edited by an organiser." 91 92 return self.attendee_is_new(attendee) or not self.obj.is_shared() 93 94 def attendee_is_new(self, attendee): 95 96 "Return whether 'attendee' is new to the current object." 97 98 return attendee not in uri_values(self.get_stored_attendees()) 99 100 # Access to stored object information. 101 102 def get_stored_attendees(self): 103 return [get_verbose_address(value, attr) for value, attr in self.obj.get_items("ATTENDEE") or []] 104 105 def get_stored_main_period(self): 106 107 "Return the main event period for the current object." 108 109 (dtstart, dtstart_attr), (dtend, dtend_attr) = self.obj.get_main_period_items(self.get_tzid()) 110 return EventPeriod(dtstart, dtend, self.get_tzid(), None, dtstart_attr, dtend_attr) 111 112 def get_stored_recurrences(self): 113 114 "Return recurrences computed using the current object." 115 116 recurrences = [] 117 for period in self.get_periods(self.obj): 118 if period.origin != "DTSTART": 119 recurrences.append(period) 120 return recurrences 121 122 # Access to current object information. 123 124 def get_current_main_period(self): 125 return self.get_stored_main_period() 126 127 def get_current_recurrences(self): 128 return self.get_stored_recurrences() 129 130 def get_current_attendees(self): 131 return self.get_stored_attendees() 132 133 # Page fragment methods. 134 135 def show_request_controls(self): 136 137 "Show form controls for a request." 138 139 page = self.page 140 args = self.env.get_args() 141 142 attendees = uri_values(self.get_current_attendees()) 143 is_attendee = self.user in attendees 144 145 if not self.obj.is_shared(): 146 page.p("This event has not been shared.") 147 148 # Show appropriate options depending on the role of the user. 149 150 if is_attendee and not self.is_organiser(): 151 page.p("An action is required for this request:") 152 153 page.p() 154 self.control("reply", "submit", "Send reply") 155 page.add(" ") 156 self.control("discard", "submit", "Discard event") 157 page.add(" ") 158 self.control("ignore", "submit", "Do nothing for now") 159 page.p.close() 160 161 if self.is_organiser(): 162 page.p("As organiser, you can perform the following:") 163 164 page.p() 165 self.control("create", "submit", "Update event") 166 page.add(" ") 167 168 if self.obj.is_shared() and not self._is_request(): 169 self.control("cancel", "submit", "Cancel event") 170 else: 171 self.control("discard", "submit", "Discard event") 172 173 page.add(" ") 174 self.control("save", "submit", "Save without sending") 175 page.p.close() 176 177 def show_object_on_page(self, errors=None): 178 179 """ 180 Show the calendar object on the current page. If 'errors' is given, show 181 a suitable message for the different errors provided. 182 """ 183 184 page = self.page 185 page.form(method="POST") 186 187 # Add a hidden control to help determine whether editing has already begun. 188 189 self.control("editing", "hidden", "true") 190 191 args = self.env.get_args() 192 193 # Obtain basic event information, generating any necessary editing controls. 194 195 attendees = self.get_current_attendees() 196 period = self.get_current_main_period() 197 stored_period = self.get_stored_main_period() 198 self.show_object_datetime_controls(period) 199 200 # Obtain any separate recurrences for this event. 201 202 recurrenceids = self._get_active_recurrences(self.uid) 203 replaced = not self.recurrenceid and period.is_replaced(recurrenceids) 204 excluded = period == stored_period and period not in self.get_periods(self.obj) 205 206 # Provide a summary of the object. 207 208 page.table(class_="object", cellspacing=5, cellpadding=5) 209 page.thead() 210 page.tr() 211 page.th("Event", class_="mainheading", colspan=2) 212 page.tr.close() 213 page.thead.close() 214 page.tbody() 215 216 for name, label in self.property_items: 217 field = name.lower() 218 219 items = uri_items(self.obj.get_items(name) or []) 220 rowspan = len(items) 221 222 if name == "ATTENDEE": 223 rowspan = len(attendees) + 1 # for the add button 224 elif not items: 225 continue 226 227 page.tr() 228 page.th(label, class_="objectheading %s%s" % (field, errors and field in errors and " error" or ""), rowspan=rowspan) 229 230 # Handle datetimes specially. 231 232 if name in ["DTSTART", "DTEND"]: 233 if not replaced and not excluded: 234 235 # Obtain the datetime. 236 237 is_start = name == "DTSTART" 238 239 # Where no end datetime exists, use the start datetime as the 240 # basis of any potential datetime specified if dt-control is 241 # set. 242 243 self.show_datetime_controls(is_start and period.get_form_start() or period.get_form_end(), is_start) 244 245 elif name == "DTSTART": 246 247 # Replaced occurrences link to their replacements. 248 249 if replaced: 250 page.td(class_="objectvalue %s replaced" % field, rowspan=2) 251 page.a("First occurrence replaced by a separate event", href=self.link_to(self.uid, replaced)) 252 page.td.close() 253 254 # NOTE: Should provide a way of editing recurrences when the 255 # NOTE: first occurrence is excluded, plus a way of 256 # NOTE: reinstating the occurrence. 257 258 elif excluded: 259 page.td(class_="objectvalue %s excluded" % field, rowspan=2) 260 page.add("First occurrence excluded") 261 page.td.close() 262 263 page.tr.close() 264 265 # Handle the summary specially. 266 267 elif name == "SUMMARY": 268 value = args.get("summary", [self.obj.get_value(name)])[0] 269 270 page.td(class_="objectvalue summary") 271 if self.is_organiser(): 272 self.control("summary", "text", value, size=80) 273 else: 274 page.add(value) 275 page.td.close() 276 page.tr.close() 277 278 # Handle attendees specially. 279 280 elif name == "ATTENDEE": 281 attendee_map = dict(items) 282 first = True 283 284 for i, value in enumerate(attendees): 285 if not first: 286 page.tr() 287 else: 288 first = False 289 290 # Obtain details of attendees to supply attributes. 291 292 self.show_attendee(i, value, attendee_map.get(get_uri(value))) 293 page.tr.close() 294 295 # Allow more attendees to be specified. 296 297 if self.can_change_object(): 298 if not first: 299 page.tr() 300 301 page.td() 302 self.control("add", "submit", "add", id="add", class_="add") 303 page.label("Add attendee", for_="add", class_="add") 304 page.td.close() 305 page.tr.close() 306 307 # NOTE: Permit attendees to suggest others for counter-proposals. 308 309 # Handle potentially many values of other kinds. 310 311 else: 312 first = True 313 314 for i, (value, attr) in enumerate(items): 315 if not first: 316 page.tr() 317 else: 318 first = False 319 320 page.td(class_="objectvalue %s" % field) 321 if name == "ORGANIZER": 322 page.add(get_verbose_address(value, attr)) 323 else: 324 page.add(value) 325 page.td.close() 326 page.tr.close() 327 328 page.tbody.close() 329 page.table.close() 330 331 self.show_recurrences(errors) 332 self.show_counters() 333 self.show_conflicting_events() 334 self.show_request_controls() 335 336 page.form.close() 337 338 def show_attendee(self, i, attendee, attendee_attr): 339 340 """ 341 For the current object, show the attendee in position 'i' with the given 342 'attendee' value, having 'attendee_attr' as any stored attributes. 343 """ 344 345 page = self.page 346 args = self.env.get_args() 347 348 attendee_uri = get_uri(attendee) 349 partstat = attendee_attr and attendee_attr.get("PARTSTAT") 350 351 page.td(class_="objectvalue") 352 353 # Show a form control as organiser for new attendees. 354 # NOTE: Permit suggested attendee editing for counter-proposals. 355 356 if self.can_change_object() and self.can_edit_attendee(attendee_uri): 357 self.control("attendee", "value", attendee, size="40") 358 else: 359 self.control("attendee", "hidden", attendee) 360 page.add(attendee) 361 page.add(" ") 362 363 # Show participation status, editable for the current user. 364 365 if attendee_uri == self.user: 366 self.menu("partstat", partstat, self.partstat_items, "partstat") 367 368 # Allow the participation indicator to act as a submit 369 # button in order to refresh the page and show a control for 370 # the current user, if indicated. 371 372 elif self.is_organiser() and self.attendee_is_new(attendee_uri): 373 self.control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh") 374 page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat") 375 376 # Otherwise, just show a label with the participation status. 377 378 else: 379 page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat") 380 381 # Permit organisers to remove attendees. 382 # NOTE: Permit the removal of suggested attendees for counter-proposals. 383 384 if self.can_change_object() and (self.can_remove_attendee(attendee_uri) or self.is_organiser()): 385 386 # Permit the removal of newly-added attendees. 387 388 remove_type = self.can_remove_attendee(attendee_uri) and "submit" or "checkbox" 389 self.control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove") 390 391 page.label("Remove", for_="remove-%d" % i, class_="remove") 392 page.label(for_="remove-%d" % i, class_="removed") 393 page.add("(Uninvited)") 394 page.span("Re-invite", class_="action") 395 page.label.close() 396 397 page.td.close() 398 399 def show_recurrences(self, errors=None): 400 401 """ 402 Show recurrences for the current object. If 'errors' is given, show a 403 suitable message for the different errors provided. 404 """ 405 406 page = self.page 407 408 # Obtain any parent object if this object is a specific recurrence. 409 410 if self.recurrenceid: 411 parent = self.get_stored_object(self.uid, None) 412 if not parent: 413 return 414 415 page.p() 416 page.a("This event modifies a recurring event.", href=self.link_to(self.uid)) 417 page.p.close() 418 419 # Obtain the periods associated with the event. 420 # NOTE: Add a control to add recurrences here. 421 422 recurrences = self.get_current_recurrences() 423 424 if len(recurrences) < 1: 425 return 426 427 recurrenceids = self._get_recurrences(self.uid) 428 429 page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) 430 431 # Show each recurrence in a separate table if editable. 432 # NOTE: Allow recurrence editing for counter-proposals. 433 434 if self.can_change_object() and recurrences: 435 436 for index, period in enumerate(recurrences): 437 self.show_recurrence(index, period, self.recurrenceid, recurrenceids, errors) 438 439 # Otherwise, use a compact single table. 440 441 else: 442 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 443 page.caption("Occurrences") 444 page.thead() 445 page.tr() 446 page.th("Start", class_="objectheading start") 447 page.th("End", class_="objectheading end") 448 page.tr.close() 449 page.thead.close() 450 page.tbody() 451 452 for index, period in enumerate(recurrences): 453 page.tr() 454 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, True) 455 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, False) 456 page.tr.close() 457 458 page.tbody.close() 459 page.table.close() 460 461 def show_recurrence(self, index, period, recurrenceid, recurrenceids, errors=None): 462 463 """ 464 Show recurrence controls for a recurrence provided by the current object 465 with the given 'index' position in the list of periods, the given 466 'period' details, where a 'recurrenceid' indicates any specific 467 recurrence, and where 'recurrenceids' indicates all known additional 468 recurrences for the object. 469 470 If 'errors' is given, show a suitable message for the different errors 471 provided. 472 """ 473 474 page = self.page 475 args = self.env.get_args() 476 477 p = event_period_from_period(period) 478 replaced = not recurrenceid and p.is_replaced(recurrenceids) 479 480 # Isolate the controls from neighbouring tables. 481 482 page.div() 483 484 self.show_object_datetime_controls(period, index) 485 486 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 487 page.caption(period.origin == "RRULE" and "Occurrence from rule" or "Occurrence") 488 page.tbody() 489 490 page.tr() 491 error = errors and ("dtstart", index) in errors and " error" or "" 492 page.th("Start", class_="objectheading start%s" % error) 493 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, True) 494 page.tr.close() 495 page.tr() 496 error = errors and ("dtend", index) in errors and " error" or "" 497 page.th("End", class_="objectheading end%s" % error) 498 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, False) 499 page.tr.close() 500 501 # Permit the removal of recurrences. 502 503 if not replaced: 504 page.tr() 505 page.th("") 506 page.td() 507 508 remove_type = self.can_remove_recurrence(period) and "submit" or "checkbox" 509 510 self.control("recur-remove", remove_type, str(index), 511 str(index) in args.get("recur-remove", []), 512 id="recur-remove-%d" % index, class_="remove") 513 514 page.label("Remove", for_="recur-remove-%d" % index, class_="remove") 515 page.label(for_="recur-remove-%d" % index, class_="removed") 516 page.add("(Removed)") 517 page.span("Re-add", class_="action") 518 page.label.close() 519 520 page.td.close() 521 page.tr.close() 522 523 page.tbody.close() 524 page.table.close() 525 526 page.div.close() 527 528 def show_counters(self): 529 530 "Show any counter-proposals for the current object." 531 532 page = self.page 533 query = self.env.get_query() 534 counter = query.get("counter", [None])[0] 535 536 attendees = self._get_counters(self.uid, self.recurrenceid) 537 tzid = self.get_tzid() 538 539 if not attendees: 540 return 541 542 attendees = self.get_verbose_attendees(attendees) 543 544 # Get suggestions. Attendees are aggregated and reference the existing 545 # attendees suggesting them. Periods are referenced by each existing 546 # attendee. 547 548 suggested_attendees = {} 549 suggested_periods = {} 550 551 for i, attendee in enumerate(attendees): 552 attendee_uri = get_uri(attendee) 553 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee_uri) 554 555 # Get suggested attendees. 556 557 for suggested_uri, suggested_attr in uri_dict(obj.get_value_map("ATTENDEE")).items(): 558 if suggested_uri == attendee_uri: 559 continue 560 suggested = get_verbose_address(suggested_uri, suggested_attr) 561 562 if not suggested_attendees.has_key(suggested): 563 suggested_attendees[suggested] = [] 564 suggested_attendees[suggested].append(attendee) 565 566 # Get suggested periods. 567 568 periods = self.get_periods(obj) 569 if set(periods).difference(self.get_periods(self.obj)): 570 suggested_periods[attendee] = periods 571 572 # Present the suggested attendees. 573 574 if suggested_attendees: 575 page.p("The following attendees have been suggested for this event:") 576 577 page.table(cellspacing=5, cellpadding=5, class_="counters") 578 page.thead() 579 page.tr() 580 page.th("Attendee") 581 page.th("Suggested by...") 582 page.tr.close() 583 page.thead.close() 584 page.tbody() 585 586 suggested_attendees = list(suggested_attendees.items()) 587 suggested_attendees.sort() 588 589 for suggested, attendees in suggested_attendees: 590 page.tr() 591 page.td(suggested) 592 page.td(", ".join(attendees)) 593 page.tr.close() 594 595 page.tbody.close() 596 page.table.close() 597 598 # Present the suggested periods. 599 600 if suggested_periods: 601 page.p("The following periods have been suggested for this event:") 602 603 page.table(cellspacing=5, cellpadding=5, class_="counters") 604 page.thead() 605 page.tr() 606 page.th("Periods", colspan=2) 607 page.th("Suggested by...", rowspan=2) 608 page.tr.close() 609 page.tr() 610 page.th("Start") 611 page.th("End") 612 page.tr.close() 613 page.thead.close() 614 page.tbody() 615 616 suggested_periods = list(suggested_periods.items()) 617 suggested_periods.sort() 618 619 for attendee, periods in suggested_periods: 620 first = True 621 for p in periods: 622 identifier = "%s-%s" % (format_datetime(p.get_start_point()), format_datetime(p.get_end_point())) 623 css = identifier == counter and "selected" or "" 624 625 page.tr(class_=css) 626 627 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 628 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 629 630 # Show each period. 631 632 page.td(start) 633 page.td(end) 634 635 # Show attendees and controls alongside the first period in each 636 # attendee's collection. 637 638 if first: 639 page.td(attendee, rowspan=len(periods)) 640 page.td(rowspan=len(periods)) 641 self.control("accept-%d" % i, "submit", "Accept") 642 self.control("decline-%d" % i, "submit", "Decline") 643 self.control("counter", "hidden", attendee) 644 page.td.close() 645 646 page.tr.close() 647 first = False 648 649 page.tbody.close() 650 page.table.close() 651 652 def show_conflicting_events(self): 653 654 "Show conflicting events for the current object." 655 656 page = self.page 657 recurrenceids = self._get_active_recurrences(self.uid) 658 659 # Obtain the user's timezone. 660 661 tzid = self.get_tzid() 662 periods = self.get_periods(self.obj) 663 664 # Indicate whether there are conflicting events. 665 666 conflicts = [] 667 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 668 669 for participant in self.get_current_attendees(): 670 if participant == self.user: 671 freebusy = self.store.get_freebusy(participant) 672 elif participant: 673 freebusy = self.store.get_freebusy_for_other(self.user, participant) 674 else: 675 continue 676 677 if not freebusy: 678 continue 679 680 # Obtain any time zone details from the suggested event. 681 682 _dtstart, attr = self.obj.get_item("DTSTART") 683 tzid = attr.get("TZID", tzid) 684 685 # Show any conflicts with periods of actual attendance. 686 687 participant_attr = attendee_map.get(participant) 688 partstat = participant_attr and participant_attr.get("PARTSTAT") 689 recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid) 690 691 for p in have_conflict(freebusy, periods, True): 692 if not self.recurrenceid and p.is_replaced(recurrences): 693 continue 694 695 if ( # Unidentified or different event 696 (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and 697 # Different period or unclear participation with the same period 698 (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and 699 # Participant not limited to organising 700 p.transp != "ORG" 701 ): 702 703 conflicts.append(p) 704 705 conflicts.sort() 706 707 # Show any conflicts with periods of actual attendance. 708 709 if conflicts: 710 page.p("This event conflicts with others:") 711 712 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 713 page.thead() 714 page.tr() 715 page.th("Event") 716 page.th("Start") 717 page.th("End") 718 page.tr.close() 719 page.thead.close() 720 page.tbody() 721 722 for p in conflicts: 723 724 # Provide details of any conflicting event. 725 726 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 727 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 728 729 page.tr() 730 731 # Show the event summary for the conflicting event. 732 733 page.td() 734 if p.summary: 735 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 736 else: 737 page.add("(Unspecified event)") 738 page.td.close() 739 740 page.td(start) 741 page.td(end) 742 743 page.tr.close() 744 745 page.tbody.close() 746 page.table.close() 747 748 class EventPage(EventPageFragment): 749 750 "A request handler for the event page." 751 752 def __init__(self, resource=None, messenger=None): 753 ResourceClientForObject.__init__(self, resource, messenger or Messenger()) 754 755 # Request logic methods. 756 757 def is_initial_load(self): 758 759 "Return whether the event is being loaded and shown for the first time." 760 761 return not self.env.get_args().has_key("editing") 762 763 def handle_request(self): 764 765 """ 766 Handle actions involving the current object, returning an error if one 767 occurred, or None if the request was successfully handled. 768 """ 769 770 # Handle a submitted form. 771 772 args = self.env.get_args() 773 774 # Get the possible actions. 775 776 reply = args.has_key("reply") 777 discard = args.has_key("discard") 778 create = args.has_key("create") 779 cancel = args.has_key("cancel") 780 ignore = args.has_key("ignore") 781 save = args.has_key("save") 782 accept = self.prefixed_args("accept-", int) 783 decline = self.prefixed_args("decline-", int) 784 785 have_action = reply or discard or create or cancel or ignore or save or accept or decline 786 787 if not have_action: 788 return ["action"] 789 790 # If ignoring the object, return to the calendar. 791 792 if ignore: 793 self.redirect(self.env.get_path()) 794 return None 795 796 # Update the object. 797 798 single_user = False 799 changed = False 800 801 if reply or create or cancel or save: 802 803 # Update principal event details if organiser. 804 # NOTE: Handle edited details for counter-proposals. 805 806 if self.can_change_object(): 807 808 # Update time periods (main and recurring). 809 810 try: 811 period = self.handle_main_period() 812 except PeriodError, exc: 813 return exc.args 814 815 try: 816 periods = self.handle_recurrence_periods() 817 except PeriodError, exc: 818 return exc.args 819 820 # Set the periods in the object, first obtaining removed and 821 # modified period information. 822 823 to_unschedule, to_exclude = self.get_removed_periods(periods) 824 825 changed = self.obj.set_period(period) or changed 826 changed = self.obj.set_periods(periods) or changed 827 changed = self.obj.update_exceptions(to_exclude) or changed 828 829 # Organiser-only changes... 830 831 if self.is_organiser(): 832 833 # Update summary. 834 835 if args.has_key("summary"): 836 self.obj["SUMMARY"] = [(args["summary"][0], {})] 837 838 # Obtain any new participants and those to be removed. 839 840 if self.can_change_object(): 841 attendees = self.get_attendees_from_page() 842 removed = [attendees[int(i)] for i in args.get("remove", [])] 843 added, to_cancel = self.update_attendees(attendees, removed) 844 single_user = not attendees or attendees == [self.user] 845 changed = added or changed 846 847 # Update attendee participation for the current user. 848 849 if args.has_key("partstat"): 850 self.update_participation(args["partstat"][0]) 851 852 # Process any action. 853 854 invite = not save and create and not single_user 855 save = save or create and single_user 856 857 handled = True 858 859 if reply or invite or cancel: 860 861 # Process the object and remove it from the list of requests. 862 863 if reply and self.process_received_request(changed): 864 self.remove_request() 865 866 elif self.is_organiser() and (invite or cancel): 867 868 # Invitation, uninvitation and unscheduling... 869 870 if self.process_created_request( 871 invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule): 872 873 self.remove_request() 874 875 # Save single user events. 876 877 elif save: 878 self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node()) 879 self.update_event_in_freebusy() 880 self.remove_request() 881 882 # Remove the request and the object. 883 884 elif discard: 885 self.remove_event_from_freebusy() 886 self.remove_event() 887 self.remove_request() 888 889 # Update counter-proposal records synchronously instead of assuming 890 # that the outgoing handler will have done so before the form is 891 # refreshed. 892 893 # Accept a counter-proposal and decline all others, sending a new 894 # request to all attendees. 895 896 elif accept: 897 898 # Take the first accepted proposal, although there should be only 899 # one anyway. 900 901 for i in accept: 902 attendee_uri = get_uri(args.get("counter", [])[i]) 903 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee_uri) 904 self.obj.set_periods(self.get_periods(obj)) 905 break 906 907 # Remove counter-proposals and issue a new invitation. 908 909 attendees = uri_values(args.get("counter", [])) 910 self.remove_counters(attendees) 911 self.process_created_request("REQUEST") 912 913 # Decline a counter-proposal individually. 914 915 elif decline: 916 for i in decline: 917 attendee_uri = get_uri(args.get("counter", [])[i]) 918 self.process_declined_counter(attendee_uri) 919 self.remove_counter(attendee_uri) 920 921 # Redirect to the event. 922 923 self.redirect(self.env.get_url()) 924 handled = False 925 926 else: 927 handled = False 928 929 # Upon handling an action, redirect to the main page. 930 931 if handled: 932 self.redirect(self.env.get_path()) 933 934 return None 935 936 def handle_main_period(self): 937 938 "Return period details for the main start/end period in an event." 939 940 return self.get_main_period_from_page().as_event_period() 941 942 def handle_recurrence_periods(self): 943 944 "Return period details for the recurrences specified for an event." 945 946 return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] 947 948 # Access to form-originating object information. 949 950 def get_main_period_from_page(self): 951 952 "Return the main period defined in the event form." 953 954 args = self.env.get_args() 955 956 dtend_enabled = args.get("dtend-control", [None])[0] 957 dttimes_enabled = args.get("dttimes-control", [None])[0] 958 start = self.get_date_control_values("dtstart") 959 end = self.get_date_control_values("dtend") 960 961 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), "DTSTART") 962 963 # Handle absent main period details. 964 965 if not period.get_start(): 966 return self.get_stored_main_period() 967 else: 968 return period 969 970 def get_recurrences_from_page(self): 971 972 "Return the recurrences defined in the event form." 973 974 args = self.env.get_args() 975 976 all_dtend_enabled = args.get("dtend-control-recur", []) 977 all_dttimes_enabled = args.get("dttimes-control-recur", []) 978 all_starts = self.get_date_control_values("dtstart-recur", multiple=True) 979 all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") 980 all_origins = args.get("recur-origin", []) 981 982 periods = [] 983 984 for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \ 985 enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)): 986 987 dtend_enabled = str(index) in all_dtend_enabled 988 dttimes_enabled = str(index) in all_dttimes_enabled 989 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin) 990 periods.append(period) 991 992 return periods 993 994 def set_recurrences_in_page(self, recurrences): 995 996 "Set the recurrences defined in the event form." 997 998 args = self.env.get_args() 999 1000 args["dtend-control-recur"] = [] 1001 args["dttimes-control-recur"] = [] 1002 args["recur-origin"] = [] 1003 1004 all_starts = [] 1005 all_ends = [] 1006 1007 for index, period in enumerate(recurrences): 1008 if period.end_enabled: 1009 args["dtend-control-recur"].append(str(index)) 1010 if period.times_enabled: 1011 args["dttimes-control-recur"].append(str(index)) 1012 args["recur-origin"].append(period.origin or "") 1013 1014 all_starts.append(period.get_form_start()) 1015 all_ends.append(period.get_form_end()) 1016 1017 self.set_date_control_values("dtstart-recur", all_starts) 1018 self.set_date_control_values("dtend-recur", all_ends, tzid_name="dtstart-recur") 1019 1020 def get_removed_periods(self, periods): 1021 1022 """ 1023 Return those from the recurrence 'periods' to remove upon updating an 1024 event along with those to exclude in a tuple of the form (unscheduled, 1025 excluded). 1026 """ 1027 1028 args = self.env.get_args() 1029 to_unschedule = [] 1030 to_exclude = [] 1031 1032 for i in args.get("recur-remove", []): 1033 try: 1034 period = periods[int(i)] 1035 except (IndexError, ValueError): 1036 continue 1037 1038 if not self.can_edit_recurrence(period): 1039 to_unschedule.append(period) 1040 else: 1041 to_exclude.append(period) 1042 1043 return to_unschedule, to_exclude 1044 1045 def get_attendees_from_page(self): 1046 1047 """ 1048 Return attendees from the request, using any stored attributes to obtain 1049 verbose details. 1050 """ 1051 1052 return self.get_verbose_attendees(self.env.get_args().get("attendee", [])) 1053 1054 def get_verbose_attendees(self, attendees): 1055 1056 """ 1057 Use any stored attributes to obtain verbose details for the given 1058 'attendees'. 1059 """ 1060 1061 attendee_map = self.obj.get_value_map("ATTENDEE") 1062 return [get_verbose_address(value, attendee_map.get(value)) for value in attendees] 1063 1064 def update_attendees_from_page(self): 1065 1066 "Add or remove attendees. This does not affect the stored object." 1067 1068 args = self.env.get_args() 1069 1070 attendees = self.get_attendees_from_page() 1071 1072 if args.has_key("add"): 1073 attendees.append("") 1074 1075 # Only actually remove attendees if the event is unsent, if the attendee 1076 # is new, or if it is the current user being removed. 1077 1078 if args.has_key("remove"): 1079 still_to_remove = [] 1080 correction = 0 1081 1082 for i in args["remove"]: 1083 try: 1084 i = int(i) - correction 1085 attendee = attendees[i] 1086 except (IndexError, ValueError): 1087 continue 1088 1089 if self.can_remove_attendee(get_uri(attendee)): 1090 del attendees[i] 1091 correction += 1 1092 else: 1093 still_to_remove.append(str(i)) 1094 1095 args["remove"] = still_to_remove 1096 1097 args["attendee"] = attendees 1098 return attendees 1099 1100 def update_recurrences_from_page(self): 1101 1102 "Add or remove recurrences. This does not affect the stored object." 1103 1104 args = self.env.get_args() 1105 1106 recurrences = self.get_recurrences_from_page() 1107 1108 # NOTE: Addition of recurrences to be supported. 1109 1110 # Only actually remove recurrences if the event is unsent, or if the 1111 # recurrence is new, but only for explicit recurrences. 1112 1113 if args.has_key("recur-remove"): 1114 still_to_remove = [] 1115 correction = 0 1116 1117 for i in args["recur-remove"]: 1118 try: 1119 i = int(i) - correction 1120 recurrence = recurrences[i] 1121 except (IndexError, ValueError): 1122 continue 1123 1124 if self.can_remove_recurrence(recurrence): 1125 del recurrences[i] 1126 correction += 1 1127 else: 1128 still_to_remove.append(str(i)) 1129 1130 args["recur-remove"] = still_to_remove 1131 1132 self.set_recurrences_in_page(recurrences) 1133 return recurrences 1134 1135 # Access to current object information. 1136 1137 def get_current_main_period(self): 1138 1139 """ 1140 Return the currently active main period for the current object depending 1141 on whether editing has begun or whether the object has just been loaded. 1142 """ 1143 1144 if self.is_initial_load() or not self.can_change_object(): 1145 return self.get_stored_main_period() 1146 else: 1147 return self.get_main_period_from_page() 1148 1149 def get_current_recurrences(self): 1150 1151 """ 1152 Return recurrences for the current object using the original object 1153 details where no editing is in progress, using form data otherwise. 1154 """ 1155 1156 if self.is_initial_load() or not self.can_change_object(): 1157 return self.get_stored_recurrences() 1158 else: 1159 return self.get_recurrences_from_page() 1160 1161 def update_current_recurrences(self): 1162 1163 "Return an updated collection of recurrences for the current object." 1164 1165 if self.is_initial_load() or not self.can_change_object(): 1166 return self.get_stored_recurrences() 1167 else: 1168 return self.update_recurrences_from_page() 1169 1170 def get_current_attendees(self): 1171 1172 """ 1173 Return attendees for the current object depending on whether the object 1174 has been edited or instead provides such information from its stored 1175 form. 1176 """ 1177 1178 if self.is_initial_load() or not self.can_change_object(): 1179 return self.get_stored_attendees() 1180 else: 1181 return self.get_attendees_from_page() 1182 1183 def update_current_attendees(self): 1184 1185 "Return an updated collection of attendees for the current object." 1186 1187 if self.is_initial_load() or not self.can_change_object(): 1188 return self.get_stored_attendees() 1189 else: 1190 return self.update_attendees_from_page() 1191 1192 # Full page output methods. 1193 1194 def show(self, path_info): 1195 1196 "Show an object request using the given 'path_info' for the current user." 1197 1198 uid, recurrenceid = self.get_identifiers(path_info) 1199 obj = self.get_stored_object(uid, recurrenceid) 1200 self.set_object(obj) 1201 1202 if not obj: 1203 return False 1204 1205 errors = self.handle_request() 1206 1207 if not errors: 1208 return True 1209 1210 self.update_current_attendees() 1211 self.update_current_recurrences() 1212 1213 self.new_page(title="Event") 1214 self.show_object_on_page(errors) 1215 1216 return True 1217 1218 # vim: tabstop=4 expandtab shiftwidth=4