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 _enable = self.element_enable 927 928 # Add a dynamic stylesheet to permit the controls to modify the display. 929 # NOTE: The style details need to be coordinated with the static 930 # NOTE: stylesheet. 931 932 if index is not None: 933 page.style(type="text/css") 934 935 # Unlike the rules for object properties, these affect recurrence 936 # properties. 937 938 page.add("""\ 939 input#dttimes-enable-%(index)d, 940 input#dtend-enable-%(index)d, 941 input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled, 942 input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled, 943 input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled, 944 input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled { 945 display: none; 946 }""" % {"index" : index}) 947 948 page.style.close() 949 950 self._control( 951 _name("dtend-control", "recur", index), "checkbox", 952 _enable(index), p.end_enabled, 953 id=_id("dtend-enable", index) 954 ) 955 956 self._control( 957 _name("dttimes-control", "recur", index), "checkbox", 958 _enable(index), p.times_enabled, 959 id=_id("dttimes-enable", index) 960 ) 961 962 def show_datetime_controls(self, obj, formdate, show_start): 963 964 """ 965 Show datetime details from the given 'obj' for the 'formdate', showing 966 start details if 'show_start' is set to a true value. Details will 967 appear as controls for organisers and labels for attendees. 968 """ 969 970 page = self.page 971 972 # Show controls for editing as organiser. 973 974 if self.is_organiser(obj): 975 page.td(class_="objectvalue dt%s" % (show_start and "start" or "end")) 976 977 if show_start: 978 page.div(class_="dt enabled") 979 self._show_date_controls("dtstart", formdate) 980 page.br() 981 page.label("Specify times", for_="dttimes-enable", class_="time disabled enable") 982 page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable") 983 page.div.close() 984 985 else: 986 page.div(class_="dt disabled") 987 page.label("Specify end date", for_="dtend-enable", class_="enable") 988 page.div.close() 989 page.div(class_="dt enabled") 990 self._show_date_controls("dtend", formdate) 991 page.br() 992 page.label("End on same day", for_="dtend-enable", class_="disable") 993 page.div.close() 994 995 page.td.close() 996 997 # Show a label as attendee. 998 999 else: 1000 dt = formdate.as_datetime() 1001 if dt: 1002 page.td(self.format_datetime(dt, "full")) 1003 else: 1004 page.td("(Unrecognised date)") 1005 1006 def show_recurrence_controls(self, obj, index, period, recurrenceid, recurrenceids, show_start): 1007 1008 """ 1009 Show datetime details from the given 'obj' for the recurrence having the 1010 given 'index', with the recurrence period described by 'period', 1011 indicating a start, end and origin of the period from the event details, 1012 employing any 'recurrenceid' and 'recurrenceids' for the object to 1013 configure the displayed information. 1014 1015 If 'show_start' is set to a true value, the start details will be shown; 1016 otherwise, the end details will be shown. 1017 """ 1018 1019 page = self.page 1020 _id = self.element_identifier 1021 _name = self.element_name 1022 1023 p = event_period_from_period(period) 1024 replaced = not recurrenceid and self.is_replaced(p, recurrenceids) 1025 1026 # Show controls for editing as organiser. 1027 1028 if self.is_organiser(obj) and not replaced: 1029 page.td(class_="objectvalue dt%s" % (show_start and "start" or "end")) 1030 1031 read_only = period.origin == "RRULE" 1032 1033 if show_start: 1034 page.div(class_="dt enabled") 1035 self._show_date_controls(_name("dtstart", "recur", index), p.get_form_start(), index=index, read_only=read_only) 1036 if not read_only: 1037 page.br() 1038 page.label("Specify times", for_=_id("dttimes-enable", index), class_="time disabled enable") 1039 page.label("Specify dates only", for_=_id("dttimes-enable", index), class_="time enabled disable") 1040 page.div.close() 1041 1042 # Put the origin somewhere. 1043 1044 self._control("recur-origin", "hidden", p.origin or "") 1045 1046 else: 1047 page.div(class_="dt disabled") 1048 if not read_only: 1049 page.label("Specify end date", for_=_id("dtend-enable", index), class_="enable") 1050 page.div.close() 1051 page.div(class_="dt enabled") 1052 self._show_date_controls(_name("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False, read_only=read_only) 1053 if not read_only: 1054 page.br() 1055 page.label("End on same day", for_=_id("dtend-enable", index), class_="disable") 1056 page.div.close() 1057 1058 page.td.close() 1059 1060 # Show label as attendee. 1061 1062 else: 1063 self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start) 1064 1065 def show_recurrence_label(self, period, recurrenceid, recurrenceids, show_start): 1066 1067 """ 1068 Show datetime details for the given 'period', employing any 1069 'recurrenceid' and 'recurrenceids' for the object to configure the 1070 displayed information. 1071 1072 If 'show_start' is set to a true value, the start details will be shown; 1073 otherwise, the end details will be shown. 1074 """ 1075 1076 page = self.page 1077 1078 p = event_period_from_period(period) 1079 replaced = not recurrenceid and self.is_replaced(p, recurrenceids) 1080 1081 css = " ".join([ 1082 replaced and "replaced" or "", 1083 self.is_affected(p, recurrenceid) and "affected" or "" 1084 ]) 1085 1086 formdate = show_start and p.get_form_start() or p.get_form_end() 1087 dt = formdate.as_datetime() 1088 if dt: 1089 page.td(self.format_datetime(dt, "long"), class_=css) 1090 else: 1091 page.td("(Unrecognised date)") 1092 1093 # Full page output methods. 1094 1095 def show(self, path_info): 1096 1097 "Show an object request using the given 'path_info' for the current user." 1098 1099 uid, recurrenceid = self._get_identifiers(path_info) 1100 obj = self.get_stored_object(uid, recurrenceid) 1101 1102 if not obj: 1103 return False 1104 1105 errors = self.handle_request(obj) 1106 1107 if not errors: 1108 return True 1109 1110 self.new_page(title="Event") 1111 self.show_object_on_page(obj, errors) 1112 1113 return True 1114 1115 # Utility methods. 1116 1117 def _control(self, name, type, value, selected=False, **kw): 1118 1119 """ 1120 Show a control with the given 'name', 'type' and 'value', with 1121 'selected' indicating whether it should be selected (checked or 1122 equivalent), and with keyword arguments setting other properties. 1123 """ 1124 1125 page = self.page 1126 if selected: 1127 page.input(name=name, type=type, value=value, checked=selected, **kw) 1128 else: 1129 page.input(name=name, type=type, value=value, **kw) 1130 1131 def _show_menu(self, name, default, items, class_="", index=None): 1132 1133 """ 1134 Show a select menu having the given 'name', set to the given 'default', 1135 providing the given (value, label) 'items', and employing the given CSS 1136 'class_' if specified. 1137 """ 1138 1139 page = self.page 1140 values = self.env.get_args().get(name, [default]) 1141 if index is not None: 1142 values = values[index:] 1143 values = values and values[0:1] or [default] 1144 1145 page.select(name=name, class_=class_) 1146 for v, label in items: 1147 if v is None: 1148 continue 1149 if v in values: 1150 page.option(label, value=v, selected="selected") 1151 else: 1152 page.option(label, value=v) 1153 page.select.close() 1154 1155 def _show_date_controls(self, name, default, index=None, show_tzid=True, read_only=False): 1156 1157 """ 1158 Show date controls for a field with the given 'name' and 'default' form 1159 date value. 1160 1161 If 'index' is specified, default field values will be overridden by the 1162 element from a collection of existing form values with the specified 1163 index; otherwise, field values will be overridden by a single form 1164 value. 1165 1166 If 'show_tzid' is set to a false value, the time zone menu will not be 1167 provided. 1168 1169 If 'read_only' is set to a true value, the controls will be hidden and 1170 labels will be employed instead. 1171 """ 1172 1173 page = self.page 1174 1175 # Show dates for up to one week around the current date. 1176 1177 dt = default.as_datetime() 1178 if not dt: 1179 dt = date.today() 1180 1181 base = to_date(dt) 1182 1183 # Show a date label with a hidden field if read-only. 1184 1185 if read_only: 1186 self._control("%s-date" % name, "hidden", format_datetime(base)) 1187 page.span(self.format_date(base, "long")) 1188 1189 # Show dates for up to one week around the current date. 1190 # NOTE: Support paging to other dates. 1191 1192 else: 1193 items = [] 1194 for i in range(-7, 8): 1195 d = base + timedelta(i) 1196 items.append((format_datetime(d), self.format_date(d, "full"))) 1197 self._show_menu("%s-date" % name, format_datetime(base), items, index=index) 1198 1199 # Show time details. 1200 1201 page.span(class_="time enabled") 1202 1203 if read_only: 1204 page.span("%s:%s:%s" % (default.get_hour(), default.get_minute(), default.get_second())) 1205 self._control("%s-hour" % name, "hidden", default.get_hour()) 1206 self._control("%s-minute" % name, "hidden", default.get_minute()) 1207 self._control("%s-second" % name, "hidden", default.get_second()) 1208 else: 1209 self._control("%s-hour" % name, "text", default.get_hour(), maxlength=2, size=2) 1210 page.add(":") 1211 self._control("%s-minute" % name, "text", default.get_minute(), maxlength=2, size=2) 1212 page.add(":") 1213 self._control("%s-second" % name, "text", default.get_second(), maxlength=2, size=2) 1214 1215 # Show time zone details. 1216 1217 if show_tzid: 1218 page.add(" ") 1219 tzid = default.get_tzid() or self.get_tzid() 1220 1221 # Show a label if read-only or a menu otherwise. 1222 1223 if read_only: 1224 self._control("%s-tzid" % name, "hidden", tzid) 1225 page.span(tzid) 1226 else: 1227 self._show_timezone_menu("%s-tzid" % name, tzid, index) 1228 1229 page.span.close() 1230 1231 def _show_timezone_menu(self, name, default, index=None): 1232 1233 """ 1234 Show timezone controls using a menu with the given 'name', set to the 1235 given 'default' unless a field of the given 'name' provides a value. 1236 """ 1237 1238 entries = [(tzid, tzid) for tzid in pytz.all_timezones] 1239 self._show_menu(name, default, entries, index=index) 1240 1241 # vim: tabstop=4 expandtab shiftwidth=4