anki_record 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +33 -4
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +5 -3
  6. data/README.md +147 -11
  7. data/anki_record.gemspec +2 -6
  8. data/lib/anki_record/anki_package/anki_package.rb +245 -0
  9. data/lib/anki_record/anki_package/database_setup_constants.rb +91 -0
  10. data/lib/anki_record/card/card.rb +108 -0
  11. data/lib/anki_record/card/card_attributes.rb +39 -0
  12. data/lib/anki_record/{card_template.rb → card_template/card_template.rb} +20 -47
  13. data/lib/anki_record/card_template/card_template_attributes.rb +69 -0
  14. data/lib/anki_record/collection/collection.rb +180 -0
  15. data/lib/anki_record/collection/collection_attributes.rb +35 -0
  16. data/lib/anki_record/deck/deck.rb +101 -0
  17. data/lib/anki_record/deck/deck_attributes.rb +30 -0
  18. data/lib/anki_record/deck/deck_defaults.rb +19 -0
  19. data/lib/anki_record/{deck_options_group.rb → deck_options_group/deck_options_group.rb} +10 -23
  20. data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
  21. data/lib/anki_record/helpers/checksum_helper.rb +17 -0
  22. data/lib/anki_record/helpers/data_query_helper.rb +13 -0
  23. data/lib/anki_record/helpers/shared_constants_helper.rb +1 -3
  24. data/lib/anki_record/helpers/time_helper.rb +7 -5
  25. data/lib/anki_record/note/note.rb +181 -0
  26. data/lib/anki_record/note/note_attributes.rb +56 -0
  27. data/lib/anki_record/note_field/note_field.rb +62 -0
  28. data/lib/anki_record/note_field/note_field_attributes.rb +39 -0
  29. data/lib/anki_record/note_field/note_field_defaults.rb +19 -0
  30. data/lib/anki_record/note_type/note_type.rb +161 -0
  31. data/lib/anki_record/note_type/note_type_attributes.rb +80 -0
  32. data/lib/anki_record/note_type/note_type_defaults.rb +38 -0
  33. data/lib/anki_record/version.rb +1 -1
  34. data/lib/anki_record.rb +1 -15
  35. metadata +32 -20
  36. data/.rdoc_options +0 -27
  37. data/lib/anki_record/anki_package.rb +0 -183
  38. data/lib/anki_record/collection.rb +0 -65
  39. data/lib/anki_record/db/anki_schema_definition.rb +0 -77
  40. data/lib/anki_record/db/clean_collection21_record.rb +0 -10
  41. data/lib/anki_record/db/clean_collection2_record.rb +0 -10
  42. data/lib/anki_record/deck.rb +0 -88
  43. data/lib/anki_record/note_field.rb +0 -63
  44. data/lib/anki_record/note_type.rb +0 -147
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ ANKI_SCHEMA_DEFINITION = <<~SQL # :nodoc:
5
+ CREATE TABLE col (
6
+ id integer PRIMARY KEY,
7
+ crt integer NOT NULL,
8
+ mod integer NOT NULL,
9
+ scm integer NOT NULL,
10
+ ver integer NOT NULL,
11
+ dty integer NOT NULL,
12
+ usn integer NOT NULL,
13
+ ls integer NOT NULL,
14
+ conf text NOT NULL,
15
+ models text NOT NULL,
16
+ decks text NOT NULL,
17
+ dconf text NOT NULL,
18
+ tags text NOT NULL
19
+ );
20
+ CREATE TABLE notes (
21
+ id integer PRIMARY KEY,
22
+ guid text NOT NULL,
23
+ mid integer NOT NULL,
24
+ mod integer NOT NULL,
25
+ usn integer NOT NULL,
26
+ tags text NOT NULL,
27
+ flds text NOT NULL,
28
+ sfld integer NOT NULL,
29
+ csum integer NOT NULL,
30
+ flags integer NOT NULL,
31
+ data text NOT NULL
32
+ );
33
+ CREATE TABLE cards (
34
+ id integer PRIMARY KEY,
35
+ nid integer NOT NULL,
36
+ did integer NOT NULL,
37
+ ord integer NOT NULL,
38
+ mod integer NOT NULL,
39
+ usn integer NOT NULL,
40
+ type integer NOT NULL,
41
+ queue integer NOT NULL,
42
+ due integer NOT NULL,
43
+ ivl integer NOT NULL,
44
+ factor integer NOT NULL,
45
+ reps integer NOT NULL,
46
+ lapses integer NOT NULL,
47
+ left integer NOT NULL,
48
+ odue integer NOT NULL,
49
+ odid integer NOT NULL,
50
+ flags integer NOT NULL,
51
+ data text NOT NULL
52
+ );
53
+ CREATE TABLE revlog (
54
+ id integer PRIMARY KEY,
55
+ cid integer NOT NULL,
56
+ usn integer NOT NULL,
57
+ ease integer NOT NULL,
58
+ ivl integer NOT NULL,
59
+ lastIvl integer NOT NULL,
60
+ factor integer NOT NULL,
61
+ time integer NOT NULL,
62
+ type integer NOT NULL
63
+ );
64
+ CREATE INDEX ix_notes_usn ON notes (usn);
65
+ CREATE INDEX ix_cards_usn ON cards (usn);
66
+ CREATE INDEX ix_revlog_usn ON revlog (usn);
67
+ CREATE INDEX ix_cards_nid ON cards (nid);
68
+ CREATE INDEX ix_cards_sched ON cards (did, queue, due);
69
+ CREATE INDEX ix_revlog_cid ON revlog (cid);
70
+ CREATE INDEX ix_notes_csum ON notes (csum);
71
+ CREATE TABLE graves (
72
+ usn integer NOT NULL,
73
+ oid integer NOT NULL,
74
+ type integer NOT NULL
75
+ );
76
+ SQL
77
+
78
+ ##
79
+ # This is the SQL insert statement (as a Ruby string/here document) for the
80
+ # default col record in the collection.anki2 database exported from a new Anki 2.1.54 profile
81
+ INSERT_COLLECTION_ANKI_2_COL_RECORD = <<~SQL
82
+ INSERT INTO col VALUES(1,1676883600,1676902390012,1676902390005,11,0,0,0,'{"addToCur":true,"_deck_1_lastNotetype":1676902390008,"nextPos":2,"sortType":"noteFld","newSpread":0,"schedVer":2,"collapseTime":1200,"estTimes":true,"curDeck":1,"creationOffset":300,"dayLearnFirst":false,"timeLim":0,"activeDecks":[1],"sortBackwards":false,"_nt_1676902390008_lastDeck":1,"curModel":1676902390008,"dueCounts":true}','{"1676902390010":{"id":1676902390010,"name":"Basic (optional reversed card)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0},{"name":"Card 2","ord":1,"qfmt":"{{#Add Reverse}}{{Back}}{{/Add Reverse}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Front}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Add Reverse","ord":2,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]],[1,"all",[1,2]]]},"1676902390009":{"id":1676902390009,"name":"Basic (and reversed card)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0},{"name":"Card 2","ord":1,"qfmt":"{{Back}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Front}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]],[1,"any",[1]]]},"1676902390008":{"id":1676902390008,"name":"Basic","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]]]},"1676902390011":{"id":1676902390011,"name":"Basic (type in the answer)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}\\n\\n{{type:Back}}","afmt":"{{Front}}\\n\\n<hr id=answer>\\n\\n{{type:Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0,1]]]},"1676902390012":{"id":1676902390012,"name":"Cloze","type":1,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Cloze","ord":0,"qfmt":"{{cloze:Text}}","afmt":"{{cloze:Text}}<br>\\n{{Back Extra}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Text","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back Extra","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n.cloze {\\n font-weight: bold;\\n color: blue;\\n}\\n.nightMode .cloze {\\n color: lightblue;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]]]}}','{"1":{"id":1,"mod":0,"name":"Default","usn":0,"lrnToday":[0,0],"revToday":[0,0],"newToday":[0,0],"timeToday":[0,0],"collapsed":true,"browserCollapsed":true,"desc":"","dyn":0,"conf":1,"extendNew":0,"extendRev":0}}','{"1":{"id":1,"mod":0,"name":"Default","usn":0,"maxTaken":60,"autoplay":true,"timer":0,"replayq":true,"new":{"bury":false,"delays":[1.0,10.0],"initialFactor":2500,"ints":[1,4,0],"order":1,"perDay":20},"rev":{"bury":false,"ease4":1.3,"ivlFct":1.0,"maxIvl":36500,"perDay":200,"hardFactor":1.2},"lapse":{"delays":[10.0],"leechAction":1,"leechFails":8,"minInt":1,"mult":0.0},"dyn":false,"newMix":0,"newPerDayMinimum":0,"interdayLearningMix":0,"reviewOrder":0,"newSortOrder":0,"newGatherPriority":0,"buryInterdayLearning":false}}','{}');
83
+ SQL
84
+
85
+ ##
86
+ # This is the SQL insert statement (as a Ruby string/here document) for the
87
+ # default col record in the collection.anki21 database exported from a new Anki 2.1.54 profile
88
+ INSERT_COLLECTION_ANKI_21_COL_RECORD = <<~SQL
89
+ INSERT INTO col VALUES(1,1676883600,0,1676902364657,11,0,0,0,'{"activeDecks":[1],"curDeck":1,"nextPos":1,"schedVer":2,"sortType":"noteFld","estTimes":true,"collapseTime":1200,"dayLearnFirst":false,"curModel":1676902364661,"sortBackwards":false,"newSpread":0,"creationOffset":300,"timeLim":0,"addToCur":true,"dueCounts":true}','{"1676902364664":{"id":1676902364664,"name":"Basic (type in the answer)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}\\n\\n{{type:Back}}","afmt":"{{Front}}\\n\\n<hr id=answer>\\n\\n{{type:Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0,1]]]},"1676902364663":{"id":1676902364663,"name":"Basic (optional reversed card)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0},{"name":"Card 2","ord":1,"qfmt":"{{#Add Reverse}}{{Back}}{{/Add Reverse}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Front}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Add Reverse","ord":2,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]],[1,"all",[1,2]]]},"1676902364665":{"id":1676902364665,"name":"Cloze","type":1,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Cloze","ord":0,"qfmt":"{{cloze:Text}}","afmt":"{{cloze:Text}}<br>\\n{{Back Extra}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Text","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back Extra","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n.cloze {\\n font-weight: bold;\\n color: blue;\\n}\\n.nightMode .cloze {\\n color: lightblue;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]]]},"1676902364661":{"id":1676902364661,"name":"Basic","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]]]},"1676902364662":{"id":1676902364662,"name":"Basic (and reversed card)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0},{"name":"Card 2","ord":1,"qfmt":"{{Back}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Front}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]],[1,"any",[1]]]}}','{"1":{"id":1,"mod":0,"name":"Default","usn":0,"lrnToday":[0,0],"revToday":[0,0],"newToday":[0,0],"timeToday":[0,0],"collapsed":true,"browserCollapsed":true,"desc":"","dyn":0,"conf":1,"extendNew":0,"extendRev":0}}','{"1":{"id":1,"mod":0,"name":"Default","usn":0,"maxTaken":60,"autoplay":true,"timer":0,"replayq":true,"new":{"bury":false,"delays":[1.0,10.0],"initialFactor":2500,"ints":[1,4,0],"order":1,"perDay":20},"rev":{"bury":false,"ease4":1.3,"ivlFct":1.0,"maxIvl":36500,"perDay":200,"hardFactor":1.2},"lapse":{"delays":[10.0],"leechAction":1,"leechFails":8,"minInt":1,"mult":0.0},"dyn":false,"newMix":0,"newPerDayMinimum":0,"interdayLearningMix":0,"reviewOrder":0,"newSortOrder":0,"newGatherPriority":0,"buryInterdayLearning":false}}','{}');
90
+ SQL
91
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helpers/shared_constants_helper"
4
+ require_relative "../helpers/time_helper"
5
+ require_relative "card_attributes"
6
+
7
+ module AnkiRecord
8
+ ##
9
+ # Card represents an Anki card.
10
+ class Card
11
+ include CardAttributes
12
+ include TimeHelper
13
+ include SharedConstantsHelper
14
+
15
+ def initialize(note:, card_template: nil, card_data: nil) # :nodoc:
16
+ @note = note
17
+ if card_template
18
+ setup_instance_variables_for_new_card(card_template: card_template)
19
+ elsif card_data
20
+ setup_instance_variables_from_existing(card_data: card_data)
21
+ else
22
+ raise ArgumentError
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def setup_instance_variables_for_new_card(card_template:)
29
+ raise ArgumentError unless @note.note_type == card_template.note_type
30
+
31
+ setup_collaborator_object_instance_variables_for_new_card(card_template: card_template)
32
+ setup_simple_instance_variables_for_new_card
33
+ end
34
+
35
+ def setup_collaborator_object_instance_variables_for_new_card(card_template:)
36
+ @card_template = card_template
37
+ @deck = @note.deck
38
+ @collection = @deck.collection
39
+ end
40
+
41
+ def setup_simple_instance_variables_for_new_card
42
+ @id = milliseconds_since_epoch
43
+ @last_modified_timestamp = seconds_since_epoch
44
+ @usn = NEW_OBJECT_USN
45
+ %w[type queue due ivl factor reps lapses left odue odid flags].each do |instance_variable_name|
46
+ instance_variable_set "@#{instance_variable_name}", 0
47
+ end
48
+ @data = "{}"
49
+ end
50
+
51
+ def setup_instance_variables_from_existing(card_data:)
52
+ setup_collaborator_object_instance_variables_from_existing(card_data: card_data)
53
+ setup_simple_instance_variables_from_existing(card_data: card_data)
54
+ end
55
+
56
+ def setup_collaborator_object_instance_variables_from_existing(card_data:)
57
+ @collection = note.note_type.collection
58
+ @deck = collection.find_deck_by id: card_data["did"]
59
+ @card_template = note.note_type.card_templates.find do |card_template|
60
+ card_template.ordinal_number == card_data["ord"]
61
+ end
62
+ end
63
+
64
+ def setup_simple_instance_variables_from_existing(card_data:)
65
+ @last_modified_timestamp = card_data["mod"]
66
+ %w[id usn type queue due ivl factor reps lapses left odue odid flags data].each do |instance_variable_name|
67
+ instance_variable_set "@#{instance_variable_name}", card_data[instance_variable_name]
68
+ end
69
+ end
70
+
71
+ public
72
+
73
+ def save(note_exists_already: false) # :nodoc:
74
+ note_exists_already ? update_card_in_collection_anki21 : insert_new_card_in_collection_anki21
75
+ end
76
+
77
+ private
78
+
79
+ def update_card_in_collection_anki21
80
+ statement = @collection.anki_package.prepare <<~SQL
81
+ update cards set nid = ?, did = ?, ord = ?, mod = ?, usn = ?, type = ?,
82
+ queue = ?, due = ?, ivl = ?, factor = ?, reps = ?, lapses = ?,
83
+ left = ?, odue = ?, odid = ?, flags = ?, data = ? where id = ?
84
+ SQL
85
+ statement.execute [@note.id, @deck.id, ordinal_number,
86
+ @last_modified_timestamp, @usn, @type, @queue,
87
+ @due, @ivl, @factor, @reps,
88
+ @lapses, @left, @odue, @odid, @flags, @data, @id]
89
+ end
90
+
91
+ def insert_new_card_in_collection_anki21
92
+ statement = @collection.anki_package.prepare <<~SQL
93
+ insert into cards (id, nid, did, ord,
94
+ mod, usn, type, queue,
95
+ due, ivl, factor, reps,
96
+ lapses, left, odue, odid, flags, data)
97
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
98
+ SQL
99
+ statement.execute [@id, @note.id, @deck.id, ordinal_number,
100
+ @last_modified_timestamp, @usn, @type, @queue,
101
+ @due, @ivl, @factor, @reps, @lapses, @left, @odue, @odid, @flags, @data]
102
+ end
103
+
104
+ def ordinal_number
105
+ @card_template&.ordinal_number
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ ##
5
+ # Module with the Card class's attribute readers, writers, and accessors.
6
+ module CardAttributes
7
+ ##
8
+ # The card's note object.
9
+ attr_reader :note
10
+
11
+ ##
12
+ # The card's deck object.
13
+ attr_reader :deck
14
+
15
+ ##
16
+ # The card's collection object.
17
+ attr_reader :collection
18
+
19
+ ##
20
+ # The card's card template object.
21
+ attr_reader :card_template
22
+
23
+ ##
24
+ # The card's id.
25
+ #
26
+ # This is also the number of milliseconds since the 1970 epoch at which the card was created.
27
+ attr_reader :id
28
+
29
+ ##
30
+ # The number of seconds since the 1970 epoch at which the card was last modified.
31
+ attr_reader :last_modified_timestamp
32
+
33
+ ##
34
+ # The card's update sequence number.
35
+ attr_reader :usn
36
+
37
+ attr_reader :type, :queue, :due, :ivl, :factor, :reps, :lapses, :left, :odue, :odid, :flags, :data
38
+ end
39
+ end
@@ -1,44 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pry"
4
-
5
- # TODO: All instance variables should at least be readable
3
+ require_relative "card_template_attributes"
6
4
 
