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, update_participation 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.handler import ManagerHandler 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. 147 148 if args.has_key("partstat"): 149 update_participation(obj, self.user, 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 handler = ManagerHandler(obj, self.user, self.messenger) 161 162 # Process the object and remove it from the list of requests. 163 164 if reply and handler.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 handler.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 self._control("editing", "hidden", "true") 509 510 uid = obj.get_uid() 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 = obj.get_recurrenceid() 528 recurrenceids = self._get_recurrences(uid) 529 replaced = not recurrenceid and self.is_replaced(p, recurrenceids) 530 531 # Provide a summary of the object. 532 533 page.table(class_="object", cellspacing=5, cellpadding=5) 534 page.thead() 535 page.tr() 536 page.th("Event", class_="mainheading", colspan=2) 537 page.tr.close() 538 page.thead.close() 539 page.tbody() 540 541 for name, label in self.property_items: 542 field = name.lower() 543 544 items = obj.get_items(name) or [] 545 rowspan = len(items) 546 547 if name == "ATTENDEE": 548 rowspan = len(attendees) + 1 # for the add button 549 elif not items: 550 continue 551 552 page.tr() 553 page.th(label, class_="objectheading %s%s" % (field, errors and field in errors and " error" or ""), rowspan=rowspan) 554 555 # Handle datetimes specially. 556 557 if name in ["DTSTART", "DTEND"]: 558 if not replaced: 559 560 # Obtain the datetime. 561 562 is_start = name == "DTSTART" 563 564 # Where no end datetime exists, use the start datetime as the 565 # basis of any potential datetime specified if dt-control is 566 # set. 567 568 self.show_datetime_controls(obj, is_start and p.get_form_start() or p.get_form_end(), is_start) 569 570 elif name == "DTSTART": 571 page.td(class_="objectvalue %s replaced" % field, rowspan=2) 572 page.a("First occurrence replaced by a separate event", href=self.link_to(uid, replaced)) 573 page.td.close() 574 575 page.tr.close() 576 577 # Handle the summary specially. 578 579 elif name == "SUMMARY": 580 value = args.get("summary", [obj.get_value(name)])[0] 581 582 page.td(class_="objectvalue summary") 583 if self.is_organiser(obj): 584 self._control("summary", "text", value, size=80) 585 else: 586 page.add(value) 587 page.td.close() 588 page.tr.close() 589 590 # Handle attendees specially. 591 592 elif name == "ATTENDEE": 593 attendee_map = dict(items) 594 first = True 595 596 for i, value in enumerate(attendees): 597 if not first: 598 page.tr() 599 else: 600 first = False 601 602 # Obtain details of attendees to supply attributes. 603 604 self.show_attendee(obj, i, value, attendee_map.get(value)) 605 page.tr.close() 606 607 # Allow more attendees to be specified. 608 609 if self.is_organiser(obj): 610 if not first: 611 page.tr() 612 613 page.td() 614 self._control("add", "submit", "add", id="add", class_="add") 615 page.label("Add attendee", for_="add", class_="add") 616 page.td.close() 617 page.tr.close() 618 619 # Handle potentially many values of other kinds. 620 621 else: 622 first = True 623 624 for i, (value, attr) in enumerate(items): 625 if not first: 626 page.tr() 627 else: 628 first = False 629 630 page.td(class_="objectvalue %s" % field) 631 page.add(value) 632 page.td.close() 633 page.tr.close() 634 635 page.tbody.close() 636 page.table.close() 637 638 self.show_recurrences(obj, errors) 639 self.show_conflicting_events(obj) 640 self.show_request_controls(obj) 641 642 page.form.close() 643 644 def show_attendee(self, obj, i, attendee, attendee_attr): 645 646 """ 647 For the given object 'obj', show the attendee in position 'i' with the 648 given 'attendee' value, having 'attendee_attr' as any stored attributes. 649 """ 650 651 page = self.page 652 args = self.env.get_args() 653 654 existing = attendee_attr is not None 655 partstat = attendee_attr and attendee_attr.get("PARTSTAT") 656 sequence = obj.get_value("SEQUENCE") 657 658 page.td(class_="objectvalue") 659 660 # Show a form control as organiser for new attendees. 661 662 if self.is_organiser(obj) and (not existing or sequence is None): 663 self._control("attendee", "value", attendee, size="40") 664 else: 665 self._control("attendee", "hidden", attendee) 666 page.add(attendee) 667 page.add(" ") 668 669 # Show participation status, editable for the current user. 670 671 if attendee == self.user: 672 self._show_menu("partstat", partstat, self.partstat_items, "partstat") 673 674 # Allow the participation indicator to act as a submit 675 # button in order to refresh the page and show a control for 676 # the current user, if indicated. 677 678 elif self.is_organiser(obj) and not existing: 679 self._control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh") 680 page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat") 681 else: 682 page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat") 683 684 # Permit organisers to remove attendees. 685 686 if self.is_organiser(obj): 687 688 # Permit the removal of newly-added attendees. 689 690 remove_type = (not existing or sequence is None or attendee == self.user) and "submit" or "checkbox" 691 self._control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove") 692 693 page.label("Remove", for_="remove-%d" % i, class_="remove") 694 page.label("Uninvited", for_="remove-%d" % i, class_="removed") 695 696 page.td.close() 697 698 def show_recurrences(self, obj, errors=None): 699 700 """ 701 Show recurrences for the object having the given representation 'obj'. 702 If 'errors' is given, show a suitable message for the different errors 703 provided. 704 """ 705 706 page = self.page 707 708 # Obtain any parent object if this object is a specific recurrence. 709 710 uid = obj.get_uid() 711 recurrenceid = obj.get_recurrenceid() 712 713 if recurrenceid: 714 parent = self._get_object(uid) 715 if not parent: 716 return 717 718 page.p() 719 page.a("This event modifies a recurring event.", href=self.link_to(uid)) 720 page.p.close() 721 722 # Obtain the periods associated with the event. 723 724 recurrences = self.get_current_recurrences(obj) 725 726 if len(recurrences) < 1: 727 return 728 729 recurrenceids = self._get_recurrences(uid) 730 731 page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) 732 733 # Show each recurrence in a separate table if editable. 734 735 if self.is_organiser(obj) and recurrences: 736 737 for index, period in enumerate(recurrences): 738 self.show_recurrence(obj, index, period, recurrenceid, recurrenceids, errors) 739 740 # Otherwise, use a compact single table. 741 742 else: 743 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 744 page.caption("Occurrences") 745 page.thead() 746 page.tr() 747 page.th("Start", class_="objectheading start") 748 page.th("End", class_="objectheading end") 749 page.tr.close() 750 page.thead.close() 751 page.tbody() 752 753 for index, period in enumerate(recurrences): 754 page.tr() 755 self.show_recurrence_label(period, recurrenceid, recurrenceids, True) 756 self.show_recurrence_label(period, recurrenceid, recurrenceids, False) 757 page.tr.close() 758 759 page.tbody.close() 760 page.table.close() 761 762 def show_recurrence(self, obj, index, period, recurrenceid, recurrenceids, errors=None): 763 764 """ 765 Show recurrence controls for a recurrence provided by 'obj' with the 766 given 'index' position in the list of periods, the given 'period' 767 details, where a 'recurrenceid' indicates any specific recurrence, and 768 where 'recurrenceids' indicates all known additional recurrences for the 769 object. 770 771 If 'errors' is given, show a suitable message for the different errors 772 provided. 773 """ 774 775 page = self.page 776 args = self.env.get_args() 777 778 sequence = obj.get_value("SEQUENCE") 779 780 p = event_period_from_period(period) 781 replaced = not recurrenceid and self.is_replaced(p, recurrenceids) 782 783 # Isolate the controls from neighbouring tables. 784 785 page.div() 786 787 self.show_object_datetime_controls(period, index) 788 789 page.table(cellspacing=5, cellpadding=5, class_="recurrence") 790 page.caption(period.origin == "RRULE" and "Occurrence from rule" or "Occurrence") 791 page.tbody() 792 793 page.tr() 794 error = errors and ("dtstart", index) in errors and " error" or "" 795 page.th("Start", class_="objectheading start%s" % error) 796 self.show_recurrence_controls(obj, index, period, recurrenceid, recurrenceids, True) 797 page.tr.close() 798 page.tr() 799 error = errors and ("dtend", index) in errors and " error" or "" 800 page.th("End", class_="objectheading end%s" % error) 801 self.show_recurrence_controls(obj, index, period, recurrenceid, recurrenceids, False) 802 page.tr.close() 803 804 # Permit the removal of recurrences. 805 806 if not replaced: 807 page.tr() 808 page.th("") 809 page.td() 810 811 remove_type = sequence is None or not period.origin and "submit" or "checkbox" 812 self._control("recur-remove", remove_type, str(index), 813 str(index) in args.get("recur-remove", []), 814 id="recur-remove-%d" % index, class_="remove") 815 816 page.label("Remove", for_="recur-remove-%d" % index, class_="remove") 817 page.label("Removed", for_="recur-remove-%d" % index, class_="removed") 818 819 page.td.close() 820 page.tr.close() 821 822 page.tbody.close() 823 page.table.close() 824 825 page.div.close() 826 827 def show_conflicting_events(self, obj): 828 829 """ 830 Show conflicting events for the object having the representation 'obj'. 831 """ 832 833 page = self.page 834 uid = obj.get_uid() 835 recurrenceid = obj.get_recurrenceid() 836 recurrenceids = self._get_recurrences(uid) 837 838 # Obtain the user's timezone. 839 840 tzid = self.get_tzid() 841 periods = obj.get_periods(self.get_tzid(), self.get_window_end()) 842 843 # Indicate whether there are conflicting events. 844 845 conflicts = [] 846 847 for participant in self.get_current_attendees(obj): 848 if participant == self.user: 849 freebusy = self.store.get_freebusy(participant) 850 else: 851 freebusy = self.store.get_freebusy_for_other(self.user, participant) 852 853 if not freebusy: 854 continue 855 856 # Obtain any time zone details from the suggested event. 857 858 _dtstart, attr = obj.get_item("DTSTART") 859 tzid = attr.get("TZID", tzid) 860 861 # Show any conflicts with periods of actual attendance. 862 863 for p in have_conflict(freebusy, periods, True): 864 if not recurrenceid and self.is_replaced(p, recurrenceids): 865 continue 866 if (p.uid != uid or (recurrenceid and p.recurrenceid) and p.recurrenceid != recurrenceid) and p.transp != "ORG": 867 conflicts.append(p) 868 869 conflicts.sort() 870 871 # Show any conflicts with periods of actual attendance. 872 873 if conflicts: 874 page.p("This event conflicts with others:") 875 876 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 877 page.thead() 878 page.tr() 879 page.th("Event") 880 page.th("Start") 881 page.th("End") 882 page.tr.close() 883 page.thead.close() 884 page.tbody() 885 886 for p in conflicts: 887 888 # Provide details of any conflicting event. 889 890 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long") 891 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long") 892 893 page.tr() 894 895 # Show the event summary for the conflicting event. 896 897 page.td() 898 if p.summary: 899 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 900 else: 901 page.add("(Unspecified event)") 902 page.td.close() 903 904 page.td(start) 905 page.td(end) 906 907 page.tr.close() 908 909 page.tbody.close() 910 page.table.close() 911 912 # Generation of controls within page fragments. 913 914 def show_object_datetime_controls(self, period, index=None): 915 916 """ 917 Show datetime-related controls if already active or if an object needs 918 them for the given 'period'. The given 'index' is used to parameterise 919 individual controls for dynamic manipulation. 920 """ 921 922 p = form_period_from_period(period) 923 924 page = self.page 925 args = self.env.get_args() 926 sn = self._suffixed_name 927 ssn = self._simple_suffixed_name 928 929 # Add a dynamic stylesheet to permit the controls to modify the display. 930 # NOTE: The style details need to be coordinated with the static 931 # NOTE: stylesheet. 932 933 if index is not None: 934 page.style(type="text/css") 935 936 # Unlike the rules for object properties, these affect recurrence 937 # properties. 938 939 page.add("""\ 940 input#dttimes-enable-%(index)d, 941 input#dtend-enable-%(index)d, 942 input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled, 943 input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled, 944 input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled, 945 input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled { 946 display: none; 947 }""" % {"index" : index}) 948 949 page.style.close() 950 951 self._control( 952 ssn("dtend-control", "recur", index), "checkbox", 953 index is not None and str(index) or "enable", p.end_enabled, 954 id=sn("dtend-enable", index) 955 ) 956 957 self._control( 958 ssn("dttimes-control", "recur", index), "checkbox", 959 index is not None and str(index) or "enable", p.times_enabled, 960 id=sn("dttimes-enable", index) 961 ) 962 963 def show_datetime_controls(self, obj, formdate, show_start): 964 965 """ 966 Show datetime details from the given 'obj' for the 'formdate', showing 967 start details if 'show_start' is set to a true value. Details will 968 appear as controls for organisers and labels for attendees. 969 """ 970 971 page = self.page 972 973 # Show controls for editing as organiser. 974 975 if self.is_organiser(obj): 976 page.td(class_="objectvalue dt%s" % (show_start and "start" or "end")) 977 978 if show_start: 979 page.div(class_="dt enabled") 980 self._show_date_controls("dtstart", formdate) 981 page.br() 982 page.label("Specify times", for_="dttimes-enable", class_="time disabled enable") 983 page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable") 984 page.div.close() 985 986 else: 987 page.div(class_="dt disabled") 988 page.label("Specify end date", for_="dtend-enable", class_="enable") 989 page.div.close() 990 page.div(class_="dt enabled") 991 self._show_date_controls("dtend", formdate) 992 page.br() 993 page.label("End on same day", for_="dtend-enable", class_="disable") 994 page.div.close() 995 996 page.td.close() 997 998 # Show a label as attendee. 999 1000 else: 1001 dt = formdate.as_datetime() 1002 if dt: 1003 page.td(self.format_datetime(dt, "full")) 1004 else: 1005 page.td("(Unrecognised date)") 1006 1007 def show_recurrence_controls(self, obj, index, period, recurrenceid, recurrenceids, show_start): 1008 1009 """ 1010 Show datetime details from the given 'obj' for the recurrence having the 1011 given 'index', with the recurrence period described by 'period', 1012 indicating a start, end and origin of the period from the event details, 1013 employing any 'recurrenceid' and 'recurrenceids' for the object to 1014 configure the displayed information. 1015 1016 If 'show_start' is set to a true value, the start details will be shown; 1017 otherwise, the end details will be shown. 1018 """ 1019 1020 page = self.page 1021 sn = self._suffixed_name 1022 ssn = self._simple_suffixed_name 1023 1024 p = event_period_from_period(period) 1025 replaced = not recurrenceid and self.is_replaced(p, recurrenceids) 1026 1027 # Show controls for editing as organiser. 1028 1029 if self.is_organiser(obj) and not replaced: 1030 page.td(class_="objectvalue dt%s" % (show_start and "start" or "end")) 1031 1032 read_only = period.origin == "RRULE" 1033 1034 if show_start: 1035 page.div(class_="dt enabled") 1036 self._show_date_controls(ssn("dtstart", "recur", index), p.get_form_start(), index=index, read_only=read_only) 1037 if not read_only: 1038 page.br() 1039 page.label("Specify times", for_=sn("dttimes-enable", index), class_="time disabled enable") 1040 page.label("Specify dates only", for_=sn("dttimes-enable", index), class_="time enabled disable") 1041 page.div.close() 1042 1043 # Put the origin somewhere. 1044 1045 self._control("recur-origin", "hidden", p.origin or "") 1046 1047 else: 1048 page.div(class_="dt disabled") 1049 if not read_only: 1050 page.label("Specify end date", for_=sn("dtend-enable", index), class_="enable") 1051 page.div.close() 1052 page.div(class_="dt enabled") 1053 self._show_date_controls(ssn("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False, read_only=read_only) 1054 if not read_only: 1055 page.br() 1056 page.label("End on same day", for_=sn("dtend-enable", index), class_="disable") 1057 page.div.close() 1058 1059 page.td.close() 1060 1061 # Show label as attendee. 1062 1063 else: 1064 self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start) 1065 1066 def show_recurrence_label(self, period, recurrenceid, recurrenceids, show_start): 1067 1068 """ 1069 Show datetime details for the given 'period', employing any 1070 'recurrenceid' and 'recurrenceids' for the object to configure the 1071 displayed information. 1072 1073 If 'show_start' is set to a true value, the start details will be shown; 1074 otherwise, the end details will be shown. 1075 """ 1076 1077 page = self.page 1078 1079 p = event_period_from_period(period) 1080 replaced = not recurrenceid and self.is_replaced(p, recurrenceids) 1081 1082 css = " ".join([ 1083 replaced and "replaced" or "", 1084 self.is_affected(p, recurrenceid) and "affected" or "" 1085 ]) 1086 1087 formdate = show_start and p.get_form_start() or p.get_form_end() 1088 dt = formdate.as_datetime() 1089 if dt: 1090 page.td(self.format_datetime(dt, "long"), class_=css) 1091 else: 1092 page.td("(Unrecognised date)") 1093 1094 # Full page output methods. 1095 1096 def show(self, path_info): 1097 1098 "Show an object request using the given 'path_info' for the current user." 1099 1100 uid, recurrenceid = self._get_identifiers(path_info) 1101 obj = self._get_object(uid, recurrenceid) 1102 1103 if not obj: 1104 return False 1105 1106 errors = self.handle_request(obj) 1107 1108 if not errors: 1109 return True 1110 1111 self.new_page(title="Event") 1112 self.show_object_on_page(obj, errors) 1113 1114 return True 1115 1116 # Utility methods. 1117 1118 def _control(self, name, type, value, selected=False, **kw): 1119 1120 """ 1121 Show a control with the given 'name', 'type' and 'value', with 1122 'selected' indicating whether it should be selected (checked or 1123 equivalent), and with keyword arguments setting other properties. 1124 """ 1125 1126 page = self.page 1127 if selected: 1128 page.input(name=name, type=type, value=value, checked=selected, **kw) 1129 else: 1130 page.input(name=name, type=type, value=value, **kw) 1131 1132 def _show_menu(self, name, default, items, class_="", index=None): 1133 1134 """ 1135 Show a select menu having the given 'name', set to the given 'default', 1136 providing the given (value, label) 'items', and employing the given CSS 1137 'class_' if specified. 1138 """ 1139 1140 page = self.page 1141 values = self.env.get_args().get(name, [default]) 1142 if index is not None: 1143 values = values[index:] 1144 values = values and values[0:1] or [default] 1145 1146 page.select(name=name, class_=class_) 1147 for v, label in items: 1148 if v is None: 1149 continue 1150 if v in values: 1151 page.option(label, value=v, selected="selected") 1152 else: 1153 page.option(label, value=v) 1154 page.select.close() 1155 1156 def _show_date_controls(self, name, default, index=None, show_tzid=True, read_only=False): 1157 1158 """ 1159 Show date controls for a field with the given 'name' and 'default' form 1160 date value. 1161 1162 If 'index' is specified, default field values will be overridden by the 1163 element from a collection of existing form values with the specified 1164 index; otherwise, field values will be overridden by a single form 1165 value. 1166 1167 If 'show_tzid' is set to a false value, the time zone menu will not be 1168 provided. 1169 1170 If 'read_only' is set to a true value, the controls will be hidden and 1171 labels will be employed instead. 1172 """ 1173 1174 page = self.page 1175 1176 # Show dates for up to one week around the current date. 1177 1178 dt = default.as_datetime() 1179 if not dt: 1180 dt = date.today() 1181 1182 base = to_date(dt) 1183 1184 # Show a date label with a hidden field if read-only. 1185 1186 if read_only: 1187 self._control("%s-date" % name, "hidden", format_datetime(base)) 1188 page.span(self.format_date(base, "long")) 1189 1190 # Show dates for up to one week around the current date. 1191 # NOTE: Support paging to other dates. 1192 1193 else: 1194 items = [] 1195 for i in range(-7, 8): 1196 d = base + timedelta(i) 1197 items.append((format_datetime(d), self.format_date(d, "full"))) 1198 self._show_menu("%s-date" % name, format_datetime(base), items, index=index) 1199 1200 # Show time details. 1201 1202 page.span(class_="time enabled") 1203 1204 if read_only: 1205 page.span("%s:%s:%s" % (default.get_hour(), default.get_minute(), default.get_second())) 1206 self._control("%s-hour" % name, "hidden", default.get_hour()) 1207 self._control("%s-minute" % name, "hidden", default.get_minute()) 1208 self._control("%s-second" % name, "hidden", default.get_second()) 1209 else: 1210 self._control("%s-hour" % name, "text", default.get_hour(), maxlength=2, size=2) 1211 page.add(":") 1212 self._control("%s-minute" % name, "text", default.get_minute(), maxlength=2, size=2) 1213 page.add(":") 1214 self._control("%s-second" % name, "text", default.get_second(), maxlength=2, size=2) 1215 1216 # Show time zone details. 1217 1218 if show_tzid: 1219 page.add(" ") 1220 tzid = default.get_tzid() or self.get_tzid() 1221 1222 # Show a label if read-only or a menu otherwise. 1223 1224 if read_only: 1225 self._control("%s-tzid" % name, "hidden", tzid) 1226 page.span(tzid) 1227 else: 1228 self._show_timezone_menu("%s-tzid" % name, tzid, index) 1229 1230 page.span.close() 1231 1232 def _show_timezone_menu(self, name, default, index=None): 1233 1234 """ 1235 Show timezone controls using a menu with the given 'name', set to the 1236 given 'default' unless a field of the given 'name' provides a value. 1237 """ 1238 1239 entries = [(tzid, tzid) for tzid in pytz.all_timezones] 1240 self._show_menu(name, default, entries, index=index) 1241 1242 # vim: tabstop=4 expandtab shiftwidth=4