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 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.get_start(), p.start_attr and p.start_attr.get("TZID"), "DTSTART", obj) 216 result = self.set_datetime_in_object(p.get_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.get_start(), p.get_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 if self.is_replaced(period, recurrenceids): 877 continue 878 if (p.uid != uid or (recurrenceid and p.recurrenceid) and p.recurrenceid != recurrenceid) and p.transp != "ORG": 879 conflicts.append(p) 880 881 conflicts.sort() 882 883 # Show any conflicts with periods of actual attendance. 884 885 if conflicts: 886 page.p("This event conflicts with others:") 887 888 page.table(cellspacing=5, cellpadding=5, class_="conflicts") 889 page.thead() 890 page.tr() 891 page.th("Event") 892 page.th("Start") 893 page.th("End") 894 page.tr.close() 895 page.thead.close() 896 page.tbody() 897 898 for p in conflicts: 899 900 # Provide details of any conflicting event. 901 902 start = self.format_datetime(p.get_start(tzid), "long") 903 end = self.format_datetime(p.get_end(tzid), "long") 904 905 page.tr() 906 907 # Show the event summary for the conflicting event. 908 909 page.td() 910 if p.summary: 911 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) 912 else: 913 page.add("(Unspecified event)") 914 page.td.close() 915 916 page.td(start) 917 page.td(end) 918 919 page.tr.close() 920 921 page.tbody.close() 922 page.table.close() 923 924 # Generation of controls within page fragments. 925 926 def show_object_datetime_controls(self, period, index=None): 927 928 """ 929 Show datetime-related controls if already active or if an object needs 930 them for the given 'period'. The given 'index' is used to parameterise 931 individual controls for dynamic manipulation. 932 """ 933 934 p = form_period_from_period(period) 935 936 page = self.page 937 args = self.env.get_args() 938 sn = self._suffixed_name 939 ssn = self._simple_suffixed_name 940 941 # Add a dynamic stylesheet to permit the controls to modify the display. 942 # NOTE: The style details need to be coordinated with the static 943 # NOTE: stylesheet. 944 945 if index is not None: 946 page.style(type="text/css") 947 948 # Unlike the rules for object properties, these affect recurrence 949 # properties. 950 951 page.add("""\ 952 input#dttimes-enable-%(index)d, 953 input#dtend-enable-%(index)d, 954 input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled, 955 input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled, 956 input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled, 957 input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled { 958 display: none; 959 }""" % {"index" : index}) 960 961 page.style.close() 962 963 self._control( 964 ssn("dtend-control", "recur", index), "checkbox", 965 index is not None and str(index) or "enable", p.end_enabled, 966 id=sn("dtend-enable", index) 967 ) 968 969 self._control( 970 ssn("dttimes-control", "recur", index), "checkbox", 971 index is not None and str(index) or "enable", p.times_enabled, 972 id=sn("dttimes-enable", index) 973 ) 974 975 def show_datetime_controls(self, obj, formdate, show_start): 976 977 """ 978 Show datetime details from the given 'obj' for the 'formdate', showing 979 start details if 'show_start' is set to a true value. Details will 980 appear as controls for organisers and labels for attendees. 981 """ 982 983 page = self.page 984 985 # Show controls for editing as organiser. 986 987 if self.is_organiser(obj): 988 page.td(class_="objectvalue dt%s" % (show_start and "start" or "end")) 989 990 if show_start: 991 page.div(class_="dt enabled") 992 self._show_date_controls("dtstart", formdate) 993 page.br() 994 page.label("Specify times", for_="dttimes-enable", class_="time disabled enable") 995 page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable") 996 page.div.close() 997 998 else: 999 page.div(class_="dt disabled") 1000 page.label("Specify end date", for_="dtend-enable", class_="enable") 1001 page.div.close() 1002 page.div(class_="dt enabled") 1003 self._show_date_controls("dtend", formdate) 1004 page.br() 1005 page.label("End on same day", for_="dtend-enable", class_="disable") 1006 page.div.close() 1007 1008 page.td.close() 1009 1010 # Show a label as attendee. 1011 1012 else: 1013 dt = formdate.as_datetime() 1014 if dt: 1015 page.td(self.format_datetime(dt, "full")) 1016 else: 1017 page.td("(Unrecognised date)") 1018 1019 def show_recurrence_controls(self, obj, index, period, recurrenceid, recurrenceids, show_start): 1020 1021 """ 1022 Show datetime details from the given 'obj' for the recurrence having the 1023 given 'index', with the recurrence period described by 'period', 1024 indicating a start, end and origin of the period from the event details, 1025 employing any 'recurrenceid' and 'recurrenceids' for the object to 1026 configure the displayed information. 1027 1028 If 'show_start' is set to a true value, the start details will be shown; 1029 otherwise, the end details will be shown. 1030 """ 1031 1032 page = self.page 1033 sn = self._suffixed_name 1034 ssn = self._simple_suffixed_name 1035 1036 p = event_period_from_period(period) 1037 replaced = self.is_replaced(p, recurrenceids) 1038 1039 # Show controls for editing as organiser. 1040 1041 if self.is_organiser(obj) and not replaced: 1042 page.td(class_="objectvalue dt%s" % (show_start and "start" or "end")) 1043 1044 read_only = period.origin == "RRULE" 1045 1046 if show_start: 1047 page.div(class_="dt enabled") 1048 self._show_date_controls(ssn("dtstart", "recur", index), p.get_form_start(), index=index, read_only=read_only) 1049 if not read_only: 1050 page.br() 1051 page.label("Specify times", for_=sn("dttimes-enable", index), class_="time disabled enable") 1052 page.label("Specify dates only", for_=sn("dttimes-enable", index), class_="time enabled disable") 1053 page.div.close() 1054 1055 # Put the origin somewhere. 1056 1057 self._control("recur-origin", "hidden", p.origin or "") 1058 1059 else: 1060 page.div(class_="dt disabled") 1061 if not read_only: 1062 page.label("Specify end date", for_=sn("dtend-enable", index), class_="enable") 1063 page.div.close() 1064 page.div(class_="dt enabled") 1065 self._show_date_controls(ssn("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False, read_only=read_only) 1066 if not read_only: 1067 page.br() 1068 page.label("End on same day", for_=sn("dtend-enable", index), class_="disable") 1069 page.div.close() 1070 1071 page.td.close() 1072 1073 # Show label as attendee. 1074 1075 else: 1076 self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start) 1077 1078 def show_recurrence_label(self, period, recurrenceid, recurrenceids, show_start): 1079 1080 """ 1081 Show datetime details for the given 'period', employing any 1082 'recurrenceid' and 'recurrenceids' for the object to configure the 1083 displayed information. 1084 1085 If 'show_start' is set to a true value, the start details will be shown; 1086 otherwise, the end details will be shown. 1087 """ 1088 1089 page = self.page 1090 1091 p = event_period_from_period(period) 1092 replaced = self.is_replaced(p, recurrenceids) 1093 1094 css = " ".join([ 1095 replaced, 1096 self.is_affected(p, recurrenceid) 1097 ]) 1098 1099 formdate = show_start and p.get_form_start() or p.get_form_end() 1100 dt = formdate.as_datetime() 1101 if dt: 1102 page.td(self.format_datetime(dt, "long"), class_=css) 1103 else: 1104 page.td("(Unrecognised date)") 1105 1106 # Full page output methods. 1107 1108 def show(self, path_info): 1109 1110 "Show an object request using the given 'path_info' for the current user." 1111 1112 uid, recurrenceid = self._get_identifiers(path_info) 1113 obj = self._get_object(uid, recurrenceid) 1114 1115 if not obj: 1116 return False 1117 1118 errors = self.handle_request(uid, recurrenceid, obj) 1119 1120 if not errors: 1121 return True 1122 1123 self.new_page(title="Event") 1124 self.show_object_on_page(uid, obj, errors) 1125 1126 return True 1127 1128 # Utility methods. 1129 1130 def _control(self, name, type, value, selected=False, **kw): 1131 1132 """ 1133 Show a control with the given 'name', 'type' and 'value', with 1134 'selected' indicating whether it should be selected (checked or 1135 equivalent), and with keyword arguments setting other properties. 1136 """ 1137 1138 page = self.page 1139 if selected: 1140 page.input(name=name, type=type, value=value, checked=selected, **kw) 1141 else: 1142 page.input(name=name, type=type, value=value, **kw) 1143 1144 def _show_menu(self, name, default, items, class_="", index=None): 1145 1146 """ 1147 Show a select menu having the given 'name', set to the given 'default', 1148 providing the given (value, label) 'items', and employing the given CSS 1149 'class_' if specified. 1150 """ 1151 1152 page = self.page 1153 values = self.env.get_args().get(name, [default]) 1154 if index is not None: 1155 values = values[index:] 1156 values = values and values[0:1] or [default] 1157 1158 page.select(name=name, class_=class_) 1159 for v, label in items: 1160 if v is None: 1161 continue 1162 if v in values: 1163 page.option(label, value=v, selected="selected") 1164 else: 1165 page.option(label, value=v) 1166 page.select.close() 1167 1168 def _show_date_controls(self, name, default, index=None, show_tzid=True, read_only=False): 1169 1170 """ 1171 Show date controls for a field with the given 'name' and 'default' form 1172 date value. 1173 1174 If 'index' is specified, default field values will be overridden by the 1175 element from a collection of existing form values with the specified 1176 index; otherwise, field values will be overridden by a single form 1177 value. 1178 1179 If 'show_tzid' is set to a false value, the time zone menu will not be 1180 provided. 1181 1182 If 'read_only' is set to a true value, the controls will be hidden and 1183 labels will be employed instead. 1184 """ 1185 1186 page = self.page 1187 1188 # Show dates for up to one week around the current date. 1189 1190 dt = default.as_datetime() 1191 if not dt: 1192 dt = date.today() 1193 1194 base = to_date(dt) 1195 1196 # Show a date label with a hidden field if read-only. 1197 1198 if read_only: 1199 self._control("%s-date" % name, "hidden", format_datetime(base)) 1200 page.span(self.format_date(base, "long")) 1201 1202 # Show dates for up to one week around the current date. 1203 1204 else: 1205 items = [] 1206 for i in range(-7, 8): 1207 d = base + timedelta(i) 1208 items.append((format_datetime(d), self.format_date(d, "full"))) 1209 self._show_menu("%s-date" % name, format_datetime(base), items, index=index) 1210 1211 # Show time details. 1212 1213 page.span(class_="time enabled") 1214 1215 if read_only: 1216 page.span("%s:%s:%s" % (default.get_hour(), default.get_minute(), default.get_second())) 1217 self._control("%s-hour" % name, "hidden", default.get_hour()) 1218 self._control("%s-minute" % name, "hidden", default.get_minute()) 1219 self._control("%s-second" % name, "hidden", default.get_second()) 1220 else: 1221 self._control("%s-hour" % name, "text", default.get_hour(), maxlength=2, size=2) 1222 page.add(":") 1223 self._control("%s-minute" % name, "text", default.get_minute(), maxlength=2, size=2) 1224 page.add(":") 1225 self._control("%s-second" % name, "text", default.get_second(), maxlength=2, size=2) 1226 1227 # Show time zone details. 1228 1229 if show_tzid: 1230 page.add(" ") 1231 tzid = default.get_tzid() or self.get_tzid() 1232 1233 # Show a label if read-only or a menu otherwise. 1234 1235 if read_only: 1236 self._control("%s-tzid" % name, "hidden", tzid) 1237 page.span(tzid) 1238 else: 1239 self._show_timezone_menu("%s-tzid" % name, tzid, index) 1240 1241 page.span.close() 1242 1243 def _show_timezone_menu(self, name, default, index=None): 1244 1245 """ 1246 Show timezone controls using a menu with the given 'name', set to the 1247 given 'default' unless a field of the given 'name' provides a value. 1248 """ 1249 1250 entries = [(tzid, tzid) for tzid in pytz.all_timezones] 1251 self._show_menu(name, default, entries, index=index) 1252 1253 # vim: tabstop=4 expandtab shiftwidth=4