7
5
  module AnkiRecord
8
6
  ##
9
- # CardTemplate represents a card template of an Anki note type
7
+ # CardTemplate represents a card template of an Anki note type.
10
8
  class CardTemplate
11
- ##
12
- # The name of this card template
13
- attr_accessor :name
14
-
15
- ##
16
- # The question format
17
- attr_accessor :question_format
18
-
19
- ##
20
- # The answer format
21
- attr_accessor :answer_format
22
-
23
- ##
24
- # The font style shown for this card template in the browser
25
- attr_accessor :browser_font_style
26
-
27
- ##
28
- # The font size used for this card template in the browser
29
- attr_accessor :browser_font_size
30
-
31
- ##
32
- # The note type that this card template belongs to
33
- attr_reader :note_type
34
-
35
- ##
36
- # 0 for the first card template of the note type, 1 for the second, etc.
37
- attr_reader :ordinal_number
9
+ include CardTemplateAttributes
38
10
 
39
- ##
40
- # Instantiates a new card template called +name+ for the given note type
41
- #
11
+ # Instantiates a new card template with name +name+ for the note type +note_type+.
42
12
  def initialize(note_type:, name: nil, args: nil)
43
13
  raise ArgumentError unless (name && args.nil?) || (args && args["name"])
44
14
 
