imip-agent

Changeset

1436:ffcb65f7b8e5
2018-01-12 Paul Boddie raw files shortlog changelog graph Added support for rule editing, also tidying up command processing and adding support for loading stored objects using identifier details. Added an option to show the store and preferences configuration. client-editing-simplification
imip_text_client.py (file)
     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