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