Beispiel #1
0
def confirm(action, default=None, skip=False):
    """A shortcut for typical confirmation prompt.

    :param action:

        a string describing the action, e.g. "Apply changes". A question mark
        will be appended.

    :param default:

        `bool` or `None`. Determines what happens when user hits :kbd:`Enter`
        without typing in a choice. If `True`, default choice is "yes". If
        `False`, it is "no". If `None` the prompt keeps reappearing until user
        types in a choice (not necessarily acceptable) or until the number of
        iteration reaches the limit. Default is `None`.

    :param skip:

        `bool`; if `True`, no interactive prompt is used and default choice is
        returned (useful for batch mode). Default is `False`.

    Usage::

        def delete(key, silent=False):
            item = db.get(Item, args.key)
            if confirm('Delete '+item.title, default=True, skip=silent):
                item.delete()
                print('Item deleted.')
            else:
                print('Operation cancelled.')

    Returns `None` on `KeyboardInterrupt` event.
    """
    MAX_ITERATIONS = 3
    if skip:
        return default
    else:
        defaults = {None: ("y", "n"), True: ("Y", "n"), False: ("y", "N")}
        y, n = defaults[default]
        prompt = text_type("{action}? ({y}/{n})").format(**locals())
        choice = None
        try:
            if default is None:
                cnt = 1
                while not choice and cnt < MAX_ITERATIONS:
                    choice = safe_input(prompt)
                    cnt += 1
            else:
                choice = safe_input(prompt)
        except KeyboardInterrupt:
            return None
    if choice in ("yes", "y", "Y"):
        return True
    if choice in ("no", "n", "N"):
        return False
    if default is not None:
        return default
    return None
Beispiel #2
0
def confirm(action, default=None, skip=False):
    """A shortcut for typical confirmation prompt.

    :param action:

        a string describing the action, e.g. "Apply changes". A question mark
        will be appended.

    :param default:

        `bool` or `None`. Determines what happens when user hits :kbd:`Enter`
        without typing in a choice. If `True`, default choice is "yes". If
        `False`, it is "no". If `None` the prompt keeps reappearing until user
        types in a choice (not necessarily acceptable) or until the number of
        iteration reaches the limit. Default is `None`.

    :param skip:

        `bool`; if `True`, no interactive prompt is used and default choice is
        returned (useful for batch mode). Default is `False`.

    Usage::

        def delete(key, silent=False):
            item = db.get(Item, args.key)
            if confirm('Delete '+item.title, default=True, skip=silent):
                item.delete()
                print('Item deleted.')
            else:
                print('Operation cancelled.')

    Returns `None` on `KeyboardInterrupt` event.
    """
    MAX_ITERATIONS = 3
    if skip:
        return default
    else:
        defaults = {
            None: ('y', 'n'),
            True: ('Y', 'n'),
            False: ('y', 'N'),
        }
        y, n = defaults[default]
        prompt = text_type('{action}? ({y}/{n})').format(**locals())
        choice = None
        try:
            if default is None:
                cnt = 1
                while not choice and cnt < MAX_ITERATIONS:
                    choice = safe_input(prompt)
                    cnt += 1
            else:
                choice = safe_input(prompt)
        except KeyboardInterrupt:
            return None
    if choice in ('yes', 'y', 'Y'):
        return True
    if choice in ('no', 'n', 'N'):
        return False
    if default is not None:
        return default
    return None
