1.1 --- a/imip_text_client.py Fri Jan 12 21:54:14 2018 +0100
1.2 +++ b/imip_text_client.py Fri Jan 12 21:56:34 2018 +0100
1.3 @@ -3,7 +3,7 @@
1.4 """
1.5 A text interface to received and new events.
1.6
1.7 -Copyright (C) 2017 Paul Boddie <paul@boddie.org.uk>
1.8 +Copyright (C) 2017, 2018 Paul Boddie <paul@boddie.org.uk>
1.9
1.10 This program is free software; you can redistribute it and/or modify it under
1.11 the terms of the GNU General Public License as published by the Free Software
1.12 @@ -25,9 +25,12 @@
1.13 from imiptools.content import get_objects_from_itip, handle_calendar_data, \
1.14 handle_calendar_object, have_itip_part, \
1.15 is_cancel_itip, parse_itip_part
1.16 -from imiptools.data import get_address, get_main_period, get_recurrence_periods, get_value, parse_object
1.17 +from imiptools.data import get_address, get_periods_using_selector, \
1.18 + get_main_period, get_recurrence_periods, \
1.19 + get_value, parse_object
1.20 from imiptools.dates import get_datetime_item, get_time, to_timezone
1.21 -from imiptools.editing import EditingClient, PeriodError
1.22 +from imiptools.editing import EditingClient, PeriodError, \
1.23 + form_periods_from_periods
1.24 from imiptools.handlers import person, person_outgoing
1.25 from imiptools.mail import Messenger
1.26 from imiptools.stores import get_journal, get_store
1.27 @@ -60,6 +63,34 @@
1.28
1.29 return read_input(label % default) or default
1.30
1.31 +def to_int_or_none(value):
1.32 +
1.33 + "Return 'value' as an integer or None for other inputs."
1.34 +
1.35 + try:
1.36 + return int(value)
1.37 + except (TypeError, ValueError):
1.38 + return None
1.39 +
1.40 +def format_value_ranges(ranges, null_value="-"):
1.41 +
1.42 + "Format 'ranges' as a single descriptive string."
1.43 +
1.44 + l = []
1.45 +
1.46 + for value_range in ranges:
1.47 + if isinstance(value_range, tuple):
1.48 + start, end = value_range
1.49 + l.append("%s...%s" % (start, end))
1.50 + elif isinstance(value_range, list):
1.51 + l.append(", ".join(value_range))
1.52 + elif isinstance(value_range, dict):
1.53 + l.append(", ".join(value_range.keys()))
1.54 + else:
1.55 + l.append(value_range or null_value)
1.56 +
1.57 + return ", ".join(l)
1.58 +
1.59 def print_title(text):
1.60
1.61 "Print 'text' with simple, fixed-width styling as a title."
1.62 @@ -135,6 +166,8 @@
1.63
1.64 return [itip]
1.65
1.66 +# Object and request display.
1.67 +
1.68 def show_objects(objects, user, store):
1.69
1.70 """
1.71 @@ -170,6 +203,8 @@
1.72 recurrence_label = recurrenceid and " %s" % recurrenceid or ""
1.73 print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label)
1.74
1.75 +# Object details display.
1.76 +
1.77 def show_attendee(attendee_item, index):
1.78
1.79 "Show the 'attendee_item' (value and attributes) at 'index'."
1.80 @@ -233,68 +268,69 @@
1.81
1.82 print p.get_start(), p.get_end(), p.origin
1.83
1.84 -def show_rule(rrule):
1.85 -
1.86 - "Show recurrence rule specification 'rrule'."
1.87 +def show_rule(selectors):
1.88
1.89 - print
1.90 - print "Recurrence rule:"
1.91 -
1.92 - count = None
1.93 - freq_interval = []
1.94 - selections = []
1.95 + "Show recurrence rule specification 'selectors'."
1.96
1.97 # Collect limit, selection and frequency details.
1.98
1.99 - for selector in vRecurrence.order_qualifiers(vRecurrence.get_qualifiers(rrule)):
1.100 -
1.101 - # COUNT
1.102 + for i, selector in enumerate(selectors):
1.103 + prefix = "(%d) " % i
1.104 + show_rule_selector(selector, prefix)
1.105
1.106 - if isinstance(selector, vRecurrence.LimitSelector):
1.107 - count = selector.args["values"][0]
1.108 + print
1.109 + print vRecurrence.to_string(selectors)
1.110
1.111 - # BYSETPOS
1.112 +def show_rule_selector(selector, prefix=""):
1.113 +
1.114 + "Show the rule 'selector', employing any given 'prefix' for formatting."
1.115
1.116 - elif isinstance(selector, vRecurrence.PositionSelector):
1.117 - for value in selector.args["values"]:
1.118 - selections.append(("-", get_frequency(selector.level), str(value)))
1.119 + # COUNT
1.120
1.121 - # BY...
1.122 + if isinstance(selector, vRecurrence.LimitSelector):
1.123 + print "%sAt most %d occurrences" % (prefix, selector.args["values"][0])
1.124 +
1.125 + # BYSETPOS
1.126
1.127 - elif isinstance(selector, vRecurrence.Enum):
1.128 - for value in selector.args["values"]:
1.129 - selections.append(("-", get_frequency(selector.level), str(value)))
1.130 + elif isinstance(selector, vRecurrence.PositionSelector):
1.131 + for value in selector.get_positions():
1.132 + print "%sSelect occurrence #%d" % (prefix, value)
1.133 + prefix = len(prefix) * " "
1.134
1.135 - # BYWEEKDAY
1.136 + # BYWEEKDAY
1.137
1.138 - elif isinstance(selector, vRecurrence.WeekDayFilter):
1.139 - for value, index in selector.args["values"]:
1.140 - selections.append((index >= 0 and "Start" or "End", get_weekday(value), str(index)))
1.141 + elif isinstance(selector, vRecurrence.WeekDayFilter):
1.142 + for value, index in selector.get_values():
1.143 + print "%sSelect occurrence #%d (from %s) of weekday %s" % (
1.144 + prefix, abs(index), index >= 0 and "start" or "end",
1.145 + get_weekday(value))
1.146 + prefix = len(prefix) * " "
1.147
1.148 - # YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY
1.149 -
1.150 - elif isinstance(selector, vRecurrence.Pattern):
1.151 - freq_interval.append((get_frequency(selector.level), str(selector.args.get("interval", 1))))
1.152 -
1.153 - # Show the details.
1.154 + # BY...
1.155
1.156 - if freq_interval:
1.157 - print
1.158 - print_table([("Frequency", "Interval")] + freq_interval)
1.159 + elif isinstance(selector, vRecurrence.Enum):
1.160 + for value in selector.get_values():
1.161 + print "%sSelect %s %r" % (prefix, get_resolution(selector.level),
1.162 + value)
1.163 + prefix = len(prefix) * " "
1.164 +
1.165 + # YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY
1.166
1.167 - if selections:
1.168 - print
1.169 - print_table([("From...", "Selecting", "Instance (1, 2, ...)")] + selections)
1.170 + elif isinstance(selector, vRecurrence.Pattern):
1.171 + print "%sEach %s with interval %d" % (prefix,
1.172 + get_resolution(selector.level), selector.args.get("interval", 1))
1.173
1.174 - if count:
1.175 - print
1.176 - print "At most", count, "occurrences."
1.177 +def get_resolution(level):
1.178
1.179 -def get_frequency(level):
1.180 - levels = ["Year", "Month", "Week", None, None, "Day", "Hour", "Minute", "Second"]
1.181 + "Return a textual description of the given resolution 'level'."
1.182 +
1.183 + levels = ["year", "month", "week", "day in year", "day in month", "day", "hour", "minute", "second"]
1.184 return levels[level]
1.185
1.186 def get_weekday(weekday):
1.187 +
1.188 + "Return the name of the given 1-based 'weekday' number."
1.189 +
1.190 weekdays = [None, "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
1.191 return weekdays[weekday]
1.192
1.193 @@ -421,8 +457,86 @@
1.194 obj["DTEND"] = [get_datetime_item(now)]
1.195 return obj
1.196
1.197 + def handle_outgoing_object(self):
1.198 +
1.199 + "Handle the current object using the outgoing handlers."
1.200 +
1.201 + unscheduled_objects, rescheduled_objects, added_objects = \
1.202 + self.get_publish_objects()
1.203 +
1.204 + handlers = get_handlers(self, person_outgoing.handlers,
1.205 + [get_address(self.user)])
1.206 +
1.207 + # Handle the parent object plus any altered periods.
1.208 +
1.209 + handle_calendar_object(self.obj, handlers, "PUBLISH")
1.210 +
1.211 + for o in unscheduled_objects:
1.212 + handle_calendar_object(o, handlers, "CANCEL")
1.213 +
1.214 + for o in rescheduled_objects:
1.215 + handle_calendar_object(o, handlers, "PUBLISH")
1.216 +
1.217 + for o in added_objects:
1.218 + handle_calendar_object(o, handlers, "ADD")
1.219 +
1.220 + def update_periods_from_rule(self):
1.221 +
1.222 + "Update the periods from the rule."
1.223 +
1.224 + selectors = self.state.get("rule")
1.225 + periods = self.state.get("periods")
1.226 +
1.227 + main_period = get_main_period(periods)
1.228 + tzid = main_period.get_tzid()
1.229 +
1.230 + start = main_period.get_start()
1.231 + end = self.get_window_end() or None
1.232 +
1.233 + selector = vRecurrence.get_selector(start, selectors)
1.234 + until = None
1.235 + inclusive = False
1.236 +
1.237 + # Generate the periods from the rule.
1.238 +
1.239 + rule_periods = form_periods_from_periods(
1.240 + get_periods_using_selector(selector, main_period, tzid,
1.241 + start, end, inclusive))
1.242 +
1.243 + # Retain any applicable replacement periods that either modify or cancel
1.244 + # rule periods.
1.245 + # NOTE: To be done.
1.246 +
1.247 + self.state.set("periods", rule_periods)
1.248 +
1.249 # Editing methods involving interaction.
1.250
1.251 + def add_rule_selectors(self):
1.252 +
1.253 + "Add rule selectors to the rule details."
1.254 +
1.255 + selectors = self.state.get("rule")
1.256 +
1.257 + while True:
1.258 +
1.259 + # Obtain a command from any arguments.
1.260 +
1.261 + s = read_input("Selector: (c)ount, (f)requency, (s)election (or return)> ")
1.262 + args = s.split()
1.263 + cmd = next_arg(args)
1.264 +
1.265 + if cmd in ("c", "count", "limit"):
1.266 + add_rule_selector_count(selectors, args)
1.267 + elif cmd in ("f", "freq", "frequency"):
1.268 + add_rule_selector_frequency(selectors, args)
1.269 + elif cmd in ("s", "select", "selection"):
1.270 + add_rule_selector_selection(selectors, args)
1.271 +
1.272 + # Remain in the loop unless explicitly terminated.
1.273 +
1.274 + elif not cmd or cmd == "end":
1.275 + break
1.276 +
1.277 def edit_attendee(self, index):
1.278
1.279 "Edit the attendee at 'index'."
1.280 @@ -436,6 +550,9 @@
1.281 attendees[attendee] = attr
1.282
1.283 def edit_period(self, index, args=None):
1.284 +
1.285 + "Edit the period at 'index'."
1.286 +
1.287 period = self.can_edit_period(index)
1.288 if period:
1.289 edit_period(period, args)
1.290 @@ -451,13 +568,64 @@
1.291 periods = self.state.get("periods")
1.292 periods.sort()
1.293
1.294 + def edit_rule_selector(self, index, args):
1.295 +
1.296 + "Edit the selector having the given 'index'."
1.297 +
1.298 + selectors = self.state.get("rule")
1.299 + selector = self.can_edit_rule_selector(index)
1.300 +
1.301 + if not selector:
1.302 + return
1.303 +
1.304 + while True:
1.305 + show_rule_selector(selector)
1.306 +
1.307 + # Obtain a command from any arguments.
1.308 +
1.309 + cmd = next_arg(args)
1.310 + if not cmd:
1.311 + s = read_input("Selector: (e)dit, (r)emove (or return)> ")
1.312 + args = s.split()
1.313 + cmd = next_arg(args)
1.314 +
1.315 + # Edit an existing selector.
1.316 +
1.317 + if cmd in ("e", "edit"):
1.318 + if isinstance(selector, vRecurrence.LimitSelector):
1.319 + add_rule_selector_count(selectors, args, selector)
1.320 + elif isinstance(selector, vRecurrence.Pattern):
1.321 + add_rule_selector_frequency(selectors, args, selector)
1.322 + else:
1.323 + add_rule_selector_selection(selectors, args, selector)
1.324 +
1.325 + # Remove an existing selector.
1.326 +
1.327 + elif cmd in ("r", "remove"):
1.328 + del selectors[index]
1.329 +
1.330 + # Exit if requested or after a successful
1.331 + # operation.
1.332 +
1.333 + elif not cmd:
1.334 + pass
1.335 + else:
1.336 + continue
1.337 + break
1.338 +
1.339 def edit_summary(self, summary=None):
1.340 +
1.341 + "Edit or set the 'summary'."
1.342 +
1.343 if self.can_edit_properties():
1.344 if not summary:
1.345 summary = input_with_default("Summary (%s)? ", self.state.get("summary"))
1.346 self.state.set("summary", summary)
1.347
1.348 def finish(self):
1.349 +
1.350 + "Finish editing, warning of errors if any occur."
1.351 +
1.352 try:
1.353 EditingClient.finish(self)
1.354 except PeriodError:
1.355 @@ -467,6 +635,9 @@
1.356 # Diagnostic methods.
1.357
1.358 def show_period_classification(self):
1.359 +
1.360 + "Show the classification of the periods."
1.361 +
1.362 try:
1.363 new, replaced, retained, cancelled, obsolete = self.classify_periods()
1.364 show_period_classification(new, replaced, retained, cancelled, obsolete)
1.365 @@ -475,6 +646,9 @@
1.366 print "Errors exist in the periods."
1.367
1.368 def show_changes(self):
1.369 +
1.370 + "Show how the periods have changed."
1.371 +
1.372 try:
1.373 modified, unmodified, removed = self.classify_period_changes()
1.374 show_changes(modified, unmodified, removed)
1.375 @@ -489,6 +663,9 @@
1.376 show_attendee_changes(new, modified, unmodified, removed)
1.377
1.378 def show_operations(self):
1.379 +
1.380 + "Show the operations required to change the periods for recipients."
1.381 +
1.382 is_changed = self.properties_changed()
1.383
1.384 try:
1.385 @@ -506,6 +683,12 @@
1.386 # Output methods.
1.387
1.388 def show_message(self, message, plain=False, filename=None):
1.389 +
1.390 + """
1.391 + Show the given mail 'message', decoding to plain text if 'plain' is set
1.392 + to a true value, writing it to 'filename' if indicated.
1.393 + """
1.394 +
1.395 if plain:
1.396 decode_part(message)
1.397 write(message_as_string(message), filename)
1.398 @@ -544,9 +727,12 @@
1.399 print "Organiser:", self.state.get("organiser")
1.400 self.show_attendees()
1.401 self.show_periods()
1.402 + self.show_rule()
1.403 self.show_suggested_attendees()
1.404 self.show_suggested_periods()
1.405 self.show_conflicting_periods()
1.406 + print
1.407 + print "Object is", self.obj.is_shared() and "shared" or "not shared"
1.408
1.409 def show_attendees(self):
1.410 print
1.411 @@ -558,10 +744,14 @@
1.412 def show_periods(self):
1.413 print
1.414 print_title("Periods")
1.415 - rrule = self.obj.get_value("RRULE")
1.416 show_periods(self.state.get("periods"), self.state.get("period_errors"))
1.417 - if rrule:
1.418 - show_rule(rrule)
1.419 +
1.420 + def show_rule(self):
1.421 + selectors = self.state.get("rule")
1.422 + if selectors:
1.423 + print
1.424 + print_title("Period recurrence rule")
1.425 + show_rule(selectors)
1.426
1.427 def show_suggested_attendees(self):
1.428 current_attendee = None
1.429 @@ -601,19 +791,39 @@
1.430 # Interaction functions.
1.431
1.432 def expand_arg(args):
1.433 +
1.434 + """
1.435 + Expand the first argument in 'args' to a pair of arguments if having the
1.436 + form <char><digit>...
1.437 + """
1.438 +
1.439 if args[0] and args[0][1:].isdigit():
1.440 args[:1] = [args[0][0], args[0][1:]]
1.441
1.442 -def get_filename_arg(cmd):
1.443 - return (cmd.split()[1:] or [None])[0]
1.444 +def get_text_arg(s):
1.445 +
1.446 + """
1.447 + Split 's' after the first whitespace occurrence, returning the remaining
1.448 + text or None if no such text exists.
1.449 + """
1.450 +
1.451 + return (s.split(None, 1)[1:] or [None])[0]
1.452
1.453 def next_arg(args):
1.454 +
1.455 + """
1.456 + Return the first argument from 'args', removing it, or return None if no
1.457 + arguments are left.
1.458 + """
1.459 +
1.460 if args:
1.461 arg = args[0]
1.462 del args[0]
1.463 return arg
1.464 return None
1.465
1.466 +# Editing functions.
1.467 +
1.468 def edit_period(period, args=None):
1.469
1.470 "Edit the given 'period'."
1.471 @@ -634,7 +844,224 @@
1.472 date.tzid = next_arg(args) or input_with_default("Time zone (%s)? ", date.tzid)
1.473 date.reset()
1.474
1.475 +def add_rule_selector_count(selectors, args, selector=None):
1.476 +
1.477 + "Add to 'selectors' a selector imposing a count restriction."
1.478 +
1.479 + while True:
1.480 + arg = next_arg(args)
1.481 + if not arg:
1.482 + if selector:
1.483 + arg = input_with_default("Number of occurrences (%d)? ",
1.484 + selector.get_limit())
1.485 + else:
1.486 + arg = read_input("Number of occurrences? ")
1.487 +
1.488 + count = to_int_or_none(arg)
1.489 +
1.490 + if count is None:
1.491 + arg = None
1.492 + continue
1.493 +
1.494 + # Change or add selector.
1.495 +
1.496 + selector = selector or selectors and \
1.497 + isinstance(selectors[0], vRecurrence.LimitSelector) and \
1.498 + selectors[0] or None
1.499 +
1.500 + if not selector:
1.501 + selector = vRecurrence.new_selector("COUNT")
1.502 + selectors.insert(0, selector)
1.503 +
1.504 + selector.set_limit(count)
1.505 + break
1.506 +
1.507 +def add_rule_selector_frequency(selectors, args, selector=None):
1.508 +
1.509 + "Add to 'selectors' a selector for a frequency."
1.510 +
1.511 + while not selector:
1.512 + arg = next_arg(args)
1.513 + if not arg:
1.514 + arg = read_input("Select (y)early, (M)onthly, (w)eekly, (d)aily, "
1.515 + "(h)ourly, (m)inutely, (s)econdly (or return)? ")
1.516 +
1.517 + if not arg:
1.518 + return
1.519 +
1.520 + arg_lower = arg.lower()
1.521 +
1.522 + if arg_lower in ("y", "year", "yearly"):
1.523 + qualifier = "YEARLY"
1.524 + elif arg == "M" or arg_lower in ("month", "monthly"):
1.525 + qualifier = "MONTHLY"
1.526 + elif arg_lower in ("w", "week", "weekly"):
1.527 + qualifier = "WEEKLY"
1.528 + elif arg_lower in ("d", "day", "daily"):
1.529 + qualifier = "DAILY"
1.530 + elif arg_lower in ("h", "hour", "hourly"):
1.531 + qualifier = "HOURLY"
1.532 + elif arg_lower in ("m", "minute", "minutely"):
1.533 + qualifier = "MINUTELY"
1.534 + elif arg_lower in ("s", "second", "secondly"):
1.535 + qualifier = "SECONDLY"
1.536 + else:
1.537 + continue
1.538 +
1.539 + break
1.540 +
1.541 + while True:
1.542 + arg = next_arg(args)
1.543 + if not arg:
1.544 + if selector:
1.545 + arg = input_with_default("Interval (%d)? ",
1.546 + selector.get_interval())
1.547 + else:
1.548 + arg = input_with_default("Interval (%d)? ", 1)
1.549 +
1.550 + interval = to_int_or_none(arg)
1.551 +
1.552 + if interval is None:
1.553 + arg = None
1.554 + else:
1.555 + break
1.556 +
1.557 + # Update an existing selector.
1.558 +
1.559 + if selector:
1.560 + selector.set_interval(interval)
1.561 + return
1.562 +
1.563 + # Create a new selector.
1.564 +
1.565 + selector = vRecurrence.new_selector(qualifier)
1.566 + selector.set_interval(interval)
1.567 +
1.568 + # Remove any existing frequency selector.
1.569 +
1.570 + for index, _selector in enumerate(selectors):
1.571 + if isinstance(_selector, vRecurrence.Pattern):
1.572 + del selectors[index]
1.573 + break
1.574 +
1.575 + # Add the new selector and keep the selectors in order.
1.576 +
1.577 + selectors.append(selector)
1.578 + vRecurrence.sort_selectors(selectors)
1.579 +
1.580 +def add_rule_selector_selection(selectors, args, selector=None):
1.581 +
1.582 + "Add to 'selectors' a selector for a particular point in time."
1.583 +
1.584 + qualifier = selector and selector.qualifier or None
1.585 +
1.586 + while not selector:
1.587 + arg = next_arg(args)
1.588 + if not arg:
1.589 + arg = read_input("Select (M)onths, (w)eeks, (y)eardays, "
1.590 + "m(o)nthdays, week(d)ays, (h)ours, (m)inutes, "
1.591 + "(s)econds (or return)? ")
1.592 +
1.593 + if not arg:
1.594 + return
1.595 +
1.596 + arg_lower = arg.lower()
1.597 +
1.598 + if arg == "M" or arg_lower in ("month", "months"):
1.599 + qualifier = "BYMONTH"
1.600 + elif arg_lower in ("w", "week", "weeks"):
1.601 + qualifier = "BYWEEKNO"
1.602 + elif arg_lower in ("y", "yearday", "yeardays"):
1.603 + qualifier = "BYYEARDAY"
1.604 + elif arg_lower in ("o", "monthday", "monthdays"):
1.605 + qualifier = "BYMONTHDAY"
1.606 + elif arg_lower in ("d", "weekday", "weekdays"):
1.607 + qualifier = "BYDAY"
1.608 + elif arg_lower in ("h", "hour", "hours"):
1.609 + qualifier = "BYHOUR"
1.610 + elif arg_lower in ("m", "minute", "minutes"):
1.611 + qualifier = "BYMINUTE"
1.612 + elif arg_lower in ("s", "second", "seconds"):
1.613 + qualifier = "BYSECOND"
1.614 + else:
1.615 + continue
1.616 +
1.617 + break
1.618 +
1.619 + if not qualifier:
1.620 + return
1.621 +
1.622 + ranges = vRecurrence.get_value_ranges(qualifier)
1.623 + ranges_str = format_value_ranges(ranges[0])
1.624 +
1.625 + values = []
1.626 +
1.627 + while True:
1.628 + arg = next_arg(args)
1.629 + if not arg:
1.630 + arg = read_input("Value (%s) (return to end)? " % ranges_str)
1.631 +
1.632 + # Stop if no more arguments.
1.633 +
1.634 + if not arg or arg == "end":
1.635 + break
1.636 +
1.637 + # Handle weekdays.
1.638 +
1.639 + if qualifier == "BYDAY":
1.640 + value = arg.upper() # help to match weekdays
1.641 +
1.642 + arg = next_arg(args)
1.643 + if not arg:
1.644 + arg = read_input("Occurrence within a month? ")
1.645 +
1.646 + index = to_int_or_none(arg)
1.647 + value = vRecurrence.check_values(qualifier, [value, index])
1.648 +
1.649 + # Handle all other values.
1.650 +
1.651 + else:
1.652 + value = to_int_or_none(arg)
1.653 + l = vRecurrence.check_values(qualifier, [value])
1.654 + value = l and l[0]
1.655 +
1.656 + # Append valid values.
1.657 +
1.658 + if value is not None:
1.659 + values.append(value)
1.660 + else:
1.661 + print "Value not recognised."
1.662 +
1.663 + if not values:
1.664 + return
1.665 +
1.666 + # Update an existing selector.
1.667 +
1.668 + if selector:
1.669 + selector.set_values(values)
1.670 + return
1.671 +
1.672 + # Create a new selector.
1.673 +
1.674 + selector = vRecurrence.new_selector(qualifier)
1.675 + selector.set_values(values)
1.676 +
1.677 + # Remove any existing selector.
1.678 +
1.679 + for index, _selector in enumerate(selectors):
1.680 + if _selector.qualifier == selector.qualifier:
1.681 + del selectors[index]
1.682 + break
1.683 +
1.684 + # Add the new selector and keep the selectors in order.
1.685 +
1.686 + selectors.append(selector)
1.687 + vRecurrence.sort_selectors(selectors)
1.688 +
1.689 def select_object(cl, objects):
1.690 +
1.691 + "Select using 'cl' an object from the given 'objects'."
1.692 +
1.693 print
1.694
1.695 if objects:
1.696 @@ -649,8 +1076,9 @@
1.697 return None
1.698
1.699 if cmd.isdigit():
1.700 - index = int(cmd)
1.701 - if 0 <= index < len(objects):
1.702 + index = to_int_or_none(cmd)
1.703 +
1.704 + if index is not None and 0 <= index < len(objects):
1.705 obj = objects[index]
1.706 return cl.load_object(obj.get_uid(), obj.get_recurrenceid())
1.707
1.708 @@ -660,6 +1088,9 @@
1.709 return None
1.710
1.711 def show_commands():
1.712 +
1.713 + "Show editing and inspection commands."
1.714 +
1.715 print
1.716 print_title("Editing commands")
1.717 print
1.718 @@ -687,7 +1118,7 @@
1.719 l, list, show
1.720 List/show all event details
1.721
1.722 -p, period
1.723 +p, period [ new ]
1.724 Add new period
1.725
1.726 p<digit>
1.727 @@ -703,8 +1134,12 @@
1.728 r, reload, reset, restart
1.729 Reset event periods (return to editing mode, if already finished)
1.730
1.731 -rrule <rule specification>
1.732 - Set a recurrence rule in the event, applying to the main period
1.733 +rule, rrule
1.734 + Add a period recurrence rule
1.735 +
1.736 +rule <digit>
1.737 +rrule <digit>
1.738 + Select period recurrence rule selector from list
1.739
1.740 s, summary
1.741 Set event summary
1.742 @@ -755,6 +1190,13 @@
1.743 """
1.744
1.745 def edit_object(cl, obj, handle_outgoing=False):
1.746 +
1.747 + """
1.748 + Edit using 'cl' the given object 'obj'. If 'handle_outgoing' is specified
1.749 + and set to a true value, the details from outgoing messages are incorporated
1.750 + into the stored data.
1.751 + """
1.752 +
1.753 cl.show_object()
1.754 print
1.755
1.756 @@ -763,13 +1205,17 @@
1.757 role = cl.is_organiser() and "Organiser" or "Attendee"
1.758 status = cl.state.get("finished") and " (editing complete)" or ""
1.759
1.760 - cmd = read_input("%s%s> " % (role, status))
1.761 -
1.762 - args = cmd.split()
1.763 + s = read_input("%s%s> " % (role, status))
1.764 + args = s.split()
1.765
1.766 if not args or not args[0]:
1.767 continue
1.768
1.769 + # Expand short-form arguments.
1.770 +
1.771 + expand_arg(args)
1.772 + cmd = next_arg(args)
1.773 +
1.774 # Check the status of the periods.
1.775
1.776 if cmd in ("c", "class", "classification"):
1.777 @@ -819,12 +1265,12 @@
1.778
1.779 # Show UID details.
1.780
1.781 - elif args[0] == "UID":
1.782 - filename = get_filename_arg(cmd)
1.783 + elif cmd == "UID":
1.784 + filename = get_text_arg(s)
1.785 write(obj.get_uid(), filename)
1.786
1.787 - elif args[0] == "RECURRENCE-ID":
1.788 - filename = get_filename_arg(cmd)
1.789 + elif cmd == "RECURRENCE-ID":
1.790 + filename = get_text_arg(s)
1.791 write(obj.get_recurrenceid() or "", filename)
1.792
1.793 # Post-editing operations.
1.794 @@ -833,21 +1279,21 @@
1.795
1.796 # Show messages.
1.797
1.798 - if args[0] in ("P", "publish"):
1.799 - filename = get_filename_arg(cmd)
1.800 + if cmd in ("P", "publish"):
1.801 + filename = get_text_arg(s)
1.802 cl.show_publish_message(plain=not filename, filename=filename)
1.803
1.804 - elif args[0] in ("R", "remove", "cancel"):
1.805 - filename = get_filename_arg(cmd)
1.806 + elif cmd in ("R", "remove", "cancel"):
1.807 + filename = get_text_arg(s)
1.808 cl.show_cancel_message(plain=not filename, filename=filename)
1.809
1.810 - elif args[0] in ("U", "update"):
1.811 - filename = get_filename_arg(cmd)
1.812 + elif cmd in ("U", "update"):
1.813 + filename = get_text_arg(s)
1.814 cl.show_update_message(plain=not filename, filename=filename)
1.815
1.816 # Definitive finishing action.
1.817
1.818 - elif args[0] in ("S", "send"):
1.819 + elif cmd in ("S", "send"):
1.820
1.821 # Send update and cancellation messages.
1.822
1.823 @@ -868,22 +1314,7 @@
1.824 # Process the object using the person outgoing handler.
1.825
1.826 if handle_outgoing:
1.827 -
1.828 - # Handle the parent object plus any rescheduled periods.
1.829 -
1.830 - unscheduled_objects, rescheduled_objects, added_objects = \
1.831 - cl.get_publish_objects()
1.832 -
1.833 - handlers = get_handlers(cl, person_outgoing.handlers,
1.834 - [get_address(cl.user)])
1.835 -
1.836 - handle_calendar_object(cl.obj, handlers, "PUBLISH")
1.837 - for o in unscheduled_objects:
1.838 - handle_calendar_object(o, handlers, "CANCEL")
1.839 - for o in rescheduled_objects:
1.840 - handle_calendar_object(o, handlers, "PUBLISH")
1.841 - for o in added_objects:
1.842 - handle_calendar_object(o, handlers, "ADD")
1.843 + cl.handle_outgoing_object()
1.844
1.845 # Otherwise, send a message to self with the event details.
1.846
1.847 @@ -904,20 +1335,13 @@
1.848
1.849 elif not cl.state.get("finished"):
1.850
1.851 - # Expand short-form arguments.
1.852 -
1.853 - expand_arg(args)
1.854 -
1.855 # Add or edit attendee.
1.856
1.857 - if args[0] in ("a", "attendee"):
1.858 -
1.859 - args = args[1:]
1.860 + if cmd in ("a", "attendee"):
1.861 value = next_arg(args)
1.862 + index = to_int_or_none(value)
1.863
1.864 - if value and value.isdigit():
1.865 - index = int(value)
1.866 - else:
1.867 + if index is None:
1.868 try:
1.869 index = cl.find_attendee(value)
1.870 except ValueError:
1.871 @@ -942,11 +1366,15 @@
1.872
1.873 cmd = next_arg(args)
1.874 if not cmd:
1.875 - cmd = read_input(" (e)dit, (r)emove (or return)> ")
1.876 + cmd = read_input("Attendee: (e)dit, (r)emove (or return)> ")
1.877 if cmd in ("e", "edit"):
1.878 cl.edit_attendee(index)
1.879 elif cmd in ("r", "remove"):
1.880 cl.remove_attendees([index])
1.881 +
1.882 + # Exit if requested or after a successful
1.883 + # operation.
1.884 +
1.885 elif not cmd:
1.886 pass
1.887 else:
1.888 @@ -958,20 +1386,19 @@
1.889
1.890 # Add suggested attendee (using index).
1.891
1.892 - elif args[0] in ("as", "attendee-suggested", "suggested-attendee"):
1.893 - try:
1.894 - index = int(args[1])
1.895 + elif cmd in ("as", "attendee-suggested", "suggested-attendee"):
1.896 + value = next_arg(args)
1.897 + index = to_int_or_none(value)
1.898 +
1.899 + if index is not None:
1.900 cl.add_suggested_attendee(index)
1.901 - except ValueError:
1.902 - pass
1.903 +
1.904 cl.show_attendees()
1.905 print
1.906
1.907 # Edit attendance.
1.908
1.909 - elif args[0] in ("A", "attend", "attendance"):
1.910 -
1.911 - args = args[1:]
1.912 + elif cmd in ("A", "attend", "attendance"):
1.913
1.914 if not cl.is_attendee() and cl.is_organiser():
1.915 cl.add_attendee(cl.user)
1.916 @@ -985,13 +1412,16 @@
1.917
1.918 cmd = next_arg(args)
1.919 if not cmd:
1.920 - cmd = read_input(" (a)ccept, (d)ecline, (t)entative (or return)> ")
1.921 + cmd = read_input("Attendance: (a)ccept, (d)ecline, (t)entative (or return)> ")
1.922 if cmd in ("a", "accept", "accepted", "attend"):
1.923 cl.edit_attendance("ACCEPTED")
1.924 elif cmd in ("d", "decline", "declined"):
1.925 cl.edit_attendance("DECLINED")
1.926 elif cmd in ("t", "tentative"):
1.927 cl.edit_attendance("TENTATIVE")
1.928 +
1.929 + # Exit if requested or after a successful operation.
1.930 +
1.931 elif not cmd:
1.932 pass
1.933 else:
1.934 @@ -1003,21 +1433,15 @@
1.935
1.936 # Add or edit period.
1.937
1.938 - elif args[0] in ("p", "period"):
1.939 -
1.940 - args = args[1:]
1.941 + elif cmd in ("p", "period"):
1.942 value = next_arg(args)
1.943 -
1.944 - if value and value.isdigit():
1.945 - index = int(value)
1.946 - else:
1.947 - index = None
1.948 + index = to_int_or_none(value)
1.949
1.950 # Add a new period.
1.951
1.952 - if index is None:
1.953 + if index is None or value == "new":
1.954 cl.add_period()
1.955 - cl.edit_period(-1)
1.956 + cl.edit_period(-1, args)
1.957
1.958 # Edit period (using index).
1.959
1.960 @@ -1031,13 +1455,17 @@
1.961
1.962 cmd = next_arg(args)
1.963 if not cmd:
1.964 - cmd = read_input(" (e)dit, (c)ancel, (u)ncancel (or return)> ")
1.965 + cmd = read_input("Period: (e)dit, (c)ancel, (u)ncancel (or return)> ")
1.966 if cmd in ("e", "edit"):
1.967 cl.edit_period(index, args)
1.968 elif cmd in ("c", "cancel"):
1.969 cl.cancel_periods([index])
1.970 elif cmd in ("u", "uncancel", "restore"):
1.971 cl.cancel_periods([index], False)
1.972 +
1.973 + # Exit if requested or after a successful
1.974 + # operation.
1.975 +
1.976 elif not cmd:
1.977 pass
1.978 else:
1.979 @@ -1049,25 +1477,37 @@
1.980
1.981 # Apply suggested period (using index).
1.982
1.983 - elif args[0] in ("ps", "period-suggested", "suggested-period"):
1.984 - try:
1.985 - index = int(args[1])
1.986 + elif cmd in ("ps", "period-suggested", "suggested-period"):
1.987 + value = next_arg(args)
1.988 + index = to_int_or_none(value)
1.989 +
1.990 + if index is not None:
1.991 cl.apply_suggested_period(index)
1.992 - except ValueError:
1.993 - pass
1.994 +
1.995 cl.show_periods()
1.996 print
1.997
1.998 # Specify a recurrence rule.
1.999
1.1000 - elif args[0] == "rrule":
1.1001 - pass
1.1002 + elif cmd in ("rule", "rrule"):
1.1003 + value = next_arg(args)
1.1004 + index = to_int_or_none(value)
1.1005 +
1.1006 + # Add a new rule.
1.1007 +
1.1008 + if index is None:
1.1009 + cl.add_rule_selectors()
1.1010 + else:
1.1011 + cl.edit_rule_selector(index, args)
1.1012 +
1.1013 + cl.show_rule()
1.1014 + cl.update_periods_from_rule()
1.1015 + print
1.1016
1.1017 # Set the summary.
1.1018
1.1019 - elif args[0] in ("s", "summary"):
1.1020 - t = cmd.split(None, 1)
1.1021 - cl.edit_summary(len(t) > 1 and t[1] or None)
1.1022 + elif cmd in ("s", "summary"):
1.1023 + cl.edit_summary(get_text_arg(s))
1.1024 cl.show_object()
1.1025 print
1.1026
1.1027 @@ -1075,11 +1515,17 @@
1.1028 return
1.1029
1.1030 def main(args):
1.1031 +
1.1032 + """
1.1033 + The main program, employing command line 'args' to initialise the editing
1.1034 + activity.
1.1035 + """
1.1036 +
1.1037 global echo
1.1038
1.1039 if "--help" in args:
1.1040 show_help(os.path.split(sys.argv[0])[-1])
1.1041 - return
1.1042 + return 0
1.1043
1.1044 # Parse command line arguments using the standard options plus some extra
1.1045 # options.
1.1046 @@ -1092,6 +1538,9 @@
1.1047 "--handle-data" : ("handle_data", False),
1.1048 "--suppress-bcc" : ("suppress_bcc", False),
1.1049 "-u" : ("user", None),
1.1050 + "--uid" : ("uid", None),
1.1051 + "--recurrence-id" : ("recurrenceid", None),
1.1052 + "--show-config" : ("show_config", False)
1.1053 })
1.1054
1.1055 charset = args["charset"]
1.1056 @@ -1102,6 +1551,27 @@
1.1057 sender = (args["senders"] or [None])[0]
1.1058 suppress_bcc = args["suppress_bcc"]
1.1059 user = args["user"]
1.1060 + uid = args["uid"]
1.1061 + recurrenceid = args["recurrenceid"]
1.1062 +
1.1063 + # Open a store.
1.1064 +
1.1065 + store_type = args.get("store_type")
1.1066 + store_dir = args.get("store_dir")
1.1067 + preferences_dir = args.get("preferences_dir")
1.1068 +
1.1069 + # Show configuration and exit if requested.
1.1070 +
1.1071 + if args["show_config"]:
1.1072 + print """\
1.1073 +Store type: %s (%s)
1.1074 +Store directory: %s (%s)
1.1075 +Preferences directory: %s
1.1076 +""" % (
1.1077 + store_type, settings["STORE_TYPE"],
1.1078 + store_dir, settings["STORE_DIR"],
1.1079 + preferences_dir)
1.1080 + return 0
1.1081
1.1082 # Determine the user and sender identities.
1.1083
1.1084 @@ -1111,13 +1581,9 @@
1.1085 sender = get_address(user)
1.1086 elif not sender and not user:
1.1087 print >>sys.stderr, "A sender or a user must be specified."
1.1088 - sys.exit(1)
1.1089 -
1.1090 - # Open a store.
1.1091 + return 1
1.1092
1.1093 - store_type = args.get("store_type")
1.1094 - store_dir = args.get("store_dir")
1.1095 - preferences_dir = args.get("preferences_dir")
1.1096 + # Obtain a store but not a journal.
1.1097
1.1098 store = get_store(store_type, store_dir)
1.1099 journal = None
1.1100 @@ -1130,7 +1596,7 @@
1.1101
1.1102 cl = TextClient(user, messenger, store, journal, preferences_dir)
1.1103
1.1104 - # Read any input resource.
1.1105 + # Read any input resource, using it to obtain identifier details.
1.1106
1.1107 if filename:
1.1108 if calendar_data:
1.1109 @@ -1161,17 +1627,22 @@
1.1110 show_objects(objects, user, store)
1.1111 obj = select_object(cl, objects)
1.1112
1.1113 - # Exit without any object.
1.1114 + # Load any indicated object.
1.1115
1.1116 - if not obj:
1.1117 - print >>sys.stderr, "No object loaded."
1.1118 - sys.exit(1)
1.1119 + elif uid:
1.1120 + obj = cl.load_object(uid, recurrenceid)
1.1121
1.1122 # Or create a new object.
1.1123
1.1124 else:
1.1125 obj = cl.new_object()
1.1126
1.1127 + # Exit without any object.
1.1128 +
1.1129 + if not obj:
1.1130 + print >>sys.stderr, "No object loaded."
1.1131 + return 1
1.1132 +
1.1133 # Edit the object.
1.1134
1.1135 edit_object(cl, obj, handle_outgoing=handle_data)
1.1136 @@ -1181,7 +1652,7 @@
1.1137
1.1138 help_text = """\
1.1139 Usage: %s -s <sender> | -u <user> \\
1.1140 - [ -f <filename> ] \\
1.1141 + [ -f <filename> | --uid <uid> [ --recurrence-id <recurrence-id> ] ] \\
1.1142 [ --calendar-data --charset ] \\
1.1143 [ --handle-data ] \\
1.1144 [ -T <store type ] [ -S <store directory> ] \\
1.1145 @@ -1195,7 +1666,10 @@
1.1146
1.1147 Input options:
1.1148
1.1149 --f Indicates a filename containing a MIME-encoded message or calendar object
1.1150 +-f Indicates a filename containing a MIME-encoded message or
1.1151 + calendar object
1.1152 +--uid Indicates the UID of a stored calendar object
1.1153 +--recurrence-id Indicates a stored object with a specific RECURRENCE-ID
1.1154
1.1155 --calendar-data Indicates that the specified file contains a calendar object
1.1156 as opposed to a mail message
1.1157 @@ -1222,6 +1696,6 @@
1.1158 """
1.1159
1.1160 if __name__ == "__main__":
1.1161 - main(sys.argv[1:])
1.1162 + sys.exit(main(sys.argv[1:]))
1.1163
1.1164 # vim: tabstop=4 expandtab shiftwidth=4