def drawQuestionAndAnswer(self): self.deck.flushMod() f = self.deck.newFact() f.tags = u"" for field in f.fields: f[field.name] = field.name f.model = self.model c = Card(f, self.card) t = "<body><br><center>" + c.htmlQuestion() + "</center></body>" bg = "body { background-color: %s; }\n" % self.card.lastFontColour self.dialog.question.setText( "<style>\n" + bg + self.deck.rebuildCSS() + "</style>\n" + t) t = "<body><br><center>" + c.htmlAnswer() + "</center></body>" self.dialog.answer.setText( "<style>\n" + bg + self.deck.rebuildCSS() + "</style>\n" + t) self.main.updateViews(self.main.state)
def _updateRevIvl(self, card: Card, ease: int) -> None: idealIvl = self._nextRevIvl(card, ease) card.ivl = min( max(self._adjRevIvl(card, idealIvl), card.ivl + 1), self._revConf(card)["maxIvl"], )
def answerCard(self, card: Card, ease: int) -> None: self.col.log() assert 1 <= ease <= 4 self.col.save_card_review_undo_info(card) if self._burySiblingsOnAnswer: self._burySiblings(card) card.reps += 1 self.reps += 1 # former is for logging new cards, latter also covers filt. decks card.wasNew = card.type == CARD_TYPE_NEW # type: ignore wasNewQ = card.queue == QUEUE_TYPE_NEW new_delta = 0 review_delta = 0 if wasNewQ: # came from the new queue, move to learning card.queue = QUEUE_TYPE_LRN # if it was a new card, it's now a learning card if card.type == CARD_TYPE_NEW: card.type = CARD_TYPE_LRN # init reps to graduation card.left = self._startingLeft(card) # dynamic? if card.odid and card.type == CARD_TYPE_REV: if self._resched(card): # reviews get their ivl boosted on first sight card.ivl = self._dynIvlBoost(card) card.odue = self.today + card.ivl new_delta = +1 if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN): self._answerLrnCard(card, ease) elif card.queue == QUEUE_TYPE_REV: self._answerRevCard(card, ease) review_delta = +1 else: raise Exception(f"Invalid queue '{card}'") self.update_stats( card.did, new_delta=new_delta, review_delta=review_delta, milliseconds_delta=+card.timeTaken(), ) card.mod = intTime() card.usn = self.col.usn() card.flush()
def _rescheduleAsRev(self, card: Card, conf: QueueConfig, early: bool) -> None: lapse = card.type == CARD_TYPE_REV if lapse: if self._resched(card): card.due = max(self.today + 1, card.odue) else: card.due = card.odue card.odue = 0 else: self._rescheduleNew(card, conf, early) card.queue = QUEUE_TYPE_REV card.type = CARD_TYPE_REV # if we were dynamic, graduating means moving back to the old deck resched = self._resched(card) if card.odid: card.did = card.odid card.odue = 0 card.odid = DeckID(0) # if rescheduling is off, it needs to be set back to a new card if not resched and not lapse: card.queue = QUEUE_TYPE_NEW card.type = CARD_TYPE_NEW card.due = self.col.nextID("pos")
def autoplay(self, card: Card) -> bool: print("use card.autoplay() instead of reviewer.autoplay(card)") return card.autoplay()
def setData(c: Card, value: str): n = c.note() n.setTagsFromStr(value) n.flush() advBrowser.editor.loadNote() return True
def is_cloze(card: Card): return card.model()['type'] == MODEL_CLOZE
def _updateRevIvl(self, card: Card, ease: int) -> None: card.ivl = self._nextRevIvl(card, ease, fuzz=True)
def templates_for_card(card: Card, browser: bool) -> Tuple[str, str]: template = card.template() q, a = browser and ("bqfmt", "bafmt") or ("qfmt", "afmt") return template.get(q), template.get(a) # type: ignore
def _newCard( self, note: Note, template: Template, due: int, flush: bool = True, did: Optional[int] = None, ) -> Card: "Create a new card." card = Card(self) card.nid = note.id card.ord = template["ord"] # type: ignore card.did = self.db.scalar( "select did from cards where nid = ? and ord = ?", card.nid, card.ord) # Use template did (deck override) if valid, otherwise did in argument, otherwise model did if not card.did: if template["did"] and str(template["did"]) in self.decks.decks: card.did = int(template["did"]) elif did: card.did = did else: card.did = note.model()["did"] # if invalid did, use default instead deck = self.decks.get(card.did) assert deck if deck["dyn"]: # must not be a filtered deck card.did = 1 else: card.did = deck["id"] card.due = self._dueForDid(card.did, due) if flush: card.flush() return card
def run(browser): cards = [Card(browser.col, cid) for cid in browser.selectedCards()] for card in cards: prepare(card, browser) for card in cards: cleanCard(card, browser)
def answerCard(self, card: Card, ease: int) -> None: self.col.log() assert 1 <= ease <= 4 self.col.markReview(card) if self._burySiblingsOnAnswer: self._burySiblings(card) card.reps += 1 # former is for logging new cards, latter also covers filt. decks card.wasNew = card.type == CARD_TYPE_NEW # type: ignore wasNewQ = card.queue == QUEUE_TYPE_NEW if wasNewQ: # came from the new queue, move to learning card.queue = QUEUE_TYPE_LRN # if it was a new card, it's now a learning card if card.type == CARD_TYPE_NEW: card.type = CARD_TYPE_LRN # init reps to graduation card.left = self._startingLeft(card) # dynamic? if card.odid and card.type == CARD_TYPE_REV: if self._resched(card): # reviews get their ivl boosted on first sight card.ivl = self._dynIvlBoost(card) card.odue = self.today + card.ivl self._updateStats(card, "new") if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN): self._answerLrnCard(card, ease) if not wasNewQ: self._updateStats(card, "lrn") elif card.queue == QUEUE_TYPE_REV: self._answerRevCard(card, ease) self._updateStats(card, "rev") else: raise Exception("Invalid queue") self._updateStats(card, "time", card.timeTaken()) card.mod = intTime() card.usn = self.col.usn() card.flush()
def __init__(self, origin: str, card: Card): super().__init__(origin) self.card_id = card.id self.question = card.question() self.answer = card.answer()
def maobi_hook(html: str, card: Card, context: str) -> str: # Only show the quiz on the front side, else it can lead to rendering issues if context not in {"reviewQuestion", "clayoutQuestion", "previewQuestion"}: return html # This reads from the config.json in the addon folder maobi_config = MaobiConfig.load() # Search the active deck configuration deck_name = mw.col.decks.current()["name"] template_name = card.template()["name"] config = maobi_config.search_active_deck_config(deck_name, template_name) # Return if we did not find it if not config: debug(maobi_config, f"Config not found") return html if not config.enabled: debug(maobi_config, f"Config disabled") return html # Get the character to write and the corresponding character data try: characters, tones = _get_characters(card, config) characters_data = [_load_character_data(c) for c in characters] except MaobiException as e: debug(maobi_config, str(e)) return _build_error_message(html, str(e)) # Style the character div depending on the configuration styles = [] # Add the background grid hanzi_grid = _build_hanzi_grid_style(config.grid) styles.append(hanzi_grid) # Load the hanzi writer JavaScript with open(PATH_HANZI_WRITER, "r") as f: hanzi_writer_script = f.read() # Load the maobi quiz JavaScript with open(PATH_QUIZ_JS, "r") as f: maobi_quiz_script = f.read() # Render the template data = { "html": html, "hanzi_writer_script": hanzi_writer_script, "maobi_quiz_script": maobi_quiz_script, "target_div": TARGET_DIV, "reveal_button": REVEAL_BUTTON, "restart_button": RESTART_BUTTON, "characters": characters, "tones": tones, "characters_data": characters_data, "size": config.size, "leniency": config.leniency / 100.0, "show_hint_after_misses": config.show_hint_after_misses, "styles": "\n".join(styles), } result = TEMPLATE.substitute(data) return result
def mergeNotes(note1, note2): """Merge note1 and note2 following the configuration rules. If they have distinct note type, fails.""" mw.checkpoint("Merge Notes") model1 = note1.model() model2 = note2.model() if model1 != model2: showWarning(_("Please select notes of the same type to merge them")) return merged_note = Note(mw.col, note1.model()) new_nid = merge_id(note1, note2) weak = maybeGetWeakNote(note1, note2) overwrite_patterns = [ re.compile(p) for p in (getUserOption("Overwrite patterns", []) or []) ] for i in range(len(merged_note.fields)): if maybeOverwriteField(overwrite_patterns, i, note1, note2): continue elif note1 == weak: merged_note.fields[i] = note2.fields[ i] if note2.fields[i] != "" else note1.fields[i] elif note2 == weak: merged_note.fields[i] = note1.fields[ i] if note1.fields[i] != "" else note2.fields[i] elif note1.fields[i] != note2.fields[i] or not getUserOption( "When identical keep a single field", True): merged_note.fields[i] = note1.fields[i] + note2.fields[i] else: # identical merged_note.fields[i] = note1.fields[i] cards = dict() # tags merged_note.addTag(note1.stringTags()) merged_note.addTag(note2.stringTags()) merged_note.addTag(f"merged merged_{note1.id} merged_{note2.id}") # Choosing which card to keep cards_to_delete = [] for card1 in note1.cards(): cards[card1.ord] = card1 for card2 in note2.cards(): ord = card2.ord card1 = cards.get(ord) if card1 is None or card1.type == CARD_NEW or card1.ivl < card2.ivl or ( card1.ivl == card2.ivl and card1.factor < card2.factor): cards[ord] = card2 cards_to_delete.append(card1) else: cards_to_delete.append(card2) if getUserOption("Delete original cards", False): mw.col._remNotes([note1.id]) mw.col._remNotes([note2.id]) mw.col.remCards( [card.id for card in cards_to_delete if card is not None], notes=False) for card in cards.values(): if card is None: continue if not getUserOption("Delete original cards", False): tmp_card = Card(mw.col) card.id = tmp_card.id card.nid = new_nid card.flush() mw.col.add_note(merged_note, 1) tmp_nid = merged_note.id mw.col.db.execute(""" update notes set id=? where id=? """, new_nid, merged_note.id) mw.col.db.execute(""" delete from cards where nid=? """, tmp_nid) merged_note.id = new_nid tooltip(_("Notes merged")) return merged_note
def _removeFromFiltered(self, card: Card) -> None: if card.odid: card.did = card.odid card.odue = 0 card.odid = DeckId(0)
def fromLocalID(cls, localDeckID): col = mw.col """We create an ankiDeckManager because thats where\ all the decks are stored""" ankiDeckManager = col.decks # After that we get the Deck we are intrested in ankiDeck = ankiDeckManager.get(localDeckID) # With this call we retrive all Card IDs ankiCardIDs = ankiDeckManager.cids(localDeckID) """with each id we create a anki Note object and\ add it to a list for futher processing""" ankiNotes = [] ankiNoteIds = [] for cardID in ankiCardIDs: nid = Card(col, cardID).nid if nid not in ankiNoteIds: ankiNoteIds.append(nid) ankiNotes.append(Note(col, None, nid)) modelsDic = {} notes = [] """for every note object we have we retrieve the model and create\ a AnkiPubSubNote object we add the models to a dic so that we only\ create models once and not for every note an extra model Model() 1 --> * Note()""" for note in ankiNotes: modelsDic.update({note.mid: note._model}) notes.append(AnkipubSubNote(note, col)) # For every Model in the dic we create a AnkipubSubModel models = [] for modelID, model in modelsDic.items(): """We deepcopy the model because otherwise with the next\ commit we would change something in the users anki database""" model = deepcopy(model) """we move the anki assign id to localID because id is used\ by our server as a reference""" model.update({'localID': modelID}) aModel = AnkipubSubModel(model) models.append(aModel) # Create a new Deck Object which we can push later to the server """ we sort all the notes belonging to our deck after the mod value, the one with the biggest mod value is the one note last changed that change also represents the last change on our deck localy so we use this to generate a datetime stamp to compare to the last change on the server """ ankiNotesByDate = sorted(ankiNotes, key=lambda note: note.mod) if not len(ankiNotesByDate) == 0: lastChange = datetime.fromtimestamp(ankiNotesByDate.pop().mod) else: lastChange = datetime.today() creationDate = None remoteID = getRemoteDeckID(localDeckID) return cls(notes, models, ankiDeck.get('name'), ankiDeck.get('desc'), lastChange, creationDate, remoteID, localDeckID) """# print(deck,localDeckID,notes,models,col)
def _updateEarlyRevIvl(self, card: Card, ease: int) -> None: card.ivl = self._earlyReviewIvl(card, ease)
def set_card_absolute_due(card: Card, absolute_due: int): if is_card_in_a_filtered_deck(card): card.odue = absolute_due else: card.due = absolute_due card.flush()
def from_existing_card(card: Card, browser: bool) -> TemplateRenderContext: return TemplateRenderContext(card.col, card, card.note(), browser)
from anki.cards import Card nid = 0 for name in mw.addonManager.allAddons(): if mw.addonManager.addonName(name) == "Straight Reward": am = name cl = __import__(am).src.lib.review_hook.review_hook_closure() cl[0]((True, 3), False, Card(mw.col, nid))
class Reviewer: def __init__(self, mw: AnkiQt) -> None: self.mw = mw self.web = mw.web self.card: Card | None = None self.cardQueue: list[Card] = [] self.previous_card: Card | None = None self.hadCardQueue = False self._answeredIds: list[CardId] = [] self._recordedAudio: str | None = None self.typeCorrect: str = None # web init happens before this is set self.state: str | None = None self._refresh_needed: RefreshNeeded | None = None self._v3: V3CardInfo | None = None self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1)) self.bottom = BottomBar(mw, mw.bottomWeb) self._card_info = ReviewerCardInfo(self.mw) self._previous_card_info = PreviousReviewerCardInfo(self.mw) hooks.card_did_leech.append(self.onLeech) def show(self) -> None: self.mw.setStateShortcuts(self._shortcutKeys()) # type: ignore self.web.set_bridge_command(self._linkHandler, self) self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self)) self._state_mutation_js = self.mw.col.get_config("cardStateCustomizer") self._reps: int = None self._refresh_needed = RefreshNeeded.QUEUES self.refresh_if_needed() # this is only used by add-ons def lastCard(self) -> Card | None: if self._answeredIds: if not self.card or self._answeredIds[-1] != self.card.id: try: return self.mw.col.getCard(self._answeredIds[-1]) except TypeError: # id was deleted return None return None def cleanup(self) -> None: gui_hooks.reviewer_will_end() self.card = None def refresh_if_needed(self) -> None: if self._refresh_needed is RefreshNeeded.QUEUES: self.mw.col.reset() self.nextCard() self.mw.fade_in_webview() self._refresh_needed = None elif self._refresh_needed is RefreshNeeded.NOTE_TEXT: self._redraw_current_card() self.mw.fade_in_webview() self._refresh_needed = None def op_executed( self, changes: OpChanges, handler: object | None, focused: bool ) -> bool: if handler is not self: if changes.study_queues: self._refresh_needed = RefreshNeeded.QUEUES elif changes.note_text: self._refresh_needed = RefreshNeeded.NOTE_TEXT if focused and self._refresh_needed: self.refresh_if_needed() return bool(self._refresh_needed) def _redraw_current_card(self) -> None: self.card.load() if self.state == "answer": self._showAnswer() else: self._showQuestion() # Fetching a card ########################################################################## def nextCard(self) -> None: self.previous_card = self.card self.card = None self._v3 = None if self.mw.col.sched.version < 3: self._get_next_v1_v2_card() else: self._get_next_v3_card() self._previous_card_info.set_card(self.previous_card) self._card_info.set_card(self.card) if not self.card: self.mw.moveToState("overview") return if self._reps is None or self._reps % 100 == 0: # we recycle the webview periodically so webkit can free memory self._initWeb() self._showQuestion() def _get_next_v1_v2_card(self) -> None: if self.cardQueue: # undone/edited cards to show card = self.cardQueue.pop() card.start_timer() self.hadCardQueue = True else: if self.hadCardQueue: # the undone/edited cards may be sitting in the regular queue; # need to reset self.mw.col.reset() self.hadCardQueue = False card = self.mw.col.sched.getCard() self.card = card def _get_next_v3_card(self) -> None: assert isinstance(self.mw.col.sched, V3Scheduler) output = self.mw.col.sched.get_queued_cards() if not output.cards: return self._v3 = V3CardInfo.from_queue(output) self.card = Card(self.mw.col, backend_card=self._v3.top_card().card) self.card.start_timer() def get_next_states(self) -> NextStates | None: if v3 := self._v3: return v3.next_states else:
def _rescheduleGraduatingLapse(self, card: Card, early: bool = False) -> None: if early: card.ivl += 1 card.due = self.today + card.ivl card.queue = QUEUE_TYPE_REV card.type = CARD_TYPE_REV
def _answerLrnCard(self, card: Card, ease: int) -> None: # ease 1=no, 2=yes, 3=remove conf = self._lrnConf(card) if card.odid and not card.wasNew: # type: ignore type = REVLOG_CRAM elif card.type == CARD_TYPE_REV: type = REVLOG_RELRN else: type = REVLOG_LRN leaving = False # lrnCount was decremented once when card was fetched lastLeft = card.left # immediate graduate? if ease == BUTTON_THREE: self._rescheduleAsRev(card, conf, True) leaving = True # graduation time? elif ease == BUTTON_TWO and (card.left % 1000) - 1 <= 0: self._rescheduleAsRev(card, conf, False) leaving = True else: # one step towards graduation if ease == BUTTON_TWO: # decrement real left count and recalculate left today left = (card.left % 1000) - 1 card.left = self._leftToday(conf["delays"], left) * 1000 + left # failed else: card.left = self._startingLeft(card) resched = self._resched(card) if "mult" in conf and resched: # review that's lapsed card.ivl = max(1, conf["minInt"], int(card.ivl * conf["mult"])) else: # new card; no ivl adjustment pass if resched and card.odid: card.odue = self.today + 1 delay = self._delayForGrade(conf, card.left) if card.due < time.time(): # not collapsed; add some randomness delay *= int(random.uniform(1, 1.25)) card.due = int(time.time() + delay) # due today? if card.due < self.dayCutoff: self.lrnCount += card.left // 1000 # if the queue is not empty and there's nothing else to do, make # sure we don't put it at the head of the queue and end up showing # it twice in a row card.queue = QUEUE_TYPE_LRN if self._lrnQueue and not self.revCount and not self.newCount: smallestDue = self._lrnQueue[0][0] card.due = max(card.due, smallestDue + 1) heappush(self._lrnQueue, (card.due, card.id)) else: # the card is due in one or more days, so we need to use the # day learn queue ahead = ((card.due - self.dayCutoff) // 86400) + 1 card.due = self.today + ahead card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN self._logLrn(card, ease, conf, leaving, type, lastLeft)
def maybe_get_setting_from_card(card) -> Optional[ScriptSetting]: the_note = Card(mw.col, card.id).note() maybe_model = the_note.model() return get_setting_from_notetype(maybe_model) if maybe_model else None
def _rescheduleNew(self, card: Card, conf: QueueConfig, early: bool) -> None: "Reschedule a new card that's graduated for the first time." card.ivl = self._graduatingIvl(card, conf, early) card.due = self.today + card.ivl card.factor = conf["initialFactor"]
def getCard(self, id: int) -> Card: return Card(self, id)
def _rescheduleLapse(self, card: Card) -> int: conf = self._lapseConf(card) card.lastIvl = card.ivl if self._resched(card): card.lapses += 1 card.ivl = self._nextLapseIvl(card, conf) card.factor = max(1300, card.factor - 200) card.due = self.today + card.ivl # if it's a filtered deck, update odue as well if card.odid: card.odue = card.due # if suspended as a leech, nothing to do delay: int = 0 if self._checkLeech(card, conf) and card.queue == QUEUE_TYPE_SUSPENDED: return delay # if no relearning steps, nothing to do if not conf["delays"]: return delay # record rev due date for later if not card.odue: card.odue = card.due delay = self._delayForGrade(conf, 0) card.due = int(delay + time.time()) card.left = self._startingLeft(card) # queue 1 if card.due < self.dayCutoff: self.lrnCount += card.left // 1000 card.queue = QUEUE_TYPE_LRN heappush(self._lrnQueue, (card.due, card.id)) else: # day learn queue ahead = ((card.due - self.dayCutoff) // 86400) + 1 card.due = self.today + ahead card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN return delay
def _updateRevIvlOnFail(self, card: Card, conf: QueueConfig) -> None: card.lastIvl = card.ivl card.ivl = self._lapseIvl(card, conf)
def _moveToNextStep(self, card: Card, conf: QueueConfig) -> None: # decrement real left count and recalculate left today left = (card.left % 1000) - 1 card.left = self._leftToday(conf["delays"], left) * 1000 + left self._rescheduleLrnCard(card, conf)
def prepare(html, card, context: str): if settings.enabled is False: return html if card.note_type()['name'] != settings.note_type: return html if mw is None: return html sched = copy.copy(mw.col.sched) for attr, value in vars(sched).items(): try: setattr(sched, attr, copy.deepcopy(value)) except Exception: pass next_card = None if sched.version == 3: sched_v3 = sched # type: Any cards = sched_v3.get_queued_cards(fetch_limit=2) if len(cards.cards) > 1: queued_card = cards.cards[1] if queued_card is not None: next_card = Card(sched.col) next_card._load_from_backend_card(queued_card.card) next_card.load() else: next_card = sched.getCard() if next_card is not None and settings.question_field is not None: try: next_term = next_card.note()[ settings.question_field] # type: Optional[str] except KeyError: next_term = next_card.note().values()[0] else: next_term = None next_id = None if next_card: next_id = next_card.note().id if context.startswith("clayout"): # in card layout preview we don't have current note, but we can use next_term current_term = "malli" if next_term is None else next_term current_id = next_id if next_id is not None else -1 else: current_id = card.note().id try: current_term = card.note()[settings.question_field] except KeyError: current_term = card.note().values()[0] card_types = { 0: "Forwards", 1: "Reversed", } app_dict = { 'context': context, 'isAnki': True, 'currentTerm': current_term, 'cardType': card_types.get(card.ord, "Unknown"), 'tags': card.note().tags, 'id': current_id, 'nextTerm': next_term, 'nextId': next_id, } return f""" <script> window.myAnkiSetup = {dumps(app_dict)}; if (window.myAnkiUpdate) window.myAnkiUpdate(window.myAnkiSetup); </script>""" + html