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