1 #!/usr/bin/env python 2 3 """ 4 A text interface to received and new events. 5 6 Copyright (C) 2017, 2018 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 email import message_from_file 23 from imiptools import get_handlers, parse_args 24 from imiptools.config import settings 25 from imiptools.content import get_objects_from_itip, handle_calendar_data, \ 26 handle_calendar_object, have_itip_part, \ 27 is_cancel_itip, parse_itip_part 28 from imiptools.data import get_address, get_periods_using_selector, \ 29 get_main_period, get_recurrence_periods, \ 30 get_value, parse_object 31 from imiptools.dates import get_datetime_item, get_time, to_timezone 32 from imiptools.editing import EditingClient, PeriodError, \ 33 form_periods_from_periods 34 from imiptools.handlers import person, person_outgoing 35 from imiptools.mail import Messenger 36 from imiptools.stores import get_journal, get_store 37 from imiptools.utils import decode_part, message_as_string 38 import vRecurrence 39 import sys, os 40 41 # User interface definitions. 42 43 NEW_COMMANDS = ("n", "new") 44 QUIT_COMMANDS = ("q", "quit", "exit") 45 46 ATTENDANCE_COMMANDS = ("A", "attend", "attendance") 47 ATTENDEE_COMMANDS = ("a", "attendee") 48 CANCEL_PERIOD_COMMANDS = ("c", "cancel") 49 CHANGE_COMMANDS = ("C", "changes") 50 CLASSIFICATION_COMMANDS = ("c", "class", "classification") 51 FINISH_COMMANDS = ("f", "finish") 52 HELP_COMMANDS = ("h", "help", "?") 53 LIST_COMMANDS = ("l", "list", "show") 54 OPERATION_COMMANDS = ("o", "ops", "operations") 55 PERIOD_COMMANDS = ("p", "period") 56 RECURRENCEID_COMMANDS = ("RECURRENCE-ID", "RID") 57 RESET_COMMANDS = ("r", "reload", "reset", "restart") 58 RULE_COMMANDS = ("rr", "rule", "rrule") 59 SUGGESTED_ATTENDEE_COMMANDS = ("as", "attendee-suggested", "suggested-attendee") 60 SUGGESTED_PERIOD_COMMANDS = ("ps", "period-suggested", "suggested-period") 61 SUMMARY_COMMANDS = ("s", "summary") 62 UID_COMMANDS = ("UID",) 63 UNCANCEL_COMMANDS = ("u", "uncancel", "restore") 64 65 CANCEL_COMMANDS = ("R", "remove", "cancel") 66 CANCEL_PUBLISH_COMMANDS = ("RP", "remove-publish", "cancel-publish") 67 FREEBUSY_COMMANDS = ("F", "fb", "freebusy", "free/busy", "free-busy") 68 PUBLISH_COMMANDS = ("P", "publish") 69 SEND_COMMANDS = ("S", "send") 70 UPDATE_COMMANDS = ("U", "update") 71 72 EDIT_COMMANDS = ("e", "edit") 73 REMOVE_COMMANDS = ("r", "remove") 74 75 ACCEPTED_VALUES = ("a", "accept", "accepted", "attend") 76 DECLINED_VALUES = ("d", "decline", "declined") 77 TENTATIVE_VALUES = ("t", "tentative") 78 79 COUNT_COMMANDS = ("c", "count", "limit") 80 FREQUENCY_COMMANDS = ("f", "freq", "frequency") 81 SELECTION_COMMANDS = ("s", "select", "selection") 82 83 YEARLY_VALUES = ("y", "year", "yearly") 84 MONTHLY_VALUES = ("month", "monthly") 85 WEEKLY_VALUES = ("w", "week", "weekly") 86 DAILY_VALUES = ("d", "day", "daily") 87 HOURLY_VALUES = ("h", "hour", "hourly") 88 MINUTELY_VALUES = ("minute", "minutely") 89 SECONDLY_VALUES = ("s", "second", "secondly") 90 91 MONTH_VALUES = ("month", "months") 92 WEEK_VALUES = ("w", "week", "weeks") 93 YEARDAY_VALUES = ("y", "yearday", "yeardays") 94 MONTHDAY_VALUES = ("o", "monthday", "monthdays") 95 DAY_VALUES = ("d", "weekday", "weekdays") 96 HOUR_VALUES = ("h", "hour", "hours") 97 MINUTE_VALUES = ("m", "minute", "minutes") 98 SECOND_VALUES = ("s", "second", "seconds") 99 100 def commandlist(l, option=None): 101 102 "Show 'l' as a command list string employing any given 'option'." 103 104 if option: 105 l2 = [] 106 for s in l: 107 l2.append("%s %s" % (s, option)) 108 return "\n".join(l2) 109 return ", ".join(l) 110 111 # User interface functions. 112 113 echo = False 114 115 def read_input(label): 116 117 """ 118 Read input, prompting using 'label', stripping leading and trailing 119 whitespace, echoing the input if the global 'echo' variable is set. 120 """ 121 122 s = raw_input(label).strip() 123 if echo: 124 print s 125 return s 126 127 def input_with_default(label, default): 128 129 """ 130 Read input, prompting using 'label', parameterising the label with the given 131 'default' and returning the default if no input is given. 132 """ 133 134 return read_input(label % default) or default 135 136 def to_int_or_none(value): 137 138 "Return 'value' as an integer or None for other inputs." 139 140 try: 141 return int(value) 142 except (TypeError, ValueError): 143 return None 144 145 def format_value_ranges(ranges, null_value="-"): 146 147 "Format 'ranges' as a single descriptive string." 148 149 l = [] 150 151 for value_range in ranges: 152 if isinstance(value_range, tuple): 153 start, end = value_range 154 l.append("%s...%s" % (start, end)) 155 elif isinstance(value_range, list): 156 l.append(", ".join(value_range)) 157 elif isinstance(value_range, dict): 158 l.append(", ".join(value_range.keys())) 159 else: 160 l.append(value_range or null_value) 161 162 return ", ".join(l) 163 164 def print_title(text): 165 166 "Print 'text' with simple, fixed-width styling as a title." 167 168 print text 169 print len(text) * "-" 170 171 def print_table(rows, separator_index=0): 172 173 """ 174 Print 'rows' as a simple, fixed-width table. If 'separator_index' is set to 175 a row index present in the rows, a table separator will be produced below 176 that row's data. Otherwise, a separator will appear below the first row. 177 """ 178 179 widths = [] 180 for row in rows: 181 for i, col in enumerate(row): 182 if i >= len(widths): 183 widths.append(len(col)) 184 else: 185 widths[i] = max(widths[i], len(col)) 186 187 for i, row in enumerate(rows): 188 for col, width in zip(row, widths): 189 print "%s%s" % (col, " " * (width - len(col))), 190 print 191 if i == separator_index: 192 for width in widths: 193 print "-" * width, 194 print 195 196 def write(s, filename): 197 198 "Write 's' to a file having the given 'filename'." 199 200 f = filename and open(filename, "w") or None 201 try: 202 print >>(f or sys.stdout), s 203 finally: 204 if f: 205 f.close() 206 207 # Interpret an input file containing a calendar resource. 208 209 def get_itip_from_message(filename): 210 211 "Return iTIP details provided by 'filename'." 212 213 f = open(filename) 214 try: 215 msg = message_from_file(f) 216 finally: 217 f.close() 218 219 all_itip = [] 220 221 for part in msg.walk(): 222 if have_itip_part(part): 223 all_itip.append(parse_itip_part(part)) 224 225 return all_itip 226 227 def get_itip_from_data(filename, charset): 228 229 "Return objects provided by 'filename'." 230 231 f = open(filename) 232 try: 233 itip = parse_object(f, charset, "VCALENDAR") 234 finally: 235 f.close() 236 237 return [itip] 238 239 # Object and request display. 240 241 def show_objects(objects, user, store): 242 243 """ 244 Show details of 'objects', accessed by the given 'user' in the given 245 'store'. 246 """ 247 248 print 249 print_title("Objects") 250 print 251 252 for index, obj in enumerate(objects): 253 recurrenceid = obj.get_recurrenceid() 254 recurrence_label = recurrenceid and " %s" % recurrenceid or "" 255 print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label) 256 257 def show_requests(user, store): 258 259 "Show requests available to the given 'user' in the given 'store'." 260 261 requests = store.get_requests(user) 262 263 print 264 print_title("Requests") 265 print 266 267 if not requests: 268 print "No requests are pending." 269 return 270 271 for index, (uid, recurrenceid) in enumerate(requests): 272 obj = store.get_event(user, uid, recurrenceid) 273 recurrence_label = recurrenceid and " %s" % recurrenceid or "" 274 print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label) 275 276 # Object details display. 277 278 def show_attendee(attendee_item, index): 279 280 "Show the 'attendee_item' (value and attributes) at 'index'." 281 282 attendee, attr = attendee_item 283 partstat = attr.get("PARTSTAT") 284 print "(%d) %s%s" % (index, attendee, partstat and " (%s)" % partstat or "") 285 286 def show_attendees_raw(attendee_map): 287 288 "Show the 'attendee_map' in a simple raw form." 289 290 for attendee, attr in attendee_map.items(): 291 print attendee 292 293 def show_periods(periods, errors=None): 294 295 "Show 'periods' with any indicated 'errors'." 296 297 main = get_main_period(periods) 298 if main: 299 show_period(main, 0, errors) 300 301 recurrences = get_recurrence_periods(periods) 302 if recurrences: 303 print 304 print_title("Recurrences") 305 for index, p in enumerate(recurrences): 306 show_period(p, index + 1, errors) 307 308 def show_period(p, index, errors=None): 309 310 "Show period 'p' at 'index' with any indicated 'errors'." 311 312 errors = errors and errors.get(index) 313 if p.cancelled: 314 label = "Cancelled" 315 elif p.replacement: 316 label = "Replaced" 317 elif p.new_replacement: 318 label = "To replace" 319 elif p.recurrenceid: 320 label = "Retained" 321 else: 322 label = "New" 323 324 error_label = errors and " (errors: %s)" % ", ".join(errors) or "" 325 print "(%d) %s%s:" % (index, label, error_label), \ 326 str(p.get_form_start()), str(p.get_form_end()), p.origin 327 328 def show_periods_raw(periods): 329 330 "Show 'periods' in a simple raw form." 331 332 periods = periods[:] 333 periods.sort() 334 map(show_period_raw, periods) 335 336 def show_period_raw(p): 337 338 "Show period 'p' in a simple raw form." 339 340 print str(p.get_form_start()), str(p.get_form_end()), p.origin 341 342 def show_rule(selectors): 343 344 "Show recurrence rule specification 'selectors'." 345 346 # Collect limit, selection and frequency details. 347 348 for i, selector in enumerate(selectors): 349 prefix = "(%d) " % i 350 show_rule_selector(selector, prefix) 351 352 print 353 print vRecurrence.to_string(selectors) 354 355 def show_rule_selector(selector, prefix=""): 356 357 "Show the rule 'selector', employing any given 'prefix' for formatting." 358 359 # COUNT 360 361 if isinstance(selector, vRecurrence.LimitSelector): 362 print "%sAt most %d occurrences" % (prefix, selector.args["values"][0]) 363 364 # BYSETPOS 365 366 elif isinstance(selector, vRecurrence.PositionSelector): 367 for value in selector.get_positions(): 368 print "%sSelect occurrence #%d" % (prefix, value) 369 prefix = len(prefix) * " " 370 371 # BYWEEKDAY 372 373 elif isinstance(selector, vRecurrence.WeekDayFilter): 374 for value, index in selector.get_values(): 375 print "%sSelect occurrence #%d (from %s) of weekday %s" % ( 376 prefix, abs(index), index >= 0 and "start" or "end", 377 get_weekday(value)) 378 prefix = len(prefix) * " " 379 380 # BY... 381 382 elif isinstance(selector, vRecurrence.Enum): 383 for value in selector.get_values(): 384 print "%sSelect %s %r" % (prefix, get_resolution(selector.level), 385 value) 386 prefix = len(prefix) * " " 387 388 # YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY 389 390 elif isinstance(selector, vRecurrence.Pattern): 391 print "%sEach %s with interval %d" % (prefix, 392 get_resolution(selector.level), selector.args.get("interval", 1)) 393 394 def get_resolution(level): 395 396 "Return a textual description of the given resolution 'level'." 397 398 levels = ["year", "month", "week", "day in year", "day in month", "day", "hour", "minute", "second"] 399 return levels[level] 400 401 def get_weekday(weekday): 402 403 "Return the name of the given 1-based 'weekday' number." 404 405 weekdays = [None, "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] 406 return weekdays[weekday] 407 408 def show_attendee_changes(new, modified, unmodified, removed): 409 410 "Show 'new', 'modified', 'unmodified' and 'removed' periods." 411 412 print 413 print_title("Changes to attendees") 414 print 415 print "New:" 416 show_attendees_raw(new) 417 print 418 print "Modified:" 419 show_attendees_raw(modified) 420 print 421 print "Unmodified:" 422 show_attendees_raw(unmodified) 423 print 424 print "Removed:" 425 show_attendees_raw(removed) 426 427 def show_period_classification(new, replaced, retained, cancelled, obsolete): 428 429 "Show 'new', 'replaced', 'retained', 'cancelled' and 'obsolete' periods." 430 431 print 432 print_title("Period classification") 433 print 434 print "New:" 435 show_periods_raw(new) 436 print 437 print "Replaced:" 438 show_periods_raw(replaced) 439 print 440 print "Retained:" 441 show_periods_raw(retained) 442 print 443 print "Cancelled:" 444 show_periods_raw(cancelled) 445 print 446 print "Obsolete:" 447 show_periods_raw(obsolete) 448 449 def show_changes(modified, unmodified, removed): 450 451 "Show 'modified', 'unmodified' and 'removed' periods." 452 453 print 454 print_title("Changes to periods") 455 print 456 print "Modified:" 457 show_periods_raw(modified) 458 print 459 print "Unmodified:" 460 show_periods_raw(unmodified) 461 print 462 print "Removed:" 463 show_periods_raw(removed) 464 465 def show_attendee_operations(to_invite, to_cancel, to_modify): 466 467 "Show attendees 'to_invite', 'to_cancel' and 'to_modify'." 468 469 print 470 print_title("Attendee update operations") 471 print 472 print "To invite:" 473 show_attendees_raw(to_invite) 474 print 475 print "To cancel:" 476 show_attendees_raw(to_cancel) 477 print 478 print "To modify:" 479 show_attendees_raw(to_modify) 480 481 def show_period_operations(to_unschedule, to_reschedule, to_add, to_exclude, to_set, 482 all_unscheduled, all_rescheduled): 483 484 """ 485 Show operations for periods 'to_unschedule', 'to_reschedule', 'to_add', 486 'to_exclude' and 'to_set' (for updating other calendar participants), and 487 for periods 'all_unscheduled' and 'all_rescheduled' (for publishing event 488 state). 489 """ 490 491 print 492 print_title("Period update and publishing operations") 493 print 494 print "Unschedule:" 495 show_periods_raw(to_unschedule) 496 print 497 print "Reschedule:" 498 show_periods_raw(to_reschedule) 499 print 500 print "Added:" 501 show_periods_raw(to_add) 502 print 503 print "Excluded:" 504 show_periods_raw(to_exclude) 505 print 506 print "Set in object:" 507 show_periods_raw(to_set) 508 print 509 print "All unscheduled:" 510 show_periods_raw(all_unscheduled) 511 print 512 print "All rescheduled:" 513 show_periods_raw(all_rescheduled) 514 515 class TextClient(EditingClient): 516 517 "Simple client with textual output." 518 519 def new_object(self): 520 521 "Create a new object with the current time." 522 523 utcnow = get_time() 524 now = to_timezone(utcnow, self.get_tzid()) 525 obj = EditingClient.new_object(self, "VEVENT") 526 obj.set_value("SUMMARY", "New event") 527 obj["DTSTART"] = [get_datetime_item(now)] 528 obj["DTEND"] = [get_datetime_item(now)] 529 return obj 530 531 def handle_outgoing_object(self): 532 533 "Handle the current object using the outgoing handlers." 534 535 unscheduled_objects, rescheduled_objects, added_objects = \ 536 self.get_publish_objects() 537 538 handlers = get_handlers(self, person_outgoing.handlers, 539 [get_address(self.user)]) 540 541 # Handle the parent object plus any altered periods. 542 543 handle_calendar_object(self.obj, handlers, "PUBLISH") 544 545 for o in unscheduled_objects: 546 handle_calendar_object(o, handlers, "CANCEL") 547 548 for o in rescheduled_objects: 549 handle_calendar_object(o, handlers, "PUBLISH") 550 551 for o in added_objects: 552 handle_calendar_object(o, handlers, "ADD") 553 554 def update_periods_from_rule(self): 555 556 "Update the periods from the rule." 557 558 selectors = self.state.get("rule") 559 periods = self.state.get("periods") 560 561 main_period = get_main_period(periods) 562 tzid = main_period.get_tzid() 563 564 start = main_period.get_start() 565 end = self.get_window_end() or None 566 567 selector = vRecurrence.get_selector(start, selectors) 568 inclusive = False 569 570 # Generate the periods from the rule. 571 572 rule_periods = form_periods_from_periods( 573 get_periods_using_selector(selector, main_period, tzid, 574 start, end, inclusive)) 575 576 # Retain any applicable replacement periods that either modify or cancel 577 # rule periods. 578 # NOTE: To be done. 579 580 self.state.set("periods", rule_periods) 581 582 # Editing methods involving interaction. 583 584 def add_rule_selectors(self): 585 586 "Add rule selectors to the rule details." 587 588 selectors = self.state.get("rule") 589 590 while True: 591 592 # Obtain a command from any arguments. 593 594 s = read_input("Selector: (c)ount, (f)requency, (s)election (or return)> ") 595 args = s.split() 596 cmd = next_arg(args) 597 598 if cmd in COUNT_COMMANDS: 599 add_rule_selector_count(selectors, args) 600 elif cmd in FREQUENCY_COMMANDS: 601 add_rule_selector_frequency(selectors, args) 602 elif cmd in SELECTION_COMMANDS: 603 add_rule_selector_selection(selectors, args) 604 605 # Remain in the loop unless explicitly terminated. 606 607 elif not cmd or cmd == "end": 608 break 609 610 def edit_attendee(self, index): 611 612 "Edit the attendee at 'index'." 613 614 t = self.can_edit_attendee(index) 615 if not t: 616 return 617 618 attendees = self.state.get("attendees") 619 old_attendee, attr = t 620 621 attendee = input_with_default("Attendee (%s) (or return)? ", old_attendee) 622 623 # Remove the old attendee if null or a replacement is given. 624 625 if attendee.strip() or not old_attendee: 626 del attendees[old_attendee] 627 628 # Add any replacement. 629 630 if attendee.strip(): 631 attendees[attendee] = attr 632 633 def edit_period(self, index, args=None): 634 635 "Edit the period at 'index'." 636 637 period = self.can_edit_period(index) 638 if period: 639 edit_period(period, args) 640 period.cancelled = False 641 642 # Change the origin of modified rule periods. 643 644 if period.origin == "RRULE": 645 period.origin = "RDATE" 646 647 # Sort the periods after this change. 648 649 periods = self.state.get("periods") 650 periods.sort() 651 652 def edit_rule_selector(self, index, args): 653 654 "Edit the selector having the given 'index'." 655 656 selectors = self.state.get("rule") 657 selector = self.can_edit_rule_selector(index) 658 659 if not selector: 660 return 661 662 while True: 663 show_rule_selector(selector) 664 665 # Obtain a command from any arguments. 666 667 cmd = next_arg(args) 668 if not cmd: 669 s = read_input("Selector: (e)dit, (r)emove (or return)> ") 670 args = s.split() 671 cmd = next_arg(args) 672 673 # Edit an existing selector. 674 675 if cmd in EDIT_COMMANDS: 676 if isinstance(selector, vRecurrence.LimitSelector): 677 add_rule_selector_count(selectors, args, selector) 678 elif isinstance(selector, vRecurrence.Pattern): 679 add_rule_selector_frequency(selectors, args, selector) 680 else: 681 add_rule_selector_selection(selectors, args, selector) 682 683 # Remove an existing selector. 684 685 elif cmd in REMOVE_COMMANDS: 686 del selectors[index] 687 688 # Exit if requested or after a successful 689 # operation. 690 691 elif not cmd: 692 pass 693 else: 694 continue 695 break 696 697 def edit_summary(self, summary=None): 698 699 "Edit or set the 'summary'." 700 701 if self.can_edit_properties(): 702 if not summary: 703 summary = input_with_default("Summary (%s)? ", self.state.get("summary")) 704 self.state.set("summary", summary) 705 706 def finish(self): 707 708 "Finish editing, warning of errors if any occur." 709 710 try: 711 EditingClient.finish(self) 712 except PeriodError: 713 print "Errors exist in the periods." 714 return 715 716 # Diagnostic methods. 717 718 def show_period_classification(self): 719 720 "Show the classification of the periods." 721 722 try: 723 new, replaced, retained, cancelled, obsolete = self.classify_periods() 724 show_period_classification(new, replaced, retained, cancelled, obsolete) 725 except PeriodError: 726 print 727 print "Errors exist in the periods." 728 729 def show_changes(self): 730 731 "Show how the periods have changed." 732 733 try: 734 modified, unmodified, removed = self.classify_period_changes() 735 show_changes(modified, unmodified, removed) 736 except PeriodError: 737 print "Errors exist in the periods." 738 739 is_changed = self.properties_changed() 740 if is_changed: 741 print 742 print "Properties changed:", ", ".join(is_changed) 743 new, modified, unmodified, removed = self.classify_attendee_changes() 744 show_attendee_changes(new, modified, unmodified, removed) 745 746 def show_operations(self): 747 748 "Show the operations required to change the periods for recipients." 749 750 is_changed = self.properties_changed() 751 752 try: 753 to_unschedule, to_reschedule, to_add, to_exclude, to_set, \ 754 all_unscheduled, all_rescheduled = self.classify_period_operations() 755 show_period_operations(to_unschedule, to_reschedule, to_add, 756 to_exclude, to_set, 757 all_unscheduled, all_rescheduled) 758 except PeriodError: 759 print "Errors exist in the periods." 760 761 to_invite, to_cancel, to_modify = self.classify_attendee_operations() 762 show_attendee_operations(to_invite, to_cancel, to_modify) 763 764 # Output methods. 765 766 def show_message(self, message, plain=False, filename=None): 767 768 """ 769 Show the given mail 'message', decoding to plain text if 'plain' is set 770 to a true value, writing it to 'filename' if indicated. 771 """ 772 773 if plain: 774 decode_part(message) 775 write(message_as_string(message), filename) 776 777 def show_cancel_message(self, plain=False, filename=None): 778 779 "Show the cancel message for uninvited attendees." 780 781 message = self.prepare_cancel_message() 782 if message: 783 self.show_message(message, plain, filename) 784 785 def show_freebusy_message(self, plain=False, filename=None): 786 787 "Show a free/busy request message for the main period." 788 789 message = self.prepare_freebusy_message() 790 self.show_message(message, plain, filename) 791 792 def show_cancel_publish_message(self, plain=False, filename=None): 793 794 "Show the cancel message for the current user." 795 796 message = self.prepare_cancel_publish_message() 797 self.show_message(message, plain, filename) 798 799 def show_publish_message(self, plain=False, filename=None): 800 801 "Show the publishing message for the updated event." 802 803 message = self.prepare_publish_message() 804 self.show_message(message, plain, filename) 805 806 def show_update_message(self, plain=False, filename=None): 807 808 "Show the update message for the updated event." 809 810 message = self.prepare_update_message() 811 if message: 812 self.show_message(message, plain, filename) 813 814 # General display methods. 815 816 def show_object(self): 817 print 818 print_title("Object details") 819 print 820 print "Summary:", self.state.get("summary") 821 print 822 print "Organiser:", self.state.get("organiser") 823 self.show_attendees() 824 self.show_periods() 825 self.show_rule() 826 self.show_suggested_attendees() 827 self.show_suggested_periods() 828 self.show_conflicting_periods() 829 print 830 print "Object is", self.obj.is_shared() and "shared" or "not shared" 831 832 def show_attendees(self): 833 print 834 print_title("Attendees") 835 attendees = self.state.get("attendees") 836 for index, attendee_item in enumerate(attendees.items()): 837 show_attendee(attendee_item, index) 838 839 def show_periods(self): 840 print 841 print_title("Periods") 842 show_periods(self.state.get("periods"), self.state.get("period_errors")) 843 844 def show_rule(self): 845 selectors = self.state.get("rule") 846 if selectors: 847 print 848 print_title("Period recurrence rule") 849 show_rule(selectors) 850 851 def show_suggested_attendees(self): 852 current_attendee = None 853 for index, (attendee, suggested_item) in enumerate(self.state.get("suggested_attendees")): 854 if attendee != current_attendee: 855 print 856 print_title("Attendees suggested by %s" % attendee) 857 current_attendee = attendee 858 show_attendee(suggested_item, index) 859 860 def show_suggested_periods(self): 861 periods = self.state.get("suggested_periods") 862 current_attendee = None 863 index = 0 864 for attendee, period, operation in periods: 865 if attendee != current_attendee: 866 print 867 print_title("Periods suggested by %s" % attendee) 868 current_attendee = attendee 869 show_period(period, index) 870 print " %s" % (operation == "add" and "Add this period" or "Remove this period") 871 index += 1 872 873 def show_conflicting_periods(self): 874 conflicts = self.get_conflicting_periods() 875 if not conflicts: 876 return 877 print 878 print_title("Conflicting periods") 879 880 conflicts = list(conflicts) 881 conflicts.sort() 882 883 for p in conflicts: 884 print p.summary, p.uid, p.get_start(), p.get_end() 885 886 # Interaction functions. 887 888 def expand_arg(args): 889 890 """ 891 Expand the first argument in 'args' to a pair of arguments if having the 892 form <char><digit>... 893 """ 894 895 if args[0] and args[0][1:].isdigit(): 896 args[:1] = [args[0][0], args[0][1:]] 897 898 def get_text_arg(s): 899 900 """ 901 Split 's' after the first whitespace occurrence, returning the remaining 902 text or None if no such text exists. 903 """ 904 905 return (s.split(None, 1)[1:] or [None])[0] 906 907 def next_arg(args): 908 909 """ 910 Return the first argument from 'args', removing it, or return None if no 911 arguments are left. 912 """ 913 914 if args: 915 arg = args[0] 916 del args[0] 917 return arg 918 return None 919 920 # Editing functions. 921 922 def edit_period(period, args=None): 923 924 "Edit the given 'period'." 925 926 print "Editing start (%s)" % period.get_start() 927 edit_date(period.start, args) 928 print "Editing end (%s)" % period.get_end() 929 edit_date(period.end, args) 930 931 period.reset() 932 933 def edit_date(date, args=None): 934 935 "Edit the given 'date' object attributes." 936 937 date.date = next_arg(args) or input_with_default("Date (%s)? ", date.date) 938 date.hour = next_arg(args) or input_with_default("Hour (%s)? ", date.hour) 939 940 # Permit day-level datetimes. 941 942 if date.hour == "-": 943 date.set_as_day() 944 else: 945 date.minute = next_arg(args) or input_with_default("Minute (%s)? ", date.minute) 946 date.second = next_arg(args) or input_with_default("Second (%s)? ", date.second) 947 date.tzid = next_arg(args) or input_with_default("Time zone (%s)? ", date.tzid) 948 949 date.reset() 950 951 def add_rule_selector_count(selectors, args, selector=None): 952 953 "Add to 'selectors' a selector imposing a count restriction." 954 955 while True: 956 arg = next_arg(args) 957 if not arg: 958 if selector: 959 arg = input_with_default("Number of occurrences (%d)? ", 960 selector.get_limit()) 961 else: 962 arg = read_input("Number of occurrences? ") 963 964 count = to_int_or_none(arg) 965 966 if count is None: 967 arg = None 968 continue 969 970 # Change or add selector. 971 972 selector = selector or selectors and \ 973 isinstance(selectors[0], vRecurrence.LimitSelector) and \ 974 selectors[0] or None 975 976 if not selector: 977 selector = vRecurrence.new_selector("COUNT") 978 selectors.insert(0, selector) 979 980 selector.set_limit(count) 981 break 982 983 def add_rule_selector_frequency(selectors, args, selector=None): 984 985 "Add to 'selectors' a selector for a frequency." 986 987 while not selector: 988 arg = next_arg(args) 989 if not arg: 990 arg = read_input("Select (y)early, (M)onthly, (w)eekly, (d)aily, " 991 "(h)ourly, (m)inutely, (s)econdly (or return)? ") 992 993 if not arg: 994 return 995 996 arg_lower = arg.lower() 997 998 if arg_lower in YEARLY_VALUES: 999 qualifier = "YEARLY" 1000 elif arg == "M" or arg_lower in MONTHLY_VALUES: 1001 qualifier = "MONTHLY" 1002 elif arg_lower in WEEKLY_VALUES: 1003 qualifier = "WEEKLY" 1004 elif arg_lower in DAILY_VALUES: 1005 qualifier = "DAILY" 1006 elif arg_lower in HOURLY_VALUES: 1007 qualifier = "HOURLY" 1008 elif arg == "m" or arg_lower in MINUTELY_VALUES: 1009 qualifier = "MINUTELY" 1010 elif arg_lower in SECONDLY_VALUES: 1011 qualifier = "SECONDLY" 1012 else: 1013 continue 1014 1015 break 1016 1017 while True: 1018 arg = next_arg(args) 1019 if not arg: 1020 if selector: 1021 arg = input_with_default("Interval (%d)? ", 1022 selector.get_interval()) 1023 else: 1024 arg = input_with_default("Interval (%d)? ", 1) 1025 1026 interval = to_int_or_none(arg) 1027 1028 if interval is None: 1029 arg = None 1030 else: 1031 break 1032 1033 # Update an existing selector. 1034 1035 if selector: 1036 selector.set_interval(interval) 1037 return 1038 1039 # Create a new selector. 1040 1041 selector = vRecurrence.new_selector(qualifier) 1042 selector.set_interval(interval) 1043 1044 # Remove any existing frequency selector. 1045 1046 for index, _selector in enumerate(selectors): 1047 if isinstance(_selector, vRecurrence.Pattern): 1048 del selectors[index] 1049 break 1050 1051 # Add the new selector and keep the selectors in order. 1052 1053 selectors.append(selector) 1054 vRecurrence.sort_selectors(selectors) 1055 1056 def add_rule_selector_selection(selectors, args, selector=None): 1057 1058 "Add to 'selectors' a selector for a particular point in time." 1059 1060 qualifier = selector and selector.qualifier or None 1061 1062 while not selector: 1063 arg = next_arg(args) 1064 if not arg: 1065 arg = read_input("Select (M)onths, (w)eeks, (y)eardays, " 1066 "m(o)nthdays, week(d)ays, (h)ours, (m)inutes, " 1067 "(s)econds (or return)? ") 1068 1069 if not arg: 1070 return 1071 1072 arg_lower = arg.lower() 1073 1074 if arg == "M" or arg_lower in MONTH_VALUES: 1075 qualifier = "BYMONTH" 1076 elif arg_lower in WEEK_VALUES: 1077 qualifier = "BYWEEKNO" 1078 elif arg_lower in YEARDAY_VALUES: 1079 qualifier = "BYYEARDAY" 1080 elif arg_lower in MONTHDAY_VALUES: 1081 qualifier = "BYMONTHDAY" 1082 elif arg_lower in DAY_VALUES: 1083 qualifier = "BYDAY" 1084 elif arg_lower in HOUR_VALUES: 1085 qualifier = "BYHOUR" 1086 elif arg == "m" or arg_lower in MINUTE_VALUES: 1087 qualifier = "BYMINUTE" 1088 elif arg_lower in SECOND_VALUES: 1089 qualifier = "BYSECOND" 1090 else: 1091 continue 1092 1093 break 1094 1095 if not qualifier: 1096 return 1097 1098 ranges = vRecurrence.get_value_ranges(qualifier) 1099 ranges_str = format_value_ranges(ranges[0]) 1100 1101 values = [] 1102 1103 while True: 1104 arg = next_arg(args) 1105 if not arg: 1106 arg = read_input("Value (%s) (return to end)? " % ranges_str) 1107 1108 # Stop if no more arguments. 1109 1110 if not arg or arg == "end": 1111 break 1112 1113 # Handle weekdays. 1114 1115 if qualifier == "BYDAY": 1116 value = arg.upper() # help to match weekdays 1117 1118 arg = next_arg(args) 1119 if not arg: 1120 arg = read_input("Occurrence within a month? ") 1121 1122 index = to_int_or_none(arg) 1123 value = vRecurrence.check_values(qualifier, [value, index]) 1124 1125 # Handle all other values. 1126 1127 else: 1128 value = to_int_or_none(arg) 1129 l = vRecurrence.check_values(qualifier, [value]) 1130 value = l and l[0] 1131 1132 # Append valid values. 1133 1134 if value is not None: 1135 values.append(value) 1136 else: 1137 print "Value not recognised." 1138 1139 if not values: 1140 return 1141 1142 # Update an existing selector. 1143 1144 if selector: 1145 selector.set_values(values) 1146 return 1147 1148 # Create a new selector. 1149 1150 selector = vRecurrence.new_selector(qualifier) 1151 selector.set_values(values) 1152 1153 # Remove any existing selector. 1154 1155 for index, _selector in enumerate(selectors): 1156 if _selector.qualifier == selector.qualifier: 1157 del selectors[index] 1158 break 1159 1160 # Add the new selector and keep the selectors in order. 1161 1162 selectors.append(selector) 1163 vRecurrence.sort_selectors(selectors) 1164 1165 def select_object(cl, objects): 1166 1167 "Select using 'cl' an object from the given 'objects'." 1168 1169 print 1170 1171 if objects: 1172 label = "Select object number or (n)ew object or (q)uit> " 1173 else: 1174 label = "Select (n)ew object or (q)uit> " 1175 1176 while True: 1177 try: 1178 cmd = read_input(label) 1179 except EOFError: 1180 return None 1181 1182 if cmd.isdigit(): 1183 index = to_int_or_none(cmd) 1184 1185 if index is not None and 0 <= index < len(objects): 1186 obj = objects[index] 1187 return cl.load_object(obj.get_uid(), obj.get_recurrenceid()) 1188 1189 elif cmd in NEW_COMMANDS: 1190 return cl.new_object() 1191 elif cmd in QUIT_COMMANDS: 1192 return None 1193 1194 def show_commands(): 1195 1196 "Show editing and inspection commands." 1197 1198 print 1199 print_title("Editing commands") 1200 print 1201 print """\ 1202 %(ATTENDEE_NEW_COMMANDS)s 1203 Add attendee 1204 1205 %(ATTENDEE_COMMANDS)s 1206 Select attendee from list 1207 1208 %(ATTENDANCE_COMMANDS)s 1209 Change attendance/participation 1210 1211 %(SUGGESTED_ATTENDEE_COMMANDS)s 1212 Add suggested attendee from list 1213 1214 %(FINISH_COMMANDS)s 1215 Finish editing, confirming changes, proceeding to messaging 1216 1217 %(HELP_COMMANDS)s 1218 Show this help message 1219 1220 %(LIST_COMMANDS)s 1221 List/show all event details 1222 1223 %(PERIOD_NEW_COMMANDS)s 1224 Add new period 1225 1226 %(PERIOD_COMMANDS)s 1227 Select period from list 1228 1229 %(SUGGESTED_PERIOD_COMMANDS)s 1230 Add or remove suggested period from list 1231 1232 %(QUIT_COMMANDS)s 1233 Exit/quit this program 1234 1235 %(RESET_COMMANDS)s 1236 Reset event periods (return to editing mode, if already finished) 1237 1238 %(RULE_NEW_COMMANDS)s 1239 Add a period recurrence rule 1240 1241 %(RULE_COMMANDS)s 1242 Select period recurrence rule selector from list 1243 1244 %(SUMMARY_COMMANDS)s 1245 Set event summary 1246 """ % { 1247 "ATTENDEE_NEW_COMMANDS" : commandlist(ATTENDEE_COMMANDS, "[ <uri> ]"), 1248 "ATTENDEE_COMMANDS" : commandlist(ATTENDEE_COMMANDS, "<number>"), 1249 "ATTENDANCE_COMMANDS" : commandlist(ATTENDANCE_COMMANDS), 1250 "SUGGESTED_ATTENDEE_COMMANDS" : commandlist(SUGGESTED_ATTENDEE_COMMANDS, "<number>"), 1251 "FINISH_COMMANDS" : commandlist(FINISH_COMMANDS), 1252 "HELP_COMMANDS" : commandlist(HELP_COMMANDS), 1253 "LIST_COMMANDS" : commandlist(LIST_COMMANDS), 1254 "PERIOD_NEW_COMMANDS" : commandlist(PERIOD_COMMANDS, "[ new ]"), 1255 "PERIOD_COMMANDS" : commandlist(PERIOD_COMMANDS, "<number>"), 1256 "SUGGESTED_PERIOD_COMMANDS" : commandlist(SUGGESTED_PERIOD_COMMANDS, "<number>"), 1257 "QUIT_COMMANDS" : commandlist(QUIT_COMMANDS), 1258 "RESET_COMMANDS" : commandlist(RESET_COMMANDS), 1259 "RULE_NEW_COMMANDS" : commandlist(RULE_COMMANDS), 1260 "RULE_COMMANDS" : commandlist(RULE_COMMANDS, "<number>"), 1261 "SUMMARY_COMMANDS" : commandlist(SUMMARY_COMMANDS), 1262 } 1263 1264 print_title("Messaging commands") 1265 print 1266 print """\ 1267 S, send 1268 Send messages to recipients and to self, if appropriate 1269 """ 1270 1271 print_title("Diagnostic commands") 1272 print 1273 print """\ 1274 c, class, classification 1275 Show period classification 1276 1277 C, changes 1278 Show changes made by editing 1279 1280 o, ops, operations 1281 Show update operations 1282 1283 RECURRENCE-ID [ <filename> ] 1284 Show event recurrence identifier, writing to <filename> if specified 1285 1286 UID [ <filename> ] 1287 Show event unique identifier, writing to <filename> if specified 1288 """ 1289 1290 print_title("Message inspection commands") 1291 print 1292 print """\ 1293 %(FREEBUSY_COMMANDS)s 1294 Show free/busy request message, writing to <filename> if specified 1295 1296 %(PUBLISH_COMMANDS)s 1297 publish [ <filename> ] 1298 Show publishing message, writing to <filename> if specified 1299 1300 %(CANCEL_COMMANDS)s 1301 Show cancellation message sent to uninvited/removed recipients, writing to 1302 <filename> if specified 1303 1304 %(CANCEL_PUBLISH_COMMANDS)s 1305 Show cancellation message for use by the organiser, writing to <filename> if 1306 specified 1307 1308 %(UPDATE_COMMANDS)s 1309 Show update message, writing to <filename> if specified 1310 """ % { 1311 "CANCEL_COMMANDS" : commandlist(CANCEL_COMMANDS, "[ <filename> ]"), 1312 "CANCEL_PUBLISH_COMMANDS" : commandlist(CANCEL_PUBLISH_COMMANDS, "[ <filename> ]"), 1313 "FREEBUSY_COMMANDS" : commandlist(FREEBUSY_COMMANDS, "[ <filename> ]"), 1314 "PUBLISH_COMMANDS" : commandlist(PUBLISH_COMMANDS, "[ <filename> ]"), 1315 "UPDATE_COMMANDS" : commandlist(UPDATE_COMMANDS, "[ <filename> ]"), 1316 } 1317 1318 def edit_object(cl, obj, handle_outgoing=False): 1319 1320 """ 1321 Edit using 'cl' the given object 'obj'. If 'handle_outgoing' is specified 1322 and set to a true value, the details from outgoing messages are incorporated 1323 into the stored data. 1324 """ 1325 1326 cl.show_object() 1327 print 1328 1329 try: 1330 while True: 1331 role = cl.is_organiser() and "Organiser" or "Attendee" 1332 status = cl.state.get("finished") and " (editing complete)" or "" 1333 1334 s = read_input("%s%s> " % (role, status)) 1335 args = s.split() 1336 1337 if not args or not args[0]: 1338 continue 1339 1340 # Expand short-form arguments. 1341 1342 expand_arg(args) 1343 cmd = next_arg(args) 1344 1345 # Check the status of the periods. 1346 1347 if cmd in CLASSIFICATION_COMMANDS: 1348 cl.show_period_classification() 1349 print 1350 1351 elif cmd in CHANGE_COMMANDS: 1352 cl.show_changes() 1353 print 1354 1355 # Finish editing. 1356 1357 elif cmd in FINISH_COMMANDS: 1358 cl.finish() 1359 1360 # Help. 1361 1362 elif cmd in HELP_COMMANDS: 1363 show_commands() 1364 1365 # Show object details. 1366 1367 elif cmd in LIST_COMMANDS: 1368 cl.show_object() 1369 print 1370 1371 # Show the operations. 1372 1373 elif cmd in OPERATION_COMMANDS: 1374 cl.show_operations() 1375 print 1376 1377 # Quit or exit. 1378 1379 elif cmd in QUIT_COMMANDS: 1380 break 1381 1382 # Restart editing. 1383 1384 elif cmd in RESET_COMMANDS: 1385 obj = cl.load_object(obj.get_uid(), obj.get_recurrenceid()) 1386 if not obj: 1387 obj = cl.new_object() 1388 cl.reset() 1389 cl.show_object() 1390 print 1391 1392 # Show UID details. 1393 1394 elif cmd in UID_COMMANDS: 1395 filename = get_text_arg(s) 1396 write(obj.get_uid(), filename) 1397 1398 elif cmd in RECURRENCEID_COMMANDS: 1399 filename = get_text_arg(s) 1400 write(obj.get_recurrenceid() or "", filename) 1401 1402 # Post-editing operations. 1403 1404 elif cl.state.get("finished"): 1405 1406 # Show messages. 1407 1408 if cmd in PUBLISH_COMMANDS: 1409 filename = get_text_arg(s) 1410 cl.show_publish_message(plain=not filename, filename=filename) 1411 1412 elif cmd in CANCEL_PUBLISH_COMMANDS: 1413 filename = get_text_arg(s) 1414 cl.show_cancel_publish_message(plain=not filename, filename=filename) 1415 1416 elif cmd in CANCEL_COMMANDS: 1417 filename = get_text_arg(s) 1418 cl.show_cancel_message(plain=not filename, filename=filename) 1419 1420 elif cmd in UPDATE_COMMANDS: 1421 filename = get_text_arg(s) 1422 cl.show_update_message(plain=not filename, filename=filename) 1423 1424 elif cmd in FREEBUSY_COMMANDS: 1425 filename = get_text_arg(s) 1426 cl.show_freebusy_message(plain=not filename, filename=filename) 1427 1428 # Definitive finishing action. 1429 1430 elif cmd in SEND_COMMANDS: 1431 1432 # Send update and cancellation messages. 1433 1434 did_send = False 1435 1436 message = cl.prepare_update_message() 1437 if message: 1438 cl.send_message(message, cl.get_recipients()) 1439 did_send = True 1440 1441 to_cancel = cl.state.get("attendees_to_cancel") 1442 if to_cancel: 1443 message = cl.prepare_cancel_message() 1444 if message: 1445 cl.send_message(message, to_cancel) 1446 did_send = True 1447 1448 # Process the object using the person outgoing handler. 1449 1450 if handle_outgoing: 1451 cl.handle_outgoing_object() 1452 1453 # Otherwise, send a message to self with the event details. 1454 1455 else: 1456 message = cl.prepare_publish_message() 1457 if message: 1458 cl.send_message_to_self(message) 1459 did_send = True 1460 1461 # Exit if sending occurred. 1462 1463 if did_send: 1464 break 1465 else: 1466 print "No messages sent. Try making edits or exit manually." 1467 1468 # Editing operations. 1469 1470 elif not cl.state.get("finished"): 1471 1472 # Add or edit attendee. 1473 1474 if cmd in ATTENDEE_COMMANDS: 1475 value = next_arg(args) 1476 index = to_int_or_none(value) 1477 1478 if index is None: 1479 try: 1480 index = cl.find_attendee(value) 1481 except ValueError: 1482 index = None 1483 1484 # Add an attendee. 1485 1486 if index is None: 1487 cl.add_attendee(value) 1488 if not value: 1489 cl.edit_attendee(-1) 1490 1491 # Edit attendee (using index). 1492 1493 else: 1494 attendee_item = cl.can_remove_attendee(index) 1495 if attendee_item: 1496 while True: 1497 show_attendee(attendee_item, index) 1498 1499 # Obtain a command from any arguments. 1500 1501 cmd = next_arg(args) 1502 if not cmd: 1503 cmd = read_input("Attendee: (e)dit, (r)emove (or return)> ") 1504 if cmd in EDIT_COMMANDS: 1505 cl.edit_attendee(index) 1506 elif cmd in REMOVE_COMMANDS: 1507 cl.remove_attendees([index]) 1508 1509 # Exit if requested or after a successful 1510 # operation. 1511 1512 elif not cmd: 1513 pass 1514 else: 1515 continue 1516 break 1517 1518 cl.show_attendees() 1519 print 1520 1521 # Add suggested attendee (using index). 1522 1523 elif cmd in SUGGESTED_ATTENDEE_COMMANDS: 1524 value = next_arg(args) 1525 index = to_int_or_none(value) 1526 1527 if index is not None: 1528 cl.add_suggested_attendee(index) 1529 1530 cl.show_attendees() 1531 print 1532 1533 # Edit attendance. 1534 1535 elif cmd in ATTENDANCE_COMMANDS: 1536 1537 if not cl.is_attendee() and cl.is_organiser(): 1538 cl.add_attendee(cl.user) 1539 1540 # NOTE: Support delegation. 1541 1542 if cl.can_edit_attendance(): 1543 while True: 1544 1545 # Obtain a command from any arguments. 1546 1547 cmd = next_arg(args) 1548 if not cmd: 1549 cmd = read_input("Attendance: (a)ccept, (d)ecline, (t)entative (or return)> ") 1550 if cmd in ACCEPTED_VALUES: 1551 cl.edit_attendance("ACCEPTED") 1552 elif cmd in DECLINED_VALUES: 1553 cl.edit_attendance("DECLINED") 1554 elif cmd in TENTATIVE_VALUES: 1555 cl.edit_attendance("TENTATIVE") 1556 1557 # Exit if requested or after a successful operation. 1558 1559 elif not cmd: 1560 pass 1561 else: 1562 continue 1563 break 1564 1565 cl.show_attendees() 1566 print 1567 1568 # Add or edit period. 1569 1570 elif cmd in PERIOD_COMMANDS: 1571 value = next_arg(args) 1572 index = to_int_or_none(value) 1573 1574 # Add a new period. 1575 1576 if index is None or value == "new": 1577 cl.add_period() 1578 cl.edit_period(-1, args) 1579 1580 # Edit period (using index). 1581 1582 else: 1583 period = cl.can_edit_period(index) 1584 if period: 1585 while True: 1586 show_period_raw(period) 1587 1588 # Obtain a command from any arguments. 1589 1590 cmd = next_arg(args) 1591 if not cmd: 1592 cmd = read_input("Period: (c)ancel, (e)dit, (u)ncancel (or return)> ") 1593 1594 if cmd in CANCEL_PERIOD_COMMANDS: 1595 cl.cancel_periods([index]) 1596 elif cmd in EDIT_COMMANDS: 1597 cl.edit_period(index, args) 1598 elif cmd in UNCANCEL_COMMANDS: 1599 cl.cancel_periods([index], False) 1600 1601 # Exit if requested or after a successful 1602 # operation. 1603 1604 elif not cmd: 1605 pass 1606 else: 1607 continue 1608 break 1609 1610 cl.show_periods() 1611 print 1612 1613 # Apply suggested period (using index). 1614 1615 elif cmd in SUGGESTED_PERIOD_COMMANDS: 1616 value = next_arg(args) 1617 index = to_int_or_none(value) 1618 1619 if index is not None: 1620 cl.apply_suggested_period(index) 1621 1622 cl.show_periods() 1623 print 1624 1625 # Specify a recurrence rule. 1626 1627 elif cmd in RULE_COMMANDS: 1628 value = next_arg(args) 1629 index = to_int_or_none(value) 1630 1631 # Add a new rule. 1632 1633 if index is None: 1634 cl.add_rule_selectors() 1635 else: 1636 cl.edit_rule_selector(index, args) 1637 1638 cl.show_rule() 1639 cl.update_periods_from_rule() 1640 print 1641 1642 # Set the summary. 1643 1644 elif cmd in SUMMARY_COMMANDS: 1645 cl.edit_summary(get_text_arg(s)) 1646 cl.show_object() 1647 print 1648 1649 except EOFError: 1650 return 1651 1652 def main(args): 1653 1654 """ 1655 The main program, employing command line 'args' to initialise the editing 1656 activity. 1657 """ 1658 1659 global echo 1660 1661 if "--help" in args: 1662 show_help(os.path.split(sys.argv[0])[-1]) 1663 return 0 1664 1665 # Parse command line arguments using the standard options plus some extra 1666 # options. 1667 1668 args = parse_args(args, { 1669 "--calendar-data" : ("calendar_data", False), 1670 "--charset" : ("charset", "utf-8"), 1671 "--echo" : ("echo", False), 1672 "-f" : ("filename", None), 1673 "--handle-data" : ("handle_data", False), 1674 "--suppress-bcc" : ("suppress_bcc", False), 1675 "-u" : ("user", None), 1676 "--uid" : ("uid", None), 1677 "--recurrence-id" : ("recurrenceid", None), 1678 "--show-config" : ("show_config", False) 1679 }) 1680 1681 charset = args["charset"] 1682 calendar_data = args["calendar_data"] 1683 echo = args["echo"] 1684 filename = args["filename"] 1685 handle_data = args["handle_data"] 1686 sender = (args["senders"] or [None])[0] 1687 suppress_bcc = args["suppress_bcc"] 1688 user = args["user"] 1689 uid = args["uid"] 1690 recurrenceid = args["recurrenceid"] 1691 1692 # Open a store. 1693 1694 store_type = args.get("store_type") 1695 store_dir = args.get("store_dir") 1696 preferences_dir = args.get("preferences_dir") 1697 1698 # Show configuration and exit if requested. 1699 1700 if args["show_config"]: 1701 print """\ 1702 Store type: %s (%s) 1703 Store directory: %s (%s) 1704 Preferences directory: %s 1705 """ % ( 1706 store_type, settings["STORE_TYPE"], 1707 store_dir, settings["STORE_DIR"], 1708 preferences_dir) 1709 return 0 1710 1711 # Determine the user and sender identities. 1712 1713 if sender and not user: 1714 user = get_uri(sender) 1715 elif user and not sender: 1716 sender = get_address(user) 1717 elif not sender and not user: 1718 print >>sys.stderr, "A sender or a user must be specified." 1719 return 1 1720 1721 # Obtain a store but not a journal. 1722 1723 store = get_store(store_type, store_dir) 1724 journal = None 1725 1726 # Open a messenger for the user. 1727 1728 messenger = Messenger(sender=sender, suppress_bcc=suppress_bcc) 1729 1730 # Open a client for the user. 1731 1732 cl = TextClient(user, messenger, store, journal, preferences_dir) 1733 1734 # Read any input resource, using it to obtain identifier details. 1735 1736 if filename: 1737 if calendar_data: 1738 all_itip = get_itip_from_data(filename, charset) 1739 else: 1740 all_itip = get_itip_from_message(filename) 1741 1742 objects = [] 1743 1744 # Process the objects using the person handler. 1745 1746 if handle_data: 1747 for itip in all_itip: 1748 handled = handle_calendar_data(itip, get_handlers(cl, person.handlers, None)) 1749 if not is_cancel_itip(itip): 1750 objects += handled 1751 1752 # Or just obtain objects from the data. 1753 1754 else: 1755 for itip in all_itip: 1756 handled = get_objects_from_itip(itip, ["VEVENT"]) 1757 if not is_cancel_itip(itip): 1758 objects += handled 1759 1760 # Choose an object to edit. 1761 1762 show_objects(objects, user, store) 1763 obj = select_object(cl, objects) 1764 1765 # Load any indicated object. 1766 1767 elif uid: 1768 obj = cl.load_object(uid, recurrenceid) 1769 1770 # Or create a new object. 1771 1772 else: 1773 obj = cl.new_object() 1774 1775 # Exit without any object. 1776 1777 if not obj: 1778 print >>sys.stderr, "No object loaded." 1779 return 1 1780 1781 # Edit the object. 1782 1783 edit_object(cl, obj, handle_outgoing=handle_data) 1784 1785 def show_help(progname): 1786 print >>sys.stderr, help_text % progname 1787 1788 help_text = """\ 1789 Usage: %s -s <sender> | -u <user> \\ 1790 [ -f <filename> | --uid <uid> [ --recurrence-id <recurrence-id> ] ] \\ 1791 [ --calendar-data --charset ] \\ 1792 [ --handle-data ] \\ 1793 [ -T <store type ] [ -S <store directory> ] \\ 1794 [ -p <preferences directory> ] \\ 1795 [ --echo ] 1796 1797 Identity options: 1798 1799 -s Indicate the user by specifying a sender address 1800 -u Indicate the user by specifying their URI 1801 1802 Input options: 1803 1804 -f Indicates a filename containing a MIME-encoded message or 1805 calendar object 1806 --uid Indicates the UID of a stored calendar object 1807 --recurrence-id Indicates a stored object with a specific RECURRENCE-ID 1808 1809 --calendar-data Indicates that the specified file contains a calendar object 1810 as opposed to a mail message 1811 --charset Specifies the character encoding used by a calendar object 1812 description 1813 1814 Processing options: 1815 1816 --handle-data Cause the input to be handled and stored in the configured 1817 data store 1818 1819 Configuration options (overriding configured defaults): 1820 1821 -p Indicates the location of user preference directories 1822 -S Indicates the location of the calendar data store containing user storage 1823 directories 1824 -T Indicates the store type (the configured value if omitted) 1825 1826 Output options: 1827 1828 --echo Echo received input, useful if consuming input from the 1829 standard input stream and producing a log of the program's 1830 activity 1831 """ 1832 1833 if __name__ == "__main__": 1834 sys.exit(main(sys.argv[1:])) 1835 1836 # vim: tabstop=4 expandtab shiftwidth=4