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