mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 16:26:40 -04:00

* Remember original id when importing notetype * Reuse notetypes with matching original id * Add field and template ids * Enable merging imported notetypes * Fix test Note should be updated if the incoming note's notetype is remapped to the existing note's notetype. On the other hand, it should be skipped if its notetype id is mapped to some new notetype. * Change field and template ids to i32 * Add merge notetypes flag to proto message * Add dialog for apkg import * Move HelpModal into components * Generalize import dialog * Move SettingTitle into components * Add help modal to ImportAnkiPackagePage * Move SwitchRow into components * Fix backend method import * Make testable in browser * Fix broken modal * Wrap in container and fix margins * Update commented Anki version of new proto fields * Check ids when comparing notetype schemas * Add tooltip for merging notetypes. * Allow updating notes regardless of mtime * Gitignore yarn-error.log * Allow updating notetypes regardless of mtime * Fix apkg help carousel * Use i64s for template and field ids * Add option to omit importing scheduling info * Restore last settings in apkg import dialog * Display error when getting metadata in webview * Update manual links for apkg importing * Apply suggestions from code review Co-authored-by: Damien Elmes <dae@users.noreply.github.com> * Omit schduling -> Import all cards as new cards * Tweak importing-update-notes-help * UpdateCondition → ImportAnkiPackageUpdateCondition * Load keyboard.ftl * Skip updating dupes in 'update alwyas' case * Explain more when merging notetypes is required * "omit scheduling" → "with scheduling" * Skip updating notetype dupes if 'update always' * Merge duplicated notetypes from previous imports * Fix rebase aftermath * Fix panic when merging * Clarify 'update notetypes' help * Mention 'merge notetypes' in the log * Add a test which covers the previously panicking path * Use nested ftl messages to ensure consistency * Make order of merged fields deterministic * Rewrite test to trigger panic * Update version comment on new fields
177 lines
5.4 KiB
Rust
177 lines
5.4 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
#![cfg(test)]
|
|
|
|
use std::collections::HashSet;
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
|
|
use anki_io::read_file;
|
|
use anki_proto::import_export::ImportAnkiPackageOptions;
|
|
|
|
use crate::media::files::sha1_of_data;
|
|
use crate::media::MediaManager;
|
|
use crate::prelude::*;
|
|
use crate::search::SearchNode;
|
|
use crate::tests::open_fs_test_collection;
|
|
|
|
const SAMPLE_JPG: &str = "sample.jpg";
|
|
const SAMPLE_MP3: &str = "sample.mp3";
|
|
const SAMPLE_JS: &str = "_sample.js";
|
|
const JPG_DATA: &[u8] = b"1";
|
|
const MP3_DATA: &[u8] = b"2";
|
|
const JS_DATA: &[u8] = b"3";
|
|
const EXISTING_MP3_DATA: &[u8] = b"4";
|
|
|
|
#[test]
|
|
fn roundtrip() {
|
|
roundtrip_inner(true);
|
|
roundtrip_inner(false);
|
|
}
|
|
|
|
fn roundtrip_inner(legacy: bool) {
|
|
let (mut src_col, src_tempdir) = open_fs_test_collection("src");
|
|
let (mut target_col, _target_tempdir) = open_fs_test_collection("target");
|
|
let apkg_path = src_tempdir.path().join("test.apkg");
|
|
|
|
let (main_deck, sibling_deck) = src_col.add_sample_decks();
|
|
let notetype = src_col.add_sample_notetype();
|
|
let note = src_col.add_sample_note(&main_deck, &sibling_deck, ¬etype);
|
|
src_col.add_sample_media();
|
|
target_col.add_conflicting_media();
|
|
|
|
src_col
|
|
.export_apkg(
|
|
&apkg_path,
|
|
SearchNode::from_deck_name("parent::sample"),
|
|
true,
|
|
true,
|
|
legacy,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
target_col
|
|
.import_apkg(&apkg_path, ImportAnkiPackageOptions::default())
|
|
.unwrap();
|
|
|
|
target_col.assert_decks();
|
|
target_col.assert_notetype(¬etype);
|
|
target_col.assert_note_and_media(¬e);
|
|
|
|
target_col.undo().unwrap();
|
|
target_col.assert_empty();
|
|
}
|
|
|
|
impl Collection {
|
|
fn add_sample_decks(&mut self) -> (Deck, Deck) {
|
|
let sample = self.add_named_deck("parent\x1fsample");
|
|
self.add_named_deck("parent\x1fsample\x1fchild");
|
|
let siblings = self.add_named_deck("siblings");
|
|
|
|
(sample, siblings)
|
|
}
|
|
|
|
fn add_named_deck(&mut self, name: &str) -> Deck {
|
|
let mut deck = Deck::new_normal();
|
|
deck.name = NativeDeckName::from_native_str(name);
|
|
self.add_deck(&mut deck).unwrap();
|
|
deck
|
|
}
|
|
|
|
fn add_sample_notetype(&mut self) -> Notetype {
|
|
let mut nt = Notetype {
|
|
name: "sample".into(),
|
|
..Default::default()
|
|
};
|
|
nt.add_field("sample");
|
|
nt.add_template("sample1", "{{sample}}", "<script src=_sample.js></script>");
|
|
nt.add_template("sample2", "{{sample}}2", "");
|
|
self.add_notetype(&mut nt, true).unwrap();
|
|
nt
|
|
}
|
|
|
|
fn add_sample_note(
|
|
&mut self,
|
|
main_deck: &Deck,
|
|
sibling_decks: &Deck,
|
|
notetype: &Notetype,
|
|
) -> Note {
|
|
let mut sample = notetype.new_note();
|
|
sample.fields_mut()[0] = format!("<img src='{SAMPLE_JPG}'> [sound:{SAMPLE_MP3}]");
|
|
sample.tags = vec!["sample".into()];
|
|
self.add_note(&mut sample, main_deck.id).unwrap();
|
|
|
|
let card = self
|
|
.storage
|
|
.get_card_by_ordinal(sample.id, 1)
|
|
.unwrap()
|
|
.unwrap();
|
|
self.set_deck(&[card.id], sibling_decks.id).unwrap();
|
|
|
|
sample
|
|
}
|
|
|
|
fn add_sample_media(&self) {
|
|
self.add_media(&[
|
|
(SAMPLE_JPG, JPG_DATA),
|
|
(SAMPLE_MP3, MP3_DATA),
|
|
(SAMPLE_JS, JS_DATA),
|
|
]);
|
|
}
|
|
|
|
fn add_conflicting_media(&mut self) {
|
|
let mut file = File::create(self.media_folder.join(SAMPLE_MP3)).unwrap();
|
|
file.write_all(EXISTING_MP3_DATA).unwrap();
|
|
}
|
|
|
|
fn assert_decks(&mut self) {
|
|
let existing_decks: HashSet<_> = self
|
|
.get_all_deck_names(true)
|
|
.unwrap()
|
|
.into_iter()
|
|
.map(|(_, name)| name)
|
|
.collect();
|
|
for deck in ["parent", "parent::sample", "siblings"] {
|
|
assert!(existing_decks.contains(deck));
|
|
}
|
|
assert!(!existing_decks.contains("parent::sample::child"));
|
|
}
|
|
|
|
fn assert_notetype(&mut self, notetype: &Notetype) {
|
|
assert!(self.get_notetype(notetype.id).unwrap().is_some());
|
|
}
|
|
|
|
fn assert_note_and_media(&mut self, note: &Note) {
|
|
let sha1 = sha1_of_data(MP3_DATA);
|
|
let new_mp3_name = format!("sample-{}.mp3", hex::encode(sha1));
|
|
let csums = MediaManager::new(&self.media_folder, &self.media_db)
|
|
.unwrap()
|
|
.all_checksums_as_is();
|
|
|
|
for (fname, orig_data) in [
|
|
(SAMPLE_JPG, JPG_DATA),
|
|
(SAMPLE_MP3, EXISTING_MP3_DATA),
|
|
(new_mp3_name.as_str(), MP3_DATA),
|
|
(SAMPLE_JS, JS_DATA),
|
|
] {
|
|
// data should have been copied correctly
|
|
assert_eq!(
|
|
read_file(&self.media_folder.join(fname)).unwrap(),
|
|
orig_data
|
|
);
|
|
// and checksums in media db should be valid
|
|
assert_eq!(*csums.get(fname).unwrap(), sha1_of_data(orig_data));
|
|
}
|
|
|
|
let imported_note = self.storage.get_note(note.id).unwrap().unwrap();
|
|
assert!(imported_note.fields()[0].contains(&new_mp3_name));
|
|
}
|
|
|
|
fn assert_empty(&self) {
|
|
assert!(self.get_all_deck_names(true).unwrap().is_empty());
|
|
assert!(self.storage.get_all_note_ids().unwrap().is_empty());
|
|
assert!(self.storage.get_all_card_ids().unwrap().is_empty());
|
|
assert!(self.storage.all_tags().unwrap().is_empty());
|
|
}
|
|
}
|