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