anki_record 0.4 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 625337f0aac620a6662e846e1bf08a4ebe7a57224386b7154e24381ff275a507
4
- data.tar.gz: cb8e0001a1b37ad480ef3b218a1e64ef1a7429da777bb7f7bd9b68b51d2cbd9a
3
+ metadata.gz: e1a86e3c0c6ba10ebfe3867e0bdc62d5a3e8e1218fc4783c1ade86c34d790318
4
+ data.tar.gz: 662ec96ab3e29c26b0de4405b1c699ba99a6b180af5dbb2a24eb6ad4e7919cef
5
5
  SHA512:
6
- metadata.gz: c0e0fbf06f373cf555bd233c6f42aff0123858f80e58b5a525ca72f53bec28572930c91988424b9c3a3756182f779177a92c4f6dc8d8eae86cb46a95d3216ae1
7
- data.tar.gz: e89bc47239ad32052e9b51f1825073bc1dd40e8586b56360437fac0dbf7258fd8b6e7eb4d9f7f5a4b4dee8b00f66482fccf3b99aa03a024c83c636a54a4c5e0a
6
+ metadata.gz: 9d6de87dfadcc19fe0777202f67a97d632ddaec65426d171eda9ac74d87e7d99acc2afeb964e02529a695c1409a6c0fe26a9ee026d7b6fdbfb8c2d3b284230de
7
+ data.tar.gz: c35379873392e86fe7d6a402212d84c7c8d557543b84909d24c07e1ac34a927819628f13eff335f1b2a2e0d8bc7eda6218b86dca742660838d6b2148c16e4eee
data/CHANGELOG.md CHANGED
@@ -54,3 +54,9 @@
54
54
  - Responsibilites of `Collection` have been reorganized to `Anki21Database`.
55
55
  - The `guid` attribute of notes ic computed in a different way that allows a larger number of possible values.
56
56
  - `globally_unique_id` is now a module method rather than an included instance method.
57
+
58
+ ## [0.4.1] - 08-21-2023
59
+ - `Anki21Database#find_note_by` now can take the sort field value of a note as argument.
60
+ - `Anki21Database#find_notes_by_exact_text_match` is a new method that returns an array of notes that have in any field text matching the argument.
61
+ - Trying to create an Anki package file with a path/name that already exists raises a more helpful error message.
62
+ - Saving a note to the database saves the `mod` column (last modified time) to make it easier to import an updated package into Anki.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- anki_record (0.4)
4
+ anki_record (0.4.1)
5
5
  rubyzip (>= 2.3)
6
6
  sqlite3 (~> 1.3)
7
7
 
data/README.md CHANGED
@@ -14,9 +14,9 @@ If bundler is not being used to manage dependencies, install the gem by executin
14
14
 
15
15
  ## Usage
16
16
 
17
- This example shows how to create a new Anki package and also most of the features of the gem:
17
+ This example shows how to create a new Anki package and most of the API:
18
18
 
19
- ```
19
+ ```ruby
20
20
  require "anki_record"
21
21
 
22
22
  AnkiRecord::AnkiPackage.create(name: "example") do |anki21_database|
@@ -25,13 +25,18 @@ AnkiRecord::AnkiPackage.create(name: "example") do |anki21_database|
25
25
  custom_deck.save
26
26
 
27
27
  # Creating a new note type
28
- custom_note_type = AnkiRecord::NoteType.new(anki21_database:, name: "New custom note type")
29
- AnkiRecord::NoteField.new(note_type: custom_note_type, name: "custom front")
30
- AnkiRecord::NoteField.new(note_type: custom_note_type, name: "custom back")
31
- custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type, name: "Custom template 1")
28
+ custom_note_type = AnkiRecord::NoteType.new(anki21_database:,
29
+ name: "New custom note type")
30
+ AnkiRecord::NoteField.new(note_type: custom_note_type,
31
+ name: "custom front")
32
+ AnkiRecord::NoteField.new(note_type: custom_note_type,
33
+ name: "custom back")
34
+ custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type,
35
+ name: "Custom template 1")
32
36
  custom_card_template.question_format = "{{custom front}}"
33
37
  custom_card_template.answer_format = "{{custom back}}"
