From 4344a33846caffe49757cd548014eb5dec4e6b85 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 18 Nov 2019 21:30:52 +0100 Subject: [PATCH 01/30] Personal Fields - First proof of concept --- crowd_anki/representation/note.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crowd_anki/representation/note.py b/crowd_anki/representation/note.py index f1891e2..d7f4f34 100644 --- a/crowd_anki/representation/note.py +++ b/crowd_anki/representation/note.py @@ -114,7 +114,7 @@ def save_to_collection(self, collection, deck, model_map_cache): else: self.handle_model_update(collection, model_map_cache) - self.anki_object.__dict__.update(self.anki_object_dict) + self.handle_dictionary_update() self.anki_object.mid = note_model.anki_dict["id"] self.anki_object.mod = anki.utils.intTime() self.anki_object.flush() @@ -123,3 +123,12 @@ def save_to_collection(self, collection, deck, model_map_cache): collection.addNote(self.anki_object) elif not self.config.import_notes_ignore_deck_movement: self.move_cards_to_deck(deck.anki_dict["id"]) + + def handle_dictionary_update(self): + fields_personal = self.anki_object_dict["fields"] + for num, field in enumerate(fields_personal): + if field is False: + fields_personal[num] = self.anki_object.fields[num] + + self.anki_object_dict["fields"] = fields_personal + self.anki_object.__dict__.update(self.anki_object_dict) From 4a2f7b44e1e2577b338a0bad1629bc4cc246749e Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Wed, 15 Apr 2020 10:39:02 +0200 Subject: [PATCH 02/30] Added "newlyAdded" to the export_filter_set for Notes --- crowd_anki/representation/note.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crowd_anki/representation/note.py b/crowd_anki/representation/note.py index d7f4f34..9e4b1a5 100644 --- a/crowd_anki/representation/note.py +++ b/crowd_anki/representation/note.py @@ -16,7 +16,8 @@ class Note(JsonSerializableAnkiObject): "_model", # Card model. Would be handled by deck. "mid", # -> uuid "scm", # todo: clarify - "config" + "config", + "newlyAdded" } def __init__(self, anki_note=None, config: ConfigSettings = None): From 79c02264b36f74d00199f596642fb7c97c2d5f54 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 10 May 2020 12:26:56 +0200 Subject: [PATCH 03/30] Start of Import UI Popup --- crowd_anki/config/config_ui.py | 2 +- crowd_anki/importer/anki_importer.py | 56 +++++++++------ crowd_anki/importer/import_dialog.py | 26 +++++++ crowd_anki/importer/import_ui.py | 60 ++++++++++++++++ crowd_anki/utils/constants.py | 1 + generate_ui.sh | 1 + ui_files/import.ui | 103 +++++++++++++++++++++++++++ 7 files changed, 228 insertions(+), 21 deletions(-) create mode 100644 crowd_anki/importer/import_dialog.py create mode 100644 crowd_anki/importer/import_ui.py create mode 100644 ui_files/import.ui diff --git a/crowd_anki/config/config_ui.py b/crowd_anki/config/config_ui.py index 3f4dea2..5dd2660 100644 --- a/crowd_anki/config/config_ui.py +++ b/crowd_anki/config/config_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'ui_files/config.ui' # -# Created by: PyQt5 UI code generator 5.13.1 +# Created by: PyQt5 UI code generator 5.14.2 # # WARNING! All changes made in this file will be lost! diff --git a/crowd_anki/importer/anki_importer.py b/crowd_anki/importer/anki_importer.py index 01e085f..830da38 100644 --- a/crowd_anki/importer/anki_importer.py +++ b/crowd_anki/importer/anki_importer.py @@ -8,29 +8,16 @@ import aqt import aqt.utils from ..representation import deck_initializer -from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME - +from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME, SETTINGS_FIELD_NAME +from ..importer.import_dialog import ImportDialog +from aqt.qt import QDialog class AnkiJsonImporter: def __init__(self, collection, deck_file_name: str = DECK_FILE_NAME): self.collection = collection self.deck_file_name = deck_file_name - def load_from_file(self, file_path): - """ - Load deck from json file - :type file_path: Path - """ - if not file_path.exists(): - raise ValueError("There is no {} file inside of the selected directory".format(file_path)) - - with file_path.open(encoding='utf8') as deck_file: - deck_json = json.load(deck_file) - deck = deck_initializer.from_json(deck_json) - - deck.save_to_collection(self.collection) - - def load_from_directory(self, directory_path, import_media=True): + def load_from_directory(self, deck_json, directory_path, import_media=True): """ Load deck serialized to directory Assumes that deck json file is located in the directory @@ -43,7 +30,8 @@ def load_from_directory(self, directory_path, import_media=True): aqt.mw.progress.start(immediate=True) try: - self.load_from_file(self.get_deck_path(directory_path)) + deck = deck_initializer.from_json(deck_json) + deck.save_to_collection(self.collection) if import_media: media_directory = directory_path.joinpath(MEDIA_SUBDIRECTORY_NAME) @@ -73,12 +61,40 @@ def path_for_name(name): inferred_path = path_for_name(directory_path.name) return convention_path if convention_path.exists() else inferred_path + def load_deck_with_settings(self, directory_path): + deck_json = self.read_file(self.get_deck_path(directory_path)) + + deck_settings = deck_json.get(SETTINGS_FIELD_NAME, []) + + import_dialog = ImportDialog(deck_settings) + if import_dialog.exec_() == QDialog.Rejected: + return None, None + + # TODO: strip settings from deck_json + + return deck_json, deck_settings + + @staticmethod + def read_file(file_path): + """ + Load deck from json file + :type file_path: Path + """ + if not file_path.exists(): + raise ValueError("There is no {} file inside of the selected directory".format(file_path)) + + with file_path.open(encoding='utf8') as deck_file: + return json.load(deck_file) + @staticmethod def import_deck_from_path(collection, directory_path, import_media=True): importer = AnkiJsonImporter(collection) try: - importer.load_from_directory(directory_path, import_media) - aqt.utils.showInfo("Import of {} deck was successful".format(directory_path.name)) + deck_json, deck_settings = importer.load_deck_with_settings(directory_path) + + if deck_json is not None: + importer.load_from_directory(deck_json, directory_path, import_media) + aqt.utils.showInfo("Import of {} deck was successful".format(directory_path.name)) except ValueError as error: aqt.utils.showWarning("Error: {}. While trying to import deck from directory {}".format( error.args[0], directory_path)) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py new file mode 100644 index 0000000..60ca9d0 --- /dev/null +++ b/crowd_anki/importer/import_dialog.py @@ -0,0 +1,26 @@ + +from aqt.qt import * + +from .import_ui import Ui_Dialog as ConfigUI +# from .config_settings import ConfigSettings + + +class ImportDialog(QDialog): + def __init__(self, config, + parent=None): + super().__init__(None) + self.parent = parent + self.config = config + self.form = ConfigUI() + self.form.setupUi(self) + self.ui_initial_setup() + + def accept(self): + super().accept() + + def ui_initial_setup(self): + self.setup_personal_fields() + + def setup_personal_fields(self): + pfs = self.config["personal_fields"] + self.form.list_personal_fields.addItems(pfs) diff --git a/crowd_anki/importer/import_ui.py b/crowd_anki/importer/import_ui.py new file mode 100644 index 0000000..571dcbd --- /dev/null +++ b/crowd_anki/importer/import_ui.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/import.ui' +# +# Created by: PyQt5 UI code generator 5.14.2 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(825, 726) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.group_deck_import = QtWidgets.QGroupBox(Dialog) + self.group_deck_import.setObjectName("group_deck_import") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.group_deck_import) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.cb_ignore_move_cards = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_ignore_move_cards.setObjectName("cb_ignore_move_cards") + self.verticalLayout_5.addWidget(self.cb_ignore_move_cards) + self.list_personal_fields = QtWidgets.QListWidget(self.group_deck_import) + self.list_personal_fields.setObjectName("list_personal_fields") + item = QtWidgets.QListWidgetItem() + item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsUserCheckable|QtCore.Qt.ItemIsEnabled) + item.setCheckState(QtCore.Qt.Checked) + self.list_personal_fields.addItem(item) + self.verticalLayout_5.addWidget(self.list_personal_fields) + self.verticalLayout_2.addWidget(self.group_deck_import) + self.horizontalLayout.addLayout(self.verticalLayout_2) + self.verticalLayout.addLayout(self.horizontalLayout) + self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(Dialog) + self.buttonBox.accepted.connect(Dialog.accept) + self.buttonBox.rejected.connect(Dialog.reject) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "CrowdAnki Configuration")) + self.group_deck_import.setTitle(_translate("Dialog", "Import")) + self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) + __sortingEnabled = self.list_personal_fields.isSortingEnabled() + self.list_personal_fields.setSortingEnabled(False) + item = self.list_personal_fields.item(0) + item.setText(_translate("Dialog", "All Note Models")) + self.list_personal_fields.setSortingEnabled(__sortingEnabled) diff --git a/crowd_anki/utils/constants.py b/crowd_anki/utils/constants.py index c04fc9e..a4cf941 100644 --- a/crowd_anki/utils/constants.py +++ b/crowd_anki/utils/constants.py @@ -2,6 +2,7 @@ UUID_FIELD_NAME = 'crowdanki_uuid' DECK_FILE_NAME = "deck" +SETTINGS_FIELD_NAME = '_settings' DECK_FILE_EXTENSION = ".json" MEDIA_SUBDIRECTORY_NAME = "media" diff --git a/generate_ui.sh b/generate_ui.sh index 3210e72..d9cfb21 100644 --- a/generate_ui.sh +++ b/generate_ui.sh @@ -3,3 +3,4 @@ echo "Generating UI Files" pyuic5 ui_files/config.ui -o crowd_anki/config/config_ui.py +pyuic5 ui_files/import.ui -o crowd_anki/importer/import_ui.py diff --git a/ui_files/import.ui b/ui_files/import.ui new file mode 100644 index 0000000..fc0fb56 --- /dev/null +++ b/ui_files/import.ui @@ -0,0 +1,103 @@ + + + Dialog + + + + 0 + 0 + 825 + 726 + + + + CrowdAnki Configuration + + + + + + + + + + Import + + + + + + Do Not Move Existing Cards + + + + + + + + All Note Models + + + Checked + + + ItemIsSelectable|ItemIsEditable|ItemIsDragEnabled|ItemIsUserCheckable|ItemIsEnabled + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 5065d44e11f577412177a0bf0ad5941aecbddb66 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Fri, 22 May 2020 19:11:42 +0200 Subject: [PATCH 04/30] Working GUI Passthrough; In-progress Usage in system --- crowd_anki/config/config_dialog.py | 19 +-- crowd_anki/importer/anki_importer.py | 32 ++--- crowd_anki/importer/import_dialog.py | 108 +++++++++++++++- crowd_anki/importer/import_ui.py | 75 +++++++++-- crowd_anki/representation/deck_initializer.py | 5 +- crowd_anki/representation/note.py | 26 ++-- crowd_anki/utils/utils.py | 8 ++ ui_files/import.ui | 117 ++++++++++++++++-- 8 files changed, 317 insertions(+), 73 deletions(-) diff --git a/crowd_anki/config/config_dialog.py b/crowd_anki/config/config_dialog.py index 22b78be..b069291 100644 --- a/crowd_anki/config/config_dialog.py +++ b/crowd_anki/config/config_dialog.py @@ -8,6 +8,7 @@ from .config_ui import Ui_Dialog as ConfigUI from .config_settings import ConfigSettings +from ..utils.utils import list_to_cs_string, string_cs_to_list class ConfigDialog(QDialog): @@ -43,9 +44,7 @@ def setup_snapshot_options(self): self.form.cb_automated_snapshot.setChecked(self.config.automated_snapshot) self.form.cb_automated_snapshot.stateChanged.connect(self.toggle_automated_snapshot) - self.form.textedit_snapshot_root_decks.appendPlainText( - self.list_to_cs_string(self.config.snapshot_root_decks) - ) + self.form.textedit_snapshot_root_decks.appendPlainText(list_to_cs_string(self.config.snapshot_root_decks)) self.form.textedit_snapshot_root_decks.textChanged.connect(self.changed_textedit_snapshot_root_decks) def setup_export_options(self): @@ -55,9 +54,7 @@ def setup_export_options(self): self.form.cb_create_deck_subdirectory.setChecked(self.config.export_create_deck_subdirectory) self.form.cb_create_deck_subdirectory.stateChanged.connect(self.toggle_create_deck_subdirectory) - self.form.textedit_deck_sort_methods.appendPlainText( - self.list_to_cs_string(self.config.export_note_sort_methods) - ) + self.form.textedit_deck_sort_methods.appendPlainText(list_to_cs_string(self.config.export_note_sort_methods)) self.form.textedit_deck_sort_methods.textChanged.connect(self.changed_textedit_deck_sort_methods) def setup_import_options(self): @@ -82,15 +79,7 @@ def changed_textedit_deck_sort_methods(self): ) def changed_textedit_snapshot_root_decks(self): - self.config.snapshot_root_decks = self.string_cs_to_list(self.form.textedit_snapshot_root_decks.toPlainText()) + self.config.snapshot_root_decks = string_cs_to_list(self.form.textedit_snapshot_root_decks.toPlainText()) def changed_textedit_snapshot_path(self): self.config.snapshot_path = self.form.textedit_snapshot_path.text() - - @staticmethod - def list_to_cs_string(uf_list: list) -> str: - return ', '.join(uf_list) - - @staticmethod - def string_cs_to_list(f_list: str) -> list: - return [x.strip() for x in f_list.split(',')] diff --git a/crowd_anki/importer/anki_importer.py b/crowd_anki/importer/anki_importer.py index 830da38..18dadd8 100644 --- a/crowd_anki/importer/anki_importer.py +++ b/crowd_anki/importer/anki_importer.py @@ -8,8 +8,8 @@ import aqt import aqt.utils from ..representation import deck_initializer -from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME, SETTINGS_FIELD_NAME -from ..importer.import_dialog import ImportDialog +from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME +from ..importer.import_dialog import ImportDialog, ImportConfig from aqt.qt import QDialog class AnkiJsonImporter: @@ -17,23 +17,24 @@ def __init__(self, collection, deck_file_name: str = DECK_FILE_NAME): self.collection = collection self.deck_file_name = deck_file_name - def load_from_directory(self, deck_json, directory_path, import_media=True): + def load_deck(self, deck_json, directory_path, import_config: ImportConfig): """ Load deck serialized to directory Assumes that deck json file is located in the directory and named 'deck.json' - :param import_media: Should we copy media? - :type directory_path: Path + :param deck_json: The deck json dictionary + :param directory_path: Path + :param import_config: Config data chosen by the user """ if aqt.mw: aqt.mw.backup() aqt.mw.progress.start(immediate=True) try: - deck = deck_initializer.from_json(deck_json) + deck = deck_initializer.from_json(deck_json, import_config) deck.save_to_collection(self.collection) - if import_media: + if import_config.use_media: media_directory = directory_path.joinpath(MEDIA_SUBDIRECTORY_NAME) if media_directory.exists(): unicode_media_directory = str(media_directory) @@ -52,6 +53,7 @@ def load_from_directory(self, deck_json, directory_path, import_media=True): def get_deck_path(self, directory_path): """ Provides compatibility layer between deck file naming conventions. + Assumes that deck json file is located in the directory and named 'deck.json' """ def path_for_name(name): @@ -61,18 +63,16 @@ def path_for_name(name): inferred_path = path_for_name(directory_path.name) return convention_path if convention_path.exists() else inferred_path - def load_deck_with_settings(self, directory_path): + def load_deck_with_settings(self, directory_path) -> (dict, ImportConfig): deck_json = self.read_file(self.get_deck_path(directory_path)) - deck_settings = deck_json.get(SETTINGS_FIELD_NAME, []) - - import_dialog = ImportDialog(deck_settings) + import_dialog = ImportDialog(deck_json) if import_dialog.exec_() == QDialog.Rejected: - return None, None + return None, None # User has cancelled # TODO: strip settings from deck_json - return deck_json, deck_settings + return deck_json, import_dialog.final_import_config @staticmethod def read_file(file_path): @@ -87,13 +87,13 @@ def read_file(file_path): return json.load(deck_file) @staticmethod - def import_deck_from_path(collection, directory_path, import_media=True): + def import_deck_from_path(collection, directory_path): importer = AnkiJsonImporter(collection) try: - deck_json, deck_settings = importer.load_deck_with_settings(directory_path) + deck_json, import_config = importer.load_deck_with_settings(directory_path) if deck_json is not None: - importer.load_from_directory(deck_json, directory_path, import_media) + importer.load_deck(deck_json, directory_path, import_config) aqt.utils.showInfo("Import of {} deck was successful".format(directory_path.name)) except ValueError as error: aqt.utils.showWarning("Error: {}. While trying to import deck from directory {}".format( diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index 60ca9d0..332c6c9 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -1,26 +1,122 @@ +from collections import namedtuple +from dataclasses import dataclass +from enum import Enum from aqt.qt import * +from typing import List, Tuple from .import_ui import Ui_Dialog as ConfigUI # from .config_settings import ConfigSettings +from ..utils.constants import SETTINGS_FIELD_NAME +from aqt.utils import showInfo + +from ..utils.utils import string_cs_to_list + +ConfigEntry = namedtuple("ConfigEntry", ["config_name", "default_value"]) + + +class ImportJsonSetting: + import_message: str + suggest_tag_imported_cards: bool + + class Properties(Enum): + IMPORT_MESSAGE = ConfigEntry("import_message", "") + SUGGEST_TAG_IMPORTED_CARDS = ConfigEntry("suggest_tag_imported_cards", False) + + def __init__(self, settings_dict: dict): + for prop in self.Properties: + setattr(self, prop.value.config_name, settings_dict.get(prop.value.config_name, prop.value.default_value)) + + +class ImportConfig: + personal_fields: List[Tuple[str, str]] = [] + add_tag_to_cards: List[str] = [] + + use_header: bool = True + use_note_models: bool = True + use_notes: bool = True + use_media: bool = True + + def __repr__(self): + return f"ImportConfig({self.personal_fields!r}, {self.add_tag_to_cards!r}, " \ + f"{self.use_header!r}, {self.use_notes!r}, {self.use_note_models!r}, {self.use_media!r})" class ImportDialog(QDialog): - def __init__(self, config, + def __init__(self, deck_json, parent=None): super().__init__(None) self.parent = parent - self.config = config self.form = ConfigUI() self.form.setupUi(self) + self.deck_json = deck_json + self.note_model_name_to_uuid: dict = {} + self.import_settings = ImportJsonSetting(self.deck_json.get(SETTINGS_FIELD_NAME, {})) self.ui_initial_setup() + self.final_import_config: ImportConfig = None + def accept(self): + self.get_import_config() super().accept() + def reject(self): + self.get_import_config() + super().reject() + def ui_initial_setup(self): - self.setup_personal_fields() + self.setup_personal_field_selection() + self.setup_misc() + + def setup_personal_field_selection(self): + def is_field_personal(f): + if "personal_field" not in f: + return False + return f["personal_field"] + + for model in self.deck_json["note_models"]: + self.note_model_name_to_uuid.setdefault(model["name"], model["crowdanki_uuid"]) + + heading_ui = QListWidgetItem(model["name"]) + heading_ui.setFlags(Qt.ItemIsEnabled) + self.form.list_personal_fields.addItem(heading_ui) + + for field in model["flds"]: + field_ui = QListWidgetItem(field["name"]) + field_ui.setCheckState(Qt.Checked if is_field_personal(field) else Qt.Unchecked) + field_ui.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) + self.form.list_personal_fields.addItem(field_ui) + + def setup_misc(self): + self.form.import_message_textbox.setText(self.import_settings.import_message) + + if self.import_settings.suggest_tag_imported_cards: + self.form.cb_tag_cards.setCheckState(Qt.Checked) + self.form.cb_tag_cards.setText("Tag Cards (Suggested by Deck Manager!)") + # else: + # set as default from config settings + + # TODO: Deck Parts to Use + + def get_import_config(self): + config = ImportConfig() + + config.personal_fields = [] + current_note_model = "" + fields: List[QListWidgetItem] = [self.form.list_personal_fields.item(i) for i in range(self.form.list_personal_fields.count())] + for field in fields: + if not field.flags() & Qt.ItemIsUserCheckable: # Note Model Header + current_note_model = self.note_model_name_to_uuid[field.text()] + elif field.checkState() == Qt.Checked: # Field + config.personal_fields.append((current_note_model, field.text())) + + config.add_tag_to_cards = string_cs_to_list(self.form.textedit_tags.text()) if self.form.cb_tag_cards.isChecked() else [] + + config.use_header = self.form.cb_headers.isChecked() + config.use_note_models = self.form.cb_note_models.isChecked() + config.use_notes = self.form.cb_notes.isChecked() + config.use_media = self.form.cb_media.isChecked() + + showInfo(str(config)) - def setup_personal_fields(self): - pfs = self.config["personal_fields"] - self.form.list_personal_fields.addItems(pfs) + self.final_import_config = config diff --git a/crowd_anki/importer/import_ui.py b/crowd_anki/importer/import_ui.py index 571dcbd..3ae275a 100644 --- a/crowd_anki/importer/import_ui.py +++ b/crowd_anki/importer/import_ui.py @@ -24,15 +24,65 @@ def setupUi(self, Dialog): self.group_deck_import.setObjectName("group_deck_import") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.group_deck_import) self.verticalLayout_5.setObjectName("verticalLayout_5") + self.importMessageLayout = QtWidgets.QVBoxLayout() + self.importMessageLayout.setSpacing(6) + self.importMessageLayout.setObjectName("importMessageLayout") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.verticalLayout_4 = QtWidgets.QVBoxLayout() + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.import_message_label = QtWidgets.QLabel(self.group_deck_import) + self.import_message_label.setObjectName("import_message_label") + self.verticalLayout_4.addWidget(self.import_message_label) + self.import_message_textbox = QtWidgets.QTextBrowser(self.group_deck_import) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.import_message_textbox.sizePolicy().hasHeightForWidth()) + self.import_message_textbox.setSizePolicy(sizePolicy) + self.import_message_textbox.setObjectName("import_message_textbox") + self.verticalLayout_4.addWidget(self.import_message_textbox) + self.horizontalLayout_2.addLayout(self.verticalLayout_4) + self.verticalLayout_3 = QtWidgets.QVBoxLayout() + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.verticalLayout_6 = QtWidgets.QVBoxLayout() + self.verticalLayout_6.setObjectName("verticalLayout_6") + self.cb_tag_cards = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_tag_cards.setObjectName("cb_tag_cards") + self.verticalLayout_6.addWidget(self.cb_tag_cards) + self.textedit_tags = QtWidgets.QLineEdit(self.group_deck_import) + self.textedit_tags.setObjectName("textedit_tags") + self.verticalLayout_6.addWidget(self.textedit_tags) + self.verticalLayout_3.addLayout(self.verticalLayout_6) + self.verticalLayout_7 = QtWidgets.QVBoxLayout() + self.verticalLayout_7.setObjectName("verticalLayout_7") + self.label_2 = QtWidgets.QLabel(self.group_deck_import) + self.label_2.setObjectName("label_2") + self.verticalLayout_7.addWidget(self.label_2) + self.cb_headers = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_headers.setObjectName("cb_headers") + self.verticalLayout_7.addWidget(self.cb_headers) + self.cb_note_models = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_note_models.setObjectName("cb_note_models") + self.verticalLayout_7.addWidget(self.cb_note_models) + self.cb_notes = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_notes.setObjectName("cb_notes") + self.verticalLayout_7.addWidget(self.cb_notes) + self.cb_media = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_media.setObjectName("cb_media") + self.verticalLayout_7.addWidget(self.cb_media) + self.verticalLayout_3.addLayout(self.verticalLayout_7) + self.horizontalLayout_2.addLayout(self.verticalLayout_3) + self.importMessageLayout.addLayout(self.horizontalLayout_2) + self.verticalLayout_5.addLayout(self.importMessageLayout) self.cb_ignore_move_cards = QtWidgets.QCheckBox(self.group_deck_import) self.cb_ignore_move_cards.setObjectName("cb_ignore_move_cards") self.verticalLayout_5.addWidget(self.cb_ignore_move_cards) + self.label = QtWidgets.QLabel(self.group_deck_import) + self.label.setObjectName("label") + self.verticalLayout_5.addWidget(self.label) self.list_personal_fields = QtWidgets.QListWidget(self.group_deck_import) self.list_personal_fields.setObjectName("list_personal_fields") - item = QtWidgets.QListWidgetItem() - item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsUserCheckable|QtCore.Qt.ItemIsEnabled) - item.setCheckState(QtCore.Qt.Checked) - self.list_personal_fields.addItem(item) self.verticalLayout_5.addWidget(self.list_personal_fields) self.verticalLayout_2.addWidget(self.group_deck_import) self.horizontalLayout.addLayout(self.verticalLayout_2) @@ -50,11 +100,16 @@ def setupUi(self, Dialog): def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "CrowdAnki Configuration")) + Dialog.setWindowTitle(_translate("Dialog", "CrowdAnki Import Settings")) self.group_deck_import.setTitle(_translate("Dialog", "Import")) + self.import_message_label.setText(_translate("Dialog", "Import Message")) + self.import_message_textbox.setPlaceholderText(_translate("Dialog", "Message from a deck manager about the recent import")) + self.cb_tag_cards.setText(_translate("Dialog", "Tag Cards")) + self.textedit_tags.setPlaceholderText(_translate("Dialog", "Tag1, RecentlyImported, Broken")) + self.label_2.setText(_translate("Dialog", "Deck Parts to Use")) + self.cb_headers.setText(_translate("Dialog", "Headers")) + self.cb_note_models.setText(_translate("Dialog", "Note Models")) + self.cb_notes.setText(_translate("Dialog", "Notes (Cards)")) + self.cb_media.setText(_translate("Dialog", "Media")) self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) - __sortingEnabled = self.list_personal_fields.isSortingEnabled() - self.list_personal_fields.setSortingEnabled(False) - item = self.list_personal_fields.item(0) - item.setText(_translate("Dialog", "All Note Models")) - self.list_personal_fields.setSortingEnabled(__sortingEnabled) + self.label.setText(_translate("Dialog", "Personal Fields - [Explanation of what that is]")) diff --git a/crowd_anki/representation/deck_initializer.py b/crowd_anki/representation/deck_initializer.py index a925d60..b0c0e38 100644 --- a/crowd_anki/representation/deck_initializer.py +++ b/crowd_anki/representation/deck_initializer.py @@ -4,6 +4,7 @@ from .note import Note from ..anki.adapters.anki_deck import AnkiDeck from ..anki.adapters.note_model_file_provider import NoteModelFileProvider +from ..importer.import_dialog import ImportConfig def from_collection(collection, name, deck_metadata=None, is_child=False) -> Deck: @@ -31,7 +32,7 @@ def from_collection(collection, name, deck_metadata=None, is_child=False) -> Dec return deck -def from_json(json_dict, deck_metadata=None) -> Deck: +def from_json(json_dict, import_config: ImportConfig, deck_metadata=None) -> Deck: """load metadata, load notes, load children""" deck = Deck(NoteModelFileProvider, json_dict) deck._update_fields() @@ -41,7 +42,7 @@ def from_json(json_dict, deck_metadata=None) -> Deck: deck._load_metadata_from_json(json_dict) deck.deck_config_uuid = json_dict["deck_config_uuid"] - deck.notes = [Note.from_json(json_note) for json_note in json_dict["notes"]] + deck.notes = [Note.from_json(json_note, import_config) for json_note in json_dict["notes"]] deck.children = [from_json(child, deck.metadata) for child in json_dict["children"]] # Todo should I call this here? diff --git a/crowd_anki/representation/note.py b/crowd_anki/representation/note.py index 9e4b1a5..ba56028 100644 --- a/crowd_anki/representation/note.py +++ b/crowd_anki/representation/note.py @@ -6,8 +6,9 @@ from .json_serializable import JsonSerializableAnkiObject from .note_model import NoteModel from ..anki.overrides.change_model_dialog import ChangeModelDialog +from ..importer.import_dialog import ImportConfig from ..utils.constants import UUID_FIELD_NAME - +from aqt.utils import showInfo class Note(JsonSerializableAnkiObject): export_filter_set = JsonSerializableAnkiObject.export_filter_set | \ @@ -20,10 +21,11 @@ class Note(JsonSerializableAnkiObject): "newlyAdded" } - def __init__(self, anki_note=None, config: ConfigSettings = None): + def __init__(self, anki_note=None, config: ConfigSettings = None, import_config: ImportConfig = None): super(Note, self).__init__(anki_note) self.note_model_uuid = None self.config = config or ConfigSettings.get_instance() + self.import_config = import_config or ImportConfig() @staticmethod def get_notes_from_collection(collection, deck_id, note_models): @@ -43,10 +45,11 @@ def from_collection(cls, collection, note_id, note_models): return note @classmethod - def from_json(cls, json_dict): - note = Note() + def from_json(cls, json_dict, import_config: ImportConfig): + note = Note(import_config=import_config) note.anki_object_dict = json_dict note.note_model_uuid = json_dict["note_model_uuid"] + return note def get_uuid(self): @@ -115,7 +118,7 @@ def save_to_collection(self, collection, deck, model_map_cache): else: self.handle_model_update(collection, model_map_cache) - self.handle_dictionary_update() + self.handle_dictionary_update(note_model) self.anki_object.mid = note_model.anki_dict["id"] self.anki_object.mod = anki.utils.intTime() self.anki_object.flush() @@ -125,11 +128,12 @@ def save_to_collection(self, collection, deck, model_map_cache): elif not self.config.import_notes_ignore_deck_movement: self.move_cards_to_deck(deck.anki_dict["id"]) - def handle_dictionary_update(self): - fields_personal = self.anki_object_dict["fields"] - for num, field in enumerate(fields_personal): - if field is False: - fields_personal[num] = self.anki_object.fields[num] + def handle_dictionary_update(self, note_model): + fields = self.anki_object_dict["fields"] + for num, field in enumerate(fields): + showInfo(str(note_model)) + if True: + fields[num] = self.anki_object.fields[num] - self.anki_object_dict["fields"] = fields_personal + self.anki_object_dict["fields"] = fields self.anki_object.__dict__.update(self.anki_object_dict) diff --git a/crowd_anki/utils/utils.py b/crowd_anki/utils/utils.py index 81f81e7..bc0f129 100644 --- a/crowd_anki/utils/utils.py +++ b/crowd_anki/utils/utils.py @@ -55,3 +55,11 @@ def fs_remove(path): shutil.rmtree(str(path)) else: os.remove(str(path)) + + +def list_to_cs_string(uf_list: list) -> str: + return ', '.join(uf_list) + + +def string_cs_to_list(f_list: str) -> list: + return [x.strip() for x in f_list.split(',')] diff --git a/ui_files/import.ui b/ui_files/import.ui index fc0fb56..1ec4784 100644 --- a/ui_files/import.ui +++ b/ui_files/import.ui @@ -11,7 +11,7 @@ - CrowdAnki Configuration + CrowdAnki Import Settings @@ -24,6 +24,102 @@ Import + + + + 6 + + + + + + + + + Import Message + + + + + + + + 0 + 0 + + + + Message from a deck manager about the recent import + + + + + + + + + + + + + Tag Cards + + + + + + + Tag1, RecentlyImported, Broken + + + + + + + + + + + Deck Parts to Use + + + + + + + Headers + + + + + + + Note Models + + + + + + + Notes (Cards) + + + + + + + Media + + + + + + + + + + + @@ -32,20 +128,15 @@ - - - - All Note Models - - - Checked - - - ItemIsSelectable|ItemIsEditable|ItemIsDragEnabled|ItemIsUserCheckable|ItemIsEnabled - - + + + Personal Fields - [Explanation of what that is] + + + + From 06b26cc60670f3fdd67c6b24c791d5a01d8f0bee Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 24 May 2020 11:55:21 +0200 Subject: [PATCH 05/30] Hotfix Import on Latest --- crowd_anki/anki/overrides/uuid_operations.py | 2 +- crowd_anki/representation/json_serializable.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crowd_anki/anki/overrides/uuid_operations.py b/crowd_anki/anki/overrides/uuid_operations.py index cbdfe53..f240ea7 100644 --- a/crowd_anki/anki/overrides/uuid_operations.py +++ b/crowd_anki/anki/overrides/uuid_operations.py @@ -34,7 +34,7 @@ def get_deck_by_uuid(self, uuid): # Deck configuration def get_deck_configuration_by_uuid(self, uuid): - return get_from_dict_by_uuid(self, "dconf", uuid) + return get_from_dict_by_uuid(self, "decks", uuid) # Note model diff --git a/crowd_anki/representation/json_serializable.py b/crowd_anki/representation/json_serializable.py index cccdd41..2626e33 100644 --- a/crowd_anki/representation/json_serializable.py +++ b/crowd_anki/representation/json_serializable.py @@ -6,8 +6,8 @@ class JsonSerializable: readable_names = {} - export_filter_set = {"mod", # Modification time - "usn", # Todo clarify + export_filter_set = { # "mod", # Modification time + # "usn", # Todo clarify "id"} import_filter_set = {"__type__"} From 67f13bcbc522c6840060bfee6eb74a42806108ff Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 24 May 2020 12:38:38 +0200 Subject: [PATCH 06/30] Personal Field Selection Working fully; Tag imported cards + DoNotMoveCards option --- crowd_anki/importer/import_dialog.py | 18 +++++++++++------- crowd_anki/representation/note.py | 16 +++++++++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index 332c6c9..78c3a07 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -7,8 +7,8 @@ from .import_ui import Ui_Dialog as ConfigUI # from .config_settings import ConfigSettings +from ..config.config_settings import ConfigSettings from ..utils.constants import SETTINGS_FIELD_NAME -from aqt.utils import showInfo from ..utils.utils import string_cs_to_list @@ -37,9 +37,12 @@ class ImportConfig: use_notes: bool = True use_media: bool = True + ignore_deck_movement: bool = False + def __repr__(self): return f"ImportConfig({self.personal_fields!r}, {self.add_tag_to_cards!r}, " \ - f"{self.use_header!r}, {self.use_notes!r}, {self.use_note_models!r}, {self.use_media!r})" + f"{self.use_header!r}, {self.use_notes!r}, {self.use_note_models!r}, {self.use_media!r} " \ + f"{self.ignore_deck_movement!r})" class ImportDialog(QDialog): @@ -49,8 +52,8 @@ def __init__(self, deck_json, self.parent = parent self.form = ConfigUI() self.form.setupUi(self) + self.userConfig = ConfigSettings.get_instance() self.deck_json = deck_json - self.note_model_name_to_uuid: dict = {} self.import_settings = ImportJsonSetting(self.deck_json.get(SETTINGS_FIELD_NAME, {})) self.ui_initial_setup() @@ -75,8 +78,6 @@ def is_field_personal(f): return f["personal_field"] for model in self.deck_json["note_models"]: - self.note_model_name_to_uuid.setdefault(model["name"], model["crowdanki_uuid"]) - heading_ui = QListWidgetItem(model["name"]) heading_ui.setFlags(Qt.ItemIsEnabled) self.form.list_personal_fields.addItem(heading_ui) @@ -96,6 +97,9 @@ def setup_misc(self): # else: # set as default from config settings + if self.userConfig.import_notes_ignore_deck_movement: + self.form.cb_ignore_move_cards.setCheckState(Qt.Checked) + # TODO: Deck Parts to Use def get_import_config(self): @@ -106,7 +110,7 @@ def get_import_config(self): fields: List[QListWidgetItem] = [self.form.list_personal_fields.item(i) for i in range(self.form.list_personal_fields.count())] for field in fields: if not field.flags() & Qt.ItemIsUserCheckable: # Note Model Header - current_note_model = self.note_model_name_to_uuid[field.text()] + current_note_model = field.text() elif field.checkState() == Qt.Checked: # Field config.personal_fields.append((current_note_model, field.text())) @@ -117,6 +121,6 @@ def get_import_config(self): config.use_notes = self.form.cb_notes.isChecked() config.use_media = self.form.cb_media.isChecked() - showInfo(str(config)) + config.ignore_deck_movement = self.form.cb_ignore_move_cards.isChecked() self.final_import_config = config diff --git a/crowd_anki/representation/note.py b/crowd_anki/representation/note.py index ba56028..7740cef 100644 --- a/crowd_anki/representation/note.py +++ b/crowd_anki/representation/note.py @@ -8,7 +8,7 @@ from ..anki.overrides.change_model_dialog import ChangeModelDialog from ..importer.import_dialog import ImportConfig from ..utils.constants import UUID_FIELD_NAME -from aqt.utils import showInfo + class Note(JsonSerializableAnkiObject): export_filter_set = JsonSerializableAnkiObject.export_filter_set | \ @@ -125,15 +125,21 @@ def save_to_collection(self, collection, deck, model_map_cache): if new_note: collection.addNote(self.anki_object) - elif not self.config.import_notes_ignore_deck_movement: + elif not self.import_config.ignore_deck_movement: self.move_cards_to_deck(deck.anki_dict["id"]) def handle_dictionary_update(self, note_model): + + # Personal Fields Resolution fields = self.anki_object_dict["fields"] for num, field in enumerate(fields): - showInfo(str(note_model)) - if True: + model_field_pair = (note_model.anki_dict['name'], note_model.anki_dict['flds'][num]['name']) + if model_field_pair in self.import_config.personal_fields: fields[num] = self.anki_object.fields[num] - + + # Tag Cards on Import + self.anki_object_dict["tags"] += self.import_config.add_tag_to_cards + + # Update dict self.anki_object_dict["fields"] = fields self.anki_object.__dict__.update(self.anki_object_dict) From 96a14fcfd8abff95badceb9cbab8d2bb55bbdd02 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 1 Jun 2020 08:06:38 +0200 Subject: [PATCH 07/30] Revert Hotfix --- crowd_anki/anki/overrides/uuid_operations.py | 2 +- crowd_anki/representation/json_serializable.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crowd_anki/anki/overrides/uuid_operations.py b/crowd_anki/anki/overrides/uuid_operations.py index f240ea7..cbdfe53 100644 --- a/crowd_anki/anki/overrides/uuid_operations.py +++ b/crowd_anki/anki/overrides/uuid_operations.py @@ -34,7 +34,7 @@ def get_deck_by_uuid(self, uuid): # Deck configuration def get_deck_configuration_by_uuid(self, uuid): - return get_from_dict_by_uuid(self, "decks", uuid) + return get_from_dict_by_uuid(self, "dconf", uuid) # Note model diff --git a/crowd_anki/representation/json_serializable.py b/crowd_anki/representation/json_serializable.py index 2626e33..cccdd41 100644 --- a/crowd_anki/representation/json_serializable.py +++ b/crowd_anki/representation/json_serializable.py @@ -6,8 +6,8 @@ class JsonSerializable: readable_names = {} - export_filter_set = { # "mod", # Modification time - # "usn", # Todo clarify + export_filter_set = {"mod", # Modification time + "usn", # Todo clarify "id"} import_filter_set = {"__type__"} From c7d9f352091b3359500b835d9dd56bf8f58162e8 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 1 Jun 2020 08:31:03 +0200 Subject: [PATCH 08/30] Minor Changes --- crowd_anki/importer/import_ui.py | 17 +++++++++-------- ui_files/import.ui | 25 ++++++++++++++----------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/crowd_anki/importer/import_ui.py b/crowd_anki/importer/import_ui.py index 3ae275a..408d97a 100644 --- a/crowd_anki/importer/import_ui.py +++ b/crowd_anki/importer/import_ui.py @@ -21,6 +21,7 @@ def setupUi(self, Dialog): self.verticalLayout_2 = QtWidgets.QVBoxLayout() self.verticalLayout_2.setObjectName("verticalLayout_2") self.group_deck_import = QtWidgets.QGroupBox(Dialog) + self.group_deck_import.setTitle("") self.group_deck_import.setObjectName("group_deck_import") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.group_deck_import) self.verticalLayout_5.setObjectName("verticalLayout_5") @@ -53,10 +54,14 @@ def setupUi(self, Dialog): self.textedit_tags = QtWidgets.QLineEdit(self.group_deck_import) self.textedit_tags.setObjectName("textedit_tags") self.verticalLayout_6.addWidget(self.textedit_tags) + self.cb_ignore_move_cards = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_ignore_move_cards.setObjectName("cb_ignore_move_cards") + self.verticalLayout_6.addWidget(self.cb_ignore_move_cards) self.verticalLayout_3.addLayout(self.verticalLayout_6) self.verticalLayout_7 = QtWidgets.QVBoxLayout() self.verticalLayout_7.setObjectName("verticalLayout_7") self.label_2 = QtWidgets.QLabel(self.group_deck_import) + self.label_2.setEnabled(True) self.label_2.setObjectName("label_2") self.verticalLayout_7.addWidget(self.label_2) self.cb_headers = QtWidgets.QCheckBox(self.group_deck_import) @@ -75,9 +80,6 @@ def setupUi(self, Dialog): self.horizontalLayout_2.addLayout(self.verticalLayout_3) self.importMessageLayout.addLayout(self.horizontalLayout_2) self.verticalLayout_5.addLayout(self.importMessageLayout) - self.cb_ignore_move_cards = QtWidgets.QCheckBox(self.group_deck_import) - self.cb_ignore_move_cards.setObjectName("cb_ignore_move_cards") - self.verticalLayout_5.addWidget(self.cb_ignore_move_cards) self.label = QtWidgets.QLabel(self.group_deck_import) self.label.setObjectName("label") self.verticalLayout_5.addWidget(self.label) @@ -101,15 +103,14 @@ def setupUi(self, Dialog): def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "CrowdAnki Import Settings")) - self.group_deck_import.setTitle(_translate("Dialog", "Import")) - self.import_message_label.setText(_translate("Dialog", "Import Message")) + self.import_message_label.setText(_translate("Dialog", "

