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