34
- second_custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type, name: "Custom template 2")
38
+ second_custom_card_template = AnkiRecord::CardTemplate.new(note_type: custom_note_type,
39
+ name: "Custom template 2")
35
40
  second_custom_card_template.question_format = "{{custom back}}"
36
41
  second_custom_card_template.answer_format = "{{custom front}}"
37
42
 
@@ -46,45 +51,49 @@ AnkiRecord::AnkiPackage.create(name: "example") do |anki21_database|
46
51
  custom_note_type.css = css
47
52
  custom_note_type.save
48
53
 
49
- # Creating a note with the custom note type
54
+ # Creating a new note with the custom note type
50
55
  note = AnkiRecord::Note.new(note_type: custom_note_type, deck: custom_deck)
51
56
  note.custom_front = "Content of the 'custom front' field"
52
57
  note.custom_back = "Content of the 'custom back' field"
53
58
  note.save
54
59
 
55
- # The default deck
60
+ # Finding the default deck
56
61
  default_deck = anki21_database.find_deck_by(name: "Default")
57
62
 
58
- # All of the default Anki note types
63
+ # Finding all of the default Anki note types
59
64
  basic_note_type = anki21_database.find_note_type_by(name: "Basic")
60
65
  basic_and_reversed_card_note_type = anki21_database.find_note_type_by(name: "Basic (and reversed card)")
61
66
  basic_and_optional_reversed_card_note_type = anki21_database.find_note_type_by(name: "Basic (optional reversed card)")
62
67
  basic_type_in_the_answer_note_type = anki21_database.find_note_type_by(name: "Basic (type in the answer)")
63
68
  cloze_note_type = anki21_database.find_note_type_by(name: "Cloze")
64
69
 
65
- # Creating notes using the default note types
70
+ # Creating new notes using the default note types
66
71
 
67
72
  basic_note = AnkiRecord::Note.new(note_type: basic_note_type, deck: default_deck)
68
73
  basic_note.front = "What molecule is most relevant to the name aerobic exercise?"
69
74
  basic_note.back = "Oxygen"
70
75
  basic_note.save
71
76
 
72
- # Creating a nested deck
73
- amino_acids_deck = AnkiRecord::Deck.new(anki21_database:, name: "Biochemistry::Amino acids")
77
+ # Creating a new nested deck
78
+ amino_acids_deck = AnkiRecord::Deck.new(anki21_database:,
79
+ name: "Biochemistry::Amino acids")
74
80
  amino_acids_deck.save
75
81
 
76
- basic_and_reversed_note = AnkiRecord::Note.new(note_type: basic_and_reversed_card_note_type, deck: amino_acids_deck)
82
+ basic_and_reversed_note = AnkiRecord::Note.new(note_type: basic_and_reversed_card_note_type,
83
+ deck: amino_acids_deck)
77
84
  basic_and_reversed_note.front = "Tyrosine"
78
85
  basic_and_reversed_note.back = "Y"
79
86
  basic_and_reversed_note.save
80
87
 
81
- basic_and_optional_reversed_note = AnkiRecord::Note.new(note_type: basic_and_optional_reversed_card_note_type, deck: default_deck)
88
+ basic_and_optional_reversed_note = AnkiRecord::Note.new(note_type: basic_and_optional_reversed_card_note_type,
89
+ deck: default_deck)
82
90
  basic_and_optional_reversed_note.front = "A technique where locations along a route are memorized and associated with ideas"
83
91
  basic_and_optional_reversed_note.back = "The method of loci"
84
92
  basic_and_optional_reversed_note.add_reverse = "Have a reverse card too"
85
93
  basic_and_optional_reversed_note.save
86
94
 
87
- basic_type_in_the_answer_note = AnkiRecord::Note.new(note_type: basic_type_in_the_answer_note_type, deck: default_deck)
95
+ basic_type_in_the_answer_note = AnkiRecord::Note.new(note_type: basic_type_in_the_answer_note_type,
96
+ deck: default_deck)
88
97
  basic_type_in_the_answer_note.front = "What Git command commits staged changes by changing the previous commit without editing the commit message?"
89
98
  basic_type_in_the_answer_note.back = "git commit --amend --no-edit"
90
99
  basic_type_in_the_answer_note.save
