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