Beispiel #3
0
def log_activity(args):
    "Logs a past activity (since last logged until now)"

    # TODO: split all changes into steps and apply each using a separate,
    # well-tested function

    since = args.since
    until = args.until
    duration = args.duration

    if not args.activity and not args.amend:
        raise CommandError(failure('activity must be specified '
                                   'unless --amend is set'))

    if args.between:
        assert not (since or until or duration), (
            '--since, --until and --duration must not be used with --between')
        since, until = args.between.split('-')

    if args.date:
        rel_date = datetime.datetime.strptime(args.date, '%Y-%m-%d')
        since = utils.parse_time_to_datetime(since, relative_to=rel_date,
                                             ensure_past_time=False)
        until = utils.parse_time_to_datetime(until, relative_to=rel_date,
                                             ensure_past_time=False)
    else:
        since = utils.parse_time_to_datetime(since)
        until = utils.parse_time_to_datetime(until)

    delta = utils.parse_delta(duration)

    tags = [TIMETRA_TAG_LOG]
    if args.tags:
        tags = list(set(tags + args.tags.split(',')))
    if args.with_person:
        tags.extend(['with-{0}'.format(x) for x in args.with_person.split(',')])

    if args.pick and not args.amend:
        raise CommandError(failure('--pick only makes sense with --amend'))

    prev = _get_last_fact(args.pick)

    if args.amend:
        if not prev:
            raise CommandError('Cannot amend: no fact found')

        # FIXME this disables --duration
        since = since or prev.start_time
        until = until or prev.end_time

    start, end = storage.get_start_end(since, until, delta)

    if end < start:
        raise CommandError('--since must be earlier than --until')

    if datetime.datetime.now() < end:
        raise CommandError('--until must not be in the future')

    # check if we aren't going to overwrite any previous facts
    try:
        for line in check_overlap(start, end,
                                  activity=(args.pick or args.activity),
                                  amend_fact=prev if args.amend else None):
            yield line
    except OverlapError as e:
        raise CommandError(failure(e))

    if args.activity:
        activity, category = parse_activity(args.activity)
    else:
        activity = category = None

    description = None
    if args.description:
        description = args.description
    elif args.no_input:
        pass
    elif args.amend and prev.description:
        # updating a fact that already has a description
        pass
    else:
        # collect multi-line description from interactive user input
        lines = []
        num = 0
        try:
            while True:
                line = safe_input('        > ' if num else warning('Describe> '))
                if line:
                    lines.append(line)
                else:
                    yield ''
                    break
                num += 1
        except (KeyboardInterrupt, EOFError):
            raise CommandError(failure('Operation cancelled.'))
        else:
            description = '\n'.join(lines) if lines else None

    if args.amend:
        #template = u'Updated {fact.activity}@{fact.category} ({delta_minutes} min)'
        assert prev
        fact = prev

        kwargs = dict(
            start_time=start,
            end_time=end,
            dry_run=args.dry_run,
        )
        if activity:
            kwargs.update(activity=activity, category=category)
        if description is not None:
            kwargs.update(description=description)
        if args.tags is not None or args.with_person is not None:
            kwargs.update(tags=tags)

        changed = []
        for key, value in kwargs.iteritems():
            if hasattr(fact, key) and getattr(fact, key) != kwargs[key]:
                changed.append(key)
                old_value = getattr(fact, key)
                if hasattr(old_value, '__iter__'):
                    # convert DBus strings to proper pythonic ones (for tags)
                    old_value = [str(x) for x in old_value]
                note = u''
                if isinstance(old_value, datetime.datetime) and value:
                    if old_value < value:
                        note = u'(+{0})'.format(value - old_value)
                    else:
                        note = u'(-{0})'.format(old_value - value)
                yield u'* {0}: {1} →  {2} {3}'.format(
                    key,
                    failure(unicode(old_value)),
                    success(unicode(value)),
                    note)

        if not changed:
            yield failure(u'Nothing changed.')
            return

        storage.update_fact(fact, **kwargs)
    else:
        #template = u'Logged {fact.activity}@{fact.category} ({delta_minutes} min)'
        try:
            fact = storage.add_fact(
                args.activity,    # NOTE: not using parsed activity + category because hamster wants the foo@bar thing
                start_time=start,
                end_time=end,
                description=description,
                tags=tags,
                dry_run=args.dry_run)
        except (storage.ActivityMatchingError, storage.CannotCreateFact) as e:
            raise CommandError(failure(e))

    # report
    #delta = fact.end_time - start  # почему-то сам факт "не знает" времени начала
    #delta_minutes = delta.seconds / 60
    #yield success(template.format(fact=fact, delta_minutes=delta_minutes))

    for output in show_last_fact(args.activity or args.pick):
        yield output

    if args.dry_run:
        yield warning(u'(Dry run, nothing changed.)')