@@ -48,6 +18,21 @@ module AnkiRecord
48
18
  else
49
19
  setup_card_template_instance_variables(name: name)
50
20
  end
21
+
22
+ @note_type.add_card_template self
23
+ end
24
+
25
+ def to_h # :nodoc:
26
+ {
27
+ name: @name,
28
+ ord: @ordinal_number,
29
+ qfmt: @question_format, afmt: @answer_format,
30
+ bqfmt: @bqfmt,
31
+ bafmt: @bafmt,
32
+ did: @deck_id,
33
+ bfont: @browser_font_style,
34
+ bsize: @browser_font_size
35
+ }
51
36
  end
52
37
 
53
38
  private
@@ -66,7 +51,7 @@ module AnkiRecord
66
51
 
67
52
  def setup_card_template_instance_variables(name:)
68
53
  @name = name
69
- @ordinal_number = @note_type.templates.length
54
+ @ordinal_number = @note_type.card_templates.length
70
55
  @question_format = ""
71
56
  @answer_format = ""
72
57
  @bqfmt = ""
@@ -75,17 +60,5 @@ module AnkiRecord
75
60
  @browser_font_style = ""
76
61
  @browser_font_size = 0
77
62
  end
78
-
79
- public
80
-
81
- ##
82
- # Returns the field names that are allowed in the answer format and question format
83
- #
84
- # These are the field_name values in {{field_name}} in those formats.
85
- #
86
- # They are equivalent to the names of the fields of the template's note type.
87
- def allowed_field_names
88
- @note_type.fields.map(&:name)
89
- end
90
63
  end
