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