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, uri_dict, uri_items, uri_values 23 from imiptools.dates import format_datetime, to_timezone 24 from imiptools.mail import Messenger 25 from imiptools.period import have_conflict 26 from imipweb.data import EventPeriod, event_period_from_period, FormPeriod, PeriodError 27 from imipweb.client import ManagerClient 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.recurrence_is_new(recurrence) or not self.obj.is_shared() 64 65 def recurrence_is_new(self, recurrence): 66 67 "Return whether 'recurrence' is new to the current object." 68 69 return recurrence not in self.get_stored_recurrences() 70 71 def can_remove_attendee(self, attendee): 72 73 """ 74 Return whether 'attendee' can be removed from the current object without 75 notification. 76 """ 77 78 return self.can_edit_attendee(attendee) or attendee == self.user 79 80 def can_edit_attendee(self, attendee): 81 82 "Return whether 'attendee' can be edited by an organiser." 83 84 return self.attendee_is_new(attendee) or not self.obj.is_shared() 85 86 def attendee_is_new(self, attendee): 87 88 "Return whether 'attendee' is new to the current object." 89 90 return attendee not in self.get_stored_attendees() 91 92 # Access to stored object information. 93 94 def is_organiser(self): 95 return get_uri(self.obj.get_value("ORGANIZER")) == self.user 96 97 def get_stored_attendees(self): 98 return uri_values(self.obj.get_values("ATTENDEE") or []) 99 100 def get_stored_main_period(self): 101 102 "Return the main event period for the current object." 103 104 (dtstart, dtstart_attr), (dtend, dtend_attr) = self.obj.get_main_period_items(self.get_tzid()) 105 return EventPeriod(dtstart, dtend, self.get_tzid(), None, dtstart_attr, dtend_attr) 106 107 def get_stored_recurrences(self): 108 109 "Return recurrences computed using the current object." 110 111 recurrences = [] 112 for period in self.get_periods(self.obj): 113 if period.origin != "DTSTART": 114 recurrences.append(period) 115 return recurrences 116 117 # Access to current object information. 118 119 def get_current_main_period(self): 120 return self.get_stored_main_period() 121 122 def get_current_recurrences(self): 123 return self.get_stored_recurrences() 124 125 def get_current_attendees(self): 126 return self.get_stored_attendees() 127 128 # Page fragment methods. 129 130 def show_request_controls(self): 131 132 "Show form controls for a request." 133 134 page = self.page 135 args = self.env.get_args() 136 137 attendees = self.get_current_attendees() 138 is_attendee = self.user in attendees 139 is_request = self._have_request(self.uid, self.recurrenceid) 140 141 # Show appropriate options depending on the role of the user. 142 143 if is_attendee and not self.is_organiser(): 144 page.p("An action is required for this request:") 145 146 page.p() 147 self.control("reply", "submit", "Send reply") 148 page.add(" ") 149 self.control("discard", "submit", "Discard event") 150 page.add(" ") 151 self.control("ignore", "submit", "Do nothing for now") 152 page.p.close() 153 154 if self.is_organiser(): 155 page.p("As organiser, you can perform the following:") 156 157 page.p() 158 self.control("create", "submit", not self.obj.is_shared() and "Create event" or "Update event") 159 page.add(" ") 160 161 if self.obj.is_shared() and not is_request: 162 self.control("cancel", "submit", "Cancel event") 163 else: 164 self.control("discard", "submit", "Discard event") 165 166 page.add(" ") 167 self.control("save", "submit", "Save without sending") 168 page.p.close() 169 170 def show_object_on_page(self, errors=None): 171 172 """ 173 Show the calendar object on the current page. If 'errors' is given, show 174 a suitable message for the different errors provided. 175 """ 176 177 page = self.page 178 page.form(method="POST") 179 180 # Add a hidden control to help determine whether editing has already begun. 181 182 self.control("editing", "hidden", "true") 183 184 args = self.env.get_args() 185 186 # Obtain basic event information, generating any necessary editing controls. 187 188 attendees = self.update_current_attendees() 189 period = self.get_current_main_period() 190 self.show_object_datetime_controls(period) 191 192 # Obtain any separate recurrences for this event. 193 194 recurrenceids = self._get_active_recurrences(self.uid) 195 replaced = not self.recurrenceid and period.is_replaced(recurrenceids) 196 197 # Provide a summary of the object. 198 199 page.table(class_="object", cellspacing=5, cellpadding=5) 200 page.thead() 201 page.tr() 202 page.th("Event", class_="mainheading", colspan=2) 203 page.tr.close() 204 page.thead.close() 205 page.tbody() 206 207 for name, label in self.property_items: 208 field = name.lower() 209 210 items = uri_items(self.obj.get_items(name) or []) 211 rowspan = len(items) 212 213 if name == "ATTENDEE": 214 rowspan = len(attendees) + 1 # for the add button 215 elif not items: 216 continue 217 218 page.tr() 219 page.th(label, class_="objectheading %s%s" % (field, errors and field in errors and " error" or ""), rowspan=rowspan) 220 221 # Handle datetimes specially. 222 223 if name in ["DTSTART", "DTEND"]: 224 if not replaced: 225 226 # Obtain the datetime. 227 228 is_start = name == "DTSTART" 229 230 # Where no end datetime exists, use the start datetime as the 231 # basis of any potential datetime specified if dt-control is 232 # set. 233 234 self.show_datetime_controls(is_start and period.get_form_start() or period.get_form_end(), is_start) 235 236 elif name == "DTSTART": 237 page.td(class_="objectvalue %s replaced" % field, rowspan=2) 238 page.a("First occurrence replaced by a separate event", href=self.link_to(self.uid, replaced)) 239 page.td.close() 240 241 page.tr.close() 242 243 # Handle the summary specially. 244 245 elif name == "SUMMARY": 246 value = args.get("summary", [self.obj.get_value(name)])[0] 247 248 page.td(class_="objectvalue summary") 249 if self.is_organiser(): 250 self.control("summary", "text", value, size=80) 251 else: 252 page.add(value) 253 page.td.close() 254 page.tr.close() 255 256 # Handle attendees specially. 257 258 elif name == "ATTENDEE": 259 attendee_map = dict(items) 260 first = True 261 262 for i, value in enumerate(attendees): 263 if not first: 264 page.tr() 265 else: 266 first = False 267 268 # Obtain details of attendees to supply attributes. 269 270 self.show_attendee(i, value, attendee_map.get(value)) 271 page.tr.close() 272 273 # Allow more attendees to be specified. 274 275 if self.is_organiser(): 276 if not first: 277 page.tr() 278 279 page.td() 280 self.control("add", "submit", "add", id="add", class_="add") 281 page.label("Add attendee", for_="add", class_="add") 282 page.td.close() 283 page.tr.close() 284 285 # Handle potentially many values of other kinds. 286 287 else: 288 first = True 289 290 for i, (value, attr) in enumerate(items): 291 if not first: 292 page.tr() 293 else: 294 first = False 295 296 page.td(class_="objectvalue %s" % field) 297 page.add(value) 298 page.td.close() 299 page.tr.close() 300 301 page.tbody.close() 302 page.table.close() 303 304 self.show_recurrences(errors) 305 self.show_counters() 306 self.show_conflicting_events() 307 self.show_request_controls() 308 309 page.form.close() 310 311 def show_attendee(self, i, attendee, attendee_attr): 312 313 """ 314 For the current object, show the attendee in position 'i' with the given 315 'attendee' value, having 'attendee_attr' as any stored attributes. 316 """ 317 318 page = self.page 319 args = self.env.get_args() 320 321 partstat = attendee_attr and attendee_attr.get("PARTSTAT") 322 323 page.td(class_="objectvalue") 324 325 # Show a form control as organiser for new attendees. 326 327 if self.is_organiser() and self.can_edit_attendee(attendee): 328 self.control("attendee", "value", attendee, size="40") 329 else: 330 self.control("attendee", "hidden", attendee) 331 page.add(attendee) 332 page.add(" ") 333 334 # Show participation status, editable for the current user. 335 336 if attendee == self.user: 337 self.menu("partstat", partstat, self.partstat_items, "partstat") 338 339 # Allow the participation indicator to act as a submit 340 # button in order to refresh the page and show a control for 341 # the current user, if indicated. 342 343 elif self.is_organiser() and self.attendee_is_new(attendee): 344 self.control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh") 345 page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat") 346 347 # Otherwise, just show a label with the participation status. 348 349 else: 350 page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat") 351 352 # Permit organisers to remove attendees. 353 354 if self.is_organiser(): 355 356 # Permit the removal of newly-added attendees. 357 358 remove_type = self.can_remove_attendee(attendee) and "submit" or "checkbox" 359 self.control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove") 360 361 page.label("Remove", for_="remove-%d" % i, class_="remove") 362 page.label(for_="remove-%d" % i, class_="removed") 363 page.add("(Uninvited)") 364 page.span("Re-invite", class_="action") 365 page.label.close() 366 367 page.td.close() 368 369 def show_recurrences(self, errors=None): 370 371 """ 372 Show recurrences for the current object. If 'errors' is given, show a 373 suitable message for the different errors provided. 374 """ 375 376 page = self.page 377 378 # Obtain any parent object if this object is a specific recurrence. 379 380 if self.recurrenceid: 381 parent = self.get_stored_object(self.uid, None) 382 if not parent: 383 return 384 385 page.p() 386 page.a("This event modifies a recurring event.", href=self.link_to(self.uid)) 387 page.p.close() 388 389 # Obtain the periods associated with the event. 390 # NOTE: Add a control to add recurrences here. 391 392 recurrences = self.update_current_recurrences() 393 394 if len(recurrences) < 1: 395 return 396 397 recurrenceids = self._get_recurrences(self.uid) 398 399 page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) 400 401 # Show each recurrence in a separate table if editable. 402 403 if self.is_organiser() and recurrences: 404 405 for index, period in enumerate(recurrences): 406 self.show_recurrence(index, period, self.recurrenceid, recurrenceids, errors) 407 408 # Otherwise, use a compact single table. 409 410 else: 411 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 412 page.caption("Occurrences") 413 page.thead() 414 page.tr() 415 page.th("Start", class_="objectheading start") 416 page.th("End", class_="objectheading end") 417 page.tr.close() 418 page.thead.close() 419 page.tbody() 420 421 for index, period in enumerate(recurrences): 422 page.tr() 423 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, True) 424 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, False) 425 page.tr.close() 426 427 page.tbody.close() 428 page.table.close() 429 430 def show_recurrence(self, index, period, recurrenceid, recurrenceids, errors=None): 431 432 """ 433 Show recurrence controls for a recurrence provided by the current object 434 with the given 'index' position in the list of periods, the given 435 'period' details, where a 'recurrenceid' indicates any specific 436 recurrence, and where 'recurrenceids' indicates all known additional 437 recurrences for the object. 438 439 If 'errors' is given, show a suitable message for the different errors 440 provided. 441 """ 442 443 page = self.page 444 args = self.env.get_args() 445 446 p = event_period_from_period(period) 447 replaced = not recurrenceid and p.is_replaced(recurrenceids) 448 449 # Isolate the controls from neighbouring tables. 450 451 page.div() 452 453 self.show_object_datetime_controls(period, index) 454 455 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 456 page.caption(period.origin == "RRULE" and "Occurrence from rule" or "Occurrence") 457 page.tbody() 458 459 page.tr() 460 error = errors and ("dtstart", index) in errors and " error" or "" 461 page.th("Start", class_="objectheading start%s" % error) 462 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, True) 463 page.tr.close() 464 page.tr() 465 error = errors and ("dtend", index) in errors and " error" or "" 466 page.th("End", class_="objectheading end%s" % error) 467 self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, False) 468 page.tr.close() 469 470 # Permit the removal of recurrences. 471 472 if not replaced: 473 page.tr() 474 page.th("") 475 page.td() 476 477 remove_type = (not self.obj.is_shared() or not period.origin) and "submit" or "checkbox" 478 self.control("recur-remove", remove_type, str(index), 479 str(index) in args.get("recur-remove", []), 480 id="recur-remove-%d" % index, class_="remove") 481 482 page.label("Remove", for_="recur-remove-%d" % index, class_="remove") 483 page.label(for_="recur-remove-%d" % index, class_="removed") 484 page.add("(Removed)") 485 page.span("Re-add", class_="action") 486 page.label.close() 487 488 page.td.close() 489 page.tr.close() 490 491 page.tbody.close() 492 page.table.close() 493 494 page.div.close() 495 496 def show_counters(self): 497 498 "Show any counter-proposals for the current object." 499 500 page = self.page 501 query = self.env.get_query() 502 counter = query.get("counter", [None])[0] 503 504 attendees = self._get_counters(self.uid, self.recurrenceid) 505 tzid = self.get_tzid() 506 507 if not attendees: 508 return 509 510 page.p("The following counter-proposals have been received for this event:") 511 512 page.table(cellspacing=5, cellpadding=5, class_="counters") 513 page.thead() 514 page.tr() 515 page.th("Attendee", rowspan=2) 516 page.th("Periods", colspan=2) 517 page.tr.close() 518 page.tr() 519 page.th("Start") 520 page.th("End") 521 page.tr.close() 522 page.thead.close() 523 page.tbody() 524 525 for attendee in attendees: 526 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee) 527 periods = self.get_periods(obj) 528 529 first = True 530 for p in periods: 531 identifier = "%s-%s" % (format_datetime(p.get_start_point()), format_datetime(p.get_end_point())) 532 css = identifier == counter and "selected" or "" 533 534 if first: 535 page.tr(rowspan=len(periods), class_=css) 536 page.td(attendee) 537 first = False 538 else: 539 page.tr(class_=css) 540 541 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 542 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 543 544 page.td(start) 545 page.td(end) 546 547 page.tr.close() 548 549 page.tbody.close() 550 page.table.close() 551 552 def show_conflicting_events(self): 553 554 "Show conflicting events for the current object." 555 556 page = self.page 557 recurrenceids = self._get_active_recurrences(self.uid) 558 559 # Obtain the user's timezone. 560 561 tzid = self.get_tzid() 562 periods = self.get_periods(self.obj) 563 564 # Indicate whether there are conflicting events. 565 566 conflicts = [] 567 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 568 569 for participant in self.get_current_attendees(): 570 if participant == self.user: 571 freebusy = self.store.get_freebusy(participant) 572 else: 573 freebusy = self.store.get_freebusy_for_other(self.user, participant) 574 575 if not freebusy: 576 continue 577 578 # Obtain any time zone details from the suggested event. 579 580 _dtstart, attr = self.obj.get_item("DTSTART") 581 tzid = attr.get("TZID", tzid) 582 583 # Show any conflicts with periods of actual attendance. 584 585 participant_attr = attendee_map.get(participant) 586 partstat = participant_attr and participant_attr.get("PARTSTAT") 587 recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid) 588 589 for p in have_conflict(freebusy, periods, True): 590 if not self.recurrenceid and p.is_replaced(recurrences): 591 continue 592 593 if ( # Unidentified or different event 594 (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and 595 # Different period or unclear participation with the same period 596 (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and 597 # Participant not limited to organising 598 p.transp != "ORG" 599 ): 600 601 conflicts.append(p) 602 603 conflicts.sort() 604 605 # Show any conflicts with periods of actual attendance. 606 607 if conflicts: 608 page.p("This event conflicts with others:") 609 610 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 611 page.thead() 612 page.tr() 613 page.th("Event") 614 page.th("Start") 615 page.th("End") 616 page.tr.close() 617 page.thead.close() 618 page.tbody() 619 620 for p in conflicts: 621 622 # Provide details of any conflicting event. 623 624 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 625 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 626 627 page.tr() 628 629 # Show the event summary for the conflicting event. 630 631 page.td() 632 if p.summary: 633 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 634 else: 635 page.add("(Unspecified event)") 636 page.td.close() 637 638 page.td(start) 639 page.td(end) 640 641 page.tr.close() 642 643 page.tbody.close() 644 page.table.close() 645 646 class EventPage(EventPageFragment): 647 648 "A request handler for the event page." 649 650 def __init__(self, resource=None, messenger=None): 651 ResourceClientForObject.__init__(self, resource) 652 self.messenger = messenger or Messenger() 653 654 # Request logic methods. 655 656 def is_initial_load(self): 657 658 "Return whether the event is being loaded and shown for the first time." 659 660 return not self.env.get_args().has_key("editing") 661 662 def handle_request(self): 663 664 """ 665 Handle actions involving the current object, returning an error if one 666 occurred, or None if the request was successfully handled. 667 """ 668 669 # Handle a submitted form. 670 671 args = self.env.get_args() 672 673 # Get the possible actions. 674 675 reply = args.has_key("reply") 676 discard = args.has_key("discard") 677 create = args.has_key("create") 678 cancel = args.has_key("cancel") 679 ignore = args.has_key("ignore") 680 save = args.has_key("save") 681 682 have_action = reply or discard or create or cancel or ignore or save 683 684 if not have_action: 685 return ["action"] 686 687 # If ignoring the object, return to the calendar. 688 689 if ignore: 690 self.redirect(self.env.get_path()) 691 return None 692 693 # Update the object. 694 695 single_user = False 696 697 if reply or create or cancel or save: 698 699 # Update principal event details if organiser. 700 701 if self.is_organiser(): 702 703 # Update time periods (main and recurring). 704 705 try: 706 period = self.handle_main_period() 707 except PeriodError, exc: 708 return exc.args 709 710 try: 711 periods = self.handle_recurrence_periods() 712 except PeriodError, exc: 713 return exc.args 714 715 # Set the periods in the object, first obtaining removed and 716 # modified period information. 717 718 to_unschedule = self.get_removed_periods(periods) 719 720 self.obj.set_period(period) 721 self.obj.set_periods(periods) 722 723 # Update summary. 724 725 if args.has_key("summary"): 726 self.obj["SUMMARY"] = [(args["summary"][0], {})] 727 728 # Obtain any participants and those to be removed. 729 730 attendees = self.get_attendees_from_page() 731 removed = [attendees[int(i)] for i in args.get("remove", [])] 732 to_cancel = self.update_attendees(self.obj, attendees, removed) 733 single_user = not attendees or attendees == [self.user] 734 735 # Update attendee participation for the current user. 736 737 if args.has_key("partstat"): 738 self.update_participation(self.obj, args["partstat"][0]) 739 740 # Process any action. 741 742 invite = not save and create and not single_user 743 save = save or create and single_user 744 745 handled = True 746 747 if reply or invite or cancel: 748 749 client = ManagerClient(self.obj, self.user, self.messenger) 750 751 # Process the object and remove it from the list of requests. 752 753 if reply and client.process_received_request(): 754 self.remove_request(self.uid, self.recurrenceid) 755 756 elif self.is_organiser() and (invite or cancel): 757 758 # Invitation, uninvitation and unscheduling... 759 760 if client.process_created_request( 761 invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule): 762 763 self.remove_request(self.uid, self.recurrenceid) 764 765 # Save single user events. 766 767 elif save: 768 self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node()) 769 self.update_event_in_freebusy() 770 self.remove_request(self.uid, self.recurrenceid) 771 772 # Remove the request and the object. 773 774 elif discard: 775 self.remove_event_from_freebusy() 776 self.remove_event(self.uid, self.recurrenceid) 777 self.remove_request(self.uid, self.recurrenceid) 778 779 else: 780 handled = False 781 782 # Upon handling an action, redirect to the main page. 783 784 if handled: 785 self.redirect(self.env.get_path()) 786 787 return None 788 789 def handle_main_period(self): 790 791 "Return period details for the main start/end period in an event." 792 793 return self.get_main_period_from_page().as_event_period() 794 795 def handle_recurrence_periods(self): 796 797 "Return period details for the recurrences specified for an event." 798 799 return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] 800 801 # Access to form-originating object information. 802 803 def get_main_period_from_page(self): 804 805 "Return the main period defined in the event form." 806 807 args = self.env.get_args() 808 809 dtend_enabled = args.get("dtend-control", [None])[0] 810 dttimes_enabled = args.get("dttimes-control", [None])[0] 811 start = self.get_date_control_values("dtstart") 812 end = self.get_date_control_values("dtend") 813 814 return FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid()) 815 816 def get_recurrences_from_page(self): 817 818 "Return the recurrences defined in the event form." 819 820 args = self.env.get_args() 821 822 all_dtend_enabled = args.get("dtend-control-recur", []) 823 all_dttimes_enabled = args.get("dttimes-control-recur", []) 824 all_starts = self.get_date_control_values("dtstart-recur", multiple=True) 825 all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") 826 all_origins = args.get("recur-origin", []) 827 828 periods = [] 829 830 for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \ 831 enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)): 832 833 dtend_enabled = str(index) in all_dtend_enabled 834 dttimes_enabled = str(index) in all_dttimes_enabled 835 period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin) 836 periods.append(period) 837 838 return periods 839 840 def get_removed_periods(self, periods): 841 842 """ 843 Return those from the recurrence 'periods' to remove upon updating an 844 event. 845 """ 846 847 to_unschedule = [] 848 args = self.env.get_args() 849 for i in args.get("recur-remove", []): 850 to_unschedule.append(periods[int(i)]) 851 return to_unschedule 852 853 def get_attendees_from_page(self): 854 855 """ 856 Return attendees from the request, normalised for iCalendar purposes, 857 and without duplicates. 858 """ 859 860 args = self.env.get_args() 861 862 attendees = args.get("attendee", []) 863 unique_attendees = set() 864 ordered_attendees = [] 865 866 for attendee in attendees: 867 if not attendee.strip(): 868 continue 869 attendee = get_uri(attendee) 870 if attendee not in unique_attendees: 871 unique_attendees.add(attendee) 872 ordered_attendees.append(attendee) 873 874 return ordered_attendees 875 876 def update_attendees_from_page(self): 877 878 "Add or remove attendees. This does not affect the stored object." 879 880 args = self.env.get_args() 881 882 attendees = self.get_attendees_from_page() 883 884 if args.has_key("add"): 885 attendees.append("") 886 887 # Only actually remove attendees if the event is unsent, if the attendee 888 # is new, or if it is the current user being removed. 889 890 if args.has_key("remove"): 891 still_to_remove = [] 892 893 for i in args["remove"]: 894 try: 895 attendee = attendees[int(i)] 896 except IndexError: 897 continue 898 899 if self.can_remove_attendee(attendee): 900 attendees.remove(attendee) 901 else: 902 still_to_remove.append(i) 903 904 args["remove"] = still_to_remove 905 906 return attendees 907 908 def update_recurrences_from_page(self): 909 910 "Add or remove recurrences. This does not affect the stored object." 911 912 args = self.env.get_args() 913 914 recurrences = self.get_recurrences_from_page() 915 916 # NOTE: Addition of recurrences to be supported. 917 918 # Only actually remove recurrences if the event is unsent, or if the 919 # recurrence is new. 920 921 if args.has_key("recur-remove"): 922 still_to_remove = [] 923 924 for i in args["recur-remove"]: 925 try: 926 recurrence = recurrences[int(i)] 927 except IndexError: 928 continue 929 930 if self.can_remove_recurrence(recurrence): 931 recurrences.remove(recurrence) 932 else: 933 still_to_remove.append(i) 934 935 args["recur-remove"] = still_to_remove 936 937 return recurrences 938 939 # Access to current object information. 940 941 def get_current_main_period(self): 942 943 """ 944 Return the currently active main period for the current object depending 945 on whether editing has begun or whether the object has just been loaded. 946 """ 947 948 if self.is_initial_load() or not self.is_organiser(): 949 return self.get_stored_main_period() 950 else: 951 return self.get_main_period_from_page() 952 953 def get_current_recurrences(self): 954 955 """ 956 Return recurrences for the current object using the original object 957 details where no editing is in progress, using form data otherwise. 958 """ 959 960 if self.is_initial_load() or not self.is_organiser(): 961 return self.get_stored_recurrences() 962 else: 963 return self.get_recurrences_from_page() 964 965 def update_current_recurrences(self): 966 967 "Return an updated collection of recurrences for the current object." 968 969 if self.is_initial_load() or not self.is_organiser(): 970 return self.get_stored_recurrences() 971 else: 972 return self.update_recurrences_from_page() 973 974 def get_current_attendees(self): 975 976 """ 977 Return attendees for the current object depending on whether the object 978 has been edited or instead provides such information from its stored 979 form. 980 """ 981 982 if self.is_initial_load() or not self.is_organiser(): 983 return self.get_stored_attendees() 984 else: 985 return self.get_attendees_from_page() 986 987 def update_current_attendees(self): 988 989 "Return an updated collection of attendees for the current object." 990 991 if self.is_initial_load() or not self.is_organiser(): 992 return self.get_stored_attendees() 993 else: 994 return self.update_attendees_from_page() 995 996 # Full page output methods. 997 998 def show(self, path_info): 999 1000 "Show an object request using the given 'path_info' for the current user." 1001 1002 uid, recurrenceid = self.get_identifiers(path_info) 1003 obj = self.get_stored_object(uid, recurrenceid) 1004 self.set_object(obj) 1005 1006 if not obj: 1007 return False 1008 1009 errors = self.handle_request() 1010 1011 if not errors: 1012 return True 1013 1014 self.new_page(title="Event") 1015 self.show_object_on_page(errors) 1016 1017 return True 1018 1019 # vim: tabstop=4 expandtab shiftwidth=4