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