From 29864f6b390f64b560fad891fdb9d3c26ca5237f Mon Sep 17 00:00:00 2001 From: Lefteris Chatzimparmpas Date: Fri, 19 Apr 2019 11:21:33 +0300 Subject: [PATCH] Support v8 of the API (#55) * Change API version to be v8. * Add user_settings manager and relevant API commands. * Update projects and items API. * Update and refactor all tests. * Re-add force_history in item complete. * Add forgotten projects.move() method. * Update items.update_date_complete() to accept a "due" parameter. * Add changelog between v7 and v8. * Move changes to changelog and change format a bit. * Remove deprecated "message" parameter Ref: https://github.com/Doist/todoist-python/pull/44 * Fix wrong call methon in uploads.py Ref: https://github.com/Doist/todoist-python/pull/52 * Include somehow missed changes to changelog file. --- CHANGELOG.md | 79 +++ tests/test_api.py | 883 +++++++++++++++++++++--------- todoist/api.py | 40 +- todoist/managers/items.py | 105 ++-- todoist/managers/projects.py | 33 +- todoist/managers/uploads.py | 4 +- todoist/managers/user_settings.py | 16 + todoist/models.py | 60 +- 8 files changed, 873 insertions(+), 347 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 todoist/managers/user_settings.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..90305e3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# Changelog + +## [8.0] - 2019-04-18 + +* All arguments expecting a date/time must be formatted according to [RFC + 3339](https://tools.ietf.org/html/rfc3339), and all return values are also + using the same format. +* The `item_order` and `indent` properties of projects, that denoted a visual + hierarchy for the projects (the order of all the projects and the level of + indent of each one of them), were replaced by `parent_id` and `child_order`, + which denote a real hierarchy (the parent project of a project and the order + of all children of a specific parent project). +* The `projects.add()` method now expects a `parent_id` and `child_order` + parameter, instead of the `item_order` and `indent` parameters. +* The `projects.update()` method doesn't expect an `item_order` and `indent` + parameters anymore, but it doesn't accept the new `parent_id` and + `child_order` parameters as well, as the way to change the hierarchy is now + different (see the `projects.move()` and `projects.reorder()` methods). +* The new `projects.move()` method must be used to move a project to become + the child of another project or become a root project. +* The new `projects.reorder()` method must be used to reorder projects in + relation to their siblings with the same parent. +* The `projects.delete()` method now expects only an `id` parameter, instead + of the `ids` parameter, and it deletes the project and all the projects's + descendants. +* The `projects.archive()` method now expects the `id` parameter, instead of + the `ids` parameter, and it archives the project and all the project's + descendants. +* The `projects.uncomplete()` method now expects an `id` parameter, instead + of the `ids` parameter, and it restores the project as a root project. +* The `projects.update_orders_indents()` method was removed. +* The `date_string`, `date_lang`, `due_date_utc` properties of items were + replaced by the `due` object. +* The `item_order` and `indent` properties of items, that denoted a visual + hierarchy for the items (the order of all the items and the level of indent + of each one of them), were replaced by `parent_id` and `child_order`, which + denote a real hierarchy (the parent item of an item and the order of all + children of a specific parent item). +* The `items.add()` method now expects a `parent_id` and `child_order` + parameter, instead of the `item_order` and `indent` parameters. +* The `items.add()` and `items.update()` methods now expect a `due` parameter, + instead of the `date_string`, `date_lang` and/or `due_date_utc` parameters. +* The `items.update()` method doesn't expect an `item_order` and `indent` + parameters anymore, but it doesn't accept the new `parent_id` and + `child_order` parameters as well, as the way to change the hierarchy is now + different (see `item_move` and `item_reorder`). +* The `items.move()` method does not accept the `project_items` and + `to_project` parameters, but a new set of parameters specifically `id`, and + one of `project_id` or `parent_id`. Another difference stemming from this is + that only a single item can be moved at a time, and also that in order to + move an item to become the child of another parent (or become a root level + item) the `item_move` command must be used as well. +* The `items.update_orders_indents()` method was removed. +* The new `items.reorder()` method must be used to reorder items in relation + to their siblings with the same parent. +* The `items.delete` method now expects only an `id` parameter, instead of + the `ids` parameter, and it deletes the item and all the item's descendants. +* The `items.complete()` method now expects the `id` parameter, instead of + the `ids` parameter, and it completes the item and all the item's + descendants. In addition the new `date_completed` parameter can also be + specified. +* The `items.uncomplete()` method now expects an `id` parameter, instead of + the `ids` parameter, and it uncompletes all the item's ancestors. +* The new `items.archive()` method can be used to move an item to history. +* The new `items.unarchive()` method can be used to move an item out of + history. +* The `items.update_date_complete()` method now expects a `due` parameter, + instead of `new_date_utc`, `date_string` and/or `is_forward` parameters. +* The possible color values of filters changed from `0-12` to `30-49`. +* The `date_string`, `date_lang`, `due_date_utc` properties of reminders were + replaced by the `due` object. +* The `reminders.add()` and `reminders.update()` methods now expect a `due` + parameter, instead of the `date_string`, `date_lang` and/or `due_date_utc` + parameters. +* The state now includes an additional new resource type called + `user_settings`. +* The user object now includes the `days_off` property. +* The `since` and `until` parameters of the `activity/get` method are + deprecated, and are replaced by the new `page` parameter. diff --git a/tests/test_api.py b/tests/test_api.py index 083559c..f92ad9d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,11 +1,10 @@ -import datetime import io import time import todoist -def test_stats(api_endpoint, api_token): +def test_stats_get(api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) response = api.completed.get_stats() assert 'days_items' in response @@ -14,7 +13,7 @@ def test_stats(api_endpoint, api_token): assert 'karma_last_update' in response -def test_user(api_endpoint, api_token): +def test_user_update(api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) api.sync() date_format = api.state['user']['date_format'] @@ -28,9 +27,21 @@ def test_user(api_endpoint, api_token): api.commit() -def test_project(cleanup, api_endpoint, api_token): +def test_user_settings_update(api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + reminder_email = api.state['user_settings']['reminder_email'] + if reminder_email: + reminder_email = False + else: + reminder_email = True + api.user_settings.update(reminder_email=reminder_email) + api.commit() + assert reminder_email == api.state['user_settings']['reminder_email'] + +def test_project_add(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) api.sync() project1 = api.projects.add('Project1') @@ -39,6 +50,48 @@ def test_project(cleanup, api_endpoint, api_token): assert 'Project1' in [p['name'] for p in api.state['projects']] assert api.projects.get_by_id(project1['id']) == project1 + project1.delete() + api.commit() + + +def test_project_delete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + project1 = api.projects.add('Project1') + api.commit() + + project1.delete() + response = api.commit() + assert response['projects'][0]['id'] == project1['id'] + assert response['projects'][0]['is_deleted'] == 1 + assert 'Project1' not in [p['name'] for p in api.state['projects']] + + +def test_project_update(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + project1 = api.projects.add('Project1') + api.commit() + + project1.update(name='UpdatedProject1') + response = api.commit() + assert response['projects'][0]['name'] == 'UpdatedProject1' + assert 'UpdatedProject1' in [p['name'] for p in api.state['projects']] + assert api.projects.get_by_id(project1['id']) == project1 + + project1.delete() + api.commit() + + +def test_project_archive(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + project1 = api.projects.add('Project1') + api.commit() + project1.archive() response = api.commit() assert response['projects'][0]['name'] == 'Project1' @@ -49,6 +102,20 @@ def test_project(cleanup, api_endpoint, api_token): if p['id'] == project1['id'] ] + project1.delete() + api.commit() + + +def test_project_unarchive(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + project1 = api.projects.add('Project1') + api.commit() + + project1.archive() + api.commit() + project1.unarchive() response = api.commit() assert response['projects'][0]['name'] == 'Project1' @@ -58,81 +125,71 @@ def test_project(cleanup, api_endpoint, api_token): if p['id'] == project1['id'] ] - project1.update(name='UpdatedProject1') - response = api.commit() - assert response['projects'][0]['name'] == 'UpdatedProject1' - assert 'UpdatedProject1' in [p['name'] for p in api.state['projects']] - assert api.projects.get_by_id(project1['id']) == project1 + project1.delete() + api.commit() + + +def test_project_move_to_parent(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + project1 = api.projects.add('Project1') + api.commit() project2 = api.projects.add('Project2') + api.commit() + + project2.move(project1['id']) response = api.commit() assert response['projects'][0]['name'] == 'Project2' - api.projects.update_orders_indents({ - project1['id']: [1, 2], - project2['id']: [2, 3] - }) + assert response['projects'][0]['parent_id'] == project1['id'] + assert project1['id'] in [ + i['parent_id'] for i in api.state['projects'] if i['id'] == project2['id'] + ] + + project2.delete() + api.commit() + project1.delete() + api.commit() + + +def test_project_reorder(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + project1 = api.projects.add('Project1') + api.commit() + + project2 = api.projects.add('Project2') + api.commit() + + api.projects.reorder(projects=[ + {'id': project1['id'], 'child_order': 2}, + {'id': project2['id'], 'child_order': 1}, + ]) response = api.commit() for project in response['projects']: if project['id'] == project1['id']: - assert project['item_order'] == 1 - assert project['indent'] == 2 + assert project['child_order'] == 2 if project['id'] == project2['id']: - assert project['item_order'] == 2 - assert project['indent'] == 3 - assert 1 in [ - p['item_order'] for p in api.state['projects'] - if p['id'] == project1['id'] - ] + assert project['child_order'] == 1 assert 2 in [ - p['indent'] for p in api.state['projects'] if p['id'] == project1['id'] - ] - assert 2 in [ - p['item_order'] for p in api.state['projects'] - if p['id'] == project2['id'] - ] - assert 3 in [ - p['indent'] for p in api.state['projects'] if p['id'] == project2['id'] + p['child_order'] for p in api.state['projects'] + if p['id'] == project1['id'] ] - - project1.delete() - response = api.commit() - assert response['projects'][0]['id'] == project1['id'] - assert response['projects'][0]['is_deleted'] == 1 - assert 'UpdatedProject1' not in [p['name'] for p in api.state['projects']] - - api.projects.archive(project2['id']) - response = api.commit() - assert response['projects'][0]['name'] == 'Project2' - assert response['projects'][0]['is_archived'] == 1 assert 1 in [ - p['is_archived'] for p in api.state['projects'] + p['child_order'] for p in api.state['projects'] if p['id'] == project2['id'] ] - api.projects.unarchive(project2['id']) - response = api.commit() - assert response['projects'][0]['name'] == 'Project2' - assert response['projects'][0]['is_archived'] == 0 - assert 0 in [ - p['is_archived'] for p in api.state['projects'] - if p['id'] == project2['id'] - ] - - api.projects.update(project2['id'], name='UpdatedProject2') - response = api.commit() - assert response['projects'][0]['name'] == 'UpdatedProject2' - assert 'UpdatedProject2' in [p['name'] for p in api.state['projects']] - - api.projects.delete([project2['id']]) - response = api.commit() - assert response['projects'][0]['id'] == project2['id'] - assert response['projects'][0]['is_deleted'] == 1 - assert 'UpdatedProject2' not in [p['name'] for p in api.state['projects']] + project1.delete() + api.commit() + project2.delete() + api.commit() -def test_item(cleanup, api_endpoint, api_token): +def test_item_add(cleanup, api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) - api.sync() response = api.add_item('Item1') @@ -141,36 +198,152 @@ def test_item(cleanup, api_endpoint, api_token): assert 'Item1' in [i['content'] for i in api.state['items']] item1 = [i for i in api.state['items'] if i['content'] == 'Item1'][0] assert api.items.get_by_id(item1['id']) == item1 + + item1.delete() + api.commit() + + +def test_item_delete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.sync() + item1.delete() response = api.commit() + assert response['items'][0]['id'] == item1['id'] + assert response['items'][0]['is_deleted'] == 1 + assert 'Item1' not in [i['content'] for i in api.state['items']] + + +def test_item_update(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() - inbox = [p for p in api.state['projects'] if p['name'] == 'Inbox'][0] - item1 = api.items.add('Item1', inbox['id']) + item1 = api.items.add('Item1') + api.commit() + + item1.update(content='UpdatedItem1') response = api.commit() - assert response['items'][0]['content'] == 'Item1' - assert 'Item1' in [i['content'] for i in api.state['items']] + assert response['items'][0]['content'] == 'UpdatedItem1' + assert 'UpdatedItem1' in [i['content'] for i in api.state['items']] assert api.items.get_by_id(item1['id']) == item1 - item1.complete() + item1.delete() + api.commit() + + +def test_item_complete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + item2 = api.items.add('Item2', parent_id=item1['id']) + api.commit() + + item2.complete() response = api.commit() - assert response['items'][0]['content'] == 'Item1' + assert response['items'][0]['content'] == 'Item2' assert response['items'][0]['checked'] == 1 assert 1 in [ - i['checked'] for i in api.state['items'] if i['id'] == item1['id'] + i['checked'] for i in api.state['items'] if i['id'] == item2['id'] ] - item1.uncomplete() + item1.delete() + api.commit() + item2.delete() + api.commit() + + +def test_item_uncomplete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + item2 = api.items.add('Item2', parent_id=item1['id']) + api.commit() + item2.complete() + api.commit() + + item2.uncomplete() response = api.commit() - assert response['items'][0]['content'] == 'Item1' + assert response['items'][0]['content'] == 'Item2' assert response['items'][0]['checked'] == 0 assert 0 in [ i['checked'] for i in api.state['items'] if i['id'] == item1['id'] ] - project1 = api.projects.add('Project1') + item1.delete() + api.commit() + item2.delete() + api.commit() + + +def test_item_archive(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + item2 = api.items.add('Item2', parent_id=item1['id']) + api.commit() + item2.complete() + api.commit() + + item2.archive() response = api.commit() + assert response['items'][0]['content'] == 'Item2' + assert response['items'][0]['in_history'] == 1 + assert 1 in [ + i['in_history'] for i in api.state['items'] if i['id'] == item2['id'] + ] + + item1.delete() + api.commit() + item2.delete() + api.commit() + + +def test_item_unarchive(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + item2 = api.items.add('Item2', parent_id=item1['id']) + api.commit() + item2.complete() + api.commit() + item2.archive() + api.commit() + + item2.unarchive() + response = api.commit() + assert response['items'][0]['content'] == 'Item2' + assert response['items'][0]['in_history'] == 0 + assert 0 in [ + i['in_history'] for i in api.state['items'] if i['id'] == item2['id'] + ] + + item1.delete() + api.commit() + item2.delete() + api.commit() + - item1.move(project1['id']) +def test_item_move_to_project(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + project1 = api.projects.add('Project1') + api.commit() + + item1.move(project_id=project1['id']) response = api.commit() assert response['items'][0]['content'] == 'Item1' assert response['items'][0]['project_id'] == project1['id'] @@ -178,48 +351,104 @@ def test_item(cleanup, api_endpoint, api_token): i['project_id'] for i in api.state['items'] if i['id'] == item1['id'] ] - item1.update(content='UpdatedItem1') + item1.delete() + api.commit() + project1.delete() + api.commit() + + +def test_item_move_to_parent(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + item2 = api.items.add('Item2') + api.commit() + + item2.move(parent_id=item1['id']) response = api.commit() - assert response['items'][0]['content'] == 'UpdatedItem1' - assert 'UpdatedItem1' in [i['content'] for i in api.state['items']] - assert api.items.get_by_id(item1['id']) == item1 + assert response['items'][0]['content'] == 'Item2' + assert response['items'][0]['parent_id'] == item1['id'] + assert item1['id'] in [ + i['parent_id'] for i in api.state['items'] if i['id'] == item2['id'] + ] + + item1.delete() + api.commit() + item2.delete() + api.commit() + + +def test_item_update_date_complete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() - date_string = datetime.datetime(2038, 1, 19, 3, 14, 7) - item2 = api.items.add('Item2', inbox['id'], date_string=date_string) + item1 = api.items.add('Item1', due={'string': 'every day'}) api.commit() now = time.time() tomorrow = time.gmtime(now + 24 * 3600) - new_date_utc = time.strftime("%Y-%m-%dT%H:%M", tomorrow) - api.items.update_date_complete(item1['id'], new_date_utc, 'every day', 0) - response = api.commit() - assert response['items'][0]['date_string'] == 'every day' + new_date_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", tomorrow) + due = { + 'date': new_date_utc, + 'string': 'every day', + 'is_forward': 0, + } + api.items.update_date_complete(item1['id'], due=due) + response = api.commit() + assert response['items'][0]['due']['string'] == 'every day' assert 'every day' in [ - i['date_string'] for i in api.state['items'] if i['id'] == item1['id'] + i['due']['string'] for i in api.state['items'] if i['id'] == item1['id'] ] - api.items.update_orders_indents({item1['id']: [2, 2], item2['id']: [1, 3]}) + item1.delete() + api.commit() + + +def test_item_reorder(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + item2 = api.items.add('Item2') + api.commit() + + api.items.reorder(items=[ + {'id': item1['id'], 'child_order': 2}, + {'id': item2['id'], 'child_order': 1}, + ]) response = api.commit() for item in response['items']: if item['id'] == item1['id']: - assert item['item_order'] == 2 - assert item['indent'] == 2 + assert item['child_order'] == 2 if item['id'] == item2['id']: - assert item['item_order'] == 1 - assert item['indent'] == 3 - assert 2 in [ - i['item_order'] for i in api.state['items'] if i['id'] == item1['id'] - ] + assert item['child_order'] == 1 assert 2 in [ - i['indent'] for i in api.state['items'] if i['id'] == item1['id'] + p['child_order'] for p in api.state['items'] + if p['id'] == item1['id'] ] assert 1 in [ - i['item_order'] for i in api.state['items'] if i['id'] == item2['id'] - ] - assert 3 in [ - i['indent'] for i in api.state['items'] if i['id'] == item2['id'] + p['child_order'] for p in api.state['items'] + if p['id'] == item2['id'] ] + item1.delete() + api.commit() + item2.delete() + api.commit() + + +def test_item_update_day_orders(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + item2 = api.items.add('Item2') + api.commit() + api.items.update_day_orders({item1['id']: 1, item2['id']: 2}) response = api.commit() for item in response['items']: @@ -231,60 +460,46 @@ def test_item(cleanup, api_endpoint, api_token): assert 2 == api.state['day_orders'][str(item2['id'])] item1.delete() - response = api.commit() - assert response['items'][0]['id'] == item1['id'] - assert response['items'][0]['is_deleted'] == 1 - assert 'UpdatedItem1' not in [i['content'] for i in api.state['items']] + api.commit() - api.items.complete([item2['id']]) - response = api.commit() - assert response['items'][0]['content'] == 'Item2' - assert response['items'][0]['checked'] == 1 - assert 1 in [ - i['checked'] for i in api.state['items'] if i['id'] == item2['id'] - ] + item2.delete() + api.commit() - api.items.uncomplete([item2['id']]) - response = api.commit() - assert response['items'][0]['content'] == 'Item2' - assert response['items'][0]['checked'] == 0 - assert 0 in [ - i['checked'] for i in api.state['items'] if i['id'] == item2['id'] - ] - api.items.move({item2['project_id']: [item2['id']]}, project1['id']) - response = api.commit() - assert response['items'][0]['content'] == 'Item2' - assert response['items'][0]['project_id'] == project1['id'] - assert project1['id'] in [ - i['project_id'] for i in api.state['items'] if i['id'] == item2['id'] - ] +def test_label_add(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() - api.items.update(item2['id'], content='UpdatedItem2') + label1 = api.labels.add('Label1') response = api.commit() - assert response['items'][0]['content'] == 'UpdatedItem2' - assert 'UpdatedItem2' in [i['content'] for i in api.state['items']] + assert response['labels'][0]['name'] == 'Label1' + assert 'Label1' in [l['name'] for l in api.state['labels']] + assert api.labels.get_by_id(label1['id']) == label1 - api.items.delete([item2['id']]) - response = api.commit() - assert response['items'][0]['id'] == item2['id'] - assert response['items'][0]['is_deleted'] == 1 - assert 'UpdatedItem2' not in [i['content'] for i in api.state['items']] + label1.delete() + api.commit() - project1.delete() + +def test_label_delete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + label1 = api.labels.add('Label1') + api.commit() + + label1.delete() response = api.commit() + assert response['labels'][0]['id'] == label1['id'] + assert response['labels'][0]['is_deleted'] == 1 + assert 'UpdatedLabel1' not in [l['name'] for l in api.state['labels']] -def test_label(cleanup, api_endpoint, api_token): +def test_label_update(cleanup, api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) - api.sync() label1 = api.labels.add('Label1') - response = api.commit() - assert response['labels'][0]['name'] == 'Label1' - assert 'Label1' in [l['name'] for l in api.state['labels']] - assert api.labels.get_by_id(label1['id']) == label1 + api.commit() label1.update(name='UpdatedLabel1') response = api.commit() @@ -292,8 +507,18 @@ def test_label(cleanup, api_endpoint, api_token): assert 'UpdatedLabel1' in [l['name'] for l in api.state['labels']] assert api.labels.get_by_id(label1['id']) == label1 + label1.delete() + api.commit() + + +def test_label_update_orders(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + label1 = api.labels.add('Label1') + api.commit() label2 = api.labels.add('Label2') - response = api.commit() + api.commit() api.labels.update_orders({label1['id']: 1, label2['id']: 2}) response = api.commit() @@ -307,33 +532,19 @@ def test_label(cleanup, api_endpoint, api_token): ] assert 2 in [ l['item_order'] for l in api.state['labels'] if l['id'] == label2['id'] - ] - - label1.delete() - response = api.commit() - assert response['labels'][0]['id'] == label1['id'] - assert response['labels'][0]['is_deleted'] == 1 - assert 'UpdatedLabel1' not in [l['name'] for l in api.state['labels']] - - api.labels.update(label2['id'], name='UpdatedLabel2') - response = api.commit() - assert response['labels'][0]['name'] == 'UpdatedLabel2' - assert 'UpdatedLabel2' in [l['name'] for l in api.state['labels']] + ] - api.labels.delete(label2['id']) - response = api.commit() - assert response['labels'][0]['id'] == label2['id'] - assert response['labels'][0]['is_deleted'] == 1 - assert 'UpdatedLabel1' not in [l['name'] for l in api.state['labels']] + label1.delete() + api.commit() + label2.delete() + api.commit() -def test_note(cleanup, api_endpoint, api_token): +def test_note_add(cleanup, api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) - api.sync() - inbox = [p for p in api.state['projects'] if p['name'] == 'Inbox'][0] - item1 = api.items.add('Item1', inbox['id']) + item1 = api.items.add('Item1') api.commit() note1 = api.notes.add(item1['id'], 'Note1') @@ -342,11 +553,20 @@ def test_note(cleanup, api_endpoint, api_token): assert 'Note1' in [n['content'] for n in api.state['notes']] assert api.notes.get_by_id(note1['id']) == note1 - note1.update(content='UpdatedNote1') - response = api.commit() - assert response['notes'][0]['content'] == 'UpdatedNote1' - assert 'UpdatedNote1' in [n['content'] for n in api.state['notes']] - assert api.notes.get_by_id(note1['id']) == note1 + note1.delete() + api.commit() + item1.delete() + api.commit() + + +def test_note_delete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + note1 = api.notes.add(item1['id'], 'Note1') + api.commit() note1.delete() response = api.commit() @@ -354,29 +574,35 @@ def test_note(cleanup, api_endpoint, api_token): assert response['notes'][0]['is_deleted'] == 1 assert 'UpdatedNote1' not in [n['content'] for n in api.state['notes']] - note2 = api.notes.add(item1['id'], 'Note2') - response = api.commit() - assert response['notes'][0]['content'] == 'Note2' - assert 'Note2' in [n['content'] for n in api.state['notes']] + note1.delete() + api.commit() + item1.delete() + api.commit() - api.notes.update(note2['id'], content='UpdatedNote2') - response = api.commit() - assert response['notes'][0]['content'] == 'UpdatedNote2' - assert 'UpdatedNote2' in [n['content'] for n in api.state['notes']] - api.notes.delete(note2['id']) +def test_note_update(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1') + api.commit() + note1 = api.notes.add(item1['id'], 'Note1') + api.commit() + + note1.update(content='UpdatedNote1') response = api.commit() - assert response['notes'][0]['id'] == note2['id'] - assert response['notes'][0]['is_deleted'] == 1 - assert 'UpdatedNote2' not in [n['content'] for n in api.state['notes']] + assert response['notes'][0]['content'] == 'UpdatedNote1' + assert 'UpdatedNote1' in [n['content'] for n in api.state['notes']] + assert api.notes.get_by_id(note1['id']) == note1 + note1.delete() + api.commit() item1.delete() - response = api.commit() + api.commit() -def test_projectnote(cleanup, api_endpoint, api_token): +def test_projectnote_add(cleanup, api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) - api.sync() project1 = api.projects.add('Project1') @@ -388,11 +614,20 @@ def test_projectnote(cleanup, api_endpoint, api_token): assert 'Note1' in [n['content'] for n in api.state['project_notes']] assert api.project_notes.get_by_id(note1['id']) == note1 - note1.update(content='UpdatedNote1') - response = api.commit() - assert response['project_notes'][0]['content'] == 'UpdatedNote1' - assert 'UpdatedNote1' in [n['content'] for n in api.state['project_notes']] - assert api.project_notes.get_by_id(note1['id']) == note1 + note1.delete() + api.commit() + project1.delete() + api.commit() + + +def test_projectnote_delete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + project1 = api.projects.add('Project1') + api.commit() + note1 = api.project_notes.add(project1['id'], 'Note1') + api.commit() note1.delete() response = api.commit() @@ -402,31 +637,33 @@ def test_projectnote(cleanup, api_endpoint, api_token): n['content'] for n in api.state['project_notes'] ] - note2 = api.project_notes.add(project1['id'], 'Note2') - response = api.commit() - assert response['project_notes'][0]['content'] == 'Note2' - assert 'Note2' in [n['content'] for n in api.state['project_notes']] + project1.delete() + api.commit() - api.project_notes.update(note2['id'], content='UpdatedNote2') - response = api.commit() - assert response['project_notes'][0]['content'] == 'UpdatedNote2' - assert 'UpdatedNote2' in [n['content'] for n in api.state['project_notes']] - api.project_notes.delete(note2['id']) +def test_projectnote_update(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + project1 = api.projects.add('Project1') + api.commit() + note1 = api.project_notes.add(project1['id'], 'Note1') + api.commit() + + note1.update(content='UpdatedNote1') response = api.commit() - assert response['project_notes'][0]['id'] == note2['id'] - assert response['project_notes'][0]['is_deleted'] == 1 - assert 'UpdatedNote2' not in [ - n['content'] for n in api.state['project_notes'] - ] + assert response['project_notes'][0]['content'] == 'UpdatedNote1' + assert 'UpdatedNote1' in [n['content'] for n in api.state['project_notes']] + assert api.project_notes.get_by_id(note1['id']) == note1 + note1.delete() + api.commit() project1.delete() - response = api.commit() + api.commit() -def test_filter(cleanup, api_endpoint, api_token): +def test_filter_add(cleanup, api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) - api.sync() filter1 = api.filters.add('Filter1', 'no due date') @@ -435,14 +672,50 @@ def test_filter(cleanup, api_endpoint, api_token): assert 'Filter1' in [f['name'] for f in api.state['filters']] assert api.filters.get_by_id(filter1['id']) == filter1 + filter1.delete() + api.commit() + + +def test_filter_delete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + filter1 = api.filters.add('Filter1', 'no due date') + api.commit() + + filter1.delete() + response = api.commit() + assert response['filters'][0]['id'] == filter1['id'] + assert response['filters'][0]['is_deleted'] == 1 + assert 'Filter1' not in [p['name'] for p in api.state['filters']] + + +def test_filter_update(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + filter1 = api.filters.add('Filter1', 'no due date') + api.commit() + filter1.update(name='UpdatedFilter1') response = api.commit() assert response['filters'][0]['name'] == 'UpdatedFilter1' assert 'UpdatedFilter1' in [f['name'] for f in api.state['filters']] assert api.filters.get_by_id(filter1['id']) == filter1 + filter1.delete() + api.commit() + + +def test_filter_update_orders(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + filter1 = api.filters.add('Filter1', 'no due date') + api.commit() + filter2 = api.filters.add('Filter2', 'today') - response = api.commit() + api.commit() api.filters.update_orders({filter1['id']: 2, filter2['id']: 1}) response = api.commit() @@ -461,39 +734,57 @@ def test_filter(cleanup, api_endpoint, api_token): ] filter1.delete() - response = api.commit() - assert response['filters'][0]['id'] == filter1['id'] - assert response['filters'][0]['is_deleted'] == 1 - assert 'UpdatedFilter1' not in [p['name'] for p in api.state['filters']] - - api.filters.update(filter2['id'], name='UpdatedFilter2') - response = api.commit() - assert response['filters'][0]['name'] == 'UpdatedFilter2' - assert 'UpdatedFilter2' in [f['name'] for f in api.state['filters']] - - api.filters.delete(filter2['id']) - response = api.commit() - assert response['filters'][0]['id'] == filter2['id'] - assert response['filters'][0]['is_deleted'] == 1 - assert 'UpdatedFilter2' not in [f['name'] for f in api.state['filters']] + api.commit() + filter2.delete() + api.commit() -def test_reminder(cleanup, api_endpoint, api_token): +def test_reminder_relative_add(cleanup, api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) - api.sync() - inbox = [p for p in api.state['projects'] if p['name'] == 'Inbox'][0] - item1 = api.items.add('Item1', inbox['id'], date_string='tomorrow 5pm') + item1 = api.items.add('Item1', due={'string': 'tomorrow 5pm'}) api.commit() - # relative reminder1 = api.reminders.add(item1['id'], minute_offset=30) response = api.commit() assert response['reminders'][0]['minute_offset'] == 30 assert reminder1['id'] in [p['id'] for p in api.state['reminders']] assert api.reminders.get_by_id(reminder1['id']) == reminder1 + reminder1.delete() + api.commit() + item1.delete() + api.commit() + + +def test_reminder_relative_delete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1', due={'string': 'tomorrow 5pm'}) + api.commit() + reminder1 = api.reminders.add(item1['id'], minute_offset=30) + api.commit() + + reminder1.delete() + response = api.commit() + assert response['reminders'][0]['is_deleted'] == 1 + assert reminder1['id'] not in [p['id'] for p in api.state['reminders']] + + item1.delete() + api.commit() + + +def test_reminder_relative_update(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1', due={'string': 'tomorrow 5pm'}) + api.commit() + reminder1 = api.reminders.add(item1['id'], minute_offset=30) + api.commit() + reminder1.update(minute_offset=str(15)) response = api.commit() assert response['reminders'][0]['minute_offset'] == 15 @@ -501,40 +792,83 @@ def test_reminder(cleanup, api_endpoint, api_token): assert api.reminders.get_by_id(reminder1['id']) == reminder1 reminder1.delete() - response = api.commit() - assert response['reminders'][0]['is_deleted'] == 1 - assert reminder1['id'] not in [p['id'] for p in api.state['reminders']] + api.commit() + item1.delete() + api.commit() + + +def test_reminder_absolute_add(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1', due={'string': 'tomorrow 5pm'}) + api.commit() - # absolute now = time.time() tomorrow = time.gmtime(now + 24 * 3600) - due_date_utc = time.strftime("%Y-%m-%dT%H:%M", tomorrow) - due_date_utc_long = time.strftime("%a %d %b %Y %H:%M:00 +0000", tomorrow) - reminder2 = api.reminders.add(item1['id'], due_date_utc=due_date_utc) + due_date_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", tomorrow) + reminder1 = api.reminders.add(item1['id'], due={'date': due_date_utc}) response = api.commit() - assert response['reminders'][0]['due_date_utc'] == due_date_utc_long + assert response['reminders'][0]['due']['date'] == due_date_utc tomorrow = time.gmtime(time.time() + 24 * 3600) - assert reminder2['id'] in [p['id'] for p in api.state['reminders']] - assert api.reminders.get_by_id(reminder2['id']) == reminder2 + assert reminder1['id'] in [p['id'] for p in api.state['reminders']] + assert api.reminders.get_by_id(reminder1['id']) == reminder1 - tomorrow = time.gmtime(now + 24 * 3600 + 60) - due_date_utc = time.strftime("%Y-%m-%dT%H:%M", tomorrow) - due_date_utc_long = time.strftime("%a %d %b %Y %H:%M:00 +0000", tomorrow) - api.reminders.update(reminder2['id'], due_date_utc=due_date_utc) - response = api.commit() - assert response['reminders'][0]['due_date_utc'] == due_date_utc_long - assert reminder2['id'] in [p['id'] for p in api.state['reminders']] - assert api.reminders.get_by_id(reminder2['id']) == reminder2 + reminder1.delete() + api.commit() + item1.delete() + api.commit() + + +def test_reminder_absolute_delete(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1', due={'string': 'tomorrow 5pm'}) + api.commit() + + now = time.time() + tomorrow = time.gmtime(now + 24 * 3600) + due_date_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", tomorrow) + reminder1 = api.reminders.add(item1['id'], due={'date': due_date_utc}) + api.commit() - api.reminders.delete(reminder2['id']) + api.reminders.delete(reminder1['id']) response = api.commit() assert response['reminders'][0]['is_deleted'] == 1 - assert reminder2['id'] not in [p['id'] for p in api.state['reminders']] + assert reminder1['id'] not in [p['id'] for p in api.state['reminders']] item1.delete() response = api.commit() +def test_reminder_absolute_update(cleanup, api_endpoint, api_token): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api.sync() + + item1 = api.items.add('Item1', due={'string': 'tomorrow 5pm'}) + api.commit() + + now = time.time() + tomorrow = time.gmtime(now + 24 * 3600) + due_date_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", tomorrow) + reminder1 = api.reminders.add(item1['id'], due={'date': due_date_utc}) + api.commit() + + tomorrow = time.gmtime(now + 24 * 3600 + 60) + due_date_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", tomorrow) + api.reminders.update(reminder1['id'], due_date_utc=due_date_utc) + response = api.commit() + assert response['reminders'][0]['due']['date'] == due_date_utc + assert reminder1['id'] in [p['id'] for p in api.state['reminders']] + assert api.reminders.get_by_id(reminder1['id']) == reminder1 + + reminder1.delete() + api.commit() + item1.delete() + api.commit() + + def test_locations(api_endpoint, api_token): api = todoist.api.TodoistAPI(api_token, api_endpoint) @@ -558,8 +892,7 @@ def test_live_notifications(api_endpoint, api_token): api.state['live_notifications_last_read_id'] -def test_share(cleanup, cleanup2, api_endpoint, api_token, api_token2): - +def test_share_accept(cleanup, cleanup2, api_endpoint, api_token, api_token2): api = todoist.api.TodoistAPI(api_token, api_endpoint) api2 = todoist.api.TodoistAPI(api_token2, api_endpoint) @@ -571,7 +904,6 @@ def test_share(cleanup, cleanup2, api_endpoint, api_token, api_token2): api2.commit() api2.sync() - # accept project1 = api.projects.add('Project1') api.commit() @@ -594,13 +926,30 @@ def test_share(cleanup, cleanup2, api_endpoint, api_token, api_token2): assert api2.state['user']['id'] in \ [p['user_id'] for p in api2.state['collaborator_states']] - # reject - project2 = api.projects.add('Project2') + api.sync() + project1 = [p for p in api.state['projects'] if p['name'] == 'Project1'][0] + project1.delete() + api.commit() + + +def test_share_reject(cleanup, cleanup2, api_endpoint, api_token, api_token2): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api2 = todoist.api.TodoistAPI(api_token2, api_endpoint) + + api.user.update(auto_invite_disabled=1) + api.commit() + api.sync() + + api2.user.update(auto_invite_disabled=1) + api2.commit() + api2.sync() + + project1 = api.projects.add('Project1') api.commit() - api.projects.share(project2['id'], api2.state['user']['email']) + api.projects.share(project1['id'], api2.state['user']['email']) response = api.commit() - assert response['projects'][0]['name'] == project2['name'] + assert response['projects'][0]['name'] == project1['name'] assert response['projects'][0]['shared'] response2 = api2.sync() @@ -608,7 +957,7 @@ def test_share(cleanup, cleanup2, api_endpoint, api_token, api_token2): if ln['notification_type'] == 'share_invitation_sent'), None) assert invitation2 is not None - assert invitation2['project_name'] == project2['name'] + assert invitation2['project_name'] == project1['name'] assert invitation2['from_user']['email'] == api.state['user']['email'] api2.invitations.reject(invitation2['id'], @@ -617,17 +966,29 @@ def test_share(cleanup, cleanup2, api_endpoint, api_token, api_token2): assert len(response2['projects']) == 0 assert len(response2['collaborator_states']) == 0 - project2 = [p for p in api.state['projects'] if p['name'] == 'Project2'][0] - project2.delete() + project1 = [p for p in api.state['projects'] if p['name'] == 'Project1'][0] + project1.delete() + api.commit() + + +def test_share_delete(cleanup, cleanup2, api_endpoint, api_token, api_token2): + api = todoist.api.TodoistAPI(api_token, api_endpoint) + api2 = todoist.api.TodoistAPI(api_token2, api_endpoint) + + api.user.update(auto_invite_disabled=1) api.commit() + api.sync() + + api2.user.update(auto_invite_disabled=1) + api2.commit() + api2.sync() - # delete - project3 = api.projects.add('Project3') + project1 = api.projects.add('Project1') api.commit() - api.projects.share(project3['id'], api2.state['user']['email']) + api.projects.share(project1['id'], api2.state['user']['email']) response = api.commit() - assert response['projects'][0]['name'] == project3['name'] + assert response['projects'][0]['name'] == project1['name'] assert response['projects'][0]['shared'] response2 = api2.sync() @@ -635,14 +996,14 @@ def test_share(cleanup, cleanup2, api_endpoint, api_token, api_token2): if ln['notification_type'] == 'share_invitation_sent'), None) assert invitation3 is not None - assert invitation3['project_name'] == project3['name'] + assert invitation3['project_name'] == project1['name'] assert invitation3['from_user']['email'] == api.state['user']['email'] api.invitations.delete(invitation3['id']) api.commit() - project3 = [p for p in api.state['projects'] if p['name'] == 'Project3'][0] - project3.delete() + project1 = [p for p in api.state['projects'] if p['name'] == 'Project1'][0] + project1.delete() api.commit() @@ -655,7 +1016,7 @@ def test_templates(cleanup, api_endpoint, api_token): project2 = api.projects.add('Project2') api.commit() - item1 = api.items.add('Item1', project1['id']) + item1 = api.items.add('Item1', project_id=project1['id']) api.commit() template = api.templates.export_as_file(project1['id']) @@ -668,6 +1029,8 @@ def test_templates(cleanup, api_endpoint, api_token): assert result == {'status': u'ok'} item1.delete() + api.commit() project1.delete() + api.commit() project2.delete() api.commit() diff --git a/todoist/api.py b/todoist/api.py index d6a8e05..0dfc53d 100644 --- a/todoist/api.py +++ b/todoist/api.py @@ -1,32 +1,34 @@ +import datetime +import functools +import json import os import uuid -import json + import requests -import datetime -import functools from todoist import models +from todoist.managers.activity import ActivityManager +from todoist.managers.backups import BackupsManager from todoist.managers.biz_invitations import BizInvitationsManager +from todoist.managers.business_users import BusinessUsersManager +from todoist.managers.collaborator_states import CollaboratorStatesManager +from todoist.managers.collaborators import CollaboratorsManager +from todoist.managers.completed import CompletedManager +from todoist.managers.emails import EmailsManager from todoist.managers.filters import FiltersManager from todoist.managers.invitations import InvitationsManager +from todoist.managers.items import ItemsManager +from todoist.managers.labels import LabelsManager from todoist.managers.live_notifications import LiveNotificationsManager +from todoist.managers.locations import LocationsManager from todoist.managers.notes import NotesManager, ProjectNotesManager from todoist.managers.projects import ProjectsManager -from todoist.managers.items import ItemsManager -from todoist.managers.labels import LabelsManager +from todoist.managers.quick import QuickManager from todoist.managers.reminders import RemindersManager -from todoist.managers.locations import LocationsManager -from todoist.managers.user import UserManager -from todoist.managers.collaborators import CollaboratorsManager -from todoist.managers.collaborator_states import CollaboratorStatesManager -from todoist.managers.completed import CompletedManager -from todoist.managers.uploads import UploadsManager -from todoist.managers.activity import ActivityManager -from todoist.managers.business_users import BusinessUsersManager from todoist.managers.templates import TemplatesManager -from todoist.managers.backups import BackupsManager -from todoist.managers.quick import QuickManager -from todoist.managers.emails import EmailsManager +from todoist.managers.uploads import UploadsManager +from todoist.managers.user import UserManager +from todoist.managers.user_settings import UserSettingsManager class SyncError(Exception): @@ -75,6 +77,7 @@ def __init__(self, self.invitations = InvitationsManager(self) self.biz_invitations = BizInvitationsManager(self) self.user = UserManager(self) + self.user_settings = UserSettingsManager(self) self.collaborators = CollaboratorsManager(self) self.collaborator_states = CollaboratorStatesManager(self) @@ -112,6 +115,7 @@ def reset_state(self): 'reminders': [], 'settings_notifications': {}, 'user': {}, + 'user_settings': {}, } def __getitem__(self, key): @@ -121,7 +125,7 @@ def serialize(self): return {key: getattr(self, key) for key in self._serialize_fields} def get_api_url(self): - return '%s/API/v7/' % self.api_endpoint + return '%s/API/v8/' % self.api_endpoint def _update_state(self, syncdata): """ @@ -150,6 +154,8 @@ def _update_state(self, syncdata): syncdata['settings_notifications']) if 'user' in syncdata: self.state['user'].update(syncdata['user']) + if 'user_settings' in syncdata: + self.state['user_settings'].update(syncdata['user_settings']) # Updating these type of data is a bit more complicated, since it is # necessary to find out whether an object in the sync data is new, diff --git a/todoist/managers/items.py b/todoist/managers/items.py index 6792e5f..b579817 100644 --- a/todoist/managers/items.py +++ b/todoist/managers/items.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from .. import models -from .generic import Manager, AllMixin, GetByIdMixin, SyncMixin +from .generic import AllMixin, GetByIdMixin, Manager, SyncMixin class ItemsManager(Manager, AllMixin, GetByIdMixin, SyncMixin): @@ -8,13 +8,16 @@ class ItemsManager(Manager, AllMixin, GetByIdMixin, SyncMixin): state_name = 'items' object_type = 'item' - def add(self, content, project_id, **kwargs): + def add(self, content, **kwargs): """ Creates a local item object. """ + project_id = kwargs.get('project_id') + if not project_id: + project_id = self.state['user']['inbox_project'] obj = models.Item({ 'content': content, - 'project_id': project_id + 'project_id': project_id, }, self.api) obj.temp_id = obj['id'] = self.api.generate_uuid() obj.data.update(kwargs) @@ -42,30 +45,36 @@ def update(self, item_id, **kwargs): } self.queue.append(cmd) - def delete(self, item_ids): + def delete(self, item_id): """ - Deletes items remotely. + Delete items remotely. """ cmd = { 'type': 'item_delete', 'uuid': self.api.generate_uuid(), 'args': { - 'ids': item_ids + 'id': item_id } } self.queue.append(cmd) - def move(self, project_items, to_project): + def move(self, item_id, **kwargs): """ - Moves items to another project remotely. + Moves item to another parent or project remotely. """ + args = { + 'id': item_id, + } + if 'parent_id' in kwargs: + args['parent_id'] = kwargs.get('parent_id') + elif 'project_id' in kwargs: + args['project_id'] = kwargs.get('project_id') + else: + raise TypeError('move() takes one of parent_id or project_id arguments') cmd = { 'type': 'item_move', 'uuid': self.api.generate_uuid(), - 'args': { - 'project_items': project_items, - 'to_project': to_project, - }, + 'args': args } self.queue.append(cmd) @@ -82,54 +91,74 @@ def close(self, item_id): } self.queue.append(cmd) - def complete(self, item_ids, force_history=0): + def complete(self, item_id, date_completed=None, force_history=None): """ - Marks items as completed remotely. + Marks item as completed remotely. """ + args = { + 'id': item_id, + } + if date_completed is not None: + args['date_completed'] = date_completed + if force_history is not None: + args['force_history'] = force_history cmd = { 'type': 'item_complete', 'uuid': self.api.generate_uuid(), + 'args': args, + } + self.queue.append(cmd) + + def uncomplete(self, item_id): + """ + Marks item as uncompleted remotely. + """ + cmd = { + 'type': 'item_uncomplete', + 'uuid': self.api.generate_uuid(), 'args': { - 'ids': item_ids, - 'force_history': force_history, + 'id': item_id, }, } self.queue.append(cmd) - def uncomplete(self, item_ids, update_item_orders=1, restore_state=None): + def archive(self, item_id): """ - Marks items as not completed remotely. + Marks item as archived remotely. """ - args = { - 'ids': item_ids, - 'update_item_orders': update_item_orders, + cmd = { + 'type': 'item_archive', + 'uuid': self.api.generate_uuid(), + 'args': { + 'id': item_id, + }, } - if restore_state: - args['restore_state'] = restore_state + self.queue.append(cmd) + + def unarchive(self, item_id): + """ + Marks item as unarchived remotely. + """ cmd = { - 'type': 'item_uncomplete', + 'type': 'item_unarchive', 'uuid': self.api.generate_uuid(), - 'args': args, + 'args': { + 'id': item_id, + }, } self.queue.append(cmd) def update_date_complete(self, item_id, - new_date_utc=None, - date_string=None, - is_forward=None): + due=None): """ Completes a recurring task remotely. """ args = { 'id': item_id, } - if new_date_utc: - args['new_date_utc'] = new_date_utc - if date_string: - args['date_string'] = date_string - if is_forward: - args['is_forward'] = is_forward + if due: + args['due'] = due cmd = { 'type': 'item_update_date_complete', 'uuid': self.api.generate_uuid(), @@ -137,15 +166,15 @@ def update_date_complete(self, } self.queue.append(cmd) - def update_orders_indents(self, ids_to_orders_indents): + def reorder(self, items): """ - Updates the order and indents of multiple items remotely. + Updates the child_order of the specified items. """ cmd = { - 'type': 'item_update_orders_indents', + 'type': 'item_reorder', 'uuid': self.api.generate_uuid(), 'args': { - 'ids_to_orders_indents': ids_to_orders_indents, + 'items': items, }, } self.queue.append(cmd) diff --git a/todoist/managers/projects.py b/todoist/managers/projects.py index 2de2c27..31b54b5 100644 --- a/todoist/managers/projects.py +++ b/todoist/managers/projects.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from .. import models -from .generic import Manager, AllMixin, GetByIdMixin, SyncMixin +from .generic import AllMixin, GetByIdMixin, Manager, SyncMixin class ProjectsManager(Manager, AllMixin, GetByIdMixin, SyncMixin): @@ -42,7 +42,7 @@ def update(self, project_id, **kwargs): } self.queue.append(cmd) - def delete(self, project_ids): + def delete(self, project_id): """ Deletes a project remotely. """ @@ -50,7 +50,7 @@ def delete(self, project_ids): 'type': 'project_delete', 'uuid': self.api.generate_uuid(), 'args': { - 'ids': project_ids, + 'id': project_id, }, } self.queue.append(cmd) @@ -70,7 +70,7 @@ def archive(self, project_id): def unarchive(self, project_id): """ - Marks project as not archived remotely. + Marks project as unarchived remotely. """ cmd = { 'type': 'project_unarchive', @@ -81,20 +81,35 @@ def unarchive(self, project_id): } self.queue.append(cmd) - def update_orders_indents(self, ids_to_orders_indents): + def move(self, project_id, parent_id): + """ + Moves project to another parent. + """ + args = { + 'id': project_id, + 'parent_id': parent_id, + } + cmd = { + 'type': 'project_move', + 'uuid': self.api.generate_uuid(), + 'args': args + } + self.queue.append(cmd) + + def reorder(self, projects): """ - Updates the orders and indents of multiple projects remotely. + Updates the child_order of the specified projects. """ cmd = { - 'type': 'project_update_orders_indents', + 'type': 'project_reorder', 'uuid': self.api.generate_uuid(), 'args': { - 'ids_to_orders_indents': ids_to_orders_indents, + 'projects': projects, }, } self.queue.append(cmd) - def share(self, project_id, email, message=''): + def share(self, project_id, email): """ Shares a project with a user. """ diff --git a/todoist/managers/uploads.py b/todoist/managers/uploads.py index 2b28fe3..9ec5916 100644 --- a/todoist/managers/uploads.py +++ b/todoist/managers/uploads.py @@ -24,7 +24,7 @@ def get(self, **kwargs): """ params = {'token': self.token} params.update(kwargs) - return self.api.get('uploads/get', params=params) + return self.api._get('uploads/get', params=params) def delete(self, file_url): """ @@ -33,4 +33,4 @@ def delete(self, file_url): param file_url: (str) uploaded file URL """ params = {'token': self.token, 'file_url': file_url} - return self.api.get('uploads/delete', params=params) + return self.api._get('uploads/delete', params=params) diff --git a/todoist/managers/user_settings.py b/todoist/managers/user_settings.py new file mode 100644 index 0000000..d081a88 --- /dev/null +++ b/todoist/managers/user_settings.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from .generic import Manager + + +class UserSettingsManager(Manager): + + def update(self, **kwargs): + """ + Updates the user's settings. + """ + cmd = { + 'type': 'user_settings_update', + 'uuid': self.api.generate_uuid(), + 'args': kwargs, + } + self.queue.append(cmd) diff --git a/todoist/models.py b/todoist/models.py index 314e010..52a23b9 100644 --- a/todoist/models.py +++ b/todoist/models.py @@ -74,15 +74,21 @@ def delete(self): """ Deletes item. """ - self.api.items.delete([self['id']]) + self.api.items.delete(self['id']) self.data['is_deleted'] = 1 - def move(self, to_project): + def move(self, **kwargs): """ - Moves item to another project. + Moves item to another parent or project. """ - self.api.items.move({self['project_id']: [self['id']]}, to_project) - self.data['project_id'] = to_project + if 'parent_id' in kwargs: + self.api.items.move(self['id'], parent_id=kwargs.get('parent_id')) + self.data['parent_id'] = kwargs.get('parent_id') + elif 'project_id' in kwargs: + self.api.items.move(self['id'], project_id=kwargs.get('project_id')) + self.data['project_id'] = kwargs.get('project_id') + else: + raise TypeError('move() takes one of parent_id or project_id arguments') def close(self): """ @@ -90,27 +96,33 @@ def close(self): """ self.api.items.close(self['id']) - def complete(self, force_history=0): + def complete(self, date_completed=None): """ Marks item as completed. """ - self.api.items.complete([self['id']], force_history) + self.api.items.complete(self['id'], date_completed=date_completed) self.data['checked'] = 1 - self.data['in_history'] = force_history - def uncomplete(self, update_item_orders=1, restore_state=None): + def uncomplete(self): """ - Marks item as not completed. + Marks item as uncompleted. """ - self.api.items.uncomplete([self['id']], update_item_orders, - restore_state) + self.api.items.uncomplete(self['id']) self.data['checked'] = 0 + + def archive(self): + """ + Marks item as archived. + """ + self.api.items.archive(self['id']) + self.data['in_history'] = 1 + + def unarchive(self): + """ + Marks item as unarchived. + """ + self.api.items.unarchive(self['id']) self.data['in_history'] = 0 - if restore_state and self['id'] in restore_state: - self.data['in_history'] = restore_state[self['id']][0] - self.data['checked'] = restore_state[self['id']][1] - self.data['item_order'] = restore_state[self['id']][2] - self.data['indent'] = restore_state[self['id']][3] def update_date_complete(self, new_date_utc=None, date_string=None, is_forward=None): @@ -206,7 +218,7 @@ def delete(self): """ Deletes project. """ - self.api.projects.delete([self['id']]) + self.api.projects.delete(self['id']) self.data['is_deleted'] = 1 def archive(self): @@ -218,16 +230,22 @@ def archive(self): def unarchive(self): """ - Marks project as not archived. + Marks project as unarchived. """ self.api.projects.unarchive(self['id']) self.data['is_archived'] = 0 - def share(self, email, message=''): + def move(self, parent_id): + """ + Moves item to another parent. + """ + self.api.projects.move(self['id'], parent_id) + + def share(self, email): """ Shares projects with a user. """ - self.api.projects.share(self['id'], email, message) + self.api.projects.share(self['id'], email) def take_ownership(self): """