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