From c323c76fcb52541a00e35d1e5c31a668eb061405 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Tue, 7 Mar 2023 16:52:44 +0100 Subject: [PATCH 01/28] feat: Support for new reminder array (read) --- vja/model.py | 22 ++++++++++++++++++++++ vja/service_command.py | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/vja/model.py b/vja/model.py index 31d51e9..407f08a 100755 --- a/vja/model.py +++ b/vja/model.py @@ -131,6 +131,26 @@ class Label: return [Label.from_json(x) for x in json_array or []] +@dataclass +@custom_output +@data_dict +# pylint: disable=too-many-instance-attributes +class TaskReminder: + json: dict = field(repr=False) + reminder: datetime + relative_period: int + relative_to: str + + @classmethod + def from_json(cls, json): + return cls(json, parse_json_date(json['reminder']), json['relative_period'], json['relative_to']) + + @classmethod + def from_json_array(cls, json_array): + return [TaskReminder.from_json(x) for x in json_array or []] + + + @dataclass @custom_output @data_dict @@ -144,6 +164,7 @@ class Task: is_favorite: bool due_date: datetime reminder_dates: typing.List[datetime] + reminders: typing.List[TaskReminder] repeat_mode: int repeat_after: timedelta start_date: datetime @@ -171,6 +192,7 @@ class Task: json['is_favorite'], parse_json_date(json['due_date']), [parse_json_date(reminder) for reminder in json['reminder_dates'] or []], + TaskReminder.from_json_array(json["reminders"]), json['repeat_mode'], timedelta(seconds=json['repeat_after']), parse_json_date(json['start_date']), diff --git a/vja/service_command.py b/vja/service_command.py index 45fbada..192d7d0 100644 --- a/vja/service_command.py +++ b/vja/service_command.py @@ -111,8 +111,9 @@ class CommandService: }) payload = self._args_to_payload(args) - logger.debug('post task: %s', payload) + logger.debug('update fields: %s', payload) task_remote.update(payload) + logger.debug('post task: %s', task_remote) task_json = self._api_client.post_task(task_id, task_remote) task_new = self._task_service.task_from_json(task_json) -- GitLab From eb7e819b08b897eeb55e0a18320a03a11442ce91 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Wed, 8 Mar 2023 07:54:56 +0100 Subject: [PATCH 02/28] feat!: Preliminary support for new reminder array Reminders will only work with new Vikunja server instances --- tests/test_command.py | 57 +++++++++++------------------------------- vja/service_command.py | 54 ++++++++++++++++++++++----------------- 2 files changed, 45 insertions(+), 66 deletions(-) diff --git a/tests/test_command.py b/tests/test_command.py index 50b3a5a..2d0e361 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -35,13 +35,13 @@ class TestAddTask: def test_default_reminder_uses_due(self, runner): res = invoke(runner, 'add title of new task --force --list=test-list --due=today --reminder') after = json_for_created_task(runner, res.output) - assert after['due_date'] == after['reminder_dates'][0] + assert after['reminders'][0]['relative_period'] == 0 + assert after['reminders'][0]['relative_to'] == 'due_date' - def test_default_reminder_with_missing_due_uses_tomorrow(self, runner): - res = invoke(runner, 'add title of new task --force --list=test-list --reminder') + def test_default_reminder_with_absolute_time(self, runner): + res = invoke(runner, 'add title of new task --force --list=test-list --reminder=tomorrow') after = json_for_created_task(runner, res.output) - assert after['due_date'] is None - assert TOMORROW_ISO[0:10] in after['reminder_dates'][0] + assert TOMORROW_ISO[0:10] in after['reminders'][0]['reminder'] class TestCloneTask: @@ -129,43 +129,14 @@ class TestEditGeneral: class TestEditReminder: - def test_set_default_reminder_to_remote_due(self, runner): - invoke(runner, f'edit 2 --due-date={TODAY_ISO}') - invoke(runner, f'edit 2 --reminder={TOMORROW_ISO}') - before = json_for_task_id(runner, 2) - - invoke(runner, 'edit 2 --reminder') - after = json_for_task_id(runner, 2) - assert before['reminder_dates'][0][:10] == TOMORROW.date().isoformat() - assert after['reminder_dates'][0][:10] == TODAY.date().isoformat() - - def test_set_default_reminder_to_tomorrow(self, runner): - invoke(runner, 'edit 2 --due-date=') - invoke(runner, f'edit 2 --reminder={TODAY_ISO}') - before = json_for_task_id(runner, 2) - - invoke(runner, 'edit 2 --reminder') - after = json_for_task_id(runner, 2) - assert before['reminder_dates'][0][:10] == TODAY.date().isoformat() - assert after['reminder_dates'][0][:10] == TOMORROW.date().isoformat() - - def test_unset_reminder(self, runner): - invoke(runner, f'edit 2 --reminder={TODAY_ISO}') - before = json_for_task_id(runner, 2) - - invoke(runner, 'edit 2 --reminder=') - after = json_for_task_id(runner, 2) - assert before['reminder_dates'][0][:10] == TODAY.date().isoformat() - assert not after['reminder_dates'] - def test_set_reminder_to_due(self, runner): invoke(runner, f'edit 2 --reminder={TODAY_ISO}') before = json_for_task_id(runner, 2) invoke(runner, f'edit 2 --due={TOMORROW_ISO} --reminder=due') after = json_for_task_id(runner, 2) - assert before['reminder_dates'][0][:10] == TODAY.date().isoformat() - assert after['reminder_dates'][0][:10] == TOMORROW.date().isoformat() + assert before['reminders'][0]['reminder'][:10] == TODAY.date().isoformat() + assert after['reminders'][0]['reminder'][:10] == TOMORROW.date().isoformat() def test_set_reminder_to_value(self, runner): invoke(runner, f'edit 2 --reminder={TODAY_ISO}') @@ -173,17 +144,17 @@ class TestEditReminder: invoke(runner, f'edit 2 --due={TODAY_ISO} --reminder={TOMORROW_ISO}') after = json_for_task_id(runner, 2) - assert before['reminder_dates'][0][:10] == TODAY.date().isoformat() - assert after['reminder_dates'][0][:10] == TOMORROW.date().isoformat() + assert before['reminders'][0]['reminder'][:10] == TODAY.date().isoformat() + assert after['reminders'][0]['reminder'][:10] == TOMORROW.date().isoformat() - def test_set_reminder_to_value_new_reminder(self, runner): - invoke(runner, 'edit 2 --reminder=') + def test_unset_reminder(self, runner): + invoke(runner, f'edit 2 --reminder={TODAY_ISO}') before = json_for_task_id(runner, 2) - invoke(runner, f'edit 2 --reminder={TOMORROW_ISO}') + invoke(runner, 'edit 2 --reminder=') after = json_for_task_id(runner, 2) - assert not before['reminder_dates'] - assert after['reminder_dates'][0][:10] == TOMORROW.date().isoformat() + assert before['reminders'][0]['reminder'][:10] == TODAY.date().isoformat() + assert not after['reminders'] class TestToggleDoneTask: diff --git a/vja/service_command.py b/vja/service_command.py index 192d7d0..11635e5 100644 --- a/vja/service_command.py +++ b/vja/service_command.py @@ -47,7 +47,7 @@ class CommandService: 'list_id': {'field': 'list_id', 'mapping': int}, 'bucket_id': {'field': 'bucket_id', 'mapping': int}, 'kanban_position': {'field': 'kanban_position', 'mapping': int}, - 'reminder': {'field': 'reminder_dates', 'mapping': (lambda x: x)} + 'reminder': {'field': 'reminders', 'mapping': (lambda x: x)} } def _args_to_payload(self, args: dict): @@ -69,10 +69,11 @@ class CommandService: list_id = self._list_service.get_default_list().id tag_name = args.pop('tag') if args.get('tag') else None is_force = args.pop('force_create') if args.get('force_create') is not None else False - if args.get('reminder') == 'due': - args.update({'reminder': args.get('due') or 'tomorrow'}) if args.get('reminder'): - args.update({'reminder': [parse_date_arg_to_iso(args.get('reminder'))]}) + if args.get('reminder') == 'due': + args.update({'reminder': [{'relative_to': 'due_date', 'relative_period': 0}]}) + else: + args.update({'reminder': [{'reminder': parse_date_arg_to_iso(args.get('reminder'))}]}) payload = self._args_to_payload(args) @@ -113,6 +114,8 @@ class CommandService: payload = self._args_to_payload(args) logger.debug('update fields: %s', payload) task_remote.update(payload) + # make sure we do not send back the old reminder_dates + task_remote.pop("reminder_dates", None) logger.debug('post task: %s', task_remote) task_json = self._api_client.post_task(task_id, task_remote) task_new = self._task_service.task_from_json(task_json) @@ -127,27 +130,32 @@ class CommandService: @staticmethod def _update_reminder(args, task_remote): + if args.get('reminder') is None: + return + + # create reminder entry if args.get('reminder') == 'due': - if args.get('due'): - args.update({'reminder': args.get('due')}) # reminder = cli argument --due - else: - if parse_json_date(task_remote['due_date']): - args.update({'reminder': task_remote['due_date']}) # reminder = due_date - else: - args.update({'reminder': 'tomorrow'}) # reminder default - if args.get('reminder') is not None: - # replace the first existing reminder - new_reminder = parse_date_arg_to_iso(args.pop('reminder')) - old_reminders = task_remote['reminder_dates'] - if old_reminders and len(old_reminders) > 0: - if new_reminder: - old_reminders[0] = new_reminder # overwrite first remote reminder_date - else: - old_reminders.pop(0) # remove first remote reminder_date + args.update( + {'reminder': [{'relative_to': 'due_date', 'relative_period': 0}]}) # --reminder=due or --reminder + elif args.get('reminder') == '': + args.update( + {'reminder': None}) # --reminder="" + else: + args.update( + {'reminder': [{'reminder': parse_date_arg_to_iso(args.get('reminder'))}]}) + + # replace the first existing reminder with our entry + new_reminder = args.pop('reminder')[0] if args.get('reminder') else None + old_reminders = task_remote['reminders'] + if old_reminders and len(old_reminders) > 0: + if new_reminder: + old_reminders[0] = new_reminder # overwrite first remote reminder else: - if new_reminder: - old_reminders = [new_reminder] # create single reminder_date - args.update({'reminder': old_reminders}) + old_reminders.pop(0) # remove first remote reminder + else: + if new_reminder: + old_reminders = [new_reminder] # create single reminder_date + args.update({'reminder': old_reminders}) def toggle_task_done(self, task_id): task_remote = self._api_client.get_task(task_id) -- GitLab From 4c59738a19bb4dabd1b871d54ba40f8833b2fe10 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Tue, 14 Mar 2023 14:52:10 +0100 Subject: [PATCH 03/28] fix: sharpen test --- tests/test_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_command.py b/tests/test_command.py index 2d0e361..981b4e3 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -137,6 +137,8 @@ class TestEditReminder: after = json_for_task_id(runner, 2) assert before['reminders'][0]['reminder'][:10] == TODAY.date().isoformat() assert after['reminders'][0]['reminder'][:10] == TOMORROW.date().isoformat() + assert after['reminders'][0]['relative_period'] == 0 + assert after['reminders'][0]['relative_to'] == 'due_date' def test_set_reminder_to_value(self, runner): invoke(runner, f'edit 2 --reminder={TODAY_ISO}') @@ -146,6 +148,7 @@ class TestEditReminder: after = json_for_task_id(runner, 2) assert before['reminders'][0]['reminder'][:10] == TODAY.date().isoformat() assert after['reminders'][0]['reminder'][:10] == TOMORROW.date().isoformat() + assert not after['reminders'][0]['relative_to'] def test_unset_reminder(self, runner): invoke(runner, f'edit 2 --reminder={TODAY_ISO}') -- GitLab From 157aeb483fa2b5d3b1d733f7682c97c8e449aebc Mon Sep 17 00:00:00 2001 From: cernst72 Date: Tue, 28 Mar 2023 12:29:10 +0200 Subject: [PATCH 04/28] feat(api)!: New projects api (only backend side) --- tests/docker-compose.yml | 14 +++++++------- vja/apiclient.py | 16 ++++++++-------- vja/model.py | 2 +- vja/service_command.py | 4 ++-- vja/task_service.py | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index d8bf998..74ab6b5 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: api: - image: vikunja/api:0.20.4 + image: vikunja/api:unstable ports: - "3456:3456" environment: @@ -13,10 +13,10 @@ services: /app/vikunja/vikunja user create -u test -p test -e test@test.test exec /app/vikunja/vikunja " - frontend: - image: vikunja/frontend:0.20.5 - ports: - - "8080:80" - environment: - VIKUNJA_API_URL: http://localhost:3456/api/v1 +# frontend: +# image: vikunja/frontend:0.20.5 +# ports: +# - "8080:80" +# environment: +# VIKUNJA_API_URL: http://localhost:3456/api/v1 diff --git a/vja/apiclient.py b/vja/apiclient.py index c40e4e8..132ff82 100755 --- a/vja/apiclient.py +++ b/vja/apiclient.py @@ -41,7 +41,7 @@ class ApiClient: def __init__(self, api_url, token_file): logger.debug('Connecting to api_url %s', api_url) self._api_url = api_url - self._cache = {'lists': None, 'labels': None, 'namespaces': None, 'tasks': None} + self._cache = {'projects': None, 'labels': None, 'namespaces': None, 'tasks': None} self._login = Login(api_url, token_file) def _create_url(self, path): @@ -117,19 +117,19 @@ class ApiClient: return self._cache['namespaces'] def get_lists(self): - if self._cache['lists'] is None: - self._cache['lists'] = self._get_json(self._create_url('/lists')) or [] - return self._cache['lists'] + if self._cache['projects'] is None: + self._cache['projects'] = self._get_json(self._create_url('/projects')) or [] + return self._cache['projects'] def get_list(self, list_id): - return self._get_json(self._create_url(f'/lists/{str(list_id)}')) + return self._get_json(self._create_url(f'/projects/{str(list_id)}')) def put_list(self, namespace_id, title): payload = {'title': title} - return self._put_json(self._create_url(f'/namespaces/{str(namespace_id)}/lists'), payload=payload) + return self._put_json(self._create_url(f'/namespaces/{str(namespace_id)}/projects'), payload=payload) def get_buckets(self, list_id): - return self._get_json(self._create_url(f'/lists/{str(list_id)}/buckets')) + return self._get_json(self._create_url(f'/projects/{str(list_id)}/buckets')) def get_labels(self): if self._cache['labels'] is None: @@ -152,7 +152,7 @@ class ApiClient: return self._get_json(url) def put_task(self, list_id, payload): - return self._put_json(self._create_url(f'/lists/{str(list_id)}'), payload=payload) + return self._put_json(self._create_url(f'/projects/{str(list_id)}'), payload=payload) def post_task(self, task_id, payload): return self._post_json(self._create_url(f'/tasks/{str(task_id)}'), payload=payload) diff --git a/vja/model.py b/vja/model.py index 407f08a..b138e32 100755 --- a/vja/model.py +++ b/vja/model.py @@ -49,7 +49,7 @@ class User: @classmethod def from_json(cls, json): - return cls(json, json['id'], json['username'], json['name'], json['settings']['default_list_id']) + return cls(json, json['id'], json['username'], json['name'], json['settings']['default_project_id']) @dataclass(frozen=True) diff --git a/vja/service_command.py b/vja/service_command.py index 11635e5..e0d9bb2 100644 --- a/vja/service_command.py +++ b/vja/service_command.py @@ -44,7 +44,7 @@ class CommandService: 'favorite': {'field': 'is_favorite', 'mapping': bool}, 'completed': {'field': 'done', 'mapping': bool}, 'position': {'field': 'position', 'mapping': int}, - 'list_id': {'field': 'list_id', 'mapping': int}, + 'list_id': {'field': 'project_id', 'mapping': int}, 'bucket_id': {'field': 'bucket_id', 'mapping': int}, 'kanban_position': {'field': 'kanban_position', 'mapping': int}, 'reminder': {'field': 'reminders', 'mapping': (lambda x: x)} @@ -94,7 +94,7 @@ class CommandService: task_remote.update({'title': title}) logger.debug('put task: %s', task_remote) - task_json = self._api_client.put_task(task_remote['list_id'], task_remote) + task_json = self._api_client.put_task(task_remote['project_id'], task_remote) task = self._task_service.task_from_json(task_json) return task diff --git a/vja/task_service.py b/vja/task_service.py index a006abd..6e127ba 100644 --- a/vja/task_service.py +++ b/vja/task_service.py @@ -13,7 +13,7 @@ class TaskService: self._urgency = urgency def task_from_json(self, task_json: dict) -> Task: - list_object = self._list_service.find_list_by_id(task_json['list_id']) + list_object = self._list_service.find_list_by_id(task_json['project_id']) labels = Label.from_json_array(task_json['labels']) task = Task.from_json(task_json, list_object, labels) task.urgency = self._urgency.compute_for(task) -- GitLab From 76ffb2007cf77d87e41b8cdb8bf16a0f9d4edf26 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Tue, 28 Mar 2023 15:22:21 +0200 Subject: [PATCH 05/28] feat(api)!: remove reminder_dates --- .vjacli/vja.rc | 2 +- Features.md | 51 +++++++++++++++++++++++++++++++++++--- tests/.vjatest/vja.rc | 2 +- tests/.vjatest_dind/vja.rc | 2 +- tests/test_command.py | 2 +- vja/model.py | 2 -- vja/output.py | 2 +- vja/service_command.py | 2 +- 8 files changed, 54 insertions(+), 11 deletions(-) diff --git a/.vjacli/vja.rc b/.vjacli/vja.rc index 579cff5..ce3994a 100755 --- a/.vjacli/vja.rc +++ b/.vjacli/vja.rc @@ -8,7 +8,7 @@ api_url=https://try.vikunja.io/api/v1 # Be careful: Any code which is entered here gets executed at runtime (python eval()). # Do not use --custom-format if you feel uncomfortable with that. -tasklist_long={x.id:5} ({x.priority}) {"*" if x.is_favorite else " "} {x.title:50.50} {x.due_date.strftime("%a %d.%m %H:%M") if x.due_date else "":15.15} {"R" if x.reminder_dates else " "} {x.tasklist.title:20.20} {x.label_titles:20.20} {x.urgency:3} +tasklist_long={x.id:5} ({x.priority}) {"*" if x.is_favorite else " "} {x.title:50.50} {x.due_date.strftime("%a %d.%m %H:%M") if x.due_date else "":15.15} {"R" if x.reminders else " "} {x.tasklist.title:20.20} {x.label_titles:20.20} {x.urgency:3} tasklist_short={x.id:5} {x.title:50.50} ids_only={x.id} diff --git a/Features.md b/Features.md index 18e56d5..2914505 100644 --- a/Features.md +++ b/Features.md @@ -22,18 +22,21 @@ vja add --help ``` for more. + ### Clone + Another option to create a new task is cloning an existing task + ```shell -vja clone 1 Clone a new task +vja clone 1 Clone a new task ``` See + ```shell vja clone --help ``` - ## List tasks List all active tasks @@ -44,7 +47,12 @@ vja ls --json ``` ### Urgency -By default tasks are sorted (amongst others) by their urgency, which is displayed in the last column. Urgency is calculated by regarding due_date, priority and is_favorite of the task, as well as the occurence of keywords in the list title or the label titles. The weights of each factor and the keywords can be specified in the configuration file ~/.vjacli/vja.rc. See Configuration section in [Readme.md](Readme.md). See [.vjacli/vja.rc](.vjacli/vja.rc) for an example. + +By default, tasks are sorted (amongst others) by their urgency, which is displayed in the last column. Urgency is +calculated by regarding due_date, priority and is_favorite of the task, as well as the occurence of keywords in the list +title or the label titles. The weights of each factor and the keywords can be specified in the configuration file ~ +/.vjacli/vja.rc. See Configuration section in [Readme.md](Readme.md). See [.vjacli/vja.rc](.vjacli/vja.rc) for an +example. ### Filter @@ -123,7 +131,9 @@ Toggle tag (=label). Use with --force to create new label: ```shell vja edit 1 -t @work ``` + Mark as done + ```shell vja edit 1 --done="true" vja check 1 # Shortcut to toggle the done flag of task 1 @@ -137,6 +147,41 @@ vja defer --help ``` This command moves the due_date (and later the reminder) ahead in time. +### Reminders + +vja manages only the first reminder of the task. That is the earliest reminder on the server. + +Set reminder to an absolute time + +```shell +vja edit 1 -r "next sunday at 11:00" +vja edit 1 --reminder="in 3 days at 11:00" +``` + +Set reminder equal to due date + +```shell +vja edit 1 -r +vja edit 1 --reminder +``` + +Set reminder relative to due date + +```shell +Not yet implemented! +``` + +Remove the earliest reminder + +```shell +vja edit 1 -r "" +vja edit 1 --reminder="" +``` + +The same goes for `vja add`. + +### Batch editing + Multiple edits are possible by giving more task ids ```shell diff --git a/tests/.vjatest/vja.rc b/tests/.vjatest/vja.rc index 4e35eae..5aa2fc1 100644 --- a/tests/.vjatest/vja.rc +++ b/tests/.vjatest/vja.rc @@ -8,7 +8,7 @@ api_url=http://localhost:3456/api/v1 # Be careful: Any code which is entered here gets executed at runtime (python eval()). # Do not use --custom-format if you feel uncomfortable with that. -tasklist_long={x.id:5} ({x.priority}) {"*" if x.is_favorite else " "} {x.title:50.50} {x.due_date.strftime("%a %d.%m %H:%M") if x.due_date else "":15.15} {"R" if x.reminder_dates else " "} {x.tasklist.title:20.20} {x.label_titles:20.20} {x.urgency:3} +tasklist_long={x.id:5} ({x.priority}) {"*" if x.is_favorite else " "} {x.title:50.50} {x.due_date.strftime("%a %d.%m %H:%M") if x.due_date else "":15.15} {"R" if x.reminders else " "} {x.tasklist.title:20.20} {x.label_titles:20.20} {x.urgency:3} tasklist_short={x.id:5} {x.title:50.50} ids_only={x.id} diff --git a/tests/.vjatest_dind/vja.rc b/tests/.vjatest_dind/vja.rc index 61a36fd..428f7dc 100644 --- a/tests/.vjatest_dind/vja.rc +++ b/tests/.vjatest_dind/vja.rc @@ -8,7 +8,7 @@ api_url=http://docker:3456/api/v1 # Be careful: Any code which is entered here gets executed at runtime (python eval()). # Do not use --custom-format if you feel uncomfortable with that. -tasklist_long={x.id:5} ({x.priority}) {"*" if x.is_favorite else " "} {x.title:50.50} {x.due_date.strftime("%a %d.%m %H:%M") if x.due_date else "":15.15} {"R" if x.reminder_dates else " "} {x.tasklist.title:20.20} {x.label_titles:20.20} {x.urgency:3} +tasklist_long={x.id:5} ({x.priority}) {"*" if x.is_favorite else " "} {x.title:50.50} {x.due_date.strftime("%a %d.%m %H:%M") if x.due_date else "":15.15} {"R" if x.reminders else " "} {x.tasklist.title:20.20} {x.label_titles:20.20} {x.urgency:3} tasklist_short={x.id:5} {x.title:50.50} ids_only={x.id} diff --git a/tests/test_command.py b/tests/test_command.py index 981b4e3..8606446 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -68,7 +68,7 @@ class TestEditGeneral: assert after['updated'] > before['updated'] # other attributes remain in place assert after['due_date'] == before['due_date'] - assert after['reminder_dates'] == before['reminder_dates'] + assert after['reminders'] == before['reminders'] assert after['position'] == before['position'] assert after['tasklist']['id'] == before['tasklist']['id'] assert after['created'] == before['created'] diff --git a/vja/model.py b/vja/model.py index b138e32..308d175 100755 --- a/vja/model.py +++ b/vja/model.py @@ -163,7 +163,6 @@ class Task: priority: int is_favorite: bool due_date: datetime - reminder_dates: typing.List[datetime] reminders: typing.List[TaskReminder] repeat_mode: int repeat_after: timedelta @@ -191,7 +190,6 @@ class Task: json['priority'], json['is_favorite'], parse_json_date(json['due_date']), - [parse_json_date(reminder) for reminder in json['reminder_dates'] or []], TaskReminder.from_json_array(json["reminders"]), json['repeat_mode'], timedelta(seconds=json['repeat_after']), diff --git a/vja/output.py b/vja/output.py index 754cfd8..485abad 100644 --- a/vja/output.py +++ b/vja/output.py @@ -16,7 +16,7 @@ LABEL_LIST_FORMAT_DEFAULT = '{x.id:5} {x.title:20.20}' TASK_LIST_FORMAT_DEFAULT = '{x.id:5} ({x.priority}) {"*" if x.is_favorite else " "} {x.title:50.50} ' \ '{x.due_date.strftime("%a %d.%m %H:%M") if x.due_date else "":15.15} ' \ - '{"R" if x.reminder_dates else " "} {x.tasklist.title:20.20} ' \ + '{"R" if x.reminders else " "} {x.tasklist.title:20.20} ' \ '{x.label_titles:20.20} {x.urgency:3.1f}' logger = logging.getLogger(__name__) diff --git a/vja/service_command.py b/vja/service_command.py index e0d9bb2..0122c61 100644 --- a/vja/service_command.py +++ b/vja/service_command.py @@ -154,7 +154,7 @@ class CommandService: old_reminders.pop(0) # remove first remote reminder else: if new_reminder: - old_reminders = [new_reminder] # create single reminder_date + old_reminders = [new_reminder] # create single reminder args.update({'reminder': old_reminders}) def toggle_task_done(self, task_id): -- GitLab From f05438d9f8c61ee04cb5432b2e1e0d0a56ff2175 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Thu, 30 Mar 2023 23:23:04 +0200 Subject: [PATCH 06/28] feat(reminders): Set relative reminders like "1h before due" --- Features.md | 5 ++-- tests/test_command.py | 60 +++++++++++++++++++++++++++++------------- vja/cli.py | 10 ++++--- vja/model.py | 8 +++++- vja/service_command.py | 45 ++++++++++++++++++------------- 5 files changed, 85 insertions(+), 43 deletions(-) diff --git a/Features.md b/Features.md index 2914505..75570cd 100644 --- a/Features.md +++ b/Features.md @@ -165,10 +165,11 @@ vja edit 1 -r vja edit 1 --reminder ``` -Set reminder relative to due date +Set reminder relative to due date (only due date is supported by vja for relative reminders) ```shell -Not yet implemented! +vja edit --reminder="1h before due_date" +vja edit -r "10m before due" ``` Remove the earliest reminder diff --git a/tests/test_command.py b/tests/test_command.py index 8606446..0558d4a 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -6,10 +6,12 @@ from tests.conftest import invoke from vja.cli import cli ADD_SUCCESS_PATTERN = re.compile(r'.*Created task (\d+) in list .*') +DATE_1 = datetime.datetime(2023, 3, 30, 15, 0, 0, 0) +DATE_2 = DATE_1 + datetime.timedelta(days=1) +DATE_1_ISO = DATE_1.isoformat() +DATE_2_ISO = DATE_2.isoformat() TODAY = datetime.datetime.now() -TOMORROW = (datetime.datetime.now() + datetime.timedelta(days=1)) TODAY_ISO = TODAY.isoformat() -TOMORROW_ISO = TOMORROW.isoformat() class TestAddTask: @@ -39,9 +41,9 @@ class TestAddTask: assert after['reminders'][0]['relative_to'] == 'due_date' def test_default_reminder_with_absolute_time(self, runner): - res = invoke(runner, 'add title of new task --force --list=test-list --reminder=tomorrow') + res = invoke(runner, f'add title of new task --force --list=test-list --reminder={DATE_2_ISO}') after = json_for_created_task(runner, res.output) - assert TOMORROW_ISO[0:10] in after['reminders'][0]['reminder'] + assert after['reminders'][0]['reminder'] == DATE_2_ISO class TestCloneTask: @@ -79,7 +81,7 @@ class TestEditGeneral: invoke(runner, 'edit 1 --due=today') after = json_for_task_id(runner, 1) - assert datetime.date.today().isoformat()[0:10] in after['due_date'] + assert after['due_date'][:10] == TODAY_ISO[:10] assert after['updated'] >= before['updated'] def test_unset_due_date(self, runner): @@ -129,34 +131,56 @@ class TestEditGeneral: class TestEditReminder: + def test_set_reminder_to_absolute_value(self, runner): + invoke(runner, f'edit 2 --reminder={DATE_1_ISO}') + before = json_for_task_id(runner, 2) + + invoke(runner, f'edit 2 --due={DATE_1_ISO} --reminder={DATE_2_ISO}') + + after = json_for_task_id(runner, 2) + assert before['reminders'][0]['reminder'] == DATE_1_ISO + assert after['reminders'][0]['reminder'] == DATE_2_ISO + assert not after['reminders'][0]['relative_to'] + def test_set_reminder_to_due(self, runner): - invoke(runner, f'edit 2 --reminder={TODAY_ISO}') + invoke(runner, f'edit 2 --reminder={DATE_1_ISO}') before = json_for_task_id(runner, 2) - invoke(runner, f'edit 2 --due={TOMORROW_ISO} --reminder=due') + invoke(runner, f'edit 2 --due={DATE_2_ISO} --reminder=due') + after = json_for_task_id(runner, 2) - assert before['reminders'][0]['reminder'][:10] == TODAY.date().isoformat() - assert after['reminders'][0]['reminder'][:10] == TOMORROW.date().isoformat() + assert before['reminders'][0]['reminder'] == DATE_1_ISO + assert after['reminders'][0]['reminder'] == DATE_2_ISO assert after['reminders'][0]['relative_period'] == 0 assert after['reminders'][0]['relative_to'] == 'due_date' - def test_set_reminder_to_value(self, runner): - invoke(runner, f'edit 2 --reminder={TODAY_ISO}') - before = json_for_task_id(runner, 2) + def test_set_reminder_to_due_empty_option(self, runner): + invoke(runner, f'edit 2 --reminder={DATE_1_ISO}') + + invoke(runner, f'edit 2 --due={DATE_2_ISO} --reminder') - invoke(runner, f'edit 2 --due={TODAY_ISO} --reminder={TOMORROW_ISO}') after = json_for_task_id(runner, 2) - assert before['reminders'][0]['reminder'][:10] == TODAY.date().isoformat() - assert after['reminders'][0]['reminder'][:10] == TOMORROW.date().isoformat() - assert not after['reminders'][0]['relative_to'] + assert after['reminders'][0]['reminder'] == DATE_2_ISO + assert after['reminders'][0]['relative_period'] == 0 + assert after['reminders'][0]['relative_to'] == 'due_date' + + def test_set_reminder_to_relative_value(self, runner): + invoke(runner, f'edit 2 --reminder={DATE_1_ISO}') + + invoke(runner, ['edit', '2', '--due={DATE_2_ISO}', '--reminder=1h1m before due_date']) + + after = json_for_task_id(runner, 2) + assert after['reminders'][0]['relative_period'] == -3660 + assert after['reminders'][0]['relative_to'] == 'due_date' def test_unset_reminder(self, runner): - invoke(runner, f'edit 2 --reminder={TODAY_ISO}') + invoke(runner, f'edit 2 --reminder={DATE_1_ISO}') before = json_for_task_id(runner, 2) invoke(runner, 'edit 2 --reminder=') + after = json_for_task_id(runner, 2) - assert before['reminders'][0]['reminder'][:10] == TODAY.date().isoformat() + assert before['reminders'][0]['reminder'] == DATE_1_ISO assert not after['reminders'] diff --git a/vja/cli.py b/vja/cli.py index cd23c2b..6b122bd 100755 --- a/vja/cli.py +++ b/vja/cli.py @@ -225,7 +225,9 @@ def label_add(application, title): @click.option('tag', '-t', '--tag', '--label', help='Set label (label must exist on server)') @click.option('reminder', '-r', '--alarm', '--remind', '--reminder', is_flag=False, flag_value='due', - help='Set reminder (supports parsedatetime expressions). Leave empty to set to due date.') + help='Set reminder (supports parsedatetime and timedelta expressions). ' + 'Absolute: "in 3 days at 18:00" or relative: "1h before due_date" or just -r to set equal to ' + 'due date.') @click.option('force_create', '--force-create', '--force', is_flag=True, help='Force creation of non existing label') @with_application @@ -257,7 +259,7 @@ def task_clone(ctx, application, task_id, title): @click.option('note', '-n', '--note', '--description', help='Set description (note)') @click.option('note_append', '-a', '--note-append', '--append-note', '--description-append', '--append-description', - help='Append description to existing note separated by new line') + help='Append note to existing description separated by new line') @click.option('prio', '-p', '--prio', '--priority', type=click.INT, help='Set priority') @click.option('list_id', '-l', '--folder-id', '--project-id', '--list-id', '--list_id', type=click.INT, @@ -277,7 +279,9 @@ def task_clone(ctx, application, task_id, title): @click.option('tag', '-t', '--tag', '--label', help='Set label (label must exist on server unless called with --force-create)') @click.option('reminder', '-r', '--alarm', '--remind', '--reminder', is_flag=False, flag_value='due', - help='Set reminder (supports parsedatetime expressions). Leave empty to set to due date.') + help='Set reminder (supports parsedatetime and timedelta expressions). ' + 'Absolute: "in 3 days at 18:00" or relative: "1h30m before due_date" or just -r to set equal to ' + 'due date.') @click.option('force_create', '--force-create', '--force', is_flag=True, default=None, help='Force creation of non existing label') @with_application diff --git a/vja/model.py b/vja/model.py index 308d175..5839a8b 100755 --- a/vja/model.py +++ b/vja/model.py @@ -14,7 +14,11 @@ def custom_output(cls): if attribute.name != 'json' and getattr(self, attribute.name)) def _str_value(attribute_value): - return [_str_value(x) for x in attribute_value] if isinstance(attribute_value, list) else str(attribute_value) + if isinstance(attribute_value, datetime): + return attribute_value.isoformat() + if isinstance(attribute_value, list): + return [_str_value(x) for x in attribute_value] + return str(attribute_value) setattr(cls, '__str__', str_function) return cls @@ -25,6 +29,8 @@ def data_dict(cls): return {k: _transform_value(v) for k, v in self.__dict__.items() if k != 'json'} def _transform_value(v): + if isinstance(v, datetime): + return v.isoformat() if _is_data_dict(v): return v.data_dict() if isinstance(v, list): diff --git a/vja/service_command.py b/vja/service_command.py index 0122c61..8345433 100644 --- a/vja/service_command.py +++ b/vja/service_command.py @@ -69,11 +69,8 @@ class CommandService: list_id = self._list_service.get_default_list().id tag_name = args.pop('tag') if args.get('tag') else None is_force = args.pop('force_create') if args.get('force_create') is not None else False - if args.get('reminder'): - if args.get('reminder') == 'due': - args.update({'reminder': [{'relative_to': 'due_date', 'relative_period': 0}]}) - else: - args.update({'reminder': [{'reminder': parse_date_arg_to_iso(args.get('reminder'))}]}) + + self._parse_reminder_arg(args.get('reminder'), args) payload = self._args_to_payload(args) @@ -130,22 +127,11 @@ class CommandService: @staticmethod def _update_reminder(args, task_remote): - if args.get('reminder') is None: - return - - # create reminder entry - if args.get('reminder') == 'due': - args.update( - {'reminder': [{'relative_to': 'due_date', 'relative_period': 0}]}) # --reminder=due or --reminder - elif args.get('reminder') == '': - args.update( - {'reminder': None}) # --reminder="" - else: - args.update( - {'reminder': [{'reminder': parse_date_arg_to_iso(args.get('reminder'))}]}) + reminder_arg = args.get('reminder') + CommandService._parse_reminder_arg(reminder_arg, args) # replace the first existing reminder with our entry - new_reminder = args.pop('reminder')[0] if args.get('reminder') else None + new_reminder = args.pop('reminder')[0] if reminder_arg else None old_reminders = task_remote['reminders'] if old_reminders and len(old_reminders) > 0: if new_reminder: @@ -157,6 +143,27 @@ class CommandService: old_reminders = [new_reminder] # create single reminder args.update({'reminder': old_reminders}) + @staticmethod + def _parse_reminder_arg(reminder_arg, args): + if reminder_arg is None: + return + if reminder_arg == 'due': + args.update( + {'reminder': [{'relative_to': 'due_date', 'relative_period': 0}]}) # --reminder=due or --reminder + elif 'due' in reminder_arg: + reminder_due_args = reminder_arg.split(" ", 2) + duration = int(parse_date_arg_to_timedelta(reminder_due_args[0]).total_seconds()) + sign = -1 if reminder_due_args[1] == 'before' else 1 + args.update( + {'reminder': [{'relative_to': 'due_date', + 'relative_period': sign * duration}]}) # --reminder="1h before due_date" + elif reminder_arg == '': + args.update( + {'reminder': None}) # --reminder="" + else: + args.update( + {'reminder': [{'reminder': parse_date_arg_to_iso(reminder_arg)}]}) + def toggle_task_done(self, task_id): task_remote = self._api_client.get_task(task_id) task_remote.update({'done': not task_remote['done']}) -- GitLab From 649a8063a39f76f91196c0f6f8261f5033f95d52 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 10:32:16 +0200 Subject: [PATCH 07/28] ci: sleep 1s after starting api --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5f129e7..db2e53b 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -59,6 +59,7 @@ integration-test: - which vja - docker-compose -f tests/docker-compose.yml up -d api + - sleep 1s - export VJA_CONFIGDIR=tests/.vjatest_dind - tests/run.sh test - coverage run --omit='*test*' -m pytest -- GitLab From 29018480b6a11c2ec3495ce6e44c4955fe6b13ec Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 10:41:17 +0200 Subject: [PATCH 08/28] chore: Remove redundant test --- tests/test_command.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_command.py b/tests/test_command.py index 0558d4a..4937375 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -28,12 +28,6 @@ class TestAddTask: def test_duplicate_task_title_rejected(self, runner): invoke(runner, 'add title of new task', 1) - def test_positions_not_null(self, runner): - res = invoke(runner, 'add any other new task --force') - after = json_for_created_task(runner, res.output) - assert after['kanban_position'] > 0 - assert after['position'] > 0 - def test_default_reminder_uses_due(self, runner): res = invoke(runner, 'add title of new task --force --list=test-list --due=today --reminder') after = json_for_created_task(runner, res.output) -- GitLab From 3a87061f39b2bf4d2a1194197f5cfef3c8d7d46e Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 10:42:05 +0200 Subject: [PATCH 09/28] doc: Add table of contents to Features.md --- Features.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Features.md b/Features.md index 75570cd..42f4d20 100644 --- a/Features.md +++ b/Features.md @@ -1,4 +1,26 @@ # Features + +* [Features](#features) + * [Create Task](#create-task) + * [Clone](#clone) + * [List tasks](#list-tasks) + * [Urgency](#urgency) + * [Filter](#filter) + * [Sort](#sort) + * [Select](#select) + * [Show single task by id](#show-single-task-by-id) + * [Modify tasks](#modify-tasks) + * [Reminders](#reminders) + * [Batch editing](#batch-editing) + * [Open Vikunja in browser](#open-vikunja-in-browser) + * [Manage lists, namespaces, labels, buckets](#manage-lists-namespaces-labels-buckets) + * [Manage namespaces](#manage-namespaces) + * [Manage lists (projects)](#manage-lists-projects) + * [Manage kanban buckets](#manage-kanban-buckets) + * [Manage labels](#manage-labels) + * [Output format](#output-format) + * [Terminate session](#terminate-session) + ## Create Task @@ -6,7 +28,7 @@ context: ```shell -vja add Make things work --note="find out how" -priority=3 --favorite=True --due="tomorrow at 11:00" --reminder --tag=@work +vja add Getting things done --note="find out how" -priority=3 --favorite=True --due="tomorrow at 11:00" --reminder --tag=@work ``` or more concise -- GitLab From 572f519a10bc97e9badb017f90b5e723eecc8e42 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 10:54:14 +0200 Subject: [PATCH 10/28] ci: update semantic-release configuration --- .releaserc.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.releaserc.yml b/.releaserc.yml index 9f59c41..6d05e0a 100644 --- a/.releaserc.yml +++ b/.releaserc.yml @@ -1,6 +1,6 @@ # spec: https://semantic-release.gitbook.io/semantic-release/usage/configuration tagFormat: "${version}" -branches: [main, next] +branches: [ main, next ] plugins: # Determine the type of release by analyzing commits. # ie: Major, Minor or Patch @@ -14,11 +14,13 @@ plugins: - { type: perf, release: patch } - { type: docs, release: patch } - { type: refactor, release: patch } - - { type: style, release: patch } - { type: build, release: patch } - - { type: ci, release: patch } - { type: test, release: patch } + - { type: tests, release: patch } - { type: update, release: patch } + - { type: chore, release: false } + - { type: ci, release: false } + - { scope: no-release, release: false } - - "@semantic-release/release-notes-generator" - preset: conventionalcommits presetConfig: @@ -26,12 +28,11 @@ plugins: - { type: feat, section: "Features" } - { type: new, section: "Features" } - { type: fix, section: "Bug Fixes" } - - { type: chore, section: "Misc" } - { type: doc, section: "Documentation" } - { type: docs, section: "Documentation" } - - { type: style, section: "Improvements" } - - { type: refactor, section: "Improvements" } - - { type: perf, section: "Improvements" } + - { type: chore, section: "Misc" } + - { type: refactor, section: "Misc" } + - { type: perf, section: "Misc" } - { type: test, section: "Automation" } - { type: tests, section: "Automation" } - { type: ci, section: "Automation" } -- GitLab From c493b07266affc2f6976a5682b1b33170c90e51b Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 15:30:38 +0200 Subject: [PATCH 11/28] feat: defer reminder --- Features.md | 9 +++++++-- tests/test_command.py | 18 ++++++++++++++---- vja/cli.py | 2 +- vja/parse.py | 4 ++-- vja/service_command.py | 27 ++++++++++++++++++++++----- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/Features.md b/Features.md index 42f4d20..6e559c4 100644 --- a/Features.md +++ b/Features.md @@ -1,4 +1,5 @@ # Features + * [Features](#features) * [Create Task](#create-task) @@ -10,6 +11,7 @@ * [Select](#select) * [Show single task by id](#show-single-task-by-id) * [Modify tasks](#modify-tasks) + * [Defer task](#defer-task) * [Reminders](#reminders) * [Batch editing](#batch-editing) * [Open Vikunja in browser](#open-vikunja-in-browser) @@ -162,12 +164,15 @@ vja check 1 # Shortcut to toggle the done flag of task 1 ``` ### Defer task + There is a shortcut for setting a delay on a task by giving a timedelta expression. + ```shell vja defer 1 1d vja defer --help ``` -This command moves the due_date (and later the reminder) ahead in time. + +This command moves the due_date and the first reminder ahead in time. ### Reminders @@ -205,7 +210,7 @@ The same goes for `vja add`. ### Batch editing -Multiple edits are possible by giving more task ids +Multiple edits and defers are possible by giving more task ids ```shell vja edit 1 5 8 --due="next monday 14:00" diff --git a/tests/test_command.py b/tests/test_command.py index 4937375..be39f0c 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -199,14 +199,24 @@ class TestToggleDoneTask: class TestDeferTask: - def test_defer_task(self, runner): - invoke(runner, f'edit 2 --due-date={TODAY_ISO}') + def test_defer_due_date_and_reminder(self, runner): + invoke(runner, f'edit 2 --due-date={DATE_1_ISO} --reminder={DATE_1_ISO}') invoke(runner, 'defer 2 1d') after = json_for_task_id(runner, 2) - assert after['due_date'][:10] == TOMORROW.date().isoformat() - assert after['reminder_dates'][0][:10] == TOMORROW.date().isoformat() + assert after['due_date'] == DATE_2_ISO + assert after['reminders'][0]['reminder'] == DATE_2_ISO + + def test_dont_modify_relative_reminder(self, runner): + invoke(runner, f'edit 2 --due-date={DATE_1_ISO} -r') + + invoke(runner, 'defer 2 1d') + + after = json_for_task_id(runner, 2) + assert after['due_date'] == DATE_2_ISO + assert after['reminders'][0]['relative_period'] == 0 + assert after['reminders'][0]['relative_to'] == 'due_date' class TestMultipleTasks: diff --git a/vja/cli.py b/vja/cli.py index 6b122bd..0f1789a 100755 --- a/vja/cli.py +++ b/vja/cli.py @@ -308,7 +308,7 @@ def task_toggle(ctx, application, task_id): @cli.command('defer', aliases=['delay'], help='Shortcut for moving the due_date and the reminders of the task. ' - 'Valid delay values are 2d, 1h30m.') + 'Examples for valid delay values are 2d, 1h30m.') @click.argument('task_ids', required=True, type=click.INT, nargs=-1) @click.argument('delay_by', required=True) @with_application diff --git a/vja/parse.py b/vja/parse.py index 288156a..5694767 100644 --- a/vja/parse.py +++ b/vja/parse.py @@ -29,10 +29,10 @@ def parse_date_arg_to_datetime(text: str) -> Optional[datetime]: def parse_date_arg_to_iso(text: str) -> str: - return format_datetime_to_json(parse_date_arg_to_datetime(text)) + return datetime_to_isoformat(parse_date_arg_to_datetime(text)) -def format_datetime_to_json(date: datetime) -> str: +def datetime_to_isoformat(date: datetime) -> str: return date.astimezone(tz.tzlocal()).isoformat() if date else None diff --git a/vja/service_command.py b/vja/service_command.py index 8345433..4229517 100644 --- a/vja/service_command.py +++ b/vja/service_command.py @@ -4,7 +4,7 @@ from vja import VjaError from vja.apiclient import ApiClient from vja.list_service import ListService from vja.model import Label -from vja.parse import parse_date_arg_to_iso, parse_json_date, parse_date_arg_to_timedelta, format_datetime_to_json +from vja.parse import parse_date_arg_to_iso, parse_json_date, parse_date_arg_to_timedelta, datetime_to_isoformat from vja.task_service import TaskService logger = logging.getLogger(__name__) @@ -90,6 +90,8 @@ class CommandService: task_remote.update({'id': None}) task_remote.update({'title': title}) + # make sure we do not send back the old reminder_dates + task_remote.pop("reminder_dates", None) logger.debug('put task: %s', task_remote) task_json = self._api_client.put_task(task_remote['project_id'], task_remote) task = self._task_service.task_from_json(task_json) @@ -166,18 +168,33 @@ class CommandService: def toggle_task_done(self, task_id): task_remote = self._api_client.get_task(task_id) + # make sure we do not send back the old reminder_dates + task_remote.pop("reminder_dates", None) task_remote.update({'done': not task_remote['done']}) task_json = self._api_client.post_task(task_id, task_remote) return self._task_service.task_from_json(task_json) def defer_task(self, task_id, delay_by): - task_remote = self._api_client.get_task(task_id) timedelta = parse_date_arg_to_timedelta(delay_by) + args = {} + + task_remote = self._api_client.get_task(task_id) due_date = parse_json_date(task_remote['due_date']) if due_date: - due_date = format_datetime_to_json(due_date + timedelta) - task_remote.update({'due_date': due_date}) - # TODO update absolute reminders + args.update({'due': datetime_to_isoformat(due_date + timedelta)}) + old_reminders = task_remote['reminders'] + if old_reminders and len(old_reminders) > 0: + reminder_date = parse_json_date(old_reminders[0]['reminder']) + is_absolute_reminder = not old_reminders[0]['relative_to'] + if reminder_date and is_absolute_reminder: + args.update({'reminder': datetime_to_isoformat(reminder_date + timedelta)}) + self._update_reminder(args, task_remote) + + payload = self._args_to_payload(args) + logger.debug('update fields: %s', payload) + task_remote.update(payload) + # make sure we do not send back the old reminder_dates + task_remote.pop("reminder_dates", None) task_json = self._api_client.post_task(task_id, task_remote) return self._task_service.task_from_json(task_json) -- GitLab From 8689f5e0a36a536187a9df97dd38a2a0ece9ed01 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 22:29:52 +0200 Subject: [PATCH 12/28] ci: pipeline on branches only manual --- .gitlab-ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index db2e53b..08a79ac 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,6 +25,8 @@ build: paths: - dist/*.whl rules: + - if: CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ + when: manual - when: always pylint: @@ -35,6 +37,8 @@ pylint: - flake8 --max-line-length=120 vja/*.py || true - pylint -d C0103,C0114,C0115,C0116,C0301 --fail-under=9 vja/*.py rules: + - if: CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ + when: manual - when: on_success integration-test: @@ -74,6 +78,8 @@ integration-test: coverage_format: cobertura path: coverage.xml rules: + - if: CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ + when: manual - when: on_success release: -- GitLab From 43fa7df6ec8fe69eb8d9d27589b65b9541b0b71a Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 22:33:47 +0200 Subject: [PATCH 13/28] ci: pipeline on branches only manual --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 08a79ac..026d802 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ build: paths: - dist/*.whl rules: - - if: CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ + - if: $CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ when: manual - when: always @@ -37,7 +37,7 @@ pylint: - flake8 --max-line-length=120 vja/*.py || true - pylint -d C0103,C0114,C0115,C0116,C0301 --fail-under=9 vja/*.py rules: - - if: CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ + - if: $CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ when: manual - when: on_success @@ -78,7 +78,7 @@ integration-test: coverage_format: cobertura path: coverage.xml rules: - - if: CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ + - if: $CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ when: manual - when: on_success -- GitLab From 7dd6dcbf9f8c373a9d8b33b8c17d29c559cc1075 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 22:38:17 +0200 Subject: [PATCH 14/28] ci: remove build stage --- .gitlab-ci.yml | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 026d802..efc3829 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,30 +9,14 @@ cache: - venv/ stages: - - build - test - release -build: - stage: build - script: - - ci/virtual-env.sh - - source venv/bin/activate - - ci/build-install.sh - - pip -V - - pip show -f vja - artifacts: - paths: - - dist/*.whl - rules: - - if: $CI_COMMIT_BRANCH && $CI_COMMIT_REF_SLUG !~ /renovate.*/ - when: manual - - when: always - pylint: stage: test allow_failure: true script: + - ci/virtual-env.sh - source venv/bin/activate - flake8 --max-line-length=120 vja/*.py || true - pylint -d C0103,C0114,C0115,C0116,C0301 --fail-under=9 vja/*.py @@ -58,7 +42,7 @@ integration-test: - pip3 install coverage - python3 --version - - pip install dist/*.whl + - ci/build-install.sh - pip show vja - which vja -- GitLab From 5b32e53277c3ee95464eb1d60f2c270dccb5bf4b Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 22:49:24 +0200 Subject: [PATCH 15/28] ci: remove build stage --- .gitlab-ci.yml | 7 ++++--- ci/build-install.sh | 7 ------- ci/virtual-env.sh | 7 ------- 3 files changed, 4 insertions(+), 17 deletions(-) delete mode 100755 ci/build-install.sh delete mode 100755 ci/virtual-env.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index efc3829..c794be4 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,8 +16,6 @@ pylint: stage: test allow_failure: true script: - - ci/virtual-env.sh - - source venv/bin/activate - flake8 --max-line-length=120 vja/*.py || true - pylint -d C0103,C0114,C0115,C0116,C0301 --fail-under=9 vja/*.py rules: @@ -42,7 +40,10 @@ integration-test: - pip3 install coverage - python3 --version - - ci/build-install.sh + - pip install -r requirements_dev.txt + - python3 setup.py sdist bdist_wheel + - pip install dist/*.whl; + - pip show vja - which vja diff --git a/ci/build-install.sh b/ci/build-install.sh deleted file mode 100755 index ddb6dbe..0000000 --- a/ci/build-install.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -euo pipefail - -pip install -r requirements_dev.txt -pip uninstall -y vja -python setup.py sdist bdist_wheel -pip install dist/*.whl diff --git a/ci/virtual-env.sh b/ci/virtual-env.sh deleted file mode 100755 index 27d4934..0000000 --- a/ci/virtual-env.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -euo pipefail - -python --version -pip install virtualenv -virtualenv venv - -- GitLab From 46387430fe4c92a02b818e200672c86ed4190e1a Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 22:53:07 +0200 Subject: [PATCH 16/28] ci: remove build stage --- .gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c794be4..ed1ff10 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,6 @@ variables: cache: paths: - .cache/pip - - venv/ stages: - test @@ -16,6 +15,7 @@ pylint: stage: test allow_failure: true script: + - pip install -r requirements_dev.txt - flake8 --max-line-length=120 vja/*.py || true - pylint -d C0103,C0114,C0115,C0116,C0301 --fail-under=9 vja/*.py rules: @@ -56,7 +56,6 @@ integration-test: - coverage xml --omit='*test*' - docker-compose -f tests/docker-compose.yml down coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' - cache: { } artifacts: reports: coverage_report: -- GitLab From 8bca866c60f3ed2cf62eeb87cd10dad06f9563e6 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 22:58:31 +0200 Subject: [PATCH 17/28] ci: remove build stage --- .gitlab-ci.yml | 4 ++-- vja/model.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ed1ff10..9b8e2a7 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,7 @@ pylint: allow_failure: true script: - pip install -r requirements_dev.txt + - python setup.py sdist bdist_wheel - flake8 --max-line-length=120 vja/*.py || true - pylint -d C0103,C0114,C0115,C0116,C0301 --fail-under=9 vja/*.py rules: @@ -41,7 +42,7 @@ integration-test: - python3 --version - pip install -r requirements_dev.txt - - python3 setup.py sdist bdist_wheel + - pip uninstall -y vja; python3 setup.py sdist bdist_wheel - pip install dist/*.whl; - pip show vja @@ -79,7 +80,6 @@ release: script: - python --version - semantic-release - cache: { } rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: on_success diff --git a/vja/model.py b/vja/model.py index 5839a8b..0a8dcc3 100755 --- a/vja/model.py +++ b/vja/model.py @@ -156,7 +156,6 @@ class TaskReminder: return [TaskReminder.from_json(x) for x in json_array or []] - @dataclass @custom_output @data_dict -- GitLab From 3294b83e1b75b2b6826333104d91e71ee1b8d478 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 23:01:24 +0200 Subject: [PATCH 18/28] ci: remove build stage --- .gitlab-ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9b8e2a7..9c8e857 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,7 +16,7 @@ pylint: allow_failure: true script: - pip install -r requirements_dev.txt - - python setup.py sdist bdist_wheel + - pip uninstall -y vja && python3 setup.py sdist bdist_wheel && pip install dist/*.whl; - flake8 --max-line-length=120 vja/*.py || true - pylint -d C0103,C0114,C0115,C0116,C0301 --fail-under=9 vja/*.py rules: @@ -42,14 +42,13 @@ integration-test: - python3 --version - pip install -r requirements_dev.txt - - pip uninstall -y vja; python3 setup.py sdist bdist_wheel - - pip install dist/*.whl; - + - pip uninstall -y vja && python3 setup.py sdist bdist_wheel && pip install dist/*.whl; - pip show vja - which vja - docker-compose -f tests/docker-compose.yml up -d api - sleep 1s + - export VJA_CONFIGDIR=tests/.vjatest_dind - tests/run.sh test - coverage run --omit='*test*' -m pytest -- GitLab From eab4b0f4ef879c64859c9796f2562b09eaa53320 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 23:15:41 +0200 Subject: [PATCH 19/28] ci: install coverage from requirements_dev.txt --- .gitlab-ci.yml | 3 --- requirements_dev.txt | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9c8e857..87c73e5 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,9 +36,6 @@ integration-test: script: - docker --version - apk add --no-cache bash curl python3-dev py3-pip docker-compose - - pip3 install wheel - - pip3 install pytest - - pip3 install coverage - python3 --version - pip install -r requirements_dev.txt diff --git a/requirements_dev.txt b/requirements_dev.txt index af94e17..f1fef98 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,3 +4,4 @@ twine==4.0.2 pylint==2.17.1 flake8==6.0.0 pytest==7.2.2 +coverage==7.2.2 -- GitLab From efb701685d11bad90eb844ac8636117225ab2f7c Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 23:22:37 +0200 Subject: [PATCH 20/28] ci: remove redundant apk packages --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 87c73e5..8f80b03 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,7 +16,7 @@ pylint: allow_failure: true script: - pip install -r requirements_dev.txt - - pip uninstall -y vja && python3 setup.py sdist bdist_wheel && pip install dist/*.whl; + - pip uninstall -y vja && python3 setup.py sdist bdist_wheel && pip install dist/*.whl - flake8 --max-line-length=120 vja/*.py || true - pylint -d C0103,C0114,C0115,C0116,C0301 --fail-under=9 vja/*.py rules: @@ -35,11 +35,11 @@ integration-test: DOCKER_TLS_CERTDIR: "" script: - docker --version - - apk add --no-cache bash curl python3-dev py3-pip docker-compose + - apk add --no-cache python3 py3-pip docker-compose - python3 --version - pip install -r requirements_dev.txt - - pip uninstall -y vja && python3 setup.py sdist bdist_wheel && pip install dist/*.whl; + - pip uninstall -y vja && python3 setup.py sdist bdist_wheel && pip install dist/*.whl - pip show vja - which vja -- GitLab From 22a3f48391bc8434fcb208d31e4db50f701b8c68 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 23:31:53 +0200 Subject: [PATCH 21/28] ci: remove redundant apk packages --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8f80b03..9d079a4 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,7 +27,7 @@ pylint: integration-test: stage: test image: - name: docker:stable + name: docker:20.10.23 services: - name: docker:dind variables: @@ -35,7 +35,7 @@ integration-test: DOCKER_TLS_CERTDIR: "" script: - docker --version - - apk add --no-cache python3 py3-pip docker-compose + - apk add --no-cache bash python3 py3-pip docker-compose - python3 --version - pip install -r requirements_dev.txt -- GitLab From 9584cac6e18e81480231b34fabd46990249a2760 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 23:52:48 +0200 Subject: [PATCH 22/28] ci: cleanup gitlab-ci --- .gitlab-ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9d079a4..30d60ee 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,5 @@ -image: python:3.11-slim - variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - cache: paths: - .cache/pip @@ -13,6 +10,7 @@ stages: pylint: stage: test + image: python:3.11-slim allow_failure: true script: - pip install -r requirements_dev.txt @@ -27,7 +25,7 @@ pylint: integration-test: stage: test image: - name: docker:20.10.23 + name: docker:23.0-cli services: - name: docker:dind variables: -- GitLab From 714545181a76b15c4341df3a4ff6830416ddf897 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 23:56:53 +0200 Subject: [PATCH 23/28] ci: cleanup gitlab-ci --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 30d60ee..6d7aff2 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -33,7 +33,7 @@ integration-test: DOCKER_TLS_CERTDIR: "" script: - docker --version - - apk add --no-cache bash python3 py3-pip docker-compose + - apk add --no-cache bash python3 py3-pip - python3 --version - pip install -r requirements_dev.txt @@ -41,7 +41,7 @@ integration-test: - pip show vja - which vja - - docker-compose -f tests/docker-compose.yml up -d api + - docker compose -f tests/docker-compose.yml up -d api - sleep 1s - export VJA_CONFIGDIR=tests/.vjatest_dind -- GitLab From 543a47928a9539982a91507ece4e9d3605bb2dbb Mon Sep 17 00:00:00 2001 From: cernst72 Date: Fri, 31 Mar 2023 23:57:11 +0200 Subject: [PATCH 24/28] ci: cleanup gitlab-ci --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6d7aff2..842073a 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,7 +49,7 @@ integration-test: - coverage run --omit='*test*' -m pytest - coverage report --omit='*test*' - coverage xml --omit='*test*' - - docker-compose -f tests/docker-compose.yml down + - docker compose -f tests/docker-compose.yml down coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: reports: -- GitLab From 450d13fe43987c3cf33c72bfa2303e452ee7be8b Mon Sep 17 00:00:00 2001 From: cernst72 Date: Sat, 1 Apr 2023 00:02:29 +0200 Subject: [PATCH 25/28] ci: cleanup gitlab-ci --- tests/docker-compose.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 74ab6b5..73621d9 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -13,10 +13,10 @@ services: /app/vikunja/vikunja user create -u test -p test -e test@test.test exec /app/vikunja/vikunja " -# frontend: -# image: vikunja/frontend:0.20.5 -# ports: -# - "8080:80" -# environment: -# VIKUNJA_API_URL: http://localhost:3456/api/v1 + frontend: + image: vikunja/frontend:0.20.5 + ports: + - "8080:80" + environment: + VIKUNJA_API_URL: http://localhost:3456/api/v1 -- GitLab From 9aafa2c426757b2068a1199410db58899356eacd Mon Sep 17 00:00:00 2001 From: cernst72 Date: Sat, 1 Apr 2023 10:13:36 +0200 Subject: [PATCH 26/28] test: capture click output --- tests/conftest.py | 10 +++++++++- tests/test_command.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 12f5b7b..e30cddf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import logging import os import subprocess import sys @@ -7,16 +8,23 @@ from click.testing import CliRunner from vja.cli import cli +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + @pytest.fixture(name='runner', scope='session') def setup_runner(): return CliRunner() -def invoke(runner, command, return_code=0, user_input=None, catch_exceptions=True): +def invoke(runner, command, return_code=0, user_input=None, catch_exceptions=False): if isinstance(command, str): command = command.split() res = runner.invoke(cli, command, input=user_input, catch_exceptions=catch_exceptions) + sys.stdout.write(res.output) + if res.stderr_bytes: + sys.stdout.write(res.stderr_bytes) + if res.exception: + logging.warning(res.exception) assert res.exit_code == return_code, res return res diff --git a/tests/test_command.py b/tests/test_command.py index be39f0c..0065b11 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -26,7 +26,7 @@ class TestAddTask: assert after['tasklist']['title'] == 'test-list' def test_duplicate_task_title_rejected(self, runner): - invoke(runner, 'add title of new task', 1) + invoke(runner, 'add title of new task', 1, catch_exceptions=True) def test_default_reminder_uses_due(self, runner): res = invoke(runner, 'add title of new task --force --list=test-list --due=today --reminder') -- GitLab From 5e5040a3254b8cc5dc75f0391352e0e1358fdeaa Mon Sep 17 00:00:00 2001 From: cernst72 Date: Sat, 1 Apr 2023 10:43:58 +0200 Subject: [PATCH 27/28] test: cleanup test setup --- tests/conftest.py | 47 +++++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e30cddf..1794a1f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,36 +30,23 @@ def invoke(runner, command, return_code=0, user_input=None, catch_exceptions=Fal def _login_as_test_user(): - result = subprocess.run('vja logout'.split(), check=False) - if result.returncode: - print(result.stdout) - print(result.stderr) - return result.returncode - - result = subprocess.run('vja --username=test --password=test user show'.split(), check=False) - if result.returncode: - print(result.stdout) - print(result.stderr) - return result.returncode + run_vja('vja logout') + run_vja('vja --username=test --password=test user show') def _create_list_and_task(): - result = subprocess.run('vja list add test-list'.split(), check=False) - if result.returncode: - print(result.stdout) - print(result.stderr) - return result.returncode - result = subprocess.run('vja add At least one task --force-create ' - '--priority=5 --due-date=today --tag=my_tag --favorite=True'.split(), check=False) - if result.returncode: - print(result.stdout) - print(result.stderr) - return result.returncode - result = subprocess.run('vja add A task without a label --force-create'.split(), check=False) + run_vja('vja list add test-list') + run_vja('vja add At least one task --force-create --priority=5 --due-date=today --tag=my_tag --favorite=True') + run_vja('vja add A task without a label --force-create') + + +def run_vja(command): + result = subprocess.run(command.split(), capture_output=True, check=False) if result.returncode: - print(result.stdout) - print(result.stderr) - return result.returncode + print('!!! Non-zero result from command: ' + command) + sys.stdout.write(result.stdout.decode('utf-8')) + sys.stdout.write(result.stderr.decode('utf-8')) + sys.exit(1) def pytest_configure(): @@ -67,10 +54,6 @@ def pytest_configure(): print('!!! Precondition not met. You must set VJA_CONFIGDIR in environment variables !!!') sys.exit(1) - if _login_as_test_user() > 0: - print('!!! Precondition not met. Cannot connect to Vikunja with user test/test') - sys.exit(1) + _login_as_test_user() - if _create_list_and_task() > 0: - print('!!! Unable to create default list') - sys.exit(1) + _create_list_and_task() -- GitLab From a3b81b0bae08841fd92e204d5557cc1ad46a59b3 Mon Sep 17 00:00:00 2001 From: cernst72 Date: Sat, 1 Apr 2023 14:47:31 +0200 Subject: [PATCH 28/28] fix: do not unset reminder if missing in vja edit --- tests/conftest.py | 2 +- tests/docker-compose.yml | 2 +- vja/service_command.py | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1794a1f..e424399 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -43,7 +43,7 @@ def _create_list_and_task(): def run_vja(command): result = subprocess.run(command.split(), capture_output=True, check=False) if result.returncode: - print('!!! Non-zero result from command: ' + command) + print(f'!!! Non-zero result ({result.returncode}) from command {command}') sys.stdout.write(result.stdout.decode('utf-8')) sys.stdout.write(result.stderr.decode('utf-8')) sys.exit(1) diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 73621d9..544e009 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -14,7 +14,7 @@ services: exec /app/vikunja/vikunja " frontend: - image: vikunja/frontend:0.20.5 + image: vikunja/frontend:unstable ports: - "8080:80" environment: diff --git a/vja/service_command.py b/vja/service_command.py index 4229517..5514933 100644 --- a/vja/service_command.py +++ b/vja/service_command.py @@ -134,16 +134,17 @@ class CommandService: # replace the first existing reminder with our entry new_reminder = args.pop('reminder')[0] if reminder_arg else None - old_reminders = task_remote['reminders'] - if old_reminders and len(old_reminders) > 0: - if new_reminder: - old_reminders[0] = new_reminder # overwrite first remote reminder + if new_reminder is not None: + old_reminders = task_remote['reminders'] + if old_reminders and len(old_reminders) > 0: + if new_reminder: + old_reminders[0] = new_reminder # overwrite first remote reminder + else: + old_reminders.pop(0) # remove first remote reminder else: - old_reminders.pop(0) # remove first remote reminder - else: - if new_reminder: - old_reminders = [new_reminder] # create single reminder - args.update({'reminder': old_reminders}) + if new_reminder: + old_reminders = [new_reminder] # create single reminder + args.update({'reminder': old_reminders}) @staticmethod def _parse_reminder_arg(reminder_arg, args): -- GitLab