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