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