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