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