Import Message

")) self.import_message_textbox.setPlaceholderText(_translate("Dialog", "Message from a deck manager about the recent import")) self.cb_tag_cards.setText(_translate("Dialog", "Tag Cards")) self.textedit_tags.setPlaceholderText(_translate("Dialog", "Tag1, RecentlyImported, Broken")) - self.label_2.setText(_translate("Dialog", "Deck Parts to Use")) + self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) + self.label_2.setText(_translate("Dialog", "

Deck Parts to Use

")) self.cb_headers.setText(_translate("Dialog", "Headers")) self.cb_note_models.setText(_translate("Dialog", "Note Models")) self.cb_notes.setText(_translate("Dialog", "Notes (Cards)")) self.cb_media.setText(_translate("Dialog", "Media")) - self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) - self.label.setText(_translate("Dialog", "Personal Fields - [Explanation of what that is]")) + self.label.setText(_translate("Dialog", "

Personal Fields - Fields which will keep their existing values instead of being imported

")) diff --git a/ui_files/import.ui b/ui_files/import.ui index 1ec4784..aa9f92e 100644 --- a/ui_files/import.ui +++ b/ui_files/import.ui @@ -21,7 +21,7 @@ - Import + @@ -36,7 +36,7 @@ - Import Message + <html><head/><body><p><span style=" font-weight:600;">Import Message</span></p></body></html> @@ -73,14 +73,24 @@ + + + + Do Not Move Existing Cards + + +
+ + true + - Deck Parts to Use + <html><head/><body><p><span style=" font-weight:600;">Deck Parts to Use</span></p></body></html> @@ -120,17 +130,10 @@ - - - - Do Not Move Existing Cards - - - - Personal Fields - [Explanation of what that is] + <html><head/><body><p><span style=" font-weight:600;">Personal Fields</span> - <span style=" font-style:italic;">Fields which will keep their existing values instead of being imported</span></p></body></html> From 2d859a6d8ac7de092d7f1313e71c1d793cb4379f Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 18 Nov 2019 21:30:52 +0100 Subject: [PATCH 09/30] Personal Fields - First proof of concept --- crowd_anki/representation/note.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crowd_anki/representation/note.py b/crowd_anki/representation/note.py index 8ddd1e5..9e4b1a5 100644 --- a/crowd_anki/representation/note.py +++ b/crowd_anki/representation/note.py @@ -115,7 +115,7 @@ def save_to_collection(self, collection, deck, model_map_cache): else: self.handle_model_update(collection, model_map_cache) - self.anki_object.__dict__.update(self.anki_object_dict) + self.handle_dictionary_update() self.anki_object.mid = note_model.anki_dict["id"] self.anki_object.mod = anki.utils.intTime() self.anki_object.flush() @@ -124,3 +124,12 @@ def save_to_collection(self, collection, deck, model_map_cache): collection.addNote(self.anki_object) elif not self.config.import_notes_ignore_deck_movement: self.move_cards_to_deck(deck.anki_dict["id"]) + + def handle_dictionary_update(self): + fields_personal = self.anki_object_dict["fields"] + for num, field in enumerate(fields_personal): + if field is False: + fields_personal[num] = self.anki_object.fields[num] + + self.anki_object_dict["fields"] = fields_personal + self.anki_object.__dict__.update(self.anki_object_dict) From 76603ae0dd8a9f07a2fcdf68fa843b4e02d75b10 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 10 May 2020 12:26:56 +0200 Subject: [PATCH 10/30] Start of Import UI Popup --- crowd_anki/importer/anki_importer.py | 56 +++++++++------ crowd_anki/importer/import_dialog.py | 26 +++++++ crowd_anki/importer/import_ui.py | 60 ++++++++++++++++ crowd_anki/utils/constants.py | 1 + generate_ui.sh | 1 + ui_files/import.ui | 103 +++++++++++++++++++++++++++ 6 files changed, 227 insertions(+), 20 deletions(-) create mode 100644 crowd_anki/importer/import_dialog.py create mode 100644 crowd_anki/importer/import_ui.py create mode 100644 ui_files/import.ui diff --git a/crowd_anki/importer/anki_importer.py b/crowd_anki/importer/anki_importer.py index 01e085f..830da38 100644 --- a/crowd_anki/importer/anki_importer.py +++ b/crowd_anki/importer/anki_importer.py @@ -8,29 +8,16 @@ import aqt import aqt.utils from ..representation import deck_initializer -from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME - +from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME, SETTINGS_FIELD_NAME +from ..importer.import_dialog import ImportDialog +from aqt.qt import QDialog class AnkiJsonImporter: def __init__(self, collection, deck_file_name: str = DECK_FILE_NAME): self.collection = collection self.deck_file_name = deck_file_name - def load_from_file(self, file_path): - """ - Load deck from json file - :type file_path: Path - """ - if not file_path.exists(): - raise ValueError("There is no {} file inside of the selected directory".format(file_path)) - - with file_path.open(encoding='utf8') as deck_file: - deck_json = json.load(deck_file) - deck = deck_initializer.from_json(deck_json) - - deck.save_to_collection(self.collection) - - def load_from_directory(self, directory_path, import_media=True): + def load_from_directory(self, deck_json, directory_path, import_media=True): """ Load deck serialized to directory Assumes that deck json file is located in the directory @@ -43,7 +30,8 @@ def load_from_directory(self, directory_path, import_media=True): aqt.mw.progress.start(immediate=True) try: - self.load_from_file(self.get_deck_path(directory_path)) + deck = deck_initializer.from_json(deck_json) + deck.save_to_collection(self.collection) if import_media: media_directory = directory_path.joinpath(MEDIA_SUBDIRECTORY_NAME) @@ -73,12 +61,40 @@ def path_for_name(name): inferred_path = path_for_name(directory_path.name) return convention_path if convention_path.exists() else inferred_path + def load_deck_with_settings(self, directory_path): + deck_json = self.read_file(self.get_deck_path(directory_path)) + + deck_settings = deck_json.get(SETTINGS_FIELD_NAME, []) + + import_dialog = ImportDialog(deck_settings) + if import_dialog.exec_() == QDialog.Rejected: + return None, None + + # TODO: strip settings from deck_json + + return deck_json, deck_settings + + @staticmethod + def read_file(file_path): + """ + Load deck from json file + :type file_path: Path + """ + if not file_path.exists(): + raise ValueError("There is no {} file inside of the selected directory".format(file_path)) + + with file_path.open(encoding='utf8') as deck_file: + return json.load(deck_file) + @staticmethod def import_deck_from_path(collection, directory_path, import_media=True): importer = AnkiJsonImporter(collection) try: - importer.load_from_directory(directory_path, import_media) - aqt.utils.showInfo("Import of {} deck was successful".format(directory_path.name)) + deck_json, deck_settings = importer.load_deck_with_settings(directory_path) + + if deck_json is not None: + importer.load_from_directory(deck_json, directory_path, import_media) + aqt.utils.showInfo("Import of {} deck was successful".format(directory_path.name)) except ValueError as error: aqt.utils.showWarning("Error: {}. While trying to import deck from directory {}".format( error.args[0], directory_path)) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py new file mode 100644 index 0000000..60ca9d0 --- /dev/null +++ b/crowd_anki/importer/import_dialog.py @@ -0,0 +1,26 @@ + +from aqt.qt import * + +from .import_ui import Ui_Dialog as ConfigUI +# from .config_settings import ConfigSettings + + +class ImportDialog(QDialog): + def __init__(self, config, + parent=None): + super().__init__(None) + self.parent = parent + self.config = config + self.form = ConfigUI() + self.form.setupUi(self) + self.ui_initial_setup() + + def accept(self): + super().accept() + + def ui_initial_setup(self): + self.setup_personal_fields() + + def setup_personal_fields(self): + pfs = self.config["personal_fields"] + self.form.list_personal_fields.addItems(pfs) diff --git a/crowd_anki/importer/import_ui.py b/crowd_anki/importer/import_ui.py new file mode 100644 index 0000000..571dcbd --- /dev/null +++ b/crowd_anki/importer/import_ui.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/import.ui' +# +# Created by: PyQt5 UI code generator 5.14.2 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(825, 726) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.group_deck_import = QtWidgets.QGroupBox(Dialog) + self.group_deck_import.setObjectName("group_deck_import") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.group_deck_import) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.cb_ignore_move_cards = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_ignore_move_cards.setObjectName("cb_ignore_move_cards") + self.verticalLayout_5.addWidget(self.cb_ignore_move_cards) + self.list_personal_fields = QtWidgets.QListWidget(self.group_deck_import) + self.list_personal_fields.setObjectName("list_personal_fields") + item = QtWidgets.QListWidgetItem() + item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsUserCheckable|QtCore.Qt.ItemIsEnabled) + item.setCheckState(QtCore.Qt.Checked) + self.list_personal_fields.addItem(item) + self.verticalLayout_5.addWidget(self.list_personal_fields) + self.verticalLayout_2.addWidget(self.group_deck_import) + self.horizontalLayout.addLayout(self.verticalLayout_2) + self.verticalLayout.addLayout(self.horizontalLayout) + self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(Dialog) + self.buttonBox.accepted.connect(Dialog.accept) + self.buttonBox.rejected.connect(Dialog.reject) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "CrowdAnki Configuration")) + self.group_deck_import.setTitle(_translate("Dialog", "Import")) + self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) + __sortingEnabled = self.list_personal_fields.isSortingEnabled() + self.list_personal_fields.setSortingEnabled(False) + item = self.list_personal_fields.item(0) + item.setText(_translate("Dialog", "All Note Models")) + self.list_personal_fields.setSortingEnabled(__sortingEnabled) diff --git a/crowd_anki/utils/constants.py b/crowd_anki/utils/constants.py index c04fc9e..a4cf941 100644 --- a/crowd_anki/utils/constants.py +++ b/crowd_anki/utils/constants.py @@ -2,6 +2,7 @@ UUID_FIELD_NAME = 'crowdanki_uuid' DECK_FILE_NAME = "deck" +SETTINGS_FIELD_NAME = '_settings' DECK_FILE_EXTENSION = ".json" MEDIA_SUBDIRECTORY_NAME = "media" diff --git a/generate_ui.sh b/generate_ui.sh index 3210e72..d9cfb21 100755 --- a/generate_ui.sh +++ b/generate_ui.sh @@ -3,3 +3,4 @@ echo "Generating UI Files" pyuic5 ui_files/config.ui -o crowd_anki/config/config_ui.py +pyuic5 ui_files/import.ui -o crowd_anki/importer/import_ui.py diff --git a/ui_files/import.ui b/ui_files/import.ui new file mode 100644 index 0000000..fc0fb56 --- /dev/null +++ b/ui_files/import.ui @@ -0,0 +1,103 @@ + + + Dialog + + + + 0 + 0 + 825 + 726 + + + + CrowdAnki Configuration + + + + + + + + + + Import + + + + + + Do Not Move Existing Cards + + + + + + + + All Note Models + + + Checked + + + ItemIsSelectable|ItemIsEditable|ItemIsDragEnabled|ItemIsUserCheckable|ItemIsEnabled + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From d3d25c6826029ccfa8e92c231cf592d222d7ae32 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Fri, 22 May 2020 19:11:42 +0200 Subject: [PATCH 11/30] Working GUI Passthrough; In-progress Usage in system --- crowd_anki/config/config_dialog.py | 19 +-- crowd_anki/importer/anki_importer.py | 32 ++--- crowd_anki/importer/import_dialog.py | 108 +++++++++++++++- crowd_anki/importer/import_ui.py | 75 +++++++++-- crowd_anki/representation/deck_initializer.py | 5 +- crowd_anki/representation/note.py | 26 ++-- crowd_anki/utils/utils.py | 8 ++ ui_files/import.ui | 117 ++++++++++++++++-- 8 files changed, 317 insertions(+), 73 deletions(-) diff --git a/crowd_anki/config/config_dialog.py b/crowd_anki/config/config_dialog.py index 22b78be..b069291 100644 --- a/crowd_anki/config/config_dialog.py +++ b/crowd_anki/config/config_dialog.py @@ -8,6 +8,7 @@ from .config_ui import Ui_Dialog as ConfigUI from .config_settings import ConfigSettings +from ..utils.utils import list_to_cs_string, string_cs_to_list class ConfigDialog(QDialog): @@ -43,9 +44,7 @@ def setup_snapshot_options(self): self.form.cb_automated_snapshot.setChecked(self.config.automated_snapshot) self.form.cb_automated_snapshot.stateChanged.connect(self.toggle_automated_snapshot) - self.form.textedit_snapshot_root_decks.appendPlainText( - self.list_to_cs_string(self.config.snapshot_root_decks) - ) + self.form.textedit_snapshot_root_decks.appendPlainText(list_to_cs_string(self.config.snapshot_root_decks)) self.form.textedit_snapshot_root_decks.textChanged.connect(self.changed_textedit_snapshot_root_decks) def setup_export_options(self): @@ -55,9 +54,7 @@ def setup_export_options(self): self.form.cb_create_deck_subdirectory.setChecked(self.config.export_create_deck_subdirectory) self.form.cb_create_deck_subdirectory.stateChanged.connect(self.toggle_create_deck_subdirectory) - self.form.textedit_deck_sort_methods.appendPlainText( - self.list_to_cs_string(self.config.export_note_sort_methods) - ) + self.form.textedit_deck_sort_methods.appendPlainText(list_to_cs_string(self.config.export_note_sort_methods)) self.form.textedit_deck_sort_methods.textChanged.connect(self.changed_textedit_deck_sort_methods) def setup_import_options(self): @@ -82,15 +79,7 @@ def changed_textedit_deck_sort_methods(self): ) def changed_textedit_snapshot_root_decks(self): - self.config.snapshot_root_decks = self.string_cs_to_list(self.form.textedit_snapshot_root_decks.toPlainText()) + self.config.snapshot_root_decks = string_cs_to_list(self.form.textedit_snapshot_root_decks.toPlainText()) def changed_textedit_snapshot_path(self): self.config.snapshot_path = self.form.textedit_snapshot_path.text() - - @staticmethod - def list_to_cs_string(uf_list: list) -> str: - return ', '.join(uf_list) - - @staticmethod - def string_cs_to_list(f_list: str) -> list: - return [x.strip() for x in f_list.split(',')] diff --git a/crowd_anki/importer/anki_importer.py b/crowd_anki/importer/anki_importer.py index 830da38..18dadd8 100644 --- a/crowd_anki/importer/anki_importer.py +++ b/crowd_anki/importer/anki_importer.py @@ -8,8 +8,8 @@ import aqt import aqt.utils from ..representation import deck_initializer -from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME, SETTINGS_FIELD_NAME -from ..importer.import_dialog import ImportDialog +from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME +from ..importer.import_dialog import ImportDialog, ImportConfig from aqt.qt import QDialog class AnkiJsonImporter: @@ -17,23 +17,24 @@ def __init__(self, collection, deck_file_name: str = DECK_FILE_NAME): self.collection = collection self.deck_file_name = deck_file_name - def load_from_directory(self, deck_json, directory_path, import_media=True): + def load_deck(self, deck_json, directory_path, import_config: ImportConfig): """ Load deck serialized to directory Assumes that deck json file is located in the directory and named 'deck.json' - :param import_media: Should we copy media? - :type directory_path: Path + :param deck_json: The deck json dictionary + :param directory_path: Path + :param import_config: Config data chosen by the user """ if aqt.mw: aqt.mw.backup() aqt.mw.progress.start(immediate=True) try: - deck = deck_initializer.from_json(deck_json) + deck = deck_initializer.from_json(deck_json, import_config) deck.save_to_collection(self.collection) - if import_media: + if import_config.use_media: media_directory = directory_path.joinpath(MEDIA_SUBDIRECTORY_NAME) if media_directory.exists(): unicode_media_directory = str(media_directory) @@ -52,6 +53,7 @@ def load_from_directory(self, deck_json, directory_path, import_media=True): def get_deck_path(self, directory_path): """ Provides compatibility layer between deck file naming conventions. + Assumes that deck json file is located in the directory and named 'deck.json' """ def path_for_name(name): @@ -61,18 +63,16 @@ def path_for_name(name): inferred_path = path_for_name(directory_path.name) return convention_path if convention_path.exists() else inferred_path - def load_deck_with_settings(self, directory_path): + def load_deck_with_settings(self, directory_path) -> (dict, ImportConfig): deck_json = self.read_file(self.get_deck_path(directory_path)) - deck_settings = deck_json.get(SETTINGS_FIELD_NAME, []) - - import_dialog = ImportDialog(deck_settings) + import_dialog = ImportDialog(deck_json) if import_dialog.exec_() == QDialog.Rejected: - return None, None + return None, None # User has cancelled # TODO: strip settings from deck_json - return deck_json, deck_settings + return deck_json, import_dialog.final_import_config @staticmethod def read_file(file_path): @@ -87,13 +87,13 @@ def read_file(file_path): return json.load(deck_file) @staticmethod - def import_deck_from_path(collection, directory_path, import_media=True): + def import_deck_from_path(collection, directory_path): importer = AnkiJsonImporter(collection) try: - deck_json, deck_settings = importer.load_deck_with_settings(directory_path) + deck_json, import_config = importer.load_deck_with_settings(directory_path) if deck_json is not None: - importer.load_from_directory(deck_json, directory_path, import_media) + importer.load_deck(deck_json, directory_path, import_config) aqt.utils.showInfo("Import of {} deck was successful".format(directory_path.name)) except ValueError as error: aqt.utils.showWarning("Error: {}. While trying to import deck from directory {}".format( diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index 60ca9d0..332c6c9 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -1,26 +1,122 @@ +from collections import namedtuple +from dataclasses import dataclass +from enum import Enum from aqt.qt import * +from typing import List, Tuple from .import_ui import Ui_Dialog as ConfigUI # from .config_settings import ConfigSettings +from ..utils.constants import SETTINGS_FIELD_NAME +from aqt.utils import showInfo + +from ..utils.utils import string_cs_to_list + +ConfigEntry = namedtuple("ConfigEntry", ["config_name", "default_value"]) + + +class ImportJsonSetting: + import_message: str + suggest_tag_imported_cards: bool + + class Properties(Enum): + IMPORT_MESSAGE = ConfigEntry("import_message", "") + SUGGEST_TAG_IMPORTED_CARDS = ConfigEntry("suggest_tag_imported_cards", False) + + def __init__(self, settings_dict: dict): + for prop in self.Properties: + setattr(self, prop.value.config_name, settings_dict.get(prop.value.config_name, prop.value.default_value)) + + +class ImportConfig: + personal_fields: List[Tuple[str, str]] = [] + add_tag_to_cards: List[str] = [] + + use_header: bool = True + use_note_models: bool = True + use_notes: bool = True + use_media: bool = True + + def __repr__(self): + return f"ImportConfig({self.personal_fields!r}, {self.add_tag_to_cards!r}, " \ + f"{self.use_header!r}, {self.use_notes!r}, {self.use_note_models!r}, {self.use_media!r})" class ImportDialog(QDialog): - def __init__(self, config, + def __init__(self, deck_json, parent=None): super().__init__(None) self.parent = parent - self.config = config self.form = ConfigUI() self.form.setupUi(self) + self.deck_json = deck_json + self.note_model_name_to_uuid: dict = {} + self.import_settings = ImportJsonSetting(self.deck_json.get(SETTINGS_FIELD_NAME, {})) self.ui_initial_setup() + self.final_import_config: ImportConfig = None + def accept(self): + self.get_import_config() super().accept() + def reject(self): + self.get_import_config() + super().reject() + def ui_initial_setup(self): - self.setup_personal_fields() + self.setup_personal_field_selection() + self.setup_misc() + + def setup_personal_field_selection(self): + def is_field_personal(f): + if "personal_field" not in f: + return False + return f["personal_field"] + + for model in self.deck_json["note_models"]: + self.note_model_name_to_uuid.setdefault(model["name"], model["crowdanki_uuid"]) + + heading_ui = QListWidgetItem(model["name"]) + heading_ui.setFlags(Qt.ItemIsEnabled) + self.form.list_personal_fields.addItem(heading_ui) + + for field in model["flds"]: + field_ui = QListWidgetItem(field["name"]) + field_ui.setCheckState(Qt.Checked if is_field_personal(field) else Qt.Unchecked) + field_ui.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) + self.form.list_personal_fields.addItem(field_ui) + + def setup_misc(self): + self.form.import_message_textbox.setText(self.import_settings.import_message) + + if self.import_settings.suggest_tag_imported_cards: + self.form.cb_tag_cards.setCheckState(Qt.Checked) + self.form.cb_tag_cards.setText("Tag Cards (Suggested by Deck Manager!)") + # else: + # set as default from config settings + + # TODO: Deck Parts to Use + + def get_import_config(self): + config = ImportConfig() + + config.personal_fields = [] + current_note_model = "" + fields: List[QListWidgetItem] = [self.form.list_personal_fields.item(i) for i in range(self.form.list_personal_fields.count())] + for field in fields: + if not field.flags() & Qt.ItemIsUserCheckable: # Note Model Header + current_note_model = self.note_model_name_to_uuid[field.text()] + elif field.checkState() == Qt.Checked: # Field + config.personal_fields.append((current_note_model, field.text())) + + config.add_tag_to_cards = string_cs_to_list(self.form.textedit_tags.text()) if self.form.cb_tag_cards.isChecked() else [] + + config.use_header = self.form.cb_headers.isChecked() + config.use_note_models = self.form.cb_note_models.isChecked() + config.use_notes = self.form.cb_notes.isChecked() + config.use_media = self.form.cb_media.isChecked() + + showInfo(str(config)) - def setup_personal_fields(self): - pfs = self.config["personal_fields"] - self.form.list_personal_fields.addItems(pfs) + self.final_import_config = config diff --git a/crowd_anki/importer/import_ui.py b/crowd_anki/importer/import_ui.py index 571dcbd..3ae275a 100644 --- a/crowd_anki/importer/import_ui.py +++ b/crowd_anki/importer/import_ui.py @@ -24,15 +24,65 @@ def setupUi(self, Dialog): self.group_deck_import.setObjectName("group_deck_import") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.group_deck_import) self.verticalLayout_5.setObjectName("verticalLayout_5") + self.importMessageLayout = QtWidgets.QVBoxLayout() + self.importMessageLayout.setSpacing(6) + self.importMessageLayout.setObjectName("importMessageLayout") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.verticalLayout_4 = QtWidgets.QVBoxLayout() + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.import_message_label = QtWidgets.QLabel(self.group_deck_import) + self.import_message_label.setObjectName("import_message_label") + self.verticalLayout_4.addWidget(self.import_message_label) + self.import_message_textbox = QtWidgets.QTextBrowser(self.group_deck_import) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.import_message_textbox.sizePolicy().hasHeightForWidth()) + self.import_message_textbox.setSizePolicy(sizePolicy) + self.import_message_textbox.setObjectName("import_message_textbox") + self.verticalLayout_4.addWidget(self.import_message_textbox) + self.horizontalLayout_2.addLayout(self.verticalLayout_4) + self.verticalLayout_3 = QtWidgets.QVBoxLayout() + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.verticalLayout_6 = QtWidgets.QVBoxLayout() + self.verticalLayout_6.setObjectName("verticalLayout_6") + self.cb_tag_cards = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_tag_cards.setObjectName("cb_tag_cards") + self.verticalLayout_6.addWidget(self.cb_tag_cards) + self.textedit_tags = QtWidgets.QLineEdit(self.group_deck_import) + self.textedit_tags.setObjectName("textedit_tags") + self.verticalLayout_6.addWidget(self.textedit_tags) + self.verticalLayout_3.addLayout(self.verticalLayout_6) + self.verticalLayout_7 = QtWidgets.QVBoxLayout() + self.verticalLayout_7.setObjectName("verticalLayout_7") + self.label_2 = QtWidgets.QLabel(self.group_deck_import) + self.label_2.setObjectName("label_2") + self.verticalLayout_7.addWidget(self.label_2) + self.cb_headers = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_headers.setObjectName("cb_headers") + self.verticalLayout_7.addWidget(self.cb_headers) + self.cb_note_models = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_note_models.setObjectName("cb_note_models") + self.verticalLayout_7.addWidget(self.cb_note_models) + self.cb_notes = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_notes.setObjectName("cb_notes") + self.verticalLayout_7.addWidget(self.cb_notes) + self.cb_media = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_media.setObjectName("cb_media") + self.verticalLayout_7.addWidget(self.cb_media) + self.verticalLayout_3.addLayout(self.verticalLayout_7) + self.horizontalLayout_2.addLayout(self.verticalLayout_3) + self.importMessageLayout.addLayout(self.horizontalLayout_2) + self.verticalLayout_5.addLayout(self.importMessageLayout) self.cb_ignore_move_cards = QtWidgets.QCheckBox(self.group_deck_import) self.cb_ignore_move_cards.setObjectName("cb_ignore_move_cards") self.verticalLayout_5.addWidget(self.cb_ignore_move_cards) + self.label = QtWidgets.QLabel(self.group_deck_import) + self.label.setObjectName("label") + self.verticalLayout_5.addWidget(self.label) self.list_personal_fields = QtWidgets.QListWidget(self.group_deck_import) self.list_personal_fields.setObjectName("list_personal_fields") - item = QtWidgets.QListWidgetItem() - item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsUserCheckable|QtCore.Qt.ItemIsEnabled) - item.setCheckState(QtCore.Qt.Checked) - self.list_personal_fields.addItem(item) self.verticalLayout_5.addWidget(self.list_personal_fields) self.verticalLayout_2.addWidget(self.group_deck_import) self.horizontalLayout.addLayout(self.verticalLayout_2) @@ -50,11 +100,16 @@ def setupUi(self, Dialog): def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "CrowdAnki Configuration")) + Dialog.setWindowTitle(_translate("Dialog", "CrowdAnki Import Settings")) self.group_deck_import.setTitle(_translate("Dialog", "Import")) + self.import_message_label.setText(_translate("Dialog", "Import Message")) + self.import_message_textbox.setPlaceholderText(_translate("Dialog", "Message from a deck manager about the recent import")) + self.cb_tag_cards.setText(_translate("Dialog", "Tag Cards")) + self.textedit_tags.setPlaceholderText(_translate("Dialog", "Tag1, RecentlyImported, Broken")) + self.label_2.setText(_translate("Dialog", "Deck Parts to Use")) + self.cb_headers.setText(_translate("Dialog", "Headers")) + self.cb_note_models.setText(_translate("Dialog", "Note Models")) + self.cb_notes.setText(_translate("Dialog", "Notes (Cards)")) + self.cb_media.setText(_translate("Dialog", "Media")) self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) - __sortingEnabled = self.list_personal_fields.isSortingEnabled() - self.list_personal_fields.setSortingEnabled(False) - item = self.list_personal_fields.item(0) - item.setText(_translate("Dialog", "All Note Models")) - self.list_personal_fields.setSortingEnabled(__sortingEnabled) + self.label.setText(_translate("Dialog", "Personal Fields - [Explanation of what that is]")) diff --git a/crowd_anki/representation/deck_initializer.py b/crowd_anki/representation/deck_initializer.py index a925d60..b0c0e38 100644 --- a/crowd_anki/representation/deck_initializer.py +++ b/crowd_anki/representation/deck_initializer.py @@ -4,6 +4,7 @@ from .note import Note from ..anki.adapters.anki_deck import AnkiDeck from ..anki.adapters.note_model_file_provider import NoteModelFileProvider +from ..importer.import_dialog import ImportConfig def from_collection(collection, name, deck_metadata=None, is_child=False) -> Deck: @@ -31,7 +32,7 @@ def from_collection(collection, name, deck_metadata=None, is_child=False) -> Dec return deck -def from_json(json_dict, deck_metadata=None) -> Deck: +def from_json(json_dict, import_config: ImportConfig, deck_metadata=None) -> Deck: """load metadata, load notes, load children""" deck = Deck(NoteModelFileProvider, json_dict) deck._update_fields() @@ -41,7 +42,7 @@ def from_json(json_dict, deck_metadata=None) -> Deck: deck._load_metadata_from_json(json_dict) deck.deck_config_uuid = json_dict["deck_config_uuid"] - deck.notes = [Note.from_json(json_note) for json_note in json_dict["notes"]] + deck.notes = [Note.from_json(json_note, import_config) for json_note in json_dict["notes"]] deck.children = [from_json(child, deck.metadata) for child in json_dict["children"]] # Todo should I call this here? diff --git a/crowd_anki/representation/note.py b/crowd_anki/representation/note.py index 9e4b1a5..ba56028 100644 --- a/crowd_anki/representation/note.py +++ b/crowd_anki/representation/note.py @@ -6,8 +6,9 @@ from .json_serializable import JsonSerializableAnkiObject from .note_model import NoteModel from ..anki.overrides.change_model_dialog import ChangeModelDialog +from ..importer.import_dialog import ImportConfig from ..utils.constants import UUID_FIELD_NAME - +from aqt.utils import showInfo class Note(JsonSerializableAnkiObject): export_filter_set = JsonSerializableAnkiObject.export_filter_set | \ @@ -20,10 +21,11 @@ class Note(JsonSerializableAnkiObject): "newlyAdded" } - def __init__(self, anki_note=None, config: ConfigSettings = None): + def __init__(self, anki_note=None, config: ConfigSettings = None, import_config: ImportConfig = None): super(Note, self).__init__(anki_note) self.note_model_uuid = None self.config = config or ConfigSettings.get_instance() + self.import_config = import_config or ImportConfig() @staticmethod def get_notes_from_collection(collection, deck_id, note_models): @@ -43,10 +45,11 @@ def from_collection(cls, collection, note_id, note_models): return note @classmethod - def from_json(cls, json_dict): - note = Note() + def from_json(cls, json_dict, import_config: ImportConfig): + note = Note(import_config=import_config) note.anki_object_dict = json_dict note.note_model_uuid = json_dict["note_model_uuid"] + return note def get_uuid(self): @@ -115,7 +118,7 @@ def save_to_collection(self, collection, deck, model_map_cache): else: self.handle_model_update(collection, model_map_cache) - self.handle_dictionary_update() + self.handle_dictionary_update(note_model) self.anki_object.mid = note_model.anki_dict["id"] self.anki_object.mod = anki.utils.intTime() self.anki_object.flush() @@ -125,11 +128,12 @@ def save_to_collection(self, collection, deck, model_map_cache): elif not self.config.import_notes_ignore_deck_movement: self.move_cards_to_deck(deck.anki_dict["id"]) - def handle_dictionary_update(self): - fields_personal = self.anki_object_dict["fields"] - for num, field in enumerate(fields_personal): - if field is False: - fields_personal[num] = self.anki_object.fields[num] + def handle_dictionary_update(self, note_model): + fields = self.anki_object_dict["fields"] + for num, field in enumerate(fields): + showInfo(str(note_model)) + if True: + fields[num] = self.anki_object.fields[num] - self.anki_object_dict["fields"] = fields_personal + self.anki_object_dict["fields"] = fields self.anki_object.__dict__.update(self.anki_object_dict) diff --git a/crowd_anki/utils/utils.py b/crowd_anki/utils/utils.py index 81f81e7..bc0f129 100644 --- a/crowd_anki/utils/utils.py +++ b/crowd_anki/utils/utils.py @@ -55,3 +55,11 @@ def fs_remove(path): shutil.rmtree(str(path)) else: os.remove(str(path)) + + +def list_to_cs_string(uf_list: list) -> str: + return ', '.join(uf_list) + + +def string_cs_to_list(f_list: str) -> list: + return [x.strip() for x in f_list.split(',')] diff --git a/ui_files/import.ui b/ui_files/import.ui index fc0fb56..1ec4784 100644 --- a/ui_files/import.ui +++ b/ui_files/import.ui @@ -11,7 +11,7 @@ - CrowdAnki Configuration + CrowdAnki Import Settings @@ -24,6 +24,102 @@ Import + + + + 6 + + + + + + + + + Import Message + + + + + + + + 0 + 0 + + + + Message from a deck manager about the recent import + + + + + + + + + + + + + Tag Cards + + + + + + + Tag1, RecentlyImported, Broken + + + + + + + + + + + Deck Parts to Use + + + + + + + Headers + + + + + + + Note Models + + + + + + + Notes (Cards) + + + + + + + Media + + + + + + + + + + + @@ -32,20 +128,15 @@ - - - - All Note Models - - - Checked - - - ItemIsSelectable|ItemIsEditable|ItemIsDragEnabled|ItemIsUserCheckable|ItemIsEnabled - - + + + Personal Fields - [Explanation of what that is] + + + + From a35972987c821d0cfe74e58a04ae7d19c6f74dfd Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 24 May 2020 11:55:21 +0200 Subject: [PATCH 12/30] Hotfix Import on Latest --- crowd_anki/anki/overrides/uuid_operations.py | 2 +- crowd_anki/representation/json_serializable.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crowd_anki/anki/overrides/uuid_operations.py b/crowd_anki/anki/overrides/uuid_operations.py index cbdfe53..f240ea7 100644 --- a/crowd_anki/anki/overrides/uuid_operations.py +++ b/crowd_anki/anki/overrides/uuid_operations.py @@ -34,7 +34,7 @@ def get_deck_by_uuid(self, uuid): # Deck configuration def get_deck_configuration_by_uuid(self, uuid): - return get_from_dict_by_uuid(self, "dconf", uuid) + return get_from_dict_by_uuid(self, "decks", uuid) # Note model diff --git a/crowd_anki/representation/json_serializable.py b/crowd_anki/representation/json_serializable.py index cccdd41..2626e33 100644 --- a/crowd_anki/representation/json_serializable.py +++ b/crowd_anki/representation/json_serializable.py @@ -6,8 +6,8 @@ class JsonSerializable: readable_names = {} - export_filter_set = {"mod", # Modification time - "usn", # Todo clarify + export_filter_set = { # "mod", # Modification time + # "usn", # Todo clarify "id"} import_filter_set = {"__type__"} From 9d574aa0536f3a10fce8ebc1f0ac264fca5f133d Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 24 May 2020 12:38:38 +0200 Subject: [PATCH 13/30] Personal Field Selection Working fully; Tag imported cards + DoNotMoveCards option --- crowd_anki/importer/import_dialog.py | 18 +++++++++++------- crowd_anki/representation/note.py | 16 +++++++++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index 332c6c9..78c3a07 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -7,8 +7,8 @@ from .import_ui import Ui_Dialog as ConfigUI # from .config_settings import ConfigSettings +from ..config.config_settings import ConfigSettings from ..utils.constants import SETTINGS_FIELD_NAME -from aqt.utils import showInfo from ..utils.utils import string_cs_to_list @@ -37,9 +37,12 @@ class ImportConfig: use_notes: bool = True use_media: bool = True + ignore_deck_movement: bool = False + def __repr__(self): return f"ImportConfig({self.personal_fields!r}, {self.add_tag_to_cards!r}, " \ - f"{self.use_header!r}, {self.use_notes!r}, {self.use_note_models!r}, {self.use_media!r})" + f"{self.use_header!r}, {self.use_notes!r}, {self.use_note_models!r}, {self.use_media!r} " \ + f"{self.ignore_deck_movement!r})" class ImportDialog(QDialog): @@ -49,8 +52,8 @@ def __init__(self, deck_json, self.parent = parent self.form = ConfigUI() self.form.setupUi(self) + self.userConfig = ConfigSettings.get_instance() self.deck_json = deck_json - self.note_model_name_to_uuid: dict = {} self.import_settings = ImportJsonSetting(self.deck_json.get(SETTINGS_FIELD_NAME, {})) self.ui_initial_setup() @@ -75,8 +78,6 @@ def is_field_personal(f): return f["personal_field"] for model in self.deck_json["note_models"]: - self.note_model_name_to_uuid.setdefault(model["name"], model["crowdanki_uuid"]) - heading_ui = QListWidgetItem(model["name"]) heading_ui.setFlags(Qt.ItemIsEnabled) self.form.list_personal_fields.addItem(heading_ui) @@ -96,6 +97,9 @@ def setup_misc(self): # else: # set as default from config settings + if self.userConfig.import_notes_ignore_deck_movement: + self.form.cb_ignore_move_cards.setCheckState(Qt.Checked) + # TODO: Deck Parts to Use def get_import_config(self): @@ -106,7 +110,7 @@ def get_import_config(self): fields: List[QListWidgetItem] = [self.form.list_personal_fields.item(i) for i in range(self.form.list_personal_fields.count())] for field in fields: if not field.flags() & Qt.ItemIsUserCheckable: # Note Model Header - current_note_model = self.note_model_name_to_uuid[field.text()] + current_note_model = field.text() elif field.checkState() == Qt.Checked: # Field config.personal_fields.append((current_note_model, field.text())) @@ -117,6 +121,6 @@ def get_import_config(self): config.use_notes = self.form.cb_notes.isChecked() config.use_media = self.form.cb_media.isChecked() - showInfo(str(config)) + config.ignore_deck_movement = self.form.cb_ignore_move_cards.isChecked() self.final_import_config = config diff --git a/crowd_anki/representation/note.py b/crowd_anki/representation/note.py index ba56028..7740cef 100644 --- a/crowd_anki/representation/note.py +++ b/crowd_anki/representation/note.py @@ -8,7 +8,7 @@ from ..anki.overrides.change_model_dialog import ChangeModelDialog from ..importer.import_dialog import ImportConfig from ..utils.constants import UUID_FIELD_NAME -from aqt.utils import showInfo + class Note(JsonSerializableAnkiObject): export_filter_set = JsonSerializableAnkiObject.export_filter_set | \ @@ -125,15 +125,21 @@ def save_to_collection(self, collection, deck, model_map_cache): if new_note: collection.addNote(self.anki_object) - elif not self.config.import_notes_ignore_deck_movement: + elif not self.import_config.ignore_deck_movement: self.move_cards_to_deck(deck.anki_dict["id"]) def handle_dictionary_update(self, note_model): + + # Personal Fields Resolution fields = self.anki_object_dict["fields"] for num, field in enumerate(fields): - showInfo(str(note_model)) - if True: + model_field_pair = (note_model.anki_dict['name'], note_model.anki_dict['flds'][num]['name']) + if model_field_pair in self.import_config.personal_fields: fields[num] = self.anki_object.fields[num] - + + # Tag Cards on Import + self.anki_object_dict["tags"] += self.import_config.add_tag_to_cards + + # Update dict self.anki_object_dict["fields"] = fields self.anki_object.__dict__.update(self.anki_object_dict) From ac2c312e7c78907729597fa975e3f831aec1956a Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 1 Jun 2020 08:06:38 +0200 Subject: [PATCH 14/30] Revert Hotfix --- crowd_anki/anki/overrides/uuid_operations.py | 2 +- crowd_anki/representation/json_serializable.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crowd_anki/anki/overrides/uuid_operations.py b/crowd_anki/anki/overrides/uuid_operations.py index f240ea7..cbdfe53 100644 --- a/crowd_anki/anki/overrides/uuid_operations.py +++ b/crowd_anki/anki/overrides/uuid_operations.py @@ -34,7 +34,7 @@ def get_deck_by_uuid(self, uuid): # Deck configuration def get_deck_configuration_by_uuid(self, uuid): - return get_from_dict_by_uuid(self, "decks", uuid) + return get_from_dict_by_uuid(self, "dconf", uuid) # Note model diff --git a/crowd_anki/representation/json_serializable.py b/crowd_anki/representation/json_serializable.py index 2626e33..cccdd41 100644 --- a/crowd_anki/representation/json_serializable.py +++ b/crowd_anki/representation/json_serializable.py @@ -6,8 +6,8 @@ class JsonSerializable: readable_names = {} - export_filter_set = { # "mod", # Modification time - # "usn", # Todo clarify + export_filter_set = {"mod", # Modification time + "usn", # Todo clarify "id"} import_filter_set = {"__type__"} From a39dcb4cc92563d99093c3b79e08ffe80e35ed5d Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 1 Jun 2020 08:31:03 +0200 Subject: [PATCH 15/30] Minor Changes --- crowd_anki/importer/import_ui.py | 17 +++++++++-------- ui_files/import.ui | 25 ++++++++++++++----------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/crowd_anki/importer/import_ui.py b/crowd_anki/importer/import_ui.py index 3ae275a..408d97a 100644 --- a/crowd_anki/importer/import_ui.py +++ b/crowd_anki/importer/import_ui.py @@ -21,6 +21,7 @@ def setupUi(self, Dialog): self.verticalLayout_2 = QtWidgets.QVBoxLayout() self.verticalLayout_2.setObjectName("verticalLayout_2") self.group_deck_import = QtWidgets.QGroupBox(Dialog) + self.group_deck_import.setTitle("") self.group_deck_import.setObjectName("group_deck_import") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.group_deck_import) self.verticalLayout_5.setObjectName("verticalLayout_5") @@ -53,10 +54,14 @@ def setupUi(self, Dialog): self.textedit_tags = QtWidgets.QLineEdit(self.group_deck_import) self.textedit_tags.setObjectName("textedit_tags") self.verticalLayout_6.addWidget(self.textedit_tags) + self.cb_ignore_move_cards = QtWidgets.QCheckBox(self.group_deck_import) + self.cb_ignore_move_cards.setObjectName("cb_ignore_move_cards") + self.verticalLayout_6.addWidget(self.cb_ignore_move_cards) self.verticalLayout_3.addLayout(self.verticalLayout_6) self.verticalLayout_7 = QtWidgets.QVBoxLayout() self.verticalLayout_7.setObjectName("verticalLayout_7") self.label_2 = QtWidgets.QLabel(self.group_deck_import) + self.label_2.setEnabled(True) self.label_2.setObjectName("label_2") self.verticalLayout_7.addWidget(self.label_2) self.cb_headers = QtWidgets.QCheckBox(self.group_deck_import) @@ -75,9 +80,6 @@ def setupUi(self, Dialog): self.horizontalLayout_2.addLayout(self.verticalLayout_3) self.importMessageLayout.addLayout(self.horizontalLayout_2) self.verticalLayout_5.addLayout(self.importMessageLayout) - self.cb_ignore_move_cards = QtWidgets.QCheckBox(self.group_deck_import) - self.cb_ignore_move_cards.setObjectName("cb_ignore_move_cards") - self.verticalLayout_5.addWidget(self.cb_ignore_move_cards) self.label = QtWidgets.QLabel(self.group_deck_import) self.label.setObjectName("label") self.verticalLayout_5.addWidget(self.label) @@ -101,15 +103,14 @@ def setupUi(self, Dialog): def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "CrowdAnki Import Settings")) - self.group_deck_import.setTitle(_translate("Dialog", "Import")) - self.import_message_label.setText(_translate("Dialog", "Import Message")) + self.import_message_label.setText(_translate("Dialog", "

Import Message

")) self.import_message_textbox.setPlaceholderText(_translate("Dialog", "Message from a deck manager about the recent import")) self.cb_tag_cards.setText(_translate("Dialog", "Tag Cards")) self.textedit_tags.setPlaceholderText(_translate("Dialog", "Tag1, RecentlyImported, Broken")) - self.label_2.setText(_translate("Dialog", "Deck Parts to Use")) + self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) + self.label_2.setText(_translate("Dialog", "

Deck Parts to Use

")) self.cb_headers.setText(_translate("Dialog", "Headers")) self.cb_note_models.setText(_translate("Dialog", "Note Models")) self.cb_notes.setText(_translate("Dialog", "Notes (Cards)")) self.cb_media.setText(_translate("Dialog", "Media")) - self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) - self.label.setText(_translate("Dialog", "Personal Fields - [Explanation of what that is]")) + self.label.setText(_translate("Dialog", "

Personal Fields - Fields which will keep their existing values instead of being imported

")) diff --git a/ui_files/import.ui b/ui_files/import.ui index 1ec4784..aa9f92e 100644 --- a/ui_files/import.ui +++ b/ui_files/import.ui @@ -21,7 +21,7 @@ - Import + @@ -36,7 +36,7 @@ - Import Message + <html><head/><body><p><span style=" font-weight:600;">Import Message</span></p></body></html> @@ -73,14 +73,24 @@ + + + + Do Not Move Existing Cards + + +
+ + true + - Deck Parts to Use + <html><head/><body><p><span style=" font-weight:600;">Deck Parts to Use</span></p></body></html> @@ -120,17 +130,10 @@ - - - - Do Not Move Existing Cards - - - - Personal Fields - [Explanation of what that is] + <html><head/><body><p><span style=" font-weight:600;">Personal Fields</span> - <span style=" font-style:italic;">Fields which will keep their existing values instead of being imported</span></p></body></html> From 8a69180f47475e16d7a06cd0aad7edbd535844dc Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 1 Jun 2020 08:53:10 +0200 Subject: [PATCH 16/30] Fix Config.ui file --- crowd_anki/config/config_ui.py | 6 ++++-- ui_files/config.ui | 11 ++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crowd_anki/config/config_ui.py b/crowd_anki/config/config_ui.py index 278bce9..44dea0a 100644 --- a/crowd_anki/config/config_ui.py +++ b/crowd_anki/config/config_ui.py @@ -7,7 +7,7 @@ # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): @@ -121,5 +121,7 @@ def retranslateUi(self, Dialog): "


\n" "

Deck Sort Methods: Methods with which the deck will be sorted. If multiple sorting methods are provided then each sorting method will be applied in order.

\n" "

Options: none, guid, flag, tag, note_model_name, note_model_id, field1, and field2.

\n" +"


\n" "

Reverse Sort Order: Swap the order of the notes, after all sorting.

\n" -"

Create directory on manual export: When exporting via File -> Export, whether to create a directory (named after the deck) in the selected destination, or whether to export directly in the selected destination.

")) +"


\n" +"

Create directory on manual export: When exporting via File -> Export, whether to create a directory (named after the deck) in the selected destination, or whether to export directly in the selected destination.

")) diff --git a/ui_files/config.ui b/ui_files/config.ui index 08cd60f..95ddc8d 100644 --- a/ui_files/config.ui +++ b/ui_files/config.ui @@ -97,6 +97,13 @@ + + + + Create directory on manual export + + + @@ -132,7 +139,9 @@ p, li { white-space: pre-wrap; } <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Deck Sort Methods</span>: Methods with which the deck will be sorted. If multiple sorting methods are provided then each sorting method will be applied in order.</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" text-decoration: underline;">Options</span>: <span style=" font-style:italic;">none, guid, flag, tag, note_model_name, note_model_id, field1, </span>and<span style=" font-style:italic;"> field2.</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Reverse Sort Order</span>: Swap the order of the notes, after all sorting.</p></body></html> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Reverse Sort Order</span>: Swap the order of the notes, after all sorting.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Create directory on manual export</span>: When exporting via File -&gt; Export, whether to create a directory (named after the deck) in the selected destination, or whether to export directly in the selected destination.</p></body></html> From 4878140f5d9e6b0c331dfc739316fba5e243ebb4 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 1 Jun 2020 10:49:02 +0200 Subject: [PATCH 17/30] Add ImportConfig to ExportFilter --- crowd_anki/importer/anki_importer.py | 4 ++-- crowd_anki/representation/deck_initializer.py | 4 ++-- crowd_anki/representation/json_serializable.py | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crowd_anki/importer/anki_importer.py b/crowd_anki/importer/anki_importer.py index 18dadd8..1ac4704 100644 --- a/crowd_anki/importer/anki_importer.py +++ b/crowd_anki/importer/anki_importer.py @@ -31,7 +31,7 @@ def load_deck(self, deck_json, directory_path, import_config: ImportConfig): aqt.mw.progress.start(immediate=True) try: - deck = deck_initializer.from_json(deck_json, import_config) + deck = deck_initializer.from_json(deck_json, import_config=import_config) deck.save_to_collection(self.collection) if import_config.use_media: @@ -93,7 +93,7 @@ def import_deck_from_path(collection, directory_path): deck_json, import_config = importer.load_deck_with_settings(directory_path) if deck_json is not None: - importer.load_deck(deck_json, directory_path, import_config) + importer.load_deck(deck_json, directory_path, import_config=import_config) aqt.utils.showInfo("Import of {} deck was successful".format(directory_path.name)) except ValueError as error: aqt.utils.showWarning("Error: {}. While trying to import deck from directory {}".format( diff --git a/crowd_anki/representation/deck_initializer.py b/crowd_anki/representation/deck_initializer.py index b0c0e38..f581e94 100644 --- a/crowd_anki/representation/deck_initializer.py +++ b/crowd_anki/representation/deck_initializer.py @@ -42,8 +42,8 @@ def from_json(json_dict, import_config: ImportConfig, deck_metadata=None) -> Dec deck._load_metadata_from_json(json_dict) deck.deck_config_uuid = json_dict["deck_config_uuid"] - deck.notes = [Note.from_json(json_note, import_config) for json_note in json_dict["notes"]] - deck.children = [from_json(child, deck.metadata) for child in json_dict["children"]] + deck.notes = [Note.from_json(json_note, import_config=import_config) for json_note in json_dict["notes"]] + deck.children = [from_json(child, import_config=import_config, deck_metadata=deck.metadata) for child in json_dict["children"]] # Todo should I call this here? deck.post_import_filter() diff --git a/crowd_anki/representation/json_serializable.py b/crowd_anki/representation/json_serializable.py index cccdd41..7531f1c 100644 --- a/crowd_anki/representation/json_serializable.py +++ b/crowd_anki/representation/json_serializable.py @@ -6,9 +6,12 @@ class JsonSerializable: readable_names = {} - export_filter_set = {"mod", # Modification time - "usn", # Todo clarify - "id"} + export_filter_set = { + "mod", # Modification time + "usn", # Todo clarify + "id", + "import_config" + } import_filter_set = {"__type__"} def __init__(self): From 7b23d9e0c9e0f24295100a06667d0e48dff54e8f Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 1 Jun 2020 12:22:02 +0200 Subject: [PATCH 18/30] Import_Config as Yaml file; Add PyYaml to Pipfile; Refactor Setup PFs --- Pipfile | 1 + Pipfile.lock | 153 ++++++++++++++++----------- crowd_anki/importer/anki_importer.py | 26 +++-- crowd_anki/importer/import_dialog.py | 48 ++++++--- crowd_anki/utils/constants.py | 3 +- 5 files changed, 145 insertions(+), 86 deletions(-) diff --git a/Pipfile b/Pipfile index 365aaef..b6015b4 100644 --- a/Pipfile +++ b/Pipfile @@ -18,6 +18,7 @@ typing = "*" dataclasses = "*" pygtrie = "*" cached-property = "*" +PyYAML = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 66e8f08..0c9b796 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "eb474dc76fad53742ee85883bdf4d6bd657000cb275ecd2db6597901a56c8e6b" + "sha256": "bcd169af3913c90e3896caa6c473903bf63ae8d269244a7b0af7b33306a851ce" }, "pipfile-spec": 6, "requires": { @@ -80,6 +80,23 @@ "index": "pypi", "version": "==2.3.3" }, + "pyyaml": { + "hashes": [ + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" + ], + "index": "pypi", + "version": "==5.3.1" + }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", @@ -221,6 +238,14 @@ ], "version": "==4.4.2" }, + "distro": { + "hashes": [ + "sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92", + "sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799" + ], + "markers": "sys_platform != 'darwin' and sys_platform != 'win32'", + "version": "==1.5.0" + }, "expects": { "hashes": [ "sha256:419902ccafe81b7e9559eeb6b7a07ef9d5c5604eddb93000f0642b3b2d594f4c" @@ -230,11 +255,11 @@ }, "hypothesis": { "hashes": [ - "sha256:d44142b80572bb3392dd018a6b2e59f54b2ba77da50e2dd0cbddd815b7a84005", - "sha256:f523846f1e323e5d74694a437ef9f681ac1643e45404dc5c5bb7d1ee947c4a99" + "sha256:21bb5fbe456f775233fe20bcbeb26f648d68025bce554c94c0698fb4c33e7008", + "sha256:7c819501a26ff82ccb4bee8a96b1ff4fff187e041d79ed176964ca550b77098b" ], "index": "pypi", - "version": "==5.15.1" + "version": "==5.16.0" }, "idna": { "hashes": [ @@ -274,78 +299,80 @@ }, "orjson": { "hashes": [ - "sha256:2b94b4cb6aeb65a0fc1444192dffbc395a7e309012163fc9d239b15aaa4cab7c", - "sha256:35e4c9b44727aaaeaae7ac058e8d68dcb653497ca6a860c7dddc3b33e0ee3d3f", - "sha256:36cf210ff2771d4d4b325abb3989d3b6162b0862212ef605aa5a46f5c471701a", - "sha256:416acc46a92a7d78b69b61b0f777f64d7abe6c6c6229eb946de1494cc07f2ba2", - "sha256:5051856c962375fd245ffea957bafb3f1bdf65595fa12c0128dd90aaba0971c8", - "sha256:53f998a5f5ee6e2002568aa4dc40d015d8e621cfa363787f44c6388432913713", - "sha256:5bf352dac1a9433a55b3558cea484f3548e58e137f883eabbf7a8eb489389fca", - "sha256:6ecdb764a81260bb65fc37b1733bbb02666c5aa97835a87febbad130c6b4b9ac", - "sha256:9a8db5baab84171c7ff600849a4760ff81673a7fbc586e00208b70975b3d5a21", - "sha256:a75f72c8a7cb0602c2fc7e9806554912b70bf79bbe559e55e1951b1f279c0ad2", - "sha256:ad625dc8360f667dd811e2b45b618bc5ca6aa997e23bdec7cf4adccda6962083", - "sha256:b9c0f7d14cac42a41084d784b93288ebdf61537fa2d160610c1f1dd9a6f1d62f", - "sha256:c2d8223f26ccbc71899c4dab07e0fac556dd820777ecb220395114811d2c1ffe", - "sha256:da84e0ba0ed646e92852d4c142f27b4d52724d6460f579eba14fbcfcd5173a1e", - "sha256:e986d35402ea9a10b7074d8457f6310a158c61e625c49c8446c685a430933401" + "sha256:09a024829a3b4628b74ba627357e0e7b40596476bac913a52f9ac8fa10728bd5", + "sha256:0d815b2f9507c9ed543d220f4edfeac987812eaa6240345ae5089c8da0781223", + "sha256:1304588cc6a3d471f25a3fc490dd4579c091a782323a3ae80b1f08cda9f353bf", + "sha256:15ce6f35df481539e03ab7557e8deba088b990f8f3b1c67e1b12dd1a2d33184a", + "sha256:15d5826376a08b3c8776f785d06173d5aeefe0e388f62231da749391f44ead48", + "sha256:18154aa8b0a6685d17879c2d1d7ec575e13643e08644e6247d07ff1af491e474", + "sha256:20da2478ee1ecf3ac8ab513888a52c97dfb3a2c9ac5669805d919cb80da4d242", + "sha256:4117175a24761dc8248ff91ce2065fc0e690305261460869418295e129ed5a99", + "sha256:7986f23e2d2a30a2a7f6db88288046b8cc9df3cc4b7f2376df27618c26a71483", + "sha256:a66a62637e88021c6d7451cc26304f1d4282ccd2c684b7e5670641722ed13cbb", + "sha256:b6033bae98b742edb01c0dd30ea45c7b5f154251c2f4ccdb5a39156a7e4d09ff", + "sha256:cb427a52ef1b29d87378c90aac6a8daf11416d66f510a53529ce88320188b4ee", + "sha256:d55088e8c1f7dd301500d19c58d05d8df5da1523acbf11c6ba302424b6dbf366", + "sha256:d6001985c41f14ed966937f4ae6a514677041c7bc56a8782c035f7765d74f35d", + "sha256:db7ba3acc1c0cec7c10740af4021ff15b9103c4f024e9d2b10d5e77fbe82ca6f" ], "markers": "platform_machine == 'x86_64'", - "version": "==3.0.1" + "version": "==3.0.2" }, "protobuf": { "hashes": [ - "sha256:04d0b2bd99050d09393875a5a25fd12337b17f3ac2e29c0c1b8e65b277cbfe72", - "sha256:05288e44638e91498f13127a3699a6528dec6f9d3084d60959d721bfb9ea5b98", - "sha256:175d85370947f89e33b3da93f4ccdda3f326bebe3e599df5915ceb7f804cd9df", - "sha256:440a8c77531b3652f24999b249256ed01fd44c498ab0973843066681bd276685", - "sha256:49fb6fab19cd3f30fa0e976eeedcbf2558e9061e5fa65b4fe51ded1f4002e04d", - "sha256:4c7cae1f56056a4a2a2e3b00b26ab8550eae738bd9548f4ea0c2fcb88ed76ae5", - "sha256:519abfacbb421c3591d26e8daf7a4957763428db7267f7207e3693e29f6978db", - "sha256:60f32af25620abc4d7928d8197f9f25d49d558c5959aa1e08c686f974ac0b71a", - "sha256:613ac49f6db266fba243daf60fb32af107cfe3678e5c003bb40a381b6786389d", - "sha256:954bb14816edd24e746ba1a6b2d48c43576393bbde2fb8e1e3bd6d4504c7feac", - "sha256:9b1462c033a2cee7f4e8eb396905c69de2c532c3b835ff8f71f8e5fb77c38023", - "sha256:c0767f4d93ce4288475afe0571663c78870924f1f8881efd5406c10f070c75e4", - "sha256:c45f5980ce32879391144b5766120fd7b8803129f127ce36bd060dd38824801f", - "sha256:eeb7502f59e889a88bcb59f299493e215d1864f3d75335ea04a413004eb4fe24", - "sha256:fdb1742f883ee4662e39fcc5916f2725fec36a5191a52123fec60f8c53b70495", - "sha256:fe554066c4962c2db0a1d4752655223eb948d2bfa0fb1c4a7f2c00ec07324f1c" - ], - "version": "==3.12.1" + "sha256:304e08440c4a41a0f3592d2a38934aad6919d692bb0edfb355548786728f9a5e", + "sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5", + "sha256:50b5fee674878b14baea73b4568dc478c46a31dd50157a5b5d2f71138243b1a9", + "sha256:5524c7020eb1fb7319472cb75c4c3206ef18b34d6034d2ee420a60f99cddeb07", + "sha256:612bc97e42b22af10ba25e4140963fbaa4c5181487d163f4eb55b0b15b3dfcd2", + "sha256:6f349adabf1c004aba53f7b4633459f8ca8a09654bf7e69b509c95a454755776", + "sha256:85b94d2653b0fdf6d879e39d51018bf5ccd86c81c04e18a98e9888694b98226f", + "sha256:87535dc2d2ef007b9d44e309d2b8ea27a03d2fa09556a72364d706fcb7090828", + "sha256:a7ab28a8f1f043c58d157bceb64f80e4d2f7f1b934bc7ff5e7f7a55a337ea8b0", + "sha256:a96f8fc625e9ff568838e556f6f6ae8eca8b4837cdfb3f90efcb7c00e342a2eb", + "sha256:b5a114ea9b7fc90c2cc4867a866512672a47f66b154c6d7ee7e48ddb68b68122", + "sha256:be04fe14ceed7f8641e30f36077c1a654ff6f17d0c7a5283b699d057d150d82a", + "sha256:bff02030bab8b969f4de597543e55bd05e968567acb25c0a87495a31eb09e925", + "sha256:c9ca9f76805e5a637605f171f6c4772fc4a81eced4e2f708f79c75166a2c99ea", + "sha256:e1464a4a2cf12f58f662c8e6421772c07947266293fb701cb39cd9c1e183f63c", + "sha256:e72736dd822748b0721f41f9aaaf6a5b6d5cfc78f6c8690263aef8bba4457f0e", + "sha256:eafe9fa19fcefef424ee089fb01ac7177ff3691af7cc2ae8791ae523eb6ca907", + "sha256:f4b73736108a416c76c17a8a09bc73af3d91edaa26c682aaa460ef91a47168d3" + ], + "version": "==3.12.2" }, "pyqt5": { "hashes": [ - "sha256:3b91dd1d0cbfaea85ad057247ba621187e511434b0c9d6d40de69fd5e833b109", - "sha256:a9bdc46ab1f6397770e6b8dca84ac07a0250d26b1a31587f25619cf31a075532", - "sha256:bd230c6fd699eabf1ceb51e13a8b79b74c00a80272c622427b80141a22269eb0", - "sha256:ee168a486c9a758511568147815e2959652cd0aabea832fa5e87cf6b241d2180", - "sha256:f61ddc78547d6ca763323ccd4a9e374c71b29feda1f5ce2d3e91e4f8d2cf1942" + "sha256:14be35c0c1bcc804791a096d2ef9950f12c6fd34dd11dbe61b8c769fefcdf98c", + "sha256:3605d34ba6291b9194c46035e228d6d01f39d120cf5ecc70301c11e7900fed21", + "sha256:5bac0fab1e9891d73400c2470a9cb810e6bdbc7027a84ae4d3ec83436f1109ec", + "sha256:c6f75488ffd5365a65893bc64ea82a6957db126fbfe33654bcd43ae1c30c52f9", + "sha256:e05c86b8c4f02d62a5b355d426fd8d063781dd44c6a3f916640a5beb40efe60a" ], "index": "pypi", - "version": "==5.14.2" + "version": "==5.15.0" }, "pyqt5-sip": { "hashes": [ - "sha256:01919371d32b26208b2f0318f1e15680d3aa60d1ced1812a5dac8bdb483fea69", - "sha256:11f8cc2de287c3457fee53e781f06fb71f04251e7ae408ed22696ed65fd2bcf4", - "sha256:168a6d700daf366b7cf255a8cabf8d07bfe2294859e6b3b2636c36c2f89265c9", - "sha256:16a19b9f36985b8bff30b89fb8859d831713dd528fba5600563e36ff077960a2", - "sha256:16a9a4daf85bfaa3aec35237ff28d8773a3ec937d9f8dc7fc3db7716de42d4a9", - "sha256:31c74602ccd6b70e4352550eb41aa980dc1d6009444f3c8eb1b844e84bd144cf", - "sha256:360de29634e2ce1df84d2b588bd8c1a29b768f3a5225869d63adb03bc21bd32a", - "sha256:3cb9076ba0e574b2f026759103eb0e12051128714f5aa136cca53229d3ad72d1", - "sha256:4f87d59d29ca1c5a4005bbec27af002be787210dc5f8f87fe5d747883a836083", - "sha256:65fceeea2ac738a92f7e3e459ece1b4e2fbf49fd1d6b732a73d0d4bcfc434452", - "sha256:85e68b8936f1756060ddcb3ef0a84af78ce89993fa6594b3049a0eca53d6d2fa", - "sha256:9dd5769e83e64d017d02981563c8159d825425b6c4998c937a880888f4dcb7a3", - "sha256:a8a6c0512641fc042726b6253b2d5f3f3f800098334d198d8ebdf337d85ab3d7", - "sha256:b068f4791e97427d82a27e7df28cc3ee33f7e4353f48ed6a123f8cdba44266b2", - "sha256:b34c1f227a8f8e97059f20e5424f117f66a302b42e34d4039158494c6371b1ce", - "sha256:b68cfe632a512c0551e8860f35c1fcab5cd1ad5e168b4814fddd88121f447b0a", - "sha256:df4f5cdb86f47df5f6fc35be29cc45df7b5a2c171d07dbf377d558b226554ea3" - ], - "version": "==12.7.2" + "sha256:0a34b6596bdd28d52da3a51fa8d9bb0b287bcb605c2512aa3251b9028cc71f4d", + "sha256:1d65ce08a56282fb0273dd06585b8927b88d4fba71c01a54f8e2ac87ac1ed387", + "sha256:224e2fbb7088595940c348d168a317caa2110cbb7a5b957a8c3fc0d9296ee069", + "sha256:2a1153cda63f2632d3d5698f0cf29f6b1f1d5162305dc6f5b23336ad8f1039ed", + "sha256:2a2239d16a49ce6eaf10166a84424543111f8ebe49d3c124d02af91b01a58425", + "sha256:58eae636e0b1926cddec98a703319a47f671cef07d73aaa525ba421cd4adfeb5", + "sha256:5c19c4ad67af087e8f4411da7422391b236b941f5f0697f615c5816455d1355d", + "sha256:61aa60fb848d740581646603a12c2dcb8d7c4cbd2a9c476a1c891ec360ff0b87", + "sha256:8d9f4dc7dbae9783c5dafd66801875a2ebf9302c3addd5739f772285c1c1e91c", + "sha256:94c80677b1e8c92fa080e24045d54ace5e4343c4ee6d0216675cd91d6f8e122a", + "sha256:9b69db29571dde679908fb237784a8e7af4a2cbf1b7bb25bdb86e487210e04d2", + "sha256:9ef12754021bcc1246f97e00ea62b5594dd5c61192830639ab4a1640bd4b7940", + "sha256:b1bbe763d431d26f9565cba3e99866768761366ab6d609d2506d194882156fa7", + "sha256:d7b8a8f89385ad9e3da38e0123c22c0efc18005e0e2731b6b95e4c21db2049d2", + "sha256:e6254647fa35e1260282aeb9c32a3dd363287b9a1ffcc4f22bd27e54178e92e4", + "sha256:f4c294bfaf2be8004583266d4621bfd3a387e12946f548f966a7fbec91845f1b", + "sha256:fa3d70f370604efc67085849d3d1d3d2109faa716c520faf601d15845df64de6" + ], + "version": "==12.8.0" }, "pyrsistent": { "hashes": [ diff --git a/crowd_anki/importer/anki_importer.py b/crowd_anki/importer/anki_importer.py index 0996c02..f041ea6 100644 --- a/crowd_anki/importer/anki_importer.py +++ b/crowd_anki/importer/anki_importer.py @@ -6,12 +6,15 @@ import aqt import aqt.utils +import yaml from ..representation import deck_initializer -from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME +from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME, IMPORT_CONFIG_NAME, \ + CONFIG_EXTENSION from ..importer.import_dialog import ImportDialog, ImportConfig from aqt.qt import QDialog + class AnkiJsonImporter: def __init__(self, collection, deck_file_name: str = DECK_FILE_NAME): self.collection = collection @@ -57,14 +60,15 @@ def get_deck_path(self, directory_path): def path_for_name(name): return directory_path.joinpath(name).with_suffix(DECK_FILE_EXTENSION) - convention_path = path_for_name(self.deck_file_name) - inferred_path = path_for_name(directory_path.name) + convention_path = path_for_name(self.deck_file_name) # [folder]/deck.json + inferred_path = path_for_name(directory_path.name) # [folder]/[folder].json return convention_path if convention_path.exists() else inferred_path def load_deck_with_settings(self, directory_path) -> (dict, ImportConfig): - deck_json = self.read_file(self.get_deck_path(directory_path)) + deck_json = self.read_deck(self.get_deck_path(directory_path)) + import_config = self.read_import_config(directory_path) - import_dialog = ImportDialog(deck_json) + import_dialog = ImportDialog(deck_json, import_config) if import_dialog.exec_() == QDialog.Rejected: return None, None # User has cancelled @@ -73,7 +77,7 @@ def load_deck_with_settings(self, directory_path) -> (dict, ImportConfig): return deck_json, import_dialog.final_import_config @staticmethod - def read_file(file_path): + def read_deck(file_path): """ Load deck from json file :type file_path: Path @@ -84,6 +88,16 @@ def read_file(file_path): with file_path.open(encoding='utf8') as deck_file: return json.load(deck_file) + @staticmethod + def read_import_config(directory_path): + file_path = directory_path.joinpath(IMPORT_CONFIG_NAME).with_suffix(CONFIG_EXTENSION) + + if not file_path.exists(): + return {} + + with file_path.open(encoding='utf8') as meta_file: + return yaml.full_load(meta_file) + @staticmethod def import_deck_from_path(collection, directory_path): importer = AnkiJsonImporter(collection) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index 78c3a07..a4eb5f9 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -8,7 +8,6 @@ from .import_ui import Ui_Dialog as ConfigUI # from .config_settings import ConfigSettings from ..config.config_settings import ConfigSettings -from ..utils.constants import SETTINGS_FIELD_NAME from ..utils.utils import string_cs_to_list @@ -18,6 +17,7 @@ class ImportJsonSetting: import_message: str suggest_tag_imported_cards: bool + personal_fields: List[Tuple[str, str]] = [] class Properties(Enum): IMPORT_MESSAGE = ConfigEntry("import_message", "") @@ -27,6 +27,12 @@ def __init__(self, settings_dict: dict): for prop in self.Properties: setattr(self, prop.value.config_name, settings_dict.get(prop.value.config_name, prop.value.default_value)) + models = settings_dict.get("note_models", []) + for model_name in models: + personal_fields = models[model_name].get("personal_fields", []) + for pf in personal_fields: + self.personal_fields.append((model_name, pf)) + class ImportConfig: personal_fields: List[Tuple[str, str]] = [] @@ -46,15 +52,14 @@ def __repr__(self): class ImportDialog(QDialog): - def __init__(self, deck_json, - parent=None): + def __init__(self, deck_json, config, parent=None): super().__init__(None) self.parent = parent self.form = ConfigUI() self.form.setupUi(self) self.userConfig = ConfigSettings.get_instance() self.deck_json = deck_json - self.import_settings = ImportJsonSetting(self.deck_json.get(SETTINGS_FIELD_NAME, {})) + self.import_settings = ImportJsonSetting(config) self.ui_initial_setup() self.final_import_config: ImportConfig = None @@ -72,21 +77,30 @@ def ui_initial_setup(self): self.setup_misc() def setup_personal_field_selection(self): - def is_field_personal(f): - if "personal_field" not in f: - return False - return f["personal_field"] + heading_font = QFont() + heading_font.setBold(True) + heading_font.setUnderline(True) - for model in self.deck_json["note_models"]: - heading_ui = QListWidgetItem(model["name"]) + def add_header(name): + heading_ui = QListWidgetItem(name) heading_ui.setFlags(Qt.ItemIsEnabled) + heading_ui.setSizeHint(QSize(self.form.list_personal_fields.width(), 30)) + heading_ui.setFont(heading_font) self.form.list_personal_fields.addItem(heading_ui) + def add_field(name, is_personal): + field_ui = QListWidgetItem(name) + field_ui.setCheckState(Qt.Checked if is_personal else Qt.Unchecked) + field_ui.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) + self.form.list_personal_fields.addItem(field_ui) + + for model in self.deck_json["note_models"]: + model_name = model["name"] + add_header(model_name) + for field in model["flds"]: - field_ui = QListWidgetItem(field["name"]) - field_ui.setCheckState(Qt.Checked if is_field_personal(field) else Qt.Unchecked) - field_ui.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) - self.form.list_personal_fields.addItem(field_ui) + field_name = field["name"] + add_field(field_name, (model_name, field_name) in self.import_settings.personal_fields) def setup_misc(self): self.form.import_message_textbox.setText(self.import_settings.import_message) @@ -107,14 +121,16 @@ def get_import_config(self): config.personal_fields = [] current_note_model = "" - fields: List[QListWidgetItem] = [self.form.list_personal_fields.item(i) for i in range(self.form.list_personal_fields.count())] + fields: List[QListWidgetItem] = [self.form.list_personal_fields.item(i) for i in + range(self.form.list_personal_fields.count())] for field in fields: if not field.flags() & Qt.ItemIsUserCheckable: # Note Model Header current_note_model = field.text() elif field.checkState() == Qt.Checked: # Field config.personal_fields.append((current_note_model, field.text())) - config.add_tag_to_cards = string_cs_to_list(self.form.textedit_tags.text()) if self.form.cb_tag_cards.isChecked() else [] + config.add_tag_to_cards = string_cs_to_list( + self.form.textedit_tags.text()) if self.form.cb_tag_cards.isChecked() else [] config.use_header = self.form.cb_headers.isChecked() config.use_note_models = self.form.cb_note_models.isChecked() diff --git a/crowd_anki/utils/constants.py b/crowd_anki/utils/constants.py index a4cf941..d628578 100644 --- a/crowd_anki/utils/constants.py +++ b/crowd_anki/utils/constants.py @@ -2,9 +2,10 @@ UUID_FIELD_NAME = 'crowdanki_uuid' DECK_FILE_NAME = "deck" -SETTINGS_FIELD_NAME = '_settings' DECK_FILE_EXTENSION = ".json" MEDIA_SUBDIRECTORY_NAME = "media" +IMPORT_CONFIG_NAME = "import_config" +CONFIG_EXTENSION = ".yaml" ANKI_EXPORT_EXTENSION = "directory" From 66b8cdf1efdbee49dee9fcefec749e458eab358a Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sat, 20 Jun 2020 08:23:14 +0200 Subject: [PATCH 19/30] Minor changes --- crowd_anki/importer/anki_importer.py | 6 +----- crowd_anki/importer/import_dialog.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/crowd_anki/importer/anki_importer.py b/crowd_anki/importer/anki_importer.py index f041ea6..72007db 100644 --- a/crowd_anki/importer/anki_importer.py +++ b/crowd_anki/importer/anki_importer.py @@ -77,11 +77,7 @@ def load_deck_with_settings(self, directory_path) -> (dict, ImportConfig): return deck_json, import_dialog.final_import_config @staticmethod - def read_deck(file_path): - """ - Load deck from json file - :type file_path: Path - """ + def read_deck(file_path: Path): if not file_path.exists(): raise ValueError("There is no {} file inside of the selected directory".format(file_path)) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index a4eb5f9..cf8d9af 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -6,7 +6,6 @@ from typing import List, Tuple from .import_ui import Ui_Dialog as ConfigUI -# from .config_settings import ConfigSettings from ..config.config_settings import ConfigSettings from ..utils.utils import string_cs_to_list From a0d9878ebe697d793acc25f1c8bc5d085151e544 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sat, 20 Jun 2020 08:33:05 +0200 Subject: [PATCH 20/30] Default True for Import Deck Parts --- crowd_anki/importer/import_dialog.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index cf8d9af..c9a4b5d 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -113,6 +113,11 @@ def setup_misc(self): if self.userConfig.import_notes_ignore_deck_movement: self.form.cb_ignore_move_cards.setCheckState(Qt.Checked) + self.form.cb_headers.setCheckState(Qt.Checked) + self.form.cb_note_models.setCheckState(Qt.Checked) + self.form.cb_notes.setCheckState(Qt.Checked) + self.form.cb_media.setCheckState(Qt.Checked) + # TODO: Deck Parts to Use def get_import_config(self): From fb93158ff9cbd5dc2cad744f3644e4ce4898af02 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 21 Jun 2020 17:42:54 +0200 Subject: [PATCH 21/30] Structural Changes and Fixes --- crowd_anki/importer/anki_importer.py | 77 +++++----- crowd_anki/importer/import_dialog.py | 133 +++++++++++------- crowd_anki/representation/deck.py | 4 +- crowd_anki/representation/deck_initializer.py | 6 +- .../representation/json_serializable.py | 3 +- crowd_anki/representation/note.py | 34 ++--- crowd_anki/utils/constants.py | 3 +- crowd_anki/utils/utils.py | 2 +- 8 files changed, 145 insertions(+), 117 deletions(-) diff --git a/crowd_anki/importer/anki_importer.py b/crowd_anki/importer/anki_importer.py index 72007db..a98e374 100644 --- a/crowd_anki/importer/anki_importer.py +++ b/crowd_anki/importer/anki_importer.py @@ -9,8 +9,7 @@ import yaml from ..representation import deck_initializer -from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME, IMPORT_CONFIG_NAME, \ - CONFIG_EXTENSION +from ..utils.constants import DECK_FILE_NAME, DECK_FILE_EXTENSION, MEDIA_SUBDIRECTORY_NAME, IMPORT_CONFIG_NAME from ..importer.import_dialog import ImportDialog, ImportConfig from aqt.qt import QDialog @@ -20,36 +19,43 @@ def __init__(self, collection, deck_file_name: str = DECK_FILE_NAME): self.collection = collection self.deck_file_name = deck_file_name - def load_deck(self, deck_json, directory_path, import_config: ImportConfig): + def load_deck(self, directory_path) -> bool: """ Load deck serialized to directory Assumes that deck json file is located in the directory - and named 'deck.json' - :param deck_json: The deck json dictionary + and named 'deck.json' or '[foldername].json :param directory_path: Path - :param import_config: Config data chosen by the user """ + deck_json = self.read_deck(self.get_deck_path(directory_path)) + + import_config, ui_accepted = self.read_import_config(directory_path, deck_json) + if not ui_accepted: + return False + if aqt.mw: aqt.mw.backup() - try: - deck = deck_initializer.from_json(deck_json, import_config=import_config) - deck.save_to_collection(self.collection) + deck = deck_initializer.from_json(deck_json) + deck.save_to_collection(self.collection, import_config=import_config) if import_config.use_media: - media_directory = directory_path.joinpath(MEDIA_SUBDIRECTORY_NAME) - if media_directory.exists(): - unicode_media_directory = str(media_directory) - src_files = os.listdir(unicode_media_directory) - for filename in src_files: - full_filename = os.path.join(unicode_media_directory, filename) - if os.path.isfile(full_filename): - shutil.copy(full_filename, self.collection.media.dir()) - else: - print("Warning: no media directory exists.") + self.import_media(directory_path) finally: if aqt.mw: aqt.mw.deckBrowser.show() + return True + + def import_media(self, directory_path): + media_directory = directory_path.joinpath(MEDIA_SUBDIRECTORY_NAME) + if media_directory.exists(): + unicode_media_directory = str(media_directory) + src_files = os.listdir(unicode_media_directory) + for filename in src_files: + full_filename = os.path.join(unicode_media_directory, filename) + if os.path.isfile(full_filename): + shutil.copy(full_filename, self.collection.media.dir()) + else: + print("Warning: no media directory exists.") def get_deck_path(self, directory_path): """ @@ -64,18 +70,6 @@ def path_for_name(name): inferred_path = path_for_name(directory_path.name) # [folder]/[folder].json return convention_path if convention_path.exists() else inferred_path - def load_deck_with_settings(self, directory_path) -> (dict, ImportConfig): - deck_json = self.read_deck(self.get_deck_path(directory_path)) - import_config = self.read_import_config(directory_path) - - import_dialog = ImportDialog(deck_json, import_config) - if import_dialog.exec_() == QDialog.Rejected: - return None, None # User has cancelled - - # TODO: strip settings from deck_json - - return deck_json, import_dialog.final_import_config - @staticmethod def read_deck(file_path: Path): if not file_path.exists(): @@ -85,23 +79,26 @@ def read_deck(file_path: Path): return json.load(deck_file) @staticmethod - def read_import_config(directory_path): - file_path = directory_path.joinpath(IMPORT_CONFIG_NAME).with_suffix(CONFIG_EXTENSION) + def read_import_config(directory_path, deck_json): + file_path = directory_path.joinpath(IMPORT_CONFIG_NAME) if not file_path.exists(): - return {} + import_dict = {} + else: + with file_path.open(encoding='utf8') as meta_file: + import_dict = yaml.full_load(meta_file) - with file_path.open(encoding='utf8') as meta_file: - return yaml.full_load(meta_file) + import_dialog = ImportDialog(deck_json, import_dict) + if import_dialog.exec_() == QDialog.Rejected: + return None, False + + return import_dialog.final_import_config, True @staticmethod def import_deck_from_path(collection, directory_path): importer = AnkiJsonImporter(collection) try: - deck_json, import_config = importer.load_deck_with_settings(directory_path) - - if deck_json is not None: - importer.load_deck(deck_json, directory_path, import_config=import_config) + if importer.load_deck(directory_path): aqt.utils.showInfo("Import of {} deck was successful".format(directory_path.name)) except ValueError as error: aqt.utils.showWarning("Error: {}. While trying to import deck from directory {}".format( diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index c9a4b5d..c4402e0 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -1,48 +1,78 @@ from collections import namedtuple -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum from aqt.qt import * -from typing import List, Tuple +from typing import List, Tuple, Dict from .import_ui import Ui_Dialog as ConfigUI from ..config.config_settings import ConfigSettings +from ..utils.constants import UUID_FIELD_NAME from ..utils.utils import string_cs_to_list -ConfigEntry = namedtuple("ConfigEntry", ["config_name", "default_value"]) +@dataclass +class ConfigEntry: + config_name: str + default_value: any -class ImportJsonSetting: - import_message: str - suggest_tag_imported_cards: bool - personal_fields: List[Tuple[str, str]] = [] + +@dataclass +class PersonalFieldsHolder: + personal_fields: Dict[str, List[str]] = field(init=False, default_factory=lambda: dict()) + + def has_pf(self, field_name, *keys): + def _check_pfs(key): + if key in self.personal_fields: + if field_name in self.personal_fields[key]: + return True + return False + return any(_check_pfs(key) for key in keys) + + def add_field(self, model_name, model_id, field_name): + def _add(key): + if key not in self.personal_fields: + self.personal_fields.setdefault(key, [field_name]) + else: + self.personal_fields[key].append(field_name) + _add(model_id) + _add(model_name) + + +@dataclass +class ImportDefaults(PersonalFieldsHolder): + import_message: str = field(init=False) + suggest_tag_imported_cards: bool = field(init=False) class Properties(Enum): IMPORT_MESSAGE = ConfigEntry("import_message", "") SUGGEST_TAG_IMPORTED_CARDS = ConfigEntry("suggest_tag_imported_cards", False) - def __init__(self, settings_dict: dict): - for prop in self.Properties: - setattr(self, prop.value.config_name, settings_dict.get(prop.value.config_name, prop.value.default_value)) + @classmethod + def from_dict(cls, settings_dict: dict) -> 'ImportDefaults': + new_cls = cls() + for prop in cls.Properties: + setattr(new_cls, prop.value.config_name, settings_dict.get(prop.value.config_name, prop.value.default_value)) + new_cls._setup_personal_fields(settings_dict) + return new_cls + def _setup_personal_fields(self, settings_dict): models = settings_dict.get("note_models", []) - for model_name in models: - personal_fields = models[model_name].get("personal_fields", []) - for pf in personal_fields: - self.personal_fields.append((model_name, pf)) + for model_name_or_id in models: + self.personal_fields.setdefault(model_name_or_id, models[model_name_or_id].get("personal_fields", [])) -class ImportConfig: - personal_fields: List[Tuple[str, str]] = [] - add_tag_to_cards: List[str] = [] +@dataclass +class ImportConfig(PersonalFieldsHolder): + add_tag_to_cards: List[str] - use_header: bool = True - use_note_models: bool = True - use_notes: bool = True - use_media: bool = True + use_header: bool + use_note_models: bool + use_notes: bool + use_media: bool - ignore_deck_movement: bool = False + ignore_deck_movement: bool def __repr__(self): return f"ImportConfig({self.personal_fields!r}, {self.add_tag_to_cards!r}, " \ @@ -58,17 +88,16 @@ def __init__(self, deck_json, config, parent=None): self.form.setupUi(self) self.userConfig = ConfigSettings.get_instance() self.deck_json = deck_json - self.import_settings = ImportJsonSetting(config) + self.import_defaults = ImportDefaults.from_dict(config) self.ui_initial_setup() self.final_import_config: ImportConfig = None def accept(self): - self.get_import_config() + self.read_import_config() super().accept() def reject(self): - self.get_import_config() super().reject() def ui_initial_setup(self): @@ -95,16 +124,17 @@ def add_field(name, is_personal): for model in self.deck_json["note_models"]: model_name = model["name"] + model_id = model[UUID_FIELD_NAME] add_header(model_name) for field in model["flds"]: field_name = field["name"] - add_field(field_name, (model_name, field_name) in self.import_settings.personal_fields) + add_field(field_name, self.import_defaults.has_pf(field_name, model_name, model_id)) def setup_misc(self): - self.form.import_message_textbox.setText(self.import_settings.import_message) + self.form.import_message_textbox.setText(self.import_defaults.import_message) - if self.import_settings.suggest_tag_imported_cards: + if self.import_defaults.suggest_tag_imported_cards: self.form.cb_tag_cards.setCheckState(Qt.Checked) self.form.cb_tag_cards.setText("Tag Cards (Suggested by Deck Manager!)") # else: @@ -118,29 +148,36 @@ def setup_misc(self): self.form.cb_notes.setCheckState(Qt.Checked) self.form.cb_media.setCheckState(Qt.Checked) - # TODO: Deck Parts to Use + # TODO: Deck Parts to Use, check which are actually in the deck_json - def get_import_config(self): - config = ImportConfig() + def read_import_config(self): + config = ImportConfig( + add_tag_to_cards=string_cs_to_list( + self.form.textedit_tags.text()) if self.form.cb_tag_cards.isChecked() else [], - config.personal_fields = [] - current_note_model = "" - fields: List[QListWidgetItem] = [self.form.list_personal_fields.item(i) for i in - range(self.form.list_personal_fields.count())] - for field in fields: - if not field.flags() & Qt.ItemIsUserCheckable: # Note Model Header - current_note_model = field.text() - elif field.checkState() == Qt.Checked: # Field - config.personal_fields.append((current_note_model, field.text())) - - config.add_tag_to_cards = string_cs_to_list( - self.form.textedit_tags.text()) if self.form.cb_tag_cards.isChecked() else [] + use_header=self.form.cb_headers.isChecked(), + use_note_models=self.form.cb_note_models.isChecked(), + use_notes=self.form.cb_notes.isChecked(), + use_media=self.form.cb_media.isChecked(), - config.use_header = self.form.cb_headers.isChecked() - config.use_note_models = self.form.cb_note_models.isChecked() - config.use_notes = self.form.cb_notes.isChecked() - config.use_media = self.form.cb_media.isChecked() + ignore_deck_movement=self.form.cb_ignore_move_cards.isChecked() + ) - config.ignore_deck_movement = self.form.cb_ignore_move_cards.isChecked() + self.read_personal_fields(config) self.final_import_config = config + + def read_personal_fields(self, config): + current_note_model = "" + current_uuid = "" + ui_fields: List[QListWidgetItem] = self.get_ui_pf_items() + for ui_field in ui_fields: + if not ui_field.flags() & Qt.ItemIsUserCheckable: # Note Model Header + current_note_model = ui_field.text() + current_uuid = next(model[UUID_FIELD_NAME] + for model in self.deck_json["note_models"] if model["name"] == current_note_model) + elif ui_field.checkState() == Qt.Checked: # Field + config.add_field(current_note_model, current_uuid, ui_field.text()) + + def get_ui_pf_items(self): + return [self.form.list_personal_fields.item(i) for i in range(self.form.list_personal_fields.count())] diff --git a/crowd_anki/representation/deck.py b/crowd_anki/representation/deck.py index 120228e..522dac0 100644 --- a/crowd_anki/representation/deck.py +++ b/crowd_anki/representation/deck.py @@ -6,6 +6,7 @@ from .json_serializable import JsonSerializableAnkiDict from .note_model import NoteModel from ..anki.adapters.file_provider import FileProvider +from ..importer.import_dialog import ImportConfig from ..utils import utils from ..utils.constants import UUID_FIELD_NAME from ..utils.uuid import UuidFetcher @@ -127,6 +128,7 @@ def _load_metadata_from_json(self, json_dict): def save_to_collection(self, collection, + import_config: ImportConfig, parent_name="", save_configs=True, save_note_models=True, @@ -150,7 +152,7 @@ def save_to_collection(self, model_map_cache=model_map_cache) for note in self.notes: - note.save_to_collection(collection, self, model_map_cache) + note.save_to_collection(collection, self, model_map_cache, import_config=import_config) def _save_deck(self, collection, parent_name): name = (parent_name + self.DECK_NAME_DELIMITER if parent_name else "") + self.anki_dict["name"] diff --git a/crowd_anki/representation/deck_initializer.py b/crowd_anki/representation/deck_initializer.py index f581e94..2d43547 100644 --- a/crowd_anki/representation/deck_initializer.py +++ b/crowd_anki/representation/deck_initializer.py @@ -32,7 +32,7 @@ def from_collection(collection, name, deck_metadata=None, is_child=False) -> Dec return deck -def from_json(json_dict, import_config: ImportConfig, deck_metadata=None) -> Deck: +def from_json(json_dict, deck_metadata=None) -> Deck: """load metadata, load notes, load children""" deck = Deck(NoteModelFileProvider, json_dict) deck._update_fields() @@ -42,8 +42,8 @@ def from_json(json_dict, import_config: ImportConfig, deck_metadata=None) -> Dec deck._load_metadata_from_json(json_dict) deck.deck_config_uuid = json_dict["deck_config_uuid"] - deck.notes = [Note.from_json(json_note, import_config=import_config) for json_note in json_dict["notes"]] - deck.children = [from_json(child, import_config=import_config, deck_metadata=deck.metadata) for child in json_dict["children"]] + deck.notes = [Note.from_json(json_note) for json_note in json_dict["notes"]] + deck.children = [from_json(child, deck_metadata=deck.metadata) for child in json_dict["children"]] # Todo should I call this here? deck.post_import_filter() diff --git a/crowd_anki/representation/json_serializable.py b/crowd_anki/representation/json_serializable.py index 7531f1c..8b6bd4f 100644 --- a/crowd_anki/representation/json_serializable.py +++ b/crowd_anki/representation/json_serializable.py @@ -9,8 +9,7 @@ class JsonSerializable: export_filter_set = { "mod", # Modification time "usn", # Todo clarify - "id", - "import_config" + "id" } import_filter_set = {"__type__"} diff --git a/crowd_anki/representation/note.py b/crowd_anki/representation/note.py index cb68caf..d5bb0b7 100644 --- a/crowd_anki/representation/note.py +++ b/crowd_anki/representation/note.py @@ -21,11 +21,10 @@ class Note(JsonSerializableAnkiObject): "newlyAdded" } - def __init__(self, anki_note=None, config: ConfigSettings = None, import_config: ImportConfig = None): + def __init__(self, anki_note=None, config: ConfigSettings = None): super(Note, self).__init__(anki_note) self.note_model_uuid = None self.config = config or ConfigSettings.get_instance() - self.import_config = import_config or ImportConfig() @staticmethod def get_notes_from_collection(collection, deck_id, note_models): @@ -45,8 +44,8 @@ def from_collection(cls, collection, note_id, note_models): return note @classmethod - def from_json(cls, json_dict, import_config: ImportConfig): - note = Note(import_config=import_config) + def from_json(cls, json_dict): + note = Note() note.anki_object_dict = json_dict note.note_model_uuid = json_dict["note_model_uuid"] return note @@ -101,7 +100,7 @@ def move_cards_to_deck(self, deck_id, move_from_dynamic_decks=False): card.move_to_deck(deck_id, move_from_dynamic_decks) card.flush() - def save_to_collection(self, collection, deck, model_map_cache): + def save_to_collection(self, collection, deck, model_map_cache, import_config): # Todo uuid match on existing notes note_model = deck.metadata.models[self.note_model_uuid] @@ -118,28 +117,23 @@ def save_to_collection(self, collection, deck, model_map_cache): else: self.handle_model_update(collection, model_map_cache) - self.handle_dictionary_update(note_model) + self.handle_import_config_changes(import_config, note_model) + + self.anki_object.__dict__.update(self.anki_object_dict) self.anki_object.mid = note_model.anki_dict["id"] self.anki_object.mod = anki.utils.intTime() self.anki_object.flush() if new_note: collection.addNote(self.anki_object) - elif not self.import_config.ignore_deck_movement: + elif not import_config.ignore_deck_movement: self.move_cards_to_deck(deck.anki_dict["id"]) - def handle_dictionary_update(self, note_model): - - # Personal Fields Resolution - fields = self.anki_object_dict["fields"] - for num, field in enumerate(fields): - model_field_pair = (note_model.anki_dict['name'], note_model.anki_dict['flds'][num]['name']) - if model_field_pair in self.import_config.personal_fields: - fields[num] = self.anki_object.fields[num] + def handle_import_config_changes(self, import_config, note_model): + # Personal Fields + for num, field in enumerate(self.anki_object_dict["fields"]): + if import_config.has_pf(note_model.anki_dict['flds'][num]['name'], note_model.anki_dict['name']): + self.anki_object_dict["fields"][num] = self.anki_object.fields[num] # Tag Cards on Import - self.anki_object_dict["tags"] += self.import_config.add_tag_to_cards - - # Update dict - self.anki_object_dict["fields"] = fields - self.anki_object.__dict__.update(self.anki_object_dict) + self.anki_object_dict["tags"] += import_config.add_tag_to_cards diff --git a/crowd_anki/utils/constants.py b/crowd_anki/utils/constants.py index d628578..0e1269f 100644 --- a/crowd_anki/utils/constants.py +++ b/crowd_anki/utils/constants.py @@ -4,8 +4,7 @@ DECK_FILE_NAME = "deck" DECK_FILE_EXTENSION = ".json" MEDIA_SUBDIRECTORY_NAME = "media" -IMPORT_CONFIG_NAME = "import_config" -CONFIG_EXTENSION = ".yaml" +IMPORT_CONFIG_NAME = "import_config.yaml" ANKI_EXPORT_EXTENSION = "directory" diff --git a/crowd_anki/utils/utils.py b/crowd_anki/utils/utils.py index bc0f129..2849f3b 100644 --- a/crowd_anki/utils/utils.py +++ b/crowd_anki/utils/utils.py @@ -62,4 +62,4 @@ def list_to_cs_string(uf_list: list) -> str: def string_cs_to_list(f_list: str) -> list: - return [x.strip() for x in f_list.split(',')] + return [x.strip() for x in f_list.split(',')] if f_list else [] From 6994cba3f90c8008e8d56c34dda95b2c9c0eec20 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 21 Jun 2020 18:31:05 +0200 Subject: [PATCH 22/30] Use Notes and Note Models Toggles --- crowd_anki/representation/deck.py | 8 ++++---- crowd_anki/representation/note_model.py | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/crowd_anki/representation/deck.py b/crowd_anki/representation/deck.py index 522dac0..c64cd5a 100644 --- a/crowd_anki/representation/deck.py +++ b/crowd_anki/representation/deck.py @@ -139,7 +139,7 @@ def save_to_collection(self, if save_note_models: for note_model in self.metadata.models.values(): - note_model.save_to_collection(collection) + note_model.save_to_collection(collection, import_config.use_note_models) name = self._save_deck(collection, parent_name) @@ -150,9 +150,9 @@ def save_to_collection(self, save_configs=False, save_note_models=False, model_map_cache=model_map_cache) - - for note in self.notes: - note.save_to_collection(collection, self, model_map_cache, import_config=import_config) + if import_config.use_notes: + for note in self.notes: + note.save_to_collection(collection, self, model_map_cache, import_config=import_config) def _save_deck(self, collection, parent_name): name = (parent_name + self.DECK_NAME_DELIMITER if parent_name else "") + self.anki_dict["name"] diff --git a/crowd_anki/representation/note_model.py b/crowd_anki/representation/note_model.py index 27b37e4..f929bba 100644 --- a/crowd_anki/representation/note_model.py +++ b/crowd_anki/representation/note_model.py @@ -33,7 +33,7 @@ def check_semantically_identical(first_model, second_model): return True - def save_to_collection(self, collection: Collection): + def save_to_collection(self, collection: Collection, should_save: bool): # Todo regenerate cards on update # look into template manipulation in "models" @@ -43,14 +43,16 @@ def save_to_collection(self, collection: Collection): new_model = note_model_dict["id"] is None self.anki_dict = utils.merge_dicts(note_model_dict, self.anki_dict) - if new_model: - collection.models.add(self.anki_dict) - else: - collection.models.update(self.anki_dict) - collection.models.flush() - - if not new_model: - self.update_cards(collection, note_model_dict) + + if should_save: + if new_model: + collection.models.add(self.anki_dict) + else: + collection.models.update(self.anki_dict) + collection.models.flush() + + if not new_model: + self.update_cards(collection, note_model_dict) def make_current(self, collection): # Sync through setting global "current" model makes me sad too, but it's ingrained on many levels down From 2f5db243b824df569afbac16f9dca8a79137b9a3 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 21 Jun 2020 18:50:40 +0200 Subject: [PATCH 23/30] Deck Parts Counter --- crowd_anki/importer/import_dialog.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index c4402e0..ea20b2a 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -143,10 +143,16 @@ def setup_misc(self): if self.userConfig.import_notes_ignore_deck_movement: self.form.cb_ignore_move_cards.setCheckState(Qt.Checked) - self.form.cb_headers.setCheckState(Qt.Checked) - self.form.cb_note_models.setCheckState(Qt.Checked) - self.form.cb_notes.setCheckState(Qt.Checked) - self.form.cb_media.setCheckState(Qt.Checked) + def set_checked_and_text(checkbox, text, count=0, checked: bool = True): + checkbox.setCheckState(Qt.Checked if checked else Qt.Unchecked) + if count: + text = f"{text}: {'{:,}'.format(count)}" + checkbox.setText(text) + + set_checked_and_text(self.form.cb_headers, "Headers") + set_checked_and_text(self.form.cb_note_models, "Note Models", len(self.deck_json['note_models'])) + set_checked_and_text(self.form.cb_notes, f"Notes", len(self.deck_json['notes'])) + set_checked_and_text(self.form.cb_media, f"Media Files", len(self.deck_json['media_files'])) # TODO: Deck Parts to Use, check which are actually in the deck_json From 008cbcf7718a6200aaf86e6dc2cb0fe5b4aeea86 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 21 Jun 2020 19:30:13 +0200 Subject: [PATCH 24/30] Improved Counter to Show 0s --- crowd_anki/importer/import_dialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index ea20b2a..275595e 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -143,13 +143,13 @@ def setup_misc(self): if self.userConfig.import_notes_ignore_deck_movement: self.form.cb_ignore_move_cards.setCheckState(Qt.Checked) - def set_checked_and_text(checkbox, text, count=0, checked: bool = True): + def set_checked_and_text(checkbox, text, count, checked: bool = True): checkbox.setCheckState(Qt.Checked if checked else Qt.Unchecked) - if count: + if count is not None: text = f"{text}: {'{:,}'.format(count)}" checkbox.setText(text) - set_checked_and_text(self.form.cb_headers, "Headers") + set_checked_and_text(self.form.cb_headers, "Headers", None) set_checked_and_text(self.form.cb_note_models, "Note Models", len(self.deck_json['note_models'])) set_checked_and_text(self.form.cb_notes, f"Notes", len(self.deck_json['notes'])) set_checked_and_text(self.form.cb_media, f"Media Files", len(self.deck_json['media_files'])) From 2e97a78e92ff825dd64e8f5d0e370a6c3740f632 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sat, 4 Jul 2020 09:20:27 +0200 Subject: [PATCH 25/30] Tidy up and Revisions --- crowd_anki/importer/anki_importer.py | 8 +- crowd_anki/importer/import_dialog.py | 76 +++++++------------ crowd_anki/representation/deck_initializer.py | 1 - crowd_anki/representation/note.py | 4 +- 4 files changed, 34 insertions(+), 55 deletions(-) diff --git a/crowd_anki/importer/anki_importer.py b/crowd_anki/importer/anki_importer.py index a98e374..ae685a4 100644 --- a/crowd_anki/importer/anki_importer.py +++ b/crowd_anki/importer/anki_importer.py @@ -28,8 +28,8 @@ def load_deck(self, directory_path) -> bool: """ deck_json = self.read_deck(self.get_deck_path(directory_path)) - import_config, ui_accepted = self.read_import_config(directory_path, deck_json) - if not ui_accepted: + import_config = self.read_import_config(directory_path, deck_json) + if import_config is None: return False if aqt.mw: @@ -90,9 +90,9 @@ def read_import_config(directory_path, deck_json): import_dialog = ImportDialog(deck_json, import_dict) if import_dialog.exec_() == QDialog.Rejected: - return None, False + return None - return import_dialog.final_import_config, True + return import_dialog.final_import_config @staticmethod def import_deck_from_path(collection, directory_path): diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index 275595e..1260c89 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -1,4 +1,4 @@ -from collections import namedtuple +from collections import namedtuple, defaultdict from dataclasses import dataclass, field from enum import Enum @@ -20,24 +20,16 @@ class ConfigEntry: @dataclass class PersonalFieldsHolder: - personal_fields: Dict[str, List[str]] = field(init=False, default_factory=lambda: dict()) - - def has_pf(self, field_name, *keys): - def _check_pfs(key): - if key in self.personal_fields: - if field_name in self.personal_fields[key]: - return True - return False - return any(_check_pfs(key) for key in keys) - - def add_field(self, model_name, model_id, field_name): - def _add(key): - if key not in self.personal_fields: - self.personal_fields.setdefault(key, [field_name]) - else: - self.personal_fields[key].append(field_name) - _add(model_id) - _add(model_name) + personal_fields: defaultdict = field(init=False, default_factory=lambda: defaultdict(list)) + + def is_personal_field(self, model_name, field_name): + if model_name in self.personal_fields: + if field_name in self.personal_fields[model_name]: + return True + return False + + def add_field(self, model_name, field_name): + self.personal_fields[model_name].append(field_name) @dataclass @@ -58,9 +50,9 @@ def from_dict(cls, settings_dict: dict) -> 'ImportDefaults': return new_cls def _setup_personal_fields(self, settings_dict): - models = settings_dict.get("note_models", []) - for model_name_or_id in models: - self.personal_fields.setdefault(model_name_or_id, models[model_name_or_id].get("personal_fields", [])) + models = settings_dict.get("note_models", dict()) + for model_name, keys in models.items(): + self.personal_fields.setdefault(model_name, keys.get("personal_fields", [])) @dataclass @@ -74,11 +66,6 @@ class ImportConfig(PersonalFieldsHolder): ignore_deck_movement: bool - def __repr__(self): - return f"ImportConfig({self.personal_fields!r}, {self.add_tag_to_cards!r}, " \ - f"{self.use_header!r}, {self.use_notes!r}, {self.use_note_models!r}, {self.use_media!r} " \ - f"{self.ignore_deck_movement!r})" - class ImportDialog(QDialog): def __init__(self, deck_json, config, parent=None): @@ -89,6 +76,7 @@ def __init__(self, deck_json, config, parent=None): self.userConfig = ConfigSettings.get_instance() self.deck_json = deck_json self.import_defaults = ImportDefaults.from_dict(config) + self.personal_field_ui_dict = defaultdict(dict) self.ui_initial_setup() self.final_import_config: ImportConfig = None @@ -97,9 +85,6 @@ def accept(self): self.read_import_config() super().accept() - def reject(self): - super().reject() - def ui_initial_setup(self): self.setup_personal_field_selection() self.setup_misc() @@ -116,11 +101,12 @@ def add_header(name): heading_ui.setFont(heading_font) self.form.list_personal_fields.addItem(heading_ui) - def add_field(name, is_personal): + def add_field(name, is_personal) -> QListWidgetItem: field_ui = QListWidgetItem(name) field_ui.setCheckState(Qt.Checked if is_personal else Qt.Unchecked) field_ui.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) self.form.list_personal_fields.addItem(field_ui) + return field_ui for model in self.deck_json["note_models"]: model_name = model["name"] @@ -129,7 +115,8 @@ def add_field(name, is_personal): for field in model["flds"]: field_name = field["name"] - add_field(field_name, self.import_defaults.has_pf(field_name, model_name, model_id)) + field_ui = add_field(field_name, self.import_defaults.is_personal_field(model_name, field_name)) + self.personal_field_ui_dict[model_name].setdefault(field_name, field_ui) def setup_misc(self): self.form.import_message_textbox.setText(self.import_defaults.import_message) @@ -149,10 +136,12 @@ def set_checked_and_text(checkbox, text, count, checked: bool = True): text = f"{text}: {'{:,}'.format(count)}" checkbox.setText(text) - set_checked_and_text(self.form.cb_headers, "Headers", None) + self.form.cb_headers.setVisible(False) # TODO: implement header selection functionality. Disabled for now + # set_checked_and_text(self.form.cb_headers, "Headers", None) + set_checked_and_text(self.form.cb_note_models, "Note Models", len(self.deck_json['note_models'])) - set_checked_and_text(self.form.cb_notes, f"Notes", len(self.deck_json['notes'])) - set_checked_and_text(self.form.cb_media, f"Media Files", len(self.deck_json['media_files'])) + set_checked_and_text(self.form.cb_notes, "Notes", len(self.deck_json['notes'])) + set_checked_and_text(self.form.cb_media, "Media Files", len(self.deck_json['media_files'])) # TODO: Deck Parts to Use, check which are actually in the deck_json @@ -174,16 +163,7 @@ def read_import_config(self): self.final_import_config = config def read_personal_fields(self, config): - current_note_model = "" - current_uuid = "" - ui_fields: List[QListWidgetItem] = self.get_ui_pf_items() - for ui_field in ui_fields: - if not ui_field.flags() & Qt.ItemIsUserCheckable: # Note Model Header - current_note_model = ui_field.text() - current_uuid = next(model[UUID_FIELD_NAME] - for model in self.deck_json["note_models"] if model["name"] == current_note_model) - elif ui_field.checkState() == Qt.Checked: # Field - config.add_field(current_note_model, current_uuid, ui_field.text()) - - def get_ui_pf_items(self): - return [self.form.list_personal_fields.item(i) for i in range(self.form.list_personal_fields.count())] + for model_name, fields_dict in self.personal_field_ui_dict.items(): + for field_name, widget_item in fields_dict.items(): + if widget_item.checkState() == Qt.Checked: + config.add_field(model_name, field_name) diff --git a/crowd_anki/representation/deck_initializer.py b/crowd_anki/representation/deck_initializer.py index 2d43547..6846dd2 100644 --- a/crowd_anki/representation/deck_initializer.py +++ b/crowd_anki/representation/deck_initializer.py @@ -4,7 +4,6 @@ from .note import Note from ..anki.adapters.anki_deck import AnkiDeck from ..anki.adapters.note_model_file_provider import NoteModelFileProvider -from ..importer.import_dialog import ImportConfig def from_collection(collection, name, deck_metadata=None, is_child=False) -> Deck: diff --git a/crowd_anki/representation/note.py b/crowd_anki/representation/note.py index d5bb0b7..1f84ef3 100644 --- a/crowd_anki/representation/note.py +++ b/crowd_anki/representation/note.py @@ -131,8 +131,8 @@ def save_to_collection(self, collection, deck, model_map_cache, import_config): def handle_import_config_changes(self, import_config, note_model): # Personal Fields - for num, field in enumerate(self.anki_object_dict["fields"]): - if import_config.has_pf(note_model.anki_dict['flds'][num]['name'], note_model.anki_dict['name']): + for num in range(len(self.anki_object_dict["fields"])): + if import_config.is_personal_field(note_model.anki_dict['name'], note_model.anki_dict['flds'][num]['name']): self.anki_object_dict["fields"][num] = self.anki_object.fields[num] # Tag Cards on Import From 4627c0cbce318e620229636d49643455fba7c078 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 5 Jul 2020 09:19:07 +0200 Subject: [PATCH 26/30] Remove Deck Header option --- crowd_anki/importer/import_dialog.py | 5 ----- crowd_anki/importer/import_ui.py | 4 ---- ui_files/import.ui | 7 ------- 3 files changed, 16 deletions(-) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index 1260c89..d5d9cdf 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -59,7 +59,6 @@ def _setup_personal_fields(self, settings_dict): class ImportConfig(PersonalFieldsHolder): add_tag_to_cards: List[str] - use_header: bool use_note_models: bool use_notes: bool use_media: bool @@ -136,9 +135,6 @@ def set_checked_and_text(checkbox, text, count, checked: bool = True): text = f"{text}: {'{:,}'.format(count)}" checkbox.setText(text) - self.form.cb_headers.setVisible(False) # TODO: implement header selection functionality. Disabled for now - # set_checked_and_text(self.form.cb_headers, "Headers", None) - set_checked_and_text(self.form.cb_note_models, "Note Models", len(self.deck_json['note_models'])) set_checked_and_text(self.form.cb_notes, "Notes", len(self.deck_json['notes'])) set_checked_and_text(self.form.cb_media, "Media Files", len(self.deck_json['media_files'])) @@ -150,7 +146,6 @@ def read_import_config(self): add_tag_to_cards=string_cs_to_list( self.form.textedit_tags.text()) if self.form.cb_tag_cards.isChecked() else [], - use_header=self.form.cb_headers.isChecked(), use_note_models=self.form.cb_note_models.isChecked(), use_notes=self.form.cb_notes.isChecked(), use_media=self.form.cb_media.isChecked(), diff --git a/crowd_anki/importer/import_ui.py b/crowd_anki/importer/import_ui.py index 408d97a..8fb3073 100644 --- a/crowd_anki/importer/import_ui.py +++ b/crowd_anki/importer/import_ui.py @@ -64,9 +64,6 @@ def setupUi(self, Dialog): self.label_2.setEnabled(True) self.label_2.setObjectName("label_2") self.verticalLayout_7.addWidget(self.label_2) - self.cb_headers = QtWidgets.QCheckBox(self.group_deck_import) - self.cb_headers.setObjectName("cb_headers") - self.verticalLayout_7.addWidget(self.cb_headers) self.cb_note_models = QtWidgets.QCheckBox(self.group_deck_import) self.cb_note_models.setObjectName("cb_note_models") self.verticalLayout_7.addWidget(self.cb_note_models) @@ -109,7 +106,6 @@ def retranslateUi(self, Dialog): self.textedit_tags.setPlaceholderText(_translate("Dialog", "Tag1, RecentlyImported, Broken")) self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) self.label_2.setText(_translate("Dialog", "

