diff --git a/README.md b/README.md index bd6982e..8a90f2e 100644 --- a/README.md +++ b/README.md @@ -2533,7 +2533,7 @@ corresponding to when the API was available for use. * **updateNoteFields** - Modify the fields of an exist note. You can also include audio, video, or picture files which will be added to the note with an + Modify the fields of an existing note. You can also include audio, video, or picture files which will be added to the note with an optional `audio`, `video`, or `picture` property. Please see the documentation for `addNote` for an explanation of objects in the `audio`, `video`, or `picture` array. > **Warning** @@ -2574,6 +2574,98 @@ corresponding to when the API was available for use. } ``` +* **updateNote** + + Modify the fields and/or tags of an existing note. + In other words, combines `updateNoteFields` and `updateNoteTags`. + Please see their documentation for an explanation of all properties. + + Either `fields` or `tags` property can be omitted without affecting the other. + Thus valid requests to `updateNoteFields` also work with `updateNote`. + The note must have the `fields` property in order to update the optional audio, video, or picture objects. + + If neither `fields` nor `tags` are provided, the method will fail. + Fields are updated first and are not rolled back if updating tags fails. + Tags are not updated if updating fields fails. + + > **Warning** + > You must not be viewing the note that you are updating on your Anki browser, otherwise + > the fields will not update. See [this issue](https://github.com/FooSoft/anki-connect/issues/82) + > for further details. + + *Sample request*: + ```json + { + "action": "updateNote", + "version": 6, + "params": { + "note": { + "id": 1514547547030, + "fields": { + "Front": "new front content", + "Back": "new back content" + }, + "tags": ["new", "tags"] + } + } + } + ``` + + *Sample result*: + ```json + { + "result": null, + "error": null + } + ``` + +* **updateNoteTags** + + Set a note's tags by note ID. Old tags will be removed. + + *Sample request*: + ```json + { + "action": "updateNoteTags", + "version": 6, + "params": { + "note": 1483959289817, + "tags": ["european-languages"] + } + } + ``` + + *Sample result*: + ```json + { + "result": null, + "error": null + } + ``` + +* **getNoteTags** + + Get a note's tags by note ID. + + *Sample request*: + ```json + { + "action": "getNoteTags", + "version": 6, + "params": { + "note": 1483959289817, + } + } + ``` + + *Sample result*: + ```json + { + "result": ["european-languages"], + "error": null + } + ``` + * **addTags** Adds tags to notes by note ID. diff --git a/plugin/__init__.py b/plugin/__init__.py index 38851c5..cfa1dc7 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -811,6 +811,37 @@ class AnkiConnect: self.stopEditing() + @util.api() + def updateNote(self, note): + updated = False + if 'fields' in note.keys(): + self.updateNoteFields(note) + updated = True + if 'tags' in note.keys(): + self.updateNoteTags(note['id'], note['tags']) + updated = True + if not updated: + raise Exception('Must provide a "fields" or "tags" property.') + + + @util.api() + def updateNoteTags(self, note, tags): + if type(tags) == str: + tags = [tags] + if type(tags) != list or not all([type(t) == str for t in tags]): + raise Exception('Must provide tags as a list of strings') + + for old_tag in self.getNoteTags(note): + self.removeTags([note], old_tag) + for new_tag in tags: + self.addTags([note], new_tag) + + + @util.api() + def getNoteTags(self, note): + return self.getNote(note).tags + + @util.api() def addTags(self, notes, tags, add=True): self.startEditing() diff --git a/tests/test_notes.py b/tests/test_notes.py index f4bc874..7558f99 100755 --- a/tests/test_notes.py +++ b/tests/test_notes.py @@ -98,6 +98,12 @@ class TestTags: ac.clearUnusedTags() assert ac.getTags() == ["tag1"] + def test_updateNoteTags_and_getNoteTags(self, setup): + ac.updateNoteTags(note=setup.note1_id, tags="footag") + assert ac.getNoteTags(note=setup.note1_id) == ["footag"] + ac.updateNoteTags(note=setup.note1_id, tags=["foo", "bar", "baz"]) + assert len(ac.getNoteTags(note=setup.note1_id)) == 3 + class TestUpdateNoteFields: def test_updateNoteFields(self, setup): @@ -107,12 +113,27 @@ class TestUpdateNoteFields: notes_info = ac.notesInfo(notes=[setup.note1_id]) assert notes_info[0]["fields"]["field2"]["value"] == "bar" - def test_updateNoteFields_will_note_update_invalid_notes(self, setup): + def test_updateNoteFields_will_not_update_invalid_notes(self, setup): bad_note = {"id": 123, "fields": make_note()["fields"]} with pytest.raises(NotFoundError): ac.updateNoteFields(note=bad_note) +class TestUpdateNote: + def test_updateNote(self, setup): + new_fields = {"field1": "frontbar", "field2": "backbar"} + new_tags = ["foobar"] + good_note = {"id": setup.note1_id, "fields": new_fields, "tags": new_tags} + ac.updateNote(note=good_note) + notes_info = ac.notesInfo(notes=[setup.note1_id]) + assert notes_info[0]["fields"]["field2"]["value"] == "backbar" + assert notes_info[0]["tags"] == ["foobar"] + + def test_updateNote_requires_either_fields_or_tags(self, setup): + with pytest.raises(Exception, match="ust provide"): + ac.updateNote(note={"id": setup.note1_id}) + + class TestCanAddNotes: foo_bar_notes = [make_note(front="foo"), make_note(front="bar")]