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