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