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