Deck Parts to Use

")) - self.cb_headers.setText(_translate("Dialog", "Headers")) self.cb_note_models.setText(_translate("Dialog", "Note Models")) self.cb_notes.setText(_translate("Dialog", "Notes (Cards)")) self.cb_media.setText(_translate("Dialog", "Media")) diff --git a/ui_files/import.ui b/ui_files/import.ui index aa9f92e..75ca22e 100644 --- a/ui_files/import.ui +++ b/ui_files/import.ui @@ -94,13 +94,6 @@ - - - - Headers - - - From 6586d1eac1246c77659c93331c04bd66d5f8a7c8 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 5 Jul 2020 09:53:58 +0200 Subject: [PATCH 27/30] Simplify Deck save_to_collection --- crowd_anki/representation/deck.py | 37 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/crowd_anki/representation/deck.py b/crowd_anki/representation/deck.py index c64cd5a..05f349e 100644 --- a/crowd_anki/representation/deck.py +++ b/crowd_anki/representation/deck.py @@ -126,30 +126,27 @@ def _load_metadata_from_json(self, json_dict): self.metadata = DeckMetadata(new_deck_configs, new_models) - def save_to_collection(self, - collection, - import_config: ImportConfig, - parent_name="", - save_configs=True, - save_note_models=True, - model_map_cache=None): - if save_configs: # Todo when update implemented multiple save can be harmless and code simpler - for config in self.metadata.deck_configs.values(): - config.save_to_collection(collection) - - if save_note_models: - for note_model in self.metadata.models.values(): - note_model.save_to_collection(collection, import_config.use_note_models) + def save_to_collection(self, collection, import_config: ImportConfig): + for config in self.metadata.deck_configs.values(): + config.save_to_collection(collection) + for note_model in self.metadata.models.values(): + note_model.save_to_collection(collection, import_config.use_note_models) + + self.save_children_and_notes(collection=collection, + import_config=import_config, + parent_name="", + model_map_cache=defaultdict(dict)) + + def save_children_and_notes(self, collection, import_config: ImportConfig, parent_name, model_map_cache): name = self._save_deck(collection, parent_name) - model_map_cache = model_map_cache or defaultdict(dict) for child in self.children: - child.save_to_collection(collection, - parent_name=name, - save_configs=False, - save_note_models=False, - model_map_cache=model_map_cache) + child.save_children_and_notes(collection=collection, + import_config=import_config, + parent_name=name, + model_map_cache=model_map_cache) + if import_config.use_notes: for note in self.notes: note.save_to_collection(collection, self, model_map_cache, import_config=import_config) From 957ad7ce43c243c206e0326481b2d383b3b796d0 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 5 Jul 2020 09:58:20 +0200 Subject: [PATCH 28/30] Manager -> Maintainer --- crowd_anki/importer/import_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index d5d9cdf..f934c84 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -122,7 +122,7 @@ def setup_misc(self): if self.import_defaults.suggest_tag_imported_cards: self.form.cb_tag_cards.setCheckState(Qt.Checked) - self.form.cb_tag_cards.setText("Tag Cards (Suggested by Deck Manager!)") + self.form.cb_tag_cards.setText("Tag Cards (Suggested by Deck Maintainer!)") # else: # set as default from config settings From 9c172f877922648502e497f073cbcaf23fe143d8 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 5 Jul 2020 10:07:23 +0200 Subject: [PATCH 29/30] Deck Part Checkboxes Separate Function --- crowd_anki/importer/import_dialog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index f934c84..ea08dcb 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -87,6 +87,7 @@ def accept(self): def ui_initial_setup(self): self.setup_personal_field_selection() self.setup_misc() + self.setup_deck_part_checkboxes() def setup_personal_field_selection(self): heading_font = QFont() @@ -129,6 +130,7 @@ def setup_misc(self): if self.userConfig.import_notes_ignore_deck_movement: self.form.cb_ignore_move_cards.setCheckState(Qt.Checked) + def setup_deck_part_checkboxes(self): def set_checked_and_text(checkbox, text, count, checked: bool = True): checkbox.setCheckState(Qt.Checked if checked else Qt.Unchecked) if count is not None: From 92a09d01337e38f053334e973c1227c24ff514fb Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Sun, 5 Jul 2020 10:16:02 +0200 Subject: [PATCH 30/30] Remove Note Model option, for new PR --- crowd_anki/importer/import_dialog.py | 3 --- crowd_anki/importer/import_ui.py | 4 ---- crowd_anki/representation/deck.py | 2 +- crowd_anki/representation/note_model.py | 17 ++++++++--------- ui_files/import.ui | 7 ------- 5 files changed, 9 insertions(+), 24 deletions(-) diff --git a/crowd_anki/importer/import_dialog.py b/crowd_anki/importer/import_dialog.py index ea08dcb..19296fb 100644 --- a/crowd_anki/importer/import_dialog.py +++ b/crowd_anki/importer/import_dialog.py @@ -59,7 +59,6 @@ def _setup_personal_fields(self, settings_dict): class ImportConfig(PersonalFieldsHolder): add_tag_to_cards: List[str] - use_note_models: bool use_notes: bool use_media: bool @@ -137,7 +136,6 @@ def set_checked_and_text(checkbox, text, count, checked: bool = True): text = f"{text}: {'{:,}'.format(count)}" checkbox.setText(text) - set_checked_and_text(self.form.cb_note_models, "Note Models", len(self.deck_json['note_models'])) set_checked_and_text(self.form.cb_notes, "Notes", len(self.deck_json['notes'])) set_checked_and_text(self.form.cb_media, "Media Files", len(self.deck_json['media_files'])) @@ -148,7 +146,6 @@ def read_import_config(self): add_tag_to_cards=string_cs_to_list( self.form.textedit_tags.text()) if self.form.cb_tag_cards.isChecked() else [], - use_note_models=self.form.cb_note_models.isChecked(), use_notes=self.form.cb_notes.isChecked(), use_media=self.form.cb_media.isChecked(), diff --git a/crowd_anki/importer/import_ui.py b/crowd_anki/importer/import_ui.py index 8fb3073..ef8770e 100644 --- a/crowd_anki/importer/import_ui.py +++ b/crowd_anki/importer/import_ui.py @@ -64,9 +64,6 @@ def setupUi(self, Dialog): self.label_2.setEnabled(True) self.label_2.setObjectName("label_2") self.verticalLayout_7.addWidget(self.label_2) - self.cb_note_models = QtWidgets.QCheckBox(self.group_deck_import) - self.cb_note_models.setObjectName("cb_note_models") - self.verticalLayout_7.addWidget(self.cb_note_models) self.cb_notes = QtWidgets.QCheckBox(self.group_deck_import) self.cb_notes.setObjectName("cb_notes") self.verticalLayout_7.addWidget(self.cb_notes) @@ -106,7 +103,6 @@ def retranslateUi(self, Dialog): self.textedit_tags.setPlaceholderText(_translate("Dialog", "Tag1, RecentlyImported, Broken")) self.cb_ignore_move_cards.setText(_translate("Dialog", "Do Not Move Existing Cards")) self.label_2.setText(_translate("Dialog", "

