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