anki_record 0.1.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +33 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +5 -3
- data/README.md +147 -11
- data/anki_record.gemspec +2 -6
- data/lib/anki_record/anki_package/anki_package.rb +245 -0
- data/lib/anki_record/anki_package/database_setup_constants.rb +91 -0
- data/lib/anki_record/card/card.rb +108 -0
- data/lib/anki_record/card/card_attributes.rb +39 -0
- data/lib/anki_record/{card_template.rb → card_template/card_template.rb} +20 -47
- data/lib/anki_record/card_template/card_template_attributes.rb +69 -0
- data/lib/anki_record/collection/collection.rb +180 -0
- data/lib/anki_record/collection/collection_attributes.rb +35 -0
- data/lib/anki_record/deck/deck.rb +101 -0
- data/lib/anki_record/deck/deck_attributes.rb +30 -0
- data/lib/anki_record/deck/deck_defaults.rb +19 -0
- data/lib/anki_record/{deck_options_group.rb → deck_options_group/deck_options_group.rb} +10 -23
- data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
- data/lib/anki_record/helpers/checksum_helper.rb +17 -0
- data/lib/anki_record/helpers/data_query_helper.rb +13 -0
- data/lib/anki_record/helpers/shared_constants_helper.rb +1 -3
- data/lib/anki_record/helpers/time_helper.rb +7 -5
- data/lib/anki_record/note/note.rb +181 -0
- data/lib/anki_record/note/note_attributes.rb +56 -0
- data/lib/anki_record/note_field/note_field.rb +62 -0
- data/lib/anki_record/note_field/note_field_attributes.rb +39 -0
- data/lib/anki_record/note_field/note_field_defaults.rb +19 -0
- data/lib/anki_record/note_type/note_type.rb +161 -0
- data/lib/anki_record/note_type/note_type_attributes.rb +80 -0
- data/lib/anki_record/note_type/note_type_defaults.rb +38 -0
- data/lib/anki_record/version.rb +1 -1
- data/lib/anki_record.rb +1 -15
- metadata +32 -20
- data/.rdoc_options +0 -27
- data/lib/anki_record/anki_package.rb +0 -183
- data/lib/anki_record/collection.rb +0 -65
- data/lib/anki_record/db/anki_schema_definition.rb +0 -77
- data/lib/anki_record/db/clean_collection21_record.rb +0 -10
- data/lib/anki_record/db/clean_collection2_record.rb +0 -10
- data/lib/anki_record/deck.rb +0 -88
- data/lib/anki_record/note_field.rb +0 -63
- data/lib/anki_record/note_type.rb +0 -147
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "deck_attributes"
|
4
|
+
require_relative "deck_defaults"
|
5
|
+
require_relative "../helpers/shared_constants_helper"
|
6
|
+
require_relative "../helpers/time_helper"
|
7
|
+
|
8
|
+
module AnkiRecord
|
9
|
+
##
|
10
|
+
# Deck represents an Anki deck.
|
11
|
+
# In the collection.anki21 database, the deck is a JSON object
|
12
|
+
# which is part of a larger JSON object: the value of the col record's decks column.
|
13
|
+
class Deck
|
14
|
+
include DeckAttributes
|
15
|
+
include DeckDefaults
|
16
|
+
include SharedConstantsHelper
|
17
|
+
include TimeHelper
|
18
|
+
|
19
|
+
##
|
20
|
+
# Instantiates a new Deck object belonging to +collection+ with name +name+.
|
21
|
+
def initialize(collection:, name: nil, args: nil)
|
22
|
+
raise ArgumentError unless (name && args.nil?) || (args && args["name"])
|
23
|
+
|
24
|
+
@collection = collection
|
25
|
+
if args
|
26
|
+
setup_deck_instance_variables_from_existing(args: args)
|
27
|
+
else
|
28
|
+
setup_deck_instance_variables(name: name)
|
29
|
+
end
|
30
|
+
|
31
|
+
@collection.add_deck self
|
32
|
+
save
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Saves the deck (or updates it) in the collection.anki21 database.
|
37
|
+
def save
|
38
|
+
collection_decks_hash = collection.decks_json
|
39
|
+
collection_decks_hash[@id] = to_h
|
40
|
+
sql = "update col set decks = ? where id = ?"
|
41
|
+
collection.anki_package.prepare(sql).execute([JSON.generate(collection_decks_hash), collection.id])
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_h # :nodoc:
|
45
|
+
{
|
46
|
+
id: @id, mod: @last_modified_timestamp, name: @name, usn: @usn,
|
47
|
+
lrnToday: @learn_today, revToday: @review_today, newToday: @new_today, timeToday: @time_today,
|
48
|
+
collapsed: @collapsed_in_main_window, browserCollapsed: @collapsed_in_browser,
|
49
|
+
desc: @description, dyn: @dyn, conf: @deck_options_group.id,
|
50
|
+
extendNew: @extend_new, extendRev: @extend_review
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# :nodoc:
|
55
|
+
# :nocov:
|
56
|
+
def inspect
|
57
|
+
"#<AnkiRecord::Deck:#{object_id} id: #{id} name: #{name} description: #{description}>"
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# rubocop:disable Metrics/MethodLength
|
63
|
+
# rubocop:disable Metrics/AbcSize
|
64
|
+
def setup_deck_instance_variables_from_existing(args:)
|
65
|
+
@id = args["id"]
|
66
|
+
@last_modified_timestamp = args["mod"]
|
67
|
+
@name = args["name"]
|
68
|
+
@usn = args["usn"]
|
69
|
+
@learn_today = args["lrnToday"]
|
70
|
+
@review_today = args["revToday"]
|
71
|
+
@new_today = args["newToday"]
|
72
|
+
@time_today = args["timeToday"]
|
73
|
+
@collapsed_in_main_window = args["collapsed"]
|
74
|
+
@collapsed_in_browser = args["browserCollapsed"]
|
75
|
+
@description = args["desc"]
|
76
|
+
@dyn = args["dyn"]
|
77
|
+
@deck_options_group = @collection.find_deck_options_group_by id: args["conf"]
|
78
|
+
@extend_new = args["extendNew"]
|
79
|
+
@extend_review = args["extendRev"]
|
80
|
+
end
|
81
|
+
# rubocop:enable Metrics/MethodLength
|
82
|
+
# rubocop:enable Metrics/AbcSize
|
83
|
+
|
84
|
+
# rubocop:disable Metrics/MethodLength
|
85
|
+
def setup_deck_instance_variables(name:)
|
86
|
+
@id = milliseconds_since_epoch
|
87
|
+
@last_modified_timestamp = seconds_since_epoch
|
88
|
+
@name = name
|
89
|
+
@usn = NEW_OBJECT_USN
|
90
|
+
@learn_today = @review_today = @new_today = @time_today = default_deck_today_array
|
91
|
+
@collapsed_in_main_window = default_collapsed
|
92
|
+
@collapsed_in_browser = default_collapsed
|
93
|
+
@description = ""
|
94
|
+
@dyn = NON_FILTERED_DECK_DYN
|
95
|
+
@deck_options_group = @collection.find_deck_options_group_by id: default_deck_options_group_id
|
96
|
+
@extend_new = 0
|
97
|
+
@extend_review = 0
|
98
|
+
end
|
99
|
+
# rubocop:enable Metrics/MethodLength
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
# Module with the Deck class's attribute readers, writers, and accessors.
|
5
|
+
module DeckAttributes
|
6
|
+
##
|
7
|
+
# The deck's collection object.
|
8
|
+
attr_reader :collection
|
9
|
+
|
10
|
+
##
|
11
|
+
# The deck's name.
|
12
|
+
attr_accessor :name
|
13
|
+
|
14
|
+
##
|
15
|
+
# The deck's description.
|
16
|
+
attr_accessor :description
|
17
|
+
|
18
|
+
##
|
19
|
+
# The deck's id.
|
20
|
+
attr_reader :id
|
21
|
+
|
22
|
+
##
|
23
|
+
# The number of seconds since the 1970 epoch when the deck was last modified.
|
24
|
+
attr_reader :last_modified_timestamp
|
25
|
+
|
26
|
+
##
|
27
|
+
# The deck's deck options group object.
|
28
|
+
attr_reader :deck_options_group
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
module DeckDefaults # :nodoc:
|
5
|
+
private
|
6
|
+
|
7
|
+
def default_deck_options_group_id
|
8
|
+
collection.deck_options_groups.min_by(&:id).id
|
9
|
+
end
|
10
|
+
|
11
|
+
def default_deck_today_array
|
12
|
+
[0, 0].freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_collapsed
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,31 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
require_relative "
|
6
|
-
require_relative "helpers/time_helper"
|
3
|
+
require_relative "../helpers/shared_constants_helper"
|
4
|
+
require_relative "../helpers/time_helper"
|
5
|
+
require_relative "deck_options_group_attributes"
|
7
6
|
|
8
7
|
module AnkiRecord
|
9
8
|
##
|
10
|
-
#
|
9
|
+
# DeckOptionsGroup represents a set of options that can be applied to an Anki deck.
|
11
10
|
class DeckOptionsGroup
|
11
|
+
include DeckOptionsGroupAttributes
|
12
12
|
include SharedConstantsHelper
|
13
13
|
include TimeHelper
|
14
14
|
|
15
15
|
##
|
16
|
-
#
|
17
|
-
attr_accessor :name
|
18
|
-
|
19
|
-
##
|
20
|
-
# One of many attributes that is currently read-only and needs to be documented.
|
21
|
-
attr_reader :collection, :id, :last_modified_time, :usn, :max_taken, :auto_play, :timer, :replay_question,
|
22
|
-
:new_options, :review_options, :lapse_options, :dyn, :new_mix, :new_per_day_minimum,
|
23
|
-
:interday_learning_mix, :review_order, :new_sort_order, :new_gather_priority, :bury_interday_learning
|
24
|
-
|
25
|
-
##
|
26
|
-
# Instantiates a new deck options group called +name+ with defaults
|
16
|
+
# Instantiates a new deck options group belonging to +collection+ with name +name+.
|
27
17
|
def initialize(collection:, name: nil, args: nil)
|
28
|
-
# TODO: extract this check to a shared helper
|
29
18
|
raise ArgumentError unless (name && args.nil?) || (args && args["name"])
|
30
19
|
|
31
20
|
@collection = collection
|
@@ -35,6 +24,8 @@ module AnkiRecord
|
|
35
24
|
else
|
36
25
|
setup_deck_options_group_instance_variables(name: name)
|
37
26
|
end
|
27
|
+
|
28
|
+
@collection.add_deck_options_group self
|
38
29
|
end
|
39
30
|
|
40
31
|
private
|
@@ -43,7 +34,7 @@ module AnkiRecord
|
|
43
34
|
# rubocop:disable Metrics/AbcSize
|
44
35
|
def setup_deck_options_group_instance_variables_from_existing(args:)
|
45
36
|
@id = args["id"]
|
46
|
-
@
|
37
|
+
@last_modified_timestamp = args["mod"]
|
47
38
|
@name = args["name"]
|
48
39
|
@usn = args["usn"]
|
49
40
|
@max_taken = args["maxTaken"]
|
@@ -62,14 +53,10 @@ module AnkiRecord
|
|
62
53
|
@new_gather_priority = args["newGatherPriority"]
|
63
54
|
@bury_interday_learning = args["buryInterdayLearning"]
|
64
55
|
end
|
65
|
-
# rubocop:enable Metrics/AbcSize
|
66
|
-
# rubocop:enable Metrics/MethodLength
|
67
56
|
|
68
|
-
# rubocop:disable Metrics/MethodLength
|
69
|
-
# rubocop:disable Metrics/AbcSize
|
70
57
|
def setup_deck_options_group_instance_variables(name:)
|
71
58
|
@id = milliseconds_since_epoch
|
72
|
-
@
|
59
|
+
@last_modified_timestamp = seconds_since_epoch
|
73
60
|
@name = name
|
74
61
|
@usn = NEW_OBJECT_USN
|
75
62
|
@max_taken = 60
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
##
|
5
|
+
# Module with the Card class's attribute readers, writers, and accessors.
|
6
|
+
module DeckOptionsGroupAttributes
|
7
|
+
##
|
8
|
+
# The deck options group's collection object.
|
9
|
+
attr_reader :collection
|
10
|
+
|
11
|
+
##
|
12
|
+
# The deck option group's name.
|
13
|
+
attr_accessor :name
|
14
|
+
|
15
|
+
##
|
16
|
+
# The deck option group's id.
|
17
|
+
attr_reader :id
|
18
|
+
|
19
|
+
##
|
20
|
+
# The number of milliseconds since the 1970 epoch at which the deck options group was modified.
|
21
|
+
attr_reader :last_modified_timestamp
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module AnkiRecord
|
6
|
+
##
|
7
|
+
# A module for the method that calculates the checksum value of notes.
|
8
|
+
#
|
9
|
+
# This checksum may be used by Anki to detect duplicates.
|
10
|
+
module ChecksumHelper
|
11
|
+
##
|
12
|
+
# Returns the integer representation of the first 8 characters of the SHA-1 digest of the +sfld+ argument
|
13
|
+
def checksum(sfld)
|
14
|
+
Digest::SHA1.hexdigest(sfld)[0...8].to_i(16).to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
module DataQueryHelper # :nodoc:
|
5
|
+
def note_cards_data_for_note_id(sql_able:, id:)
|
6
|
+
note_data = sql_able.prepare("select * from notes where id = ?").execute([id]).first
|
7
|
+
return nil unless note_data
|
8
|
+
|
9
|
+
cards_data = sql_able.prepare("select * from cards where nid = ?").execute([id]).to_a
|
10
|
+
{ note_data: note_data, cards_data: cards_data }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,9 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module AnkiRecord
|
4
|
-
|
5
|
-
# Helper module to hold the constants used by multiple classes
|
6
|
-
module SharedConstantsHelper
|
4
|
+
module SharedConstantsHelper # :nodoc:
|
7
5
|
NEW_OBJECT_USN = -1
|
8
6
|
NON_FILTERED_DECK_DYN = 0
|
9
7
|
private_constant :NEW_OBJECT_USN, :NON_FILTERED_DECK_DYN
|
@@ -4,18 +4,20 @@ require "date"
|
|
4
4
|
|
5
5
|
module AnkiRecord
|
6
6
|
##
|
7
|
-
# Helper module to calculate integer time values since the 1970 epoch
|
7
|
+
# Helper module to calculate integer time values since the 1970 epoch.
|
8
8
|
#
|
9
|
-
# Specifically, the time that has passed since 00:00:00 UTC Jan 1 1970
|
9
|
+
# Specifically, the time that has passed since 00:00:00 UTC Jan 1 1970.
|
10
10
|
module TimeHelper
|
11
11
|
##
|
12
|
-
#
|
12
|
+
# Returns approximately the number of milliseconds since the 1970 epoch.
|
13
|
+
# A random amount of milliseconds between -5000 and 5000 is added so that
|
14
|
+
# primary key ids calculated with this should be unique.
|
13
15
|
def milliseconds_since_epoch
|
14
|
-
DateTime.now.strftime("%Q").to_i
|
16
|
+
DateTime.now.strftime("%Q").to_i + rand(-5000..5000)
|
15
17
|
end
|
16
18
|
|
17
19
|
##
|
18
|
-
#
|
20
|
+
# Returns approximately the number of seconds since the 1970 epoch.
|
19
21
|
def seconds_since_epoch
|
20
22
|
Time.now.to_i
|
21
23
|
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
require_relative "../helpers/checksum_helper"
|
6
|
+
require_relative "../helpers/time_helper"
|
7
|
+
require_relative "note_attributes"
|
8
|
+
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
10
|
+
module AnkiRecord
|
11
|
+
##
|
12
|
+
# Represents an Anki note. The note object corresponds to a record in the `notes`
|
13
|
+
# table in the collection.anki21 database.
|
14
|
+
class Note
|
15
|
+
include ChecksumHelper
|
16
|
+
include NoteAttributes
|
17
|
+
include TimeHelper
|
18
|
+
include SharedConstantsHelper
|
19
|
+
|
20
|
+
##
|
21
|
+
# Instantiates a note of type +note_type+ and belonging to deck +deck+.
|
22
|
+
#
|
23
|
+
# If +note_type+ and +deck+ arguments are used, +collection+ and +data should not be given.
|
24
|
+
def initialize(note_type: nil, deck: nil, collection: nil, data: nil)
|
25
|
+
if note_type && deck
|
26
|
+
setup_instance_variables_for_new_note(note_type: note_type, deck: deck)
|
27
|
+
elsif collection && data
|
28
|
+
setup_instance_variables_from_existing(collection: collection,
|
29
|
+
note_data: data[:note_data], cards_data: data[:cards_data])
|
30
|
+
else
|
31
|
+
raise ArgumentError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def setup_instance_variables_for_new_note(note_type:, deck:)
|
38
|
+
raise ArgumentError unless note_type.collection == deck.collection
|
39
|
+
|
40
|
+
setup_collaborator_object_instance_variables_for_new_note(note_type: note_type, deck: deck)
|
41
|
+
setup_simple_instance_variables_for_new_note
|
42
|
+
end
|
43
|
+
|
44
|
+
def setup_collaborator_object_instance_variables_for_new_note(note_type:, deck:)
|
45
|
+
@note_type = note_type
|
46
|
+
@deck = deck
|
47
|
+
@collection = deck.collection
|
48
|
+
@field_contents = setup_empty_field_contents_hash
|
49
|
+
@cards = @note_type.card_templates.map do |card_template|
|
50
|
+
Card.new(note: self, card_template: card_template)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def setup_simple_instance_variables_for_new_note
|
55
|
+
@id = milliseconds_since_epoch
|
56
|
+
@guid = globally_unique_id
|
57
|
+
@last_modified_timestamp = seconds_since_epoch
|
58
|
+
@usn = NEW_OBJECT_USN
|
59
|
+
@tags = []
|
60
|
+
@flags = 0
|
61
|
+
@data = ""
|
62
|
+
end
|
63
|
+
|
64
|
+
def setup_instance_variables_from_existing(collection:, note_data:, cards_data:)
|
65
|
+
setup_collaborator_object_instance_variables_from_existing(collection: collection, note_data: note_data,
|
66
|
+
cards_data: cards_data)
|
67
|
+
setup_simple_instance_variables_from_existing(note_data: note_data)
|
68
|
+
end
|
69
|
+
|
70
|
+
def setup_collaborator_object_instance_variables_from_existing(collection:, note_data:, cards_data:)
|
71
|
+
@collection = collection
|
72
|
+
@note_type = collection.find_note_type_by id: note_data["mid"]
|
73
|
+
@field_contents = setup_field_contents_hash_from_existing(note_data: note_data)
|
74
|
+
@cards = @note_type.card_templates.map.with_index do |_card_template, index|
|
75
|
+
Card.new(note: self, card_data: cards_data[index])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def setup_field_contents_hash_from_existing(note_data:)
|
80
|
+
field_contents = setup_empty_field_contents_hash
|
81
|
+
snake_case_field_names_in_order = note_type.snake_case_field_names
|
82
|
+
note_data["flds"].split("\x1F").each_with_index do |fld, ordinal|
|
83
|
+
field_contents[snake_case_field_names_in_order[ordinal]] = fld
|
84
|
+
end
|
85
|
+
field_contents
|
86
|
+
end
|
87
|
+
|
88
|
+
def setup_simple_instance_variables_from_existing(note_data:)
|
89
|
+
@id = note_data["id"]
|
90
|
+
@guid = note_data["guid"]
|
91
|
+
@last_modified_timestamp = note_data["mod"]
|
92
|
+
@usn = note_data["usn"]
|
93
|
+
@tags = note_data["tags"].split
|
94
|
+
@flags = note_data["flags"]
|
95
|
+
@data = note_data["data"]
|
96
|
+
end
|
97
|
+
|
98
|
+
def setup_empty_field_contents_hash
|
99
|
+
field_contents = {}
|
100
|
+
note_type.snake_case_field_names.each { |field_name| field_contents[field_name] = "" }
|
101
|
+
field_contents
|
102
|
+
end
|
103
|
+
|
104
|
+
public
|
105
|
+
|
106
|
+
##
|
107
|
+
# Saves the note to the collection.anki21 database.
|
108
|
+
#
|
109
|
+
# This also saves the note's cards.
|
110
|
+
def save
|
111
|
+
collection.find_note_by(id: @id) ? update_note_in_collection_anki21 : insert_new_note_in_collection_anki21
|
112
|
+
true
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def update_note_in_collection_anki21
|
118
|
+
statement = @collection.anki_package.prepare <<~SQL
|
119
|
+
update notes set guid = ?, mid = ?, mod = ?, usn = ?, tags = ?,
|
120
|
+
flds = ?, sfld = ?, csum = ?, flags = ?, data = ? where id = ?
|
121
|
+
SQL
|
122
|
+
statement.execute([@guid, note_type.id, @last_modified_timestamp,
|
123
|
+
@usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
|
124
|
+
checksum(sort_field_value), @flags, @data, @id])
|
125
|
+
cards.each { |card| card.save(note_exists_already: true) }
|
126
|
+
end
|
127
|
+
|
128
|
+
def insert_new_note_in_collection_anki21
|
129
|
+
statement = @collection.anki_package.prepare <<~SQL
|
130
|
+
insert into notes (id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data)
|
131
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
132
|
+
SQL
|
133
|
+
statement.execute([@id, @guid, note_type.id, @last_modified_timestamp,
|
134
|
+
@usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
|
135
|
+
checksum(sort_field_value), @flags, @data])
|
136
|
+
cards.each(&:save)
|
137
|
+
end
|
138
|
+
|
139
|
+
public
|
140
|
+
|
141
|
+
##
|
142
|
+
# Overrides BasicObject#method_missing and creates "ghost methods".
|
143
|
+
#
|
144
|
+
# The ghost methods are the setters and getters for the note field values.
|
145
|
+
def method_missing(method_name, field_content = nil)
|
146
|
+
raise NoMethodError, "##{method_name} is not defined or a ghost method" unless respond_to_missing? method_name
|
147
|
+
|
148
|
+
method_name = method_name.to_s
|
149
|
+
return @field_contents[method_name] unless method_name.end_with?("=")
|
150
|
+
|
151
|
+
@field_contents[method_name.chomp("=")] = field_content
|
152
|
+
end
|
153
|
+
|
154
|
+
##
|
155
|
+
# This allows #respond_to? to be accurate for the ghost methods created by #method_missing.
|
156
|
+
def respond_to_missing?(method_name, *)
|
157
|
+
method_name = method_name.to_s
|
158
|
+
if method_name.end_with?("=")
|
159
|
+
note_type.snake_case_field_names.include?(method_name.chomp("="))
|
160
|
+
else
|
161
|
+
note_type.snake_case_field_names.include?(method_name)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def globally_unique_id
|
168
|
+
SecureRandom.uuid.slice(5...15)
|
169
|
+
end
|
170
|
+
|
171
|
+
def field_values_separated_by_us
|
172
|
+
# The ASCII control code represented by hexadecimal 1F is the Unit Separator (US)
|
173
|
+
note_type.snake_case_field_names.map { |field_name| @field_contents[field_name] }.join("\x1F")
|
174
|
+
end
|
175
|
+
|
176
|
+
def sort_field_value
|
177
|
+
@field_contents[note_type.snake_case_sort_field_name]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
# Module with the Note class's attribute readers, writers, and accessors.
|
5
|
+
module NoteAttributes
|
6
|
+
##
|
7
|
+
# The note's id.
|
8
|
+
attr_reader :id
|
9
|
+
|
10
|
+
##
|
11
|
+
# The note's globally unique id.
|
12
|
+
attr_reader :guid
|
13
|
+
|
14
|
+
##
|
15
|
+
# The number of seconds since the 1970 epoch at which the note was last modified.
|
16
|
+
attr_reader :last_modified_timestamp
|
17
|
+
|
18
|
+
##
|
19
|
+
# The note's update sequence number.
|
20
|
+
attr_reader :usn
|
21
|
+
|
22
|
+
##
|
23
|
+
# The note's tags, as an array. The tags are strings.
|
24
|
+
attr_reader :tags
|
25
|
+
|
26
|
+
##
|
27
|
+
# The note's field contents, as a hash.
|
28
|
+
attr_reader :field_contents
|
29
|
+
|
30
|
+
##
|
31
|
+
# The note's deck object.
|
32
|
+
#
|
33
|
+
# When the note is saved, the cards will belong to this deck.
|
34
|
+
attr_reader :deck
|
35
|
+
|
36
|
+
##
|
37
|
+
# The note's note type object.
|
38
|
+
attr_reader :note_type
|
39
|
+
|
40
|
+
##
|
41
|
+
# The note's collection object.
|
42
|
+
attr_reader :collection
|
43
|
+
|
44
|
+
##
|
45
|
+
# The note's card objects, as an array.
|
46
|
+
attr_reader :cards
|
47
|
+
|
48
|
+
##
|
49
|
+
# Corresponds to the flags column in the collection.anki21 notes table.
|
50
|
+
attr_reader :flags
|
51
|
+
|
52
|
+
##
|
53
|
+
# Corresponds to the data column in the collection.anki21 notes table.
|
54
|
+
attr_reader :data
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "note_field_attributes"
|
4
|
+
require_relative "note_field_defaults"
|
5
|
+
|
6
|
+
module AnkiRecord
|
7
|
+
##
|
8
|
+
# NoteField represents a field of an Anki note type.
|
9
|
+
class NoteField
|
10
|
+
include NoteFieldAttributes
|
11
|
+
include NoteFieldDefaults
|
12
|
+
|
13
|
+
##
|
14
|
+
# Instantiates a new field for the note type +note_type+ with name +name+.
|
15
|
+
def initialize(note_type:, name: nil, args: nil)
|
16
|
+
raise ArgumentError unless (name && args.nil?) || (args && args["name"])
|
17
|
+
|
18
|
+
@note_type = note_type
|
19
|
+
if args
|
20
|
+
setup_note_field_instance_variables_from_existing(args: args)
|
21
|
+
else
|
22
|
+
setup_note_field_instance_variables_for_new_field(name: name)
|
23
|
+
end
|
24
|
+
|
25
|
+
@note_type.add_note_field self
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_h # :nodoc:
|
29
|
+
{
|
30
|
+
name: @name,
|
31
|
+
ord: @ordinal_number,
|
32
|
+
sticky: @sticky,
|
33
|
+
rtl: @right_to_left,
|
34
|
+
font: @font_style,
|
35
|
+
size: @font_size,
|
36
|
+
description: @description
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def setup_note_field_instance_variables_from_existing(args:)
|
43
|
+
@name = args["name"]
|
44
|
+
@ordinal_number = args["ord"]
|
45
|
+
@sticky = args["sticky"]
|
46
|
+
@right_to_left = args["rtl"]
|
47
|
+
@font_style = args["font"]
|
48
|
+
@font_size = args["size"]
|
49
|
+
@description = args["description"]
|
50
|
+
end
|
51
|
+
|
52
|
+
def setup_note_field_instance_variables_for_new_field(name:)
|
53
|
+
@name = name
|
54
|
+
@ordinal_number = @note_type.note_fields.length
|
55
|
+
@sticky = false
|
56
|
+
@right_to_left = false
|
57
|
+
@font_style = default_field_font_style
|
58
|
+
@font_size = default_field_font_size
|
59
|
+
@description = default_field_description
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
##
|
5
|
+
# Module with the NoteField class's attribute readers, writers, and accessors.
|
6
|
+
module NoteFieldAttributes
|
7
|
+
##
|
8
|
+
# The field's note type.
|
9
|
+
attr_reader :note_type
|
10
|
+
|
11
|
+
##
|
12
|
+
# The field's name.
|
13
|
+
attr_accessor :name
|
14
|
+
|
15
|
+
##
|
16
|
+
# A boolean that indicates if the field is sticky.
|
17
|
+
attr_accessor :sticky
|
18
|
+
|
19
|
+
##
|
20
|
+
# A boolean that indicates if the field is right to left.
|
21
|
+
attr_accessor :right_to_left
|
22
|
+
|
23
|
+
##
|
24
|
+
# The field's font style used when editing.
|
25
|
+
attr_accessor :font_style
|
26
|
+
|
27
|
+
##
|
28
|
+
# The field's font size used when editing.
|
29
|
+
attr_accessor :font_size
|
30
|
+
|
31
|
+
##
|
32
|
+
# The field's description.
|
33
|
+
attr_accessor :description
|
34
|
+
|
35
|
+
##
|
36
|
+
# 0 for the first field of the note type, 1 for the second, etc.
|
37
|
+
attr_reader :ordinal_number
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
module NoteFieldDefaults # :nodoc:
|
5
|
+
private
|
6
|
+
|
7
|
+
def default_field_font_style
|
8
|
+
"Arial"
|
9
|
+
end
|
10
|
+
|
11
|
+
def default_field_font_size
|
12
|
+
20
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_field_description
|
16
|
+
""
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|