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