anki_record 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +31 -9
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +4 -2
  6. data/README.md +114 -31
  7. data/anki_record.gemspec +1 -5
  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/card_template.rb +64 -0
  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 -29
  20. data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
  21. data/lib/anki_record/helpers/checksum_helper.rb +2 -5
  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 -16
  35. metadata +26 -16
  36. data/lib/anki_record/anki_package.rb +0 -194
  37. data/lib/anki_record/card.rb +0 -75
  38. data/lib/anki_record/card_template.rb +0 -105
  39. data/lib/anki_record/collection.rb +0 -105
  40. data/lib/anki_record/db/anki_schema_definition.rb +0 -77
  41. data/lib/anki_record/db/clean_collection21_record.rb +0 -10
  42. data/lib/anki_record/db/clean_collection2_record.rb +0 -10
  43. data/lib/anki_record/deck.rb +0 -101
  44. data/lib/anki_record/note.rb +0 -135
  45. data/lib/anki_record/note_field.rb +0 -84
  46. data/lib/anki_record/note_type.rb +0 -233
@@ -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
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "card_template_attributes"
4
+
5
+ module AnkiRecord
6
+ ##
7
+ # CardTemplate represents a card template of an Anki note type.
8
+ class CardTemplate
9
+ include CardTemplateAttributes
10
+
11
+ # Instantiates a new card template with name +name+ for the note type +note_type+.
12
+ def initialize(note_type:, name: nil, args: nil)
13
+ raise ArgumentError unless (name && args.nil?) || (args && args["name"])
14
+
15
+ @note_type = note_type
16
+ if args
17
+ setup_card_template_instance_variables_from_existing(args: args)
18
+ else
19
+ setup_card_template_instance_variables(name: name)
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
+ }
36
+ end
37
+
38
+ private
39
+
40
+ def setup_card_template_instance_variables_from_existing(args:)
41
+ @name = args["name"]
42
+ @ordinal_number = args["ord"]
43
+ @question_format = args["qfmt"]
44
+ @answer_format = args["afmt"]
45
+ @bqfmt = args["bqfmt"]
46
+ @bafmt = args["bafmt"]
47
+ @deck_id = args["did"]
48
+ @browser_font_style = args["bfont"]
49
+ @browser_font_size = args["bsize"]
50
+ end
51
+
52
+ def setup_card_template_instance_variables(name:)
53
+ @name = name
54
+ @ordinal_number = @note_type.card_templates.length
55
+ @question_format = ""
56
+ @answer_format = ""
57
+ @bqfmt = ""
58
+ @bafmt = ""
59
+ @deck_id = nil
60
+ @browser_font_style = ""
61
+ @browser_font_size = 0
62
+ end
63
+ end
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