Skip to content

Commit

Permalink
mass-interactive-edit
Browse files Browse the repository at this point in the history
  • Loading branch information
tobixen committed Sep 23, 2023
1 parent 592888e commit db79992
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 20 deletions.
45 changes: 35 additions & 10 deletions plann/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
import tempfile
import subprocess
from plann.panic_planning import timeline_suggestion
from plann.lib import _now, _ensure_ts, parse_dt, parse_add_dur, parse_timespec, find_calendars, _summary, _procrastinate, tz, _relships_by_type, _get_summary, _relationship_text, _adjust_relations, parentlike, childlike, _remove_reverse_relations, command_edit, _process_set_arg, attr_txt_one, attr_txt_many, attr_time, attr_int, _set_something
from plann.lib import _now, _ensure_ts, parse_dt, parse_add_dur, parse_timespec, find_calendars, _summary, _procrastinate, tz, _relships_by_type, _get_summary, _relationship_text, _adjust_relations, parentlike, childlike, _remove_reverse_relations, command_edit, _process_set_arg, attr_txt_one, attr_txt_many, attr_time, attr_int, _set_something, _command_line_edit

list_type = list

Expand Down Expand Up @@ -376,7 +376,7 @@ def list(ctx, ics, template, top_down=False, bottom_up=False):
"""
return _list(ctx.obj['objs'], ics, template, top_down=top_down, bottom_up=bottom_up)

def _list(objs, ics=False, template="{DTSTART:?{DUE:?(date missing)?}?%F %H:%M:%S %Z}: {SUMMARY:?{DESCRIPTION:?(no summary given)?}?}", top_down=False, bottom_up=False, indent=0, echo=True, uids=None):
def _list(objs, ics=False, template="{DTSTART:?{DUE:?(date missing)?}?%F %H:%M:%S %Z}: {SUMMARY:?{DESCRIPTION:?(no summary given)?}?}", top_down=False, bottom_up=False, indent=0, echo=True, uids=None, filter=lambda obj: True):
"""
Actual implementation of list
Expand Down Expand Up @@ -404,6 +404,9 @@ def _list(objs, ics=False, template="{DTSTART:?{DUE:?(date missing)?}?%F %H:%M:%
output.append(obj)
continue

if not filter(obj):
continue

uid = obj.icalendar_component['UID']
if uid in uids and not 'RECURRENCE-ID' in obj.icalendar_component:
continue
Expand Down Expand Up @@ -520,6 +523,8 @@ def _interactive_edit(obj):
dtstart = _ensure_ts(dtstart)
click.echo(f"pri={pri} {dtstart:%F %H:%M:%S %Z} - {due:%F %H:%M:%S %Z}: {summary}")
input = click.prompt("postpone <n>d / ignore / part(ially-complete) / complete / split / cancel / set foo=bar / edit / family / pdb?", default='ignore')
if input.startswith('postpone') and not 'ask' in input:
input += ' ask'
command_edit(obj, input)

def _editor(sometext):
Expand Down Expand Up @@ -656,13 +661,7 @@ def _edit(ctx, add_category=None, cancel=None, interactive_ical=False, interacti
_interactive_relation_edit(ctx.obj['objs'])

if mass_interactive:
## send things through the editor
indented_family = "\n".join(_list(
ctx.obj['objs'], top_down=True, echo=False,
template="ignore {UID}: due={DUE} Pri={PRI:?0?} {SUMMARY:?{DESCRIPTION:?(no summary given)?}?} (STATUS={STATUS:-})"))
edited = _editor(indented_family)
for line in edited.split('\n'):
command_line_edit(line)
_mass_interactive_edit(ctx.obj['objs'])

for obj in ctx.obj['objs']:
if interactive:
Expand Down Expand Up @@ -697,6 +696,30 @@ def _edit(ctx, add_category=None, cancel=None, interactive_ical=False, interacti
comp[attrib].dt = parse_add_dur(comp[attrib].dt, postpone, for_storage=True)
obj.save()

def _mass_interactive_edit(objs, default='ignore'):
"""send things through the editor, and expect commands back"""
instructions = """
## Prepend a line with one of the following commands:
## postpone <n>d [with parents] [with children] [with family] [ask]
## ignore
## part(ially-complete)
## complete
## split
## cancel
## set foo=bar
## edit
## family
## pdb
"""
text = instructions + "\n".join(_list(
objs, top_down=True, echo=False,
## We only deal with tasks so far, and only tasks that needs action
## TODO: this is bad design
template=default + " {UID}: due={DUE} Pri={PRI:?0?} {SUMMARY:?{DESCRIPTION:?(no summary given)?}?} (STATUS={STATUS:-})", filter=lambda obj: obj.icalendar_component['STATUS']=='NEEDS-ACTION'))
edited = _editor(text)
for line in edited.split('\n'):
## BUG: does not work if the source data comes from multiple calendars!
_command_line_edit(line, interactive=True, calendar=objs[0].parent)

@select.command()
@click.pass_context
Expand Down Expand Up @@ -1088,10 +1111,12 @@ def _dismiss_panic(ctx, hours_per_day, lookahead='60d'):
procrastination_time = f"{procrastination_time.days+1}d"
else:
procrastination_time = f"{procrastination_time.seconds//3600+1}h"
procrastination_time = click.prompt(f"Push the due-date with ... (press O for one-by-one)", default=procrastination_time)
procrastination_time = click.prompt(f"Push the due-date with ... (press O for one-by-one, E for edit all)", default=procrastination_time)
if procrastination_time == 'O':
for item in first_low_pri_tasks:
_interactive_edit(item['obj'])
elif procrastination_time == 'E':
_mass_interactive_edit([x['obj'] for x in first_low_pri_tasks], default=f"postpone {procrastination_time} ask")
else:
_procrastinate([x['obj'] for x in first_low_pri_tasks], procrastination_time, check_dependent='interactive', err_callback=click.echo, confirm_callback=click.confirm)

Expand Down
53 changes: 45 additions & 8 deletions plann/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ def _parse_dt(input, return_type=None):
else:
return _ensure_ts(ret)

def _command_line_edit(line, calendar, interactive=True):
strip1 = re.compile("#.*$")
regexp = re.compile("((?:postpone [0-9]+[smhdwy](?:with [^ ])*(?:ask)?)|[^ ]*) (.*?)(: |$)")
line = strip1.sub('', line)
line = line.strip()
if not line:
return
splitted = regexp.match(line)
assert splitted
command = splitted.group(1)
uid = splitted.group(2)
obj = calendar.object_by_uid(uid)
command_edit(obj, command, interactive)

def parse_add_dur(dt, dur, for_storage=False, ts_allowed=False):
"""
duration may be something like this:
Expand Down Expand Up @@ -484,15 +498,34 @@ def _relationship_text(obj, reltype_wanted=None):
ret.append(reltype + "\n" + "\n".join(objs) + "\n")
return "\n".join(ret)

def command_edit(obj, command):


def command_edit(obj, command, interactive=True):
if command == 'ignore':
return
elif command == 'part':
elif command in ('part', 'partially-complete'):
interactive_split_task(obj, partially_complete=True, too_big=False)
elif command == 'split':
interactive_split_task(obj, too_big=False)
elif command.startswith('postpone'):
## TODO: make this into an interactive recursive function
commands = command.split(' ')
if len(commands) > 2:
with_params = {}
if interactive:
with_params['confirm_callback'] = click.confirm
with_params['err_callback'] = click.echo
if interactive and 'ask' in command:
true = 'interactive'
with_params['check_dependent'] = 'interactive'
else:
true = True
if 'with family' in command:
with_params['with_family'] = true
if 'with children' in command:
with_params['with_children'] = true
if 'with family' in command:
with_params['with_family'] = true
## TODO: we probably shouldn't be doing this interactively here?
parent = _procrastinate([obj], command.split(' ')[1], with_children='interactive', with_parent='interactive', with_family='interactive', check_dependent="interactive", err_callback=click.echo, confirm_callback=click.confirm)
elif command == 'complete':
obj.complete(handle_rrule=True)
Expand All @@ -509,13 +542,17 @@ def command_edit(obj, command):
elif command == 'family':
_interactive_relation_edit([obj])
elif command == 'pdb':
click.echo("icalendar component available as comp")
click.echo("caldav object available as obj")
click.echo("do the necessary changes and press c to continue normal code execution")
click.echo("happy hacking")
if interactive:
click.echo("icalendar component available as comp")
click.echo("caldav object available as obj")
click.echo("do the necessary changes and press c to continue normal code execution")
click.echo("happy hacking")
import pdb; pdb.set_trace()
else:
click.echo(f"unknown instruction '{command}' - ignoring")
if interactive:
click.echo(f"unknown instruction '{command}' - ignoring")
else:
raise NameError(f"unknown instruction '{command}' - ignoring")
return
obj.save()

Expand Down
13 changes: 11 additions & 2 deletions tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

## TODO: work in progress

## TODO: tests with multiple source calendars. Some of the interactive edit-through-editor functions will probably break.

from xandikos.web import XandikosBackend, XandikosApp
import plann.lib
from plann.lib import find_calendars, _adjust_relations, _adjust_ical_relations
from plann.cli import _add_todo, _select, _list, _check_for_panic, _interactive_relation_edit, _interactive_edit
from plann.cli import _add_todo, _select, _list, _check_for_panic, _interactive_relation_edit, _interactive_edit, _mass_interactive_edit
from plann.panic_planning import timeline_suggestion
from caldav import Todo
import aiohttp
Expand Down Expand Up @@ -359,7 +361,14 @@ def prompt(*largs, **kwargs):
todo1.load()
assert([str(x) for x in todo1.icalendar_component['CATEGORIES'].cats] == ['foo'])
## TODO: part, split, family
## TODO: cancel,
## TODO: cancel,

## testing mass interactive edit
with patch('plann.cli._editor', new=passthrough) as _editor:
_mass_interactive_edit([todo1, todo2, todo3], default='complete')
for todo in (todo1, todo2, todo3, todo4, todo5):
todo.load()
assert todo.icalendar_component['STATUS'] == 'COMPLETED'

finally:
stop_xandikos_server(conn_details)
Expand Down

0 comments on commit db79992

Please sign in to comment.