Deck Parts to Use

")) - self.cb_note_models.setText(_translate("Dialog", "Note Models")) self.cb_notes.setText(_translate("Dialog", "Notes (Cards)")) self.cb_media.setText(_translate("Dialog", "Media")) self.label.setText(_translate("Dialog", "

Personal Fields - Fields which will keep their existing values instead of being imported

")) diff --git a/crowd_anki/representation/deck.py b/crowd_anki/representation/deck.py index 05f349e..77bbf5c 100644 --- a/crowd_anki/representation/deck.py +++ b/crowd_anki/representation/deck.py @@ -131,7 +131,7 @@ def save_to_collection(self, collection, import_config: ImportConfig): config.save_to_collection(collection) for note_model in self.metadata.models.values(): - note_model.save_to_collection(collection, import_config.use_note_models) + note_model.save_to_collection(collection) self.save_children_and_notes(collection=collection, import_config=import_config, diff --git a/crowd_anki/representation/note_model.py b/crowd_anki/representation/note_model.py index f929bba..bdc4190 100644 --- a/crowd_anki/representation/note_model.py +++ b/crowd_anki/representation/note_model.py @@ -33,7 +33,7 @@ def check_semantically_identical(first_model, second_model): return True - def save_to_collection(self, collection: Collection, should_save: bool): + def save_to_collection(self, collection: Collection): # Todo regenerate cards on update # look into template manipulation in "models" @@ -44,15 +44,14 @@ def save_to_collection(self, collection: Collection, should_save: bool): self.anki_dict = utils.merge_dicts(note_model_dict, self.anki_dict) - if should_save: - if new_model: - collection.models.add(self.anki_dict) - else: - collection.models.update(self.anki_dict) - collection.models.flush() + if new_model: + collection.models.add(self.anki_dict) + else: + collection.models.update(self.anki_dict) + collection.models.flush() - if not new_model: - self.update_cards(collection, note_model_dict) + if not new_model: + self.update_cards(collection, note_model_dict) def make_current(self, collection): # Sync through setting global "current" model makes me sad too, but it's ingrained on many levels down diff --git a/ui_files/import.ui b/ui_files/import.ui index 75ca22e..972731d 100644 --- a/ui_files/import.ui +++ b/ui_files/import.ui @@ -94,13 +94,6 @@
- - - - Note Models - - -