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