@@ -94,7 +103,8 @@ AnkiRecord::AnkiPackage.create(name: "example") do |anki21_database|
94
103
  cloze_note.back_extra = "This condition involves one cranial nerve but can have myriad neurological symptoms"
95
104
  cloze_note.save
96
105
  end
97
- # The example.apkg package now exists in the current working directory and contains 6 notes.
106
+ # An example.apkg file should be in the current
107
+ # working directory with 6 notes.
98
108
 
99
109
  ```
100
110
 
@@ -102,7 +112,7 @@ end
102
112
 
103
113
  The gem can also be used to update an existing Anki package:
104
114
 
105
- ```
115
+ ```ruby
106
116
  require "anki_record"
107
117
 
108
118
  AnkiRecord::AnkiPackage.update(path: "./example.apkg") do |anki21_database|
@@ -117,7 +127,7 @@ If an error is thrown in the block here, the original Anki package will not be c
117
127
 
118
128
  ## Documentation
119
129
 
120
- The [API Documentation](https://kylerego.github.io/anki_record_docs) is generated from comments in the source code could be useful if the above examples are not sufficient for your use case.
130
+ The [API Documentation](https://kylerego.github.io/anki_record_docs) generated from source code comments might be useful but I think the examples above show everything you can do that you would want to do.
121
131
 
122
132
  ## Development
123
133
 
@@ -30,21 +30,49 @@ module AnkiRecord
30
30
  database.prepare sql
31
31
  end
32
32
 
33
+ # rubocop:disable Metrics/MethodLength
33
34
  ##
34
- # Returns the note found by +id+, or nil if it is not found.
35
- def find_note_by(id:)
36
- note_cards_data = note_cards_data_for_note_id(id:)
35
+ # Returns the note found by either +sfld" or +id+, or nil if it is not found.
36
+ def find_note_by(sfld: nil, id: nil)
37
+ if (id && sfld) || (id.nil? && sfld.nil?)
38
+ raise ArgumentError,
39
+ "You must pass either an id or sfld keyword argument to find_note_by."
40
+ end
41
+ note_cards_data = if id
42
+ note_cards_data_for_note(id:)
43
+ else
44
+ note_cards_data_for_note(sfld:)
45
+ end
37
46
  return nil unless note_cards_data
38
47
 
39
48
  AnkiRecord::Note.new(anki21_database: self, data: note_cards_data)
40
49
  end
50
+ # rubocop:enable Metrics/MethodLength
51
+
52
+ ##
53
+ # Returns an array of notes that have any field value matching +text+
54
+ def find_notes_by_exact_text_match(text:)
55
+ return [] if text == ""
56
+
57
+ like_matcher = "%#{text}%"
58
+ note_datas = prepare("select * from notes where flds LIKE ?").execute([like_matcher])
59
+
60
+ note_datas.map do |note_data|
61
+ id = note_data["id"]
62
+
63
+ cards_data = prepare("select * from cards where nid = ?").execute([id]).to_a
64
+ note_cards_data = { note_data:, cards_data: }
65
+
66
+ AnkiRecord::Note.new(anki21_database: self, data: note_cards_data)
67
+ end
68
+ end
41
69
 
42
70
  ##
43
71
  # Returns the note type found by either +name+ or +id+, or nil if it is not found.
44
72
  def find_note_type_by(name: nil, id: nil)
45
73
  if (id && name) || (id.nil? && name.nil?)
46
74
  raise ArgumentError,
47
- "You must pass either an id or name keyword argument."
75
+ "You must pass either an id or name keyword argument to find_note_type_by."
48
76
  end
49
77
 
50
78
  name ? find_note_type_by_name(name:) : find_note_type_by_id(id:)
@@ -55,7 +83,7 @@ module AnkiRecord
55
83
  def find_deck_by(name: nil, id: nil)
56
84
  if (id && name) || (id.nil? && name.nil?)
57
85
  raise ArgumentError,
58
- "You must pass either an id or name keyword argument."
86
+ "You must pass either an id or name keyword argument to find_deck_by."
59
87
  end
60
88
 
61
89
  name ? find_deck_by_name(name:) : find_deck_by_id(id:)
@@ -127,10 +155,16 @@ module AnkiRecord
127
155
  decks.find { |deck| deck.id == id }
128
156
  end
129
157
 
130
- def note_cards_data_for_note_id(id:)
131
- note_data = prepare("select * from notes where id = ?").execute([id]).first
158
+ def note_cards_data_for_note(id: nil, sfld: nil)
159
+ note_data = if id
160
+ prepare("select * from notes where id = ?").execute([id]).first
161
+ elsif sfld
162
+ prepare("select * from notes where sfld = ?").execute([sfld]).first
163
+ end
132
164
  return nil unless note_data
133
165
 
166
+ id ||= note_data["id"]
167
+
134
168
  cards_data = prepare("select * from cards where nid = ?").execute([id]).to_a
135
169
  { note_data:, cards_data: }
136
170
  end
@@ -96,6 +96,7 @@ module AnkiRecord
96
96
  def validate_arguments(name:, target_directory:)
97
97
  check_name_argument_is_valid(name:)
98
98
  check_target_directory_argument_is_valid(target_directory:)
99
+ check_anki_package_does_not_already_exist(name:, target_directory:)
99
100
  end
100
101
 
101
102
  def check_name_argument_is_valid(name:)
@@ -110,6 +111,12 @@ module AnkiRecord
110
111
  raise ArgumentError, "No directory was found at the given path."
111
112
  end
112
113
 
114
+ def check_anki_package_does_not_already_exist(name:, target_directory:)
115
+ return unless Pathname.new("#{target_directory}/#{new_apkg_name(name:)}.apkg").exist?
116
+
117
+ raise ArgumentError, "An Anki package with that name already exists."
118
+ end
119
+
113
120
  def new_apkg_name(name:)
114
121
  name.end_with?(".apkg") ? name[0, name.length - 5] : name
115
122
  end
@@ -62,6 +62,13 @@ module AnkiRecord
62
62
  end
63
63
  end
64
64
 
65
+ ##
66
+ # Returns the text value of the note's sfld (sort field) which is determined by
67
+ # the note's note type
68
+ def sort_field_value
69
+ @field_contents[note_type.snake_case_sort_field_name]
70
+ end
71
+
65
72
  private
66
73
 
67
74
  # rubocop:disable Metrics/AbcSize
@@ -78,7 +85,7 @@ module AnkiRecord
78
85
  end
79
86
  @id = milliseconds_since_epoch
80
87
  @guid = Helpers::AnkiGuidHelper.globally_unique_id
81
- @last_modified_timestamp = seconds_since_epoch
88
+ @last_modified_timestamp = nil
82
89
  @usn = NEW_OBJECT_USN
83
90
  @tags = []
84
91
  @flags = 0
@@ -123,7 +130,7 @@ module AnkiRecord
123
130
  update notes set guid = ?, mid = ?, mod = ?, usn = ?, tags = ?,
124
131
  flds = ?, sfld = ?, csum = ?, flags = ?, data = ? where id = ?
125
132
  SQL
126
- statement.execute([@guid, note_type.id, @last_modified_timestamp,
133
+ statement.execute([@guid, note_type.id, seconds_since_epoch,
127
134
  @usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
128
135
  checksum(sort_field_value), @flags, @data, @id])
129
136
  cards.each { |card| card.save(note_exists_already: true) }
@@ -134,7 +141,7 @@ module AnkiRecord
134
141
  insert into notes (id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data)
135
142
  values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
136
143
  SQL
137
- statement.execute([@id, @guid, note_type.id, @last_modified_timestamp,
144
+ statement.execute([@id, @guid, note_type.id, seconds_since_epoch,
138
145
  @usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
139
146
  checksum(sort_field_value), @flags, @data])
140
147
  cards.each(&:save)
@@ -144,9 +151,5 @@ module AnkiRecord
144
151
  # The ASCII control code represented by hexadecimal 1F is the Unit Separator (US)
145
152
  note_type.snake_case_field_names.map { |field_name| @field_contents[field_name] }.join("\x1F")
146
153
  end
147
-
148
- def sort_field_value
149
- @field_contents[note_type.snake_case_sort_field_name]
150
- end
151
154
  end
152
155
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnkiRecord
4
- VERSION = "0.4" # :nodoc:
4
+ VERSION = "0.4.1" # :nodoc:
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anki_record
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.4'
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyle Rego
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-07-08 00:00:00.000000000 Z
11
+ date: 2023-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubyzip