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