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