diff options
author | Tobias Brox <tobias@redpill-linpro.com> | 2022-10-10 01:43:50 +0200 |
---|---|---|
committer | Tobias Brox <tobias@redpill-linpro.com> | 2022-10-10 01:43:50 +0200 |
commit | b1067d6ad1ecfb13b99d0d40c5def8e6a7b0a9a9 (patch) | |
tree | 7c02ec296872ceea6c1aa69ecc24657413f050c7 | |
parent | b6303af75b155edf6239df088e7b2634787b7a72 (diff) | |
download | calendar-cli-b1067d6ad1ecfb13b99d0d40c5def8e6a7b0a9a9.zip |
support for child/parent-relations and skipping children or parents
-rw-r--r-- | USER_GUIDE.md | 4 | ||||
-rwxr-xr-x | cal.py | 56 | ||||
-rwxr-xr-x | tests/tests_kal.sh | 45 |
3 files changed, 74 insertions, 31 deletions
diff --git a/USER_GUIDE.md b/USER_GUIDE.md index d66c1c9..1a8e4aa 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -109,6 +109,10 @@ One thing that may be particularly useful is to take out the UID fields. With U kal select --todo list --template='{UID} {SUMMARY}' ``` +### Printing a UID + +The subcommand `print-uid` will print out an UID. It's for convenience, the same can be achieved by doing a `select (...) --limit 1 list --template='{UID}'` + ### Editing and deleting objects ``` @@ -32,6 +32,8 @@ import re from icalendar import prop from lib.template import Template +list_type = list + ## should make some subclasses of click.ParamType: ## class DateOrDateTime - perhaps a subclass of click.DateTime, returns date @@ -46,7 +48,7 @@ from lib.template import Template ## TODO: maybe find those attributes through the icalendar library? icalendar.cal.singletons, icalendar.cal.multiple, etc attr_txt_one = ['location', 'description', 'geo', 'organizer', 'summary', 'class'] -attr_txt_many = ['category', 'comment', 'contact', 'resources'] +attr_txt_many = ['category', 'comment', 'contact', 'resources', 'parent', 'child'] def parse_dt(input, return_type=None): """Parse a datetime or a date. @@ -191,6 +193,10 @@ def _set_attr_options_(func, verb): func = click.option(f"--{verb1}{foo}", help=f"{verb} ical attribute {foo}", multiple=True)(func) return func +def _abort(message): + click.echo(message) + raise click.Abort(message) + def _set_attr_options(verb=""): return lambda func: _set_attr_options_(func,verb) @@ -206,10 +212,12 @@ def _set_attr_options(verb=""): @click.option('--end', help='do a time search, with this end timestamp (or duration)') @click.option('--timespan', help='do a time search for this interval') @click.option('--sort-key', help='use this attributes for sorting. Templating can be used. Prepend with - for reverse sort', multiple=True) +@click.option('--skip-parents/--include-parents', help="Skip parents if it's children is selected. Useful for finding tasks that can be started if parent depends on child", default=False) +@click.option('--skip-children/--include-children', help="Skip children if it's parent is selected. Useful for getting an overview of the big picture if children are subtasks", default=False) @click.option('--limit', help='Number of objects to show', type=int) @click.option('--offset', help='SKip the first objects', type=int) @click.pass_context -def select(ctx, all, uid, abort_on_missing_uid, sort_key, limit, offset, **kwargs_): +def select(ctx, all, uid, abort_on_missing_uid, sort_key, skip_parents, skip_children, limit, offset, **kwargs_): """ select/search/filter tasks/events, for listing/editing/deleting, etc """ @@ -249,7 +257,7 @@ def select(ctx, all, uid, abort_on_missing_uid, sort_key, limit, offset, **kwarg if not cnt: missing_uids.append(uid_) if abort_on_missing_uid and missing_uids: - raise click.Abort(f"Did not find the following uids in any calendars: {missing_uids}") + _abort(f"Did not find the following uids in any calendars: {missing_uids}") if uid: return @@ -270,9 +278,27 @@ def select(ctx, all, uid, abort_on_missing_uid, sort_key, limit, offset, **kwarg elif kwargs_[attr]: kwargs[attr] = kwargs[attr][0] + ## TODO: special handling of parent and child! (and test for that!) + for c in ctx.obj['calendars']: objs.extend(c.search(**kwargs)) + if skip_children or skip_parents: + objs_by_uid = {} + for obj in objs: + objs_by_uid[obj.icalendar_instance.subcomponents[0]['uid']] = obj + for obj in objs: + rels = obj.icalendar_instance.subcomponents[0].get('RELATED-TO', []) + rels = rels if isinstance(rels, list_type) else [ rels ] + for rel in rels: + rel_uid = rel + rel_type = rel.params.get('REL-TYPE', None) + if ((rel_type == 'CHILD' and skip_parents) or (rel_type == 'PARENT' and skip_children)) and rel_uid in objs_by_uid and uid in objs_by_uid: + del objs_by_uid[uid] + if ((rel_type == 'PARENT' and skip_parents) or (rel_type == 'CHILD' and skip_children)) and rel_uid in objs_by_uid: + del objs_by_uid[rel_uid] + objs = objs_by_uid.values() + ## OPTIMIZE TODO: sorting the list multiple times rather than once is a bit of brute force, if there are several sort keys and long list of objects, we should sort once and consider all sort keys while sorting ## TODO: Consider that an object may be expanded and contain lots of event instances. We will then need to expand the caldav.Event object into multiple objects, each containing one recurrance instance. This should probably be done on the caldav side of things. for skey in reversed(sort_key): @@ -294,7 +320,7 @@ def select(ctx, all, uid, abort_on_missing_uid, sort_key, limit, offset, **kwarg ctx.obj['objs'] = ctx.obj['objs'][offset:] if limit is not None: ctx.obj['objs'] = ctx.obj['objs'][0:limit] - + @select.command() @click.option('--ics/--no-ics', default=False, help="Output in ics format") @click.option('--template', default="{DUE.dt:?{DTSTART.dt:?(date missing)?}?:%F %H:%M:%S}: {SUMMARY:?{DESCRIPTION:?(no summary given)?}?}") @@ -317,6 +343,11 @@ def list(ctx, ics, template): click.echo(template.format(**sub)) @select.command() +@click.pass_context +def print_uid(ctx): + click.echo(ctx.obj['objs'][0].icalendar_instance.subcomponents[0]['UID']) + +@select.command() @click.option('--multi-delete/--no-multi-delete', default=None, help="Delete multiple things without confirmation prompt") @click.pass_context def delete(ctx, multi_delete, **kwargs): @@ -327,7 +358,7 @@ def delete(ctx, multi_delete, **kwargs): if multi_delete is None and len(objs)>1: multi_delete = click.confirm(f"OK to delete {len(objs)} items?") if len(objs)>1 and not multi_delete: - raise click.Abort(f"Not going to delete {len(objs)} items") + _abort(f"Not going to delete {len(objs)} items") for obj in objs: obj.delete() @@ -341,9 +372,12 @@ def edit(ctx, add_category=None, complete=None, **kwargs): for obj in ctx.obj['objs']: ie = obj.icalendar_instance.subcomponents[0] for arg in ctx.obj['set_args']: - if arg in ie: - ie.pop(arg) - ie.add(arg, ctx.obj['set_args'][arg]) + if arg in ('child', 'parent'): + obj.set_relation(arg, ctx.obj['set_args'][arg]) + else: + if arg in ie: + ie.pop(arg) + ie.add(arg, ctx.obj['set_args'][arg]) if add_category: if 'categories' in ie: cats = ie.pop('categories').cats @@ -384,7 +418,7 @@ def add(ctx, **kwargs): Save new objects on calendar(s) """ if len(ctx.obj['calendars'])>1 and kwargs['multi_add'] is False: - raise click.Abort("Giving up: Multiple calendars given, but --no-multi-add is given") + _abort("Giving up: Multiple calendars given, but --no-multi-add is given") ## TODO: crazy-long if-conditions can be refactored - see delete on how it's done there if (kwargs['first_calendar'] or (len(ctx.obj['calendars'])>1 and @@ -397,7 +431,7 @@ def add(ctx, **kwargs): click.confirm(f"First calendar on the list has url {calendar.url} - should we add there? (tip: use --calendar-url={calendar.url} or --first_calendar to avoid this prompt in the future)"))): ctx.obj['calendars'] = [ calendar ] else: - raise click.Abort("Giving up: Multiple calendars found/given, please specify which calendar you want to use") + _abort("Giving up: Multiple calendars found/given, please specify which calendar you want to use") ctx.obj['ical_fragment'] = "\n".join(kwargs['add_ical_line']) @@ -442,7 +476,7 @@ def todo(ctx, **kwargs): kwargs['summary'] = " ".join(kwargs['summary']) _process_set_args(ctx, kwargs) if not ctx.obj['set_args']['summary']: - raise click.Abort("denying to add a TODO with no summary given") + _abort("denying to add a TODO with no summary given") return for cal in ctx.obj['calendars']: todo = cal.save_todo(ical=ctx.obj['ical_fragment'], **ctx.obj['set_args'], no_overwrite=True) diff --git a/tests/tests_kal.sh b/tests/tests_kal.sh index efd73ec..597fea3 100755 --- a/tests/tests_kal.sh +++ b/tests/tests_kal.sh @@ -163,7 +163,7 @@ uidtodo1=$(echo $output | perl -ne '/uid=(.*)$/ && print $1') kal add todo --set-class=CONFIDENTIAL --set-category scripttest "edit this task2" uidtodo2=$(echo $output | perl -ne '/uid=(.*)$/ && print $1') kal add todo --set-class=PRIVATE "another task for testing sorting, offset and limit" -uidtodo2=$(echo $output | perl -ne '/uid=(.*)$/ && print $1') +uidtodo3=$(echo $output | perl -ne '/uid=(.*)$/ && print $1') echo "## Listing out all tasks with category set to 'scripttest'" kal select --todo --category scripttest list @@ -186,6 +186,9 @@ echo "## Sort order and limit. CONFIDENTIAL class should come first. Only one kal select --todo --sort-key=CLASS --limit 1 list --template '{CLASS}' echo "$output" | grep -q CONFIDENTIAL || error "Sorting does not work as expected" echo "$output" | grep -q PRIVATE && error "Limit does not work as expected" +echo "## print-uid subcommand will print the uid of the first thing found" +kal select --todo --sort-key=CLASS print-uid +[ $output == $uidtodo2 ] || error "print-uid subcommand does not work" echo "## Offset. PRIVATE should come in the middle" kal select --todo --sort-key=class --limit 1 --offset 1 list --template '{CLASS}' @@ -212,37 +215,39 @@ echo "## Test that we can list out completed tasks, and also undo completion" kal select --todo --category scripttest --include-completed edit --uncomplete kal select --todo --category scripttest list [ -z "$output" ] && error "--uncomplete does not work!" +kal select --todo --uid $uidtodo1 --uid $uidtodo2 --uid $uidtodo3 delete --multi-delete -kal select --todo --uid $uidtodo1 delete - -if [ -n "" ]; then -## parent-child relationships +echo "## parent-child relationships" echo "## Going to add three todo-items with children/parent relationships" -calendar_cli todo add --ste-categories scripttest "this is a grandparent" +kal add todo --set-category scripttest "this is a grandparent" +uidtodo1=$(echo $output | perl -ne '/uid=(.*)$/ && print $1') +kal add todo --set-category scripttest --set-parent $uidtodo1 "this is both a parent and a child" uidtodo2=$(echo $output | perl -ne '/uid=(.*)$/ && print $1') -calendar_cli todo --categories=scripttest add --set-categories scripttest --is-child "this is a parent and a child" +kal add todo --set-category scripttest --set-parent $uidtodo1 --set-parent $uidtodo2 "this task is a child of it's grandparent ... (what?)" uidtodo3=$(echo $output | perl -ne '/uid=(.*)$/ && print $1') -calendar_cli todo --categories=scripttest add --set-categories scripttest --is-child "this task has two parents" -uidtodo4=$(echo $output | perl -ne '/uid=(.*)$/ && print $1') -calendar_cli todo --categories scripttest list +kal select --todo --category scripttest list [ $(echo "$output" | wc -l) == 3 ] && echo "## OK: found three tasks" -calendar_cli todo --hide-parents --categories scripttest list +kal select --todo --category scripttest --skip-parents list [ $(echo "$output" | wc -l) == 1 ] && echo "## OK: found only one task now" -echo "## Going to complete the children task" -calendar_cli todo --hide-parents --categories scripttest complete -calendar_cli todo --hide-parents --categories scripttest list +kal select --todo --category scripttest --skip-children list [ $(echo "$output" | wc -l) == 1 ] && echo "## OK: found only one task now" -calendar_cli todo --hide-parents --categories scripttest complete -calendar_cli todo --hide-parents --categories scripttest list +echo "## Going to complete the grandchildren task" +kal select --todo --skip-parents --category scripttest edit --complete +kal select --todo --skip-parents --category scripttest list [ $(echo "$output" | wc -l) == 1 ] && echo "## OK: found only one task now" -calendar_cli todo --hide-parents --categories scripttest complete -calendar_cli todo --hide-parents --categories scripttest list +echo "## Going to complete the child task" +kal select --todo --skip-parents --category scripttest edit --complete +kal select --todo --skip-parents --category scripttest list +[ $(echo "$output" | wc -l) == 1 ] && echo "## OK: found only one task now" +echo "## Going to complete the grandparent task" +kal select --todo --skip-parents --category scripttest edit --complete +kal select --todo --skip-parents --category scripttest list +[ -z "$output" ] && echo "## OK: found no tasks now" +kal select --todo --category scripttest list [ -z "$output" ] && echo "## OK: found no tasks now" ## TODO: test completion of recurring task -fi - echo "## some kal TESTS COMPLETED SUCCESSFULLY! YAY!" rm $outfile |