91
64
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ ##
5
+ # Module with the CardTemplate class's attribute readers, writers, and accessors.
6
+ module CardTemplateAttributes
7
+ ##
8
+ # The card template's name.
9
+ attr_accessor :name
10
+
11
+ ##
12
+ # The card template's font style in the browser.
13
+ attr_accessor :browser_font_style
14
+
15
+ ##
16
+ # The card template's font size used in the browser.
17
+ attr_accessor :browser_font_size
18
+
19
+ ##
20
+ # The card template's question format.
21
+ attr_reader :question_format
22
+
23
+ ##
24
+ # Sets the question format of the card template.
25
+ #
26
+ # Raises an ArgumentError if the specified format attempts to use invalid fields.
27
+ def question_format=(format)
28
+ fields_in_specified_format = format.scan(/{{.+?}}/).map do |capture|
29
+ capture.chomp("}}").reverse.chomp("{{").reverse
30
+ end
31
+ if fields_in_specified_format.any? do |field_name|
32
+ !note_type.allowed_card_template_question_format_field_names.include?(field_name)
33
+ end
34
+ raise ArgumentError, "You tried to use a field that the note type does not have."
35
+ end
36
+
37
+ @question_format = format
38
+ end
39
+
40
+ ##
41
+ # The card template's answer format.
42
+ attr_reader :answer_format
43
+
44
+ ##
45
+ # Sets the answer format of the card template.
46
+ #
47
+ # Raises an ArgumentError if the specified format attempts to use invalid fields.
48
+ def answer_format=(format)
49
+ fields_in_specified_format = format.scan(/{{.+?}}/).map do |capture|
50
+ capture.chomp("}}").reverse.chomp("{{").reverse
51
+ end
52
+ if fields_in_specified_format.any? do |field_name|
53
+ !note_type.allowed_card_template_answer_format_field_names.include?(field_name)
54
+ end
55
+ raise ArgumentError, "You tried to use a field that the note type does not have."
56
+ end
57
+
58
+ @answer_format = format
59
+ end
60
+
61
+ ##
62
+ # The card template's note type object.
63
+ attr_reader :note_type
64
+
65
+ ##
66
+ # 0 for the first card template of the note type, 1 for the second, etc.
67
+ attr_reader :ordinal_number
68
+ end
69
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "../deck/deck"
6
+ require_relative "../deck_options_group/deck_options_group"
7
+ require_relative "../helpers/data_query_helper"
8
+ require_relative "../helpers/time_helper"
9
+ require_relative "../note_type/note_type"
10
+ require_relative "collection_attributes"
11
+
12
+ module AnkiRecord
13
+ ##
14
+ # Collection represents the single record in the Anki collection.anki21 database's `col` table.
15
+ # The note types, decks, and deck options groups data are contained within this record.
16
+ class Collection
17
+ include AnkiRecord::DataQueryHelper
18
+ include AnkiRecord::TimeHelper
19
+ include AnkiRecord::CollectionAttributes
20
+
21
+ def initialize(anki_package:) # :nodoc:
22
+ setup_collection_instance_variables(anki_package: anki_package)
23
+ end
24
+
25
+ def add_note_type(note_type) # :nodoc:
26
+ raise ArgumentError unless note_type.instance_of?(AnkiRecord::NoteType)
27
+
28
+ existing_note_type = nil
29
+ @note_types.each do |nt|
30
+ existing_note_type = nt if nt.id == note_type.id
31
+ end
32
+ @note_types.delete(existing_note_type) if existing_note_type
33
+
34
+ @note_types << note_type
35
+ end
36
+
37
+ def add_deck(deck) # :nodoc:
38
+ raise ArgumentError unless deck.instance_of?(AnkiRecord::Deck)
39
+
40
+ @decks << deck
41
+ end
42
+
43
+ def add_deck_options_group(deck_options_group) # :nodoc:
44
+ raise ArgumentError unless deck_options_group.instance_of?(AnkiRecord::DeckOptionsGroup)
45
+
46
+ @deck_options_groups << deck_options_group
47
+ end
48
+
49
+ ##
50
+ # Returns the collection's note type object found by either +name+ or +id+, or nil if it is not found.
51
+ def find_note_type_by(name: nil, id: nil)
52
+ if (id && name) || (id.nil? && name.nil?)
53
+ raise ArgumentError,
54
+ "You must pass either an id or name keyword argument."
55
+ end
56
+
57
+ name ? find_note_type_by_name(name: name) : find_note_type_by_id(id: id)
58
+ end
59
+
60
+ private
61
+
62
+ def find_note_type_by_name(name:)
63
+ note_types.find { |note_type| note_type.name == name }
64
+ end
65
+
66
+ def find_note_type_by_id(id:)
67
+ note_types.find { |note_type| note_type.id == id }
68
+ end
69
+
70
+ public
71
+
72
+ ##
73
+ # Returns the collection's deck object found by either +name+ or +id+, or nil if it is not found.
74
+ def find_deck_by(name: nil, id: nil)
75
+ if (id && name) || (id.nil? && name.nil?)
76
+ raise ArgumentError,
77
+ "You must pass either an id or name keyword argument."
78
+ end
79
+
80
+ name ? find_deck_by_name(name: name) : find_deck_by_id(id: id)
81
+ end
82
+
83
+ private
84
+
85
+ def find_deck_by_name(name:)
86
+ decks.find { |deck| deck.name == name }
87
+ end
88
+
89
+ def find_deck_by_id(id:)
90
+ decks.find { |deck| deck.id == id }
91
+ end
92
+
93
+ public
94
+
95
+ ##
96
+ # Returns the collection's deck options group object found by +id+, or nil if it is not found.
97
+ def find_deck_options_group_by(id:)
98
+ deck_options_groups.find { |deck_options_group| deck_options_group.id == id }
99
+ end
100
+
101
+ ##
102
+ # Returns the collection's note object found by +id+, or nil if it is not found.
103
+ def find_note_by(id:)
104
+ note_cards_data = note_cards_data_for_note_id sql_able: anki_package, id: id
105
+ return nil unless note_cards_data
106
+
107
+ AnkiRecord::Note.new collection: self, data: note_cards_data
108
+ end
109
+
110
+ def decks_json # :nodoc:
111
+ JSON.parse(anki_package.prepare("select decks from col;").execute.first["decks"])
112
+ end
113
+
114
+ def models_json # :nodoc:
115
+ JSON.parse(anki_package.prepare("select models from col;").execute.first["models"])
116
+ end
117
+
118
+ def copy_over_existing(col_record:) # :nodoc:
119
+ @col_record = col_record
120
+ setup_simple_collaborator_objects
121
+ setup_custom_collaborator_objects
122
+ remove_instance_variable(:@col_record)
123
+ end
124
+
125
+ private
126
+
127
+ def setup_collection_instance_variables(anki_package:)
128
+ @anki_package = anki_package
129
+ setup_simple_collaborator_objects
130
+ setup_custom_collaborator_objects
131
+ remove_instance_variable(:@col_record)
132
+ end
133
+
134
+ def col_record
135
+ @col_record ||= @anki_package.prepare("select * from col").execute.first
136
+ end
137
+
138
+ # rubocop:disable Metrics/AbcSize
139
+ def setup_simple_collaborator_objects
140
+ @id = col_record["id"]
141
+ @created_at_timestamp = col_record["crt"]
142
+ @last_modified_timestamp = col_record["mod"]
143
+ @scm = col_record["scm"]
144
+ @ver = col_record["ver"]
145
+ @dty = col_record["dty"]
146
+ @usn = col_record["usn"]
147
+ @ls = col_record["ls"]
148
+ @configuration = JSON.parse(col_record["conf"])
149
+ @tags = JSON.parse(col_record["tags"])
150
+ end
151
+ # rubocop:enable Metrics/AbcSize
152
+
153
+ def setup_custom_collaborator_objects
154
+ setup_note_type_collaborators
155
+ setup_deck_options_groups_collaborators
156
+ setup_deck_collaborators
157
+ end
158
+
159
+ def setup_note_type_collaborators
160
+ @note_types = []
161
+ JSON.parse(col_record["models"]).values.map do |model_hash|
162
+ NoteType.new(collection: self, args: model_hash)
163
+ end
164
+ end
165
+
166
+ def setup_deck_collaborators
167
+ @decks = []
168
+ JSON.parse(col_record["decks"]).values.map do |deck_hash|
169
+ Deck.new(collection: self, args: deck_hash)
170
+ end
171
+ end
172
+
173
+ def setup_deck_options_groups_collaborators
174
+ @deck_options_groups = []
175
+ JSON.parse(col_record["dconf"]).values.map do |dconf_hash|
176
+ DeckOptionsGroup.new(collection: self, args: dconf_hash)
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ ##
5
+ # Module with the Collection class's attribute readers, writers, and accessors.
6
+ module CollectionAttributes
7
+ ##
8
+ # The collection's Anki package object.
9
+ attr_reader :anki_package
10
+
11
+ ##
12
+ # The collection's id, which is also the id of the col record in the collection.anki21 database (usually 1).
13
+ attr_reader :id
14
+
15
+ ##
16
+ # The number of milliseconds since the 1970 epoch when the collection record was created.
17
+ attr_reader :created_at_timestamp
18
+
19
+ ##
20
+ # The number of milliseconds since the 1970 epoch at which the collection record was last modified.
21
+ attr_reader :last_modified_timestamp
22
+
23
+ ##
24
+ # The collection's note type objects as an array.
25
+ attr_reader :note_types
26
+
27
+ ##
28
+ # The collection's deck objects as an array
29
+ attr_reader :decks
30
+
31
+ ##
32
+ # The collection's deck option group objects as an array.
33
+ attr_reader :deck_options_groups
34
+ end
35
+ end