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