zjosua / anki-mc

Multiple choice questions for Anki.
https://ankiweb.net/shared/info/1566095810
GNU Affero General Public License v3.0
49 stars 16 forks source link

Error message immediately after launching Anki desktop (Windows 10) #113

Closed Nihilhem closed 1 year ago

Nihilhem commented 1 year ago

Describe the bug

Anki desktop on Windows 10 shows an error message immediately after launch.

If Anki shows an error message, copy and paste it between the backticks below.

Debug info:
Anki 2.1.65 (aa9a734f) Python 3.9.15 Qt 6.4.3 PyQt 6.4.0
Platform: Windows-10-10.0.19045
Flags: frz=True ao=True sv=2
Add-ons, last update check: 2023-07-21 01:51:32

Caught exception:
Traceback (most recent call last):
  File "aqt.progress", line 118, in handler
  File "aqt.main", line 217, in on_window_init
  File "aqt.main", line 264, in setupProfileAfterWebviewsLoaded
  File "aqt.main", line 316, in setupProfile
  File "aqt.main", line 496, in loadProfile
  File "_aqt.hooks", line 3881, in __call__
  File "C:\Users\charl\AppData\Roaming\Anki2\addons21\1566095810\template.py", line 240, in manage_multiple_choice_note_type
    AddOrUpdateModel()
  File "C:\Users\charl\AppData\Roaming\Anki2\addons21\1566095810\template.py", line 229, in AddOrUpdateModel
    updateTemplate(mw.col)
  File "C:\Users\charl\AppData\Roaming\Anki2\addons21\1566095810\template.py", line 218, in updateTemplate
    col.models.save(model)
  File "anki.models", line 569, in save
  File "anki.models", line 552, in update
  File "anki._backend_generated", line 873, in add_or_update_notetype
  File "anki._backend", line 156, in _run_command
anki.errors.CardTypeError: Card template ⁨1⁩ in notetype '⁨AllInOne (kprim, mc, sc)⁩' has a problem.<br>See the preview for more information.

To Reproduce

  1. Open Anki
  2. Error message pops up

Expected behavior

I expected Anki to open normally without any error.

What have you tried?

I tried disabling add-ons and that got rid of the error. Anki showed the error when I only had this add-on enabled,

Screenshots

-

Information about your set-up

On which platform(s) does the error occur?

Only if the error occurs with the Anki desktop app: Open Anki, go to Help > About and click on "Copy Debug Info". Paste the result between the backticks below.

Debug info:
Anki 2.1.65 (aa9a734f) Python 3.9.15 Qt 6.4.3 PyQt 6.4.0
Platform: Windows-10-10.0.19045
Flags: frz=True ao=True sv=2
Add-ons, last update check: 2023-07-21 01:51:32

Caught exception:
Traceback (most recent call last):
  File "aqt.progress", line 118, in handler
  File "aqt.main", line 217, in on_window_init
  File "aqt.main", line 264, in setupProfileAfterWebviewsLoaded
  File "aqt.main", line 316, in setupProfile
  File "aqt.main", line 496, in loadProfile
  File "_aqt.hooks", line 3881, in __call__
  File "C:\Users\charl\AppData\Roaming\Anki2\addons21\1566095810\template.py", line 240, in manage_multiple_choice_note_type
    AddOrUpdateModel()
  File "C:\Users\charl\AppData\Roaming\Anki2\addons21\1566095810\template.py", line 229, in AddOrUpdateModel
    updateTemplate(mw.col)
  File "C:\Users\charl\AppData\Roaming\Anki2\addons21\1566095810\template.py", line 218, in updateTemplate
    col.models.save(model)
  File "anki.models", line 569, in save
  File "anki.models", line 552, in update
  File "anki._backend_generated", line 873, in add_or_update_notetype
  File "anki._backend", line 156, in _run_command
anki.errors.CardTypeError: Card template ⁨1⁩ in notetype '⁨AllInOne (kprim, mc, sc)⁩' has a problem.<br>See the preview for more information.

Additional context

-

zjosua commented 1 year ago

What does the card preview show?

I can't reproduce the error with my setup. Did you use the add-on before or is this your first time using it?

Nihilhem commented 1 year ago

Everything seems just fine, except for the error message popping up every time I open Anki. I don't know if it affects functionality in any way.

I have used this add-on before just fine. The error started happening only after I updated Anki to the current, latest version ⁨(2.1.65). I don't know which is the version I had prior to this, but it was not too old. It happened both with the previous version of this add-on, and with the new version that was released yesterday.

3ter commented 1 year ago

@Nihilhem Can you show us the current code (best copy the whole thing) on the template (through "manage note types" selecting AllInOne (kprim, mc, sc) and then cards) and a screenshot of the fields for this note type?

It certainly would help to know what is wrong with the template after the addon had its way with it 😃. The numbers of question fields in the fields section and in the template should match.

Thank you for your report!

image image

Nihilhem commented 1 year ago

@3ter Sure, here you go. Thanks for your interest.

1) Template code:

<script>
    // Loading Persistence
    // https://github.com/SimonLammer/anki-persistence
    // v0.5.2 - https://github.com/SimonLammer/anki-persistence/blob/62463a7f63e79ce12f7a622a8ca0beb4c1c5d556/script.js
    if (void 0 === window.Persistence) { var _persistenceKey = "github.com/SimonLammer/anki-persistence/", _defaultKey = "_default"; if (window.Persistence_sessionStorage = function () { var e = !1; try { "object" == typeof window.sessionStorage && (e = !0, this.clear = function () { for (var e = 0; e < sessionStorage.length; e++) { var t = sessionStorage.key(e); 0 == t.indexOf(_persistenceKey) && (sessionStorage.removeItem(t), e--) } }, this.setItem = function (e, t) { void 0 == t && (t = e, e = _defaultKey), sessionStorage.setItem(_persistenceKey + e, JSON.stringify(t)) }, this.getItem = function (e) { return void 0 == e && (e = _defaultKey), JSON.parse(sessionStorage.getItem(_persistenceKey + e)) }, this.removeItem = function (e) { void 0 == e && (e = _defaultKey), sessionStorage.removeItem(_persistenceKey + e) }) } catch (e) { } this.isAvailable = function () { return e } }, window.Persistence_windowKey = function (e) { var t = window[e], i = !1; "object" == typeof t && (i = !0, this.clear = function () { t[_persistenceKey] = {} }, this.setItem = function (e, i) { void 0 == i && (i = e, e = _defaultKey), t[_persistenceKey][e] = i }, this.getItem = function (e) { return void 0 == e && (e = _defaultKey), t[_persistenceKey][e] || null }, this.removeItem = function (e) { void 0 == e && (e = _defaultKey), delete t[_persistenceKey][e] }, void 0 == t[_persistenceKey] && this.clear()), this.isAvailable = function () { return i } }, window.Persistence = new Persistence_sessionStorage, Persistence.isAvailable() || (window.Persistence = new Persistence_windowKey("py")), !Persistence.isAvailable()) { var titleStartIndex = window.location.toString().indexOf("title"), titleContentIndex = window.location.toString().indexOf("main", titleStartIndex); titleStartIndex > 0 && titleContentIndex > 0 && titleContentIndex - titleStartIndex < 10 && (window.Persistence = new Persistence_windowKey("qt")) } }
</script>

{{#Image}}<p>{{Image}}</p>{{/Image}}

<h3 id="myH1"></h3>
{{#Question}}<p>{{Question}}</p>{{/Question}}

<div class="tappable">
    <table style="border: 1px solid black" id="qtable"></table>
</div>

<div class="hidden" id="Q_solutions">{{Answers}}</div>
<div class="hidden" id="Card_Type">{{QType (0=kprim,1=mc,2=sc)}}</div>

<div class="hidden" id="Q_1">{{Q_1}}</div>
<div class="hidden" id="Q_2">{{Q_2}}</div>
<div class="hidden" id="Q_3">{{Q_3}}</div>
<div class="hidden" id="Q_4"></div>
<div class="hidden" id="Q_5"></div>

<script>
    // Generate the table depending on the type.
    function generateTable() {
        var type = document.getElementById("Card_Type").innerHTML;
        var table = document.createElement("table");
        var tbody = document.createElement("tbody");
        for (var i = 0; true; i++) {
            if (type == 0 && i == 0) {
                tbody.innerHTML = tbody.innerHTML + '<tr><th>yes</th><th>no</th><th></th></tr>';
            }
            if (document.getElementById('Q_' + (i + 1)) != undefined) {
                if (document.getElementById('Q_' + (i + 1)).innerHTML != '') {
                    var html = [];

                    let answerText = document.getElementById('Q_' + (i + 1)).innerHTML;
                    let labelTag = (type == 0) ? '' :
                        '<label for="inputQuestion' + (i + 1) + '">' + answerText + '</label>';
                    let textAlign = (type == 0) ? 'center' : 'left';

                    html.push('<tr>');
                    var maxColumns = ((type == 0) ? 2 : 1);
                    for (var j = 0; j < maxColumns; j++) {
                        let inputTag = '<input id="inputQuestion' + (i + 1) +
                            '" name="ans_' + ((type != 2) ? (i + 1) : 'A') +
                            '" type="' + ((type == 1) ? 'checkbox' : 'radio') +
                            // TODO: I don't see how these values are used, please add a comment
                            '" value="' + ((j == 0) ? 1 : 0) + '">';
                        html.push(
                            '<td onInput="onCheck()" style="text-align: ' + textAlign + '">' + inputTag +
                            labelTag +
                            '</td>');
                    }
                    if (type == 0) {
                        html.push('<td>' + answerText + '</td>');
                    }
                    html.push('</tr>');
                    tbody.innerHTML = tbody.innerHTML + html.join("");
                }
            } else {
                break;
            }
        }

        table.appendChild(tbody);
        document.getElementById('qtable').innerHTML = table.innerHTML;
        onShuffle();
    }

    function shuffle(array) {
        var currentIndex = array.length, temporaryValue, randomIndex;

        // While there remain elements to shuffle...
        while (0 !== currentIndex) {

            // Pick a remaining element...
            randomIndex = Math.floor(Math.random() * currentIndex);
            currentIndex -= 1;

            // And swap it with the current element.
            temporaryValue = array[currentIndex];
            array[currentIndex] = array[randomIndex];
            array[randomIndex] = temporaryValue;
        }

        return array;
    }

    function onShuffle() {
        var solutions = document.getElementById("Q_solutions").innerHTML;
        solutions = solutions.replace(/(<([^>]+)>)/gi, "").split(" ");
        for (var i = 0; i < solutions.length; i++) {
            solutions[i] = Number(solutions[i]);
        }

        var output = document.getElementById("output");

        var qrows = document.getElementById("qtable").getElementsByTagName("tr");

        var qanda = new Array();

        var type = document.getElementById("Card_Type").innerHTML;

        for (i = 0; i < ((type == 0) ? qrows.length - 1 : qrows.length); i++) {
            qanda[i] = new Object();
            qanda[i].question = qrows[(type == 0) ? i + 1 : i].getElementsByTagName("td")[(type == 0) ? 2 : 0].innerHTML;
            qanda[i].answer = solutions[i];
        }

        qanda = shuffle(qanda);

        var mc_solutions = new String();

        for (i = 0; i < ((type == 0) ? qrows.length - 1 : qrows.length); i++) {
            qrows[(type == 0) ? i + 1 : i].getElementsByTagName("td")[(type == 0) ? 2 : 0].innerHTML = qanda[i].question;
            solutions[i] = qanda[i].answer;
            mc_solutions += qanda[i].answer + " ";
        }
        mc_solutions = mc_solutions.substring(0, mc_solutions.lastIndexOf(" "));
        document.getElementById("Q_solutions").innerHTML = mc_solutions;

        document.getElementById("qtable").HTML = qrows;
        onCheck();
    }

    /**
     * Returns true if the option box/circle is checked.
     *
     * In case of kprim the second box is used as reference.
     *
     * @param   {HTMLTableRowElement}    optionRow    Row containing option boxes/circles.
     * @param   {number}    index   Index of the option in question.
     */
    function isOptionChecked(optionRow, index) {
        return optionRow.getElementsByTagName("td")[index].getElementsByTagName("input")[0].checked
    }

    function getUserAnswers() {
        let type = document.getElementById("Card_Type").innerHTML;
        let qrows = document.getElementById("qtable").getElementsByTagName('tbody')[0].getElementsByTagName("tr");
        let userAnswers = [];
        for (let i = 0; i < qrows.length; i++) {
            if (type == 0 && i == 0) {
                i++; // to skip the first row containing no checkboxes when type is 'kprim'
            }
            if (type == 0) {
                if (isOptionChecked(qrows[i], 0)) {
                    userAnswers.push(1);
                } else if (isOptionChecked(qrows[i], 1)) {
                    userAnswers.push(0);
                } else {
                    userAnswers.push(2);
                }
            } else {
                if (isOptionChecked(qrows[i], 0)) {
                    userAnswers.push(1);
                } else {
                    userAnswers.push(0);
                }
            }
        }
        return userAnswers
    }

    function getCorrectAnswers() {
        let solutions = document.getElementById("Q_solutions").innerHTML.split(" ").map(string => Number(string));

        return solutions;
    }

    /**
     * On checking an option this collects and stores answers in between front/back of the card.
     *
     * In case of kprim only the first box is looked at, if it isn't checked the second box has to be.
     * By default a '1' in the answers stands for 'yes' which is the first option from the left.
     */
    function onCheck() {
        // Send question table and encoded answers to Persistence along with the provided solutions
        if (Persistence.isAvailable()) {
            Persistence.clear();
            Persistence.setItem('user_answers', getUserAnswers());
            Persistence.setItem('Q_solutions', getCorrectAnswers());
            Persistence.setItem('qtable', document.getElementById("qtable").innerHTML);
        }
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function tickCheckboxOnNumberKeyDown(event) {
        const keyName = event.key;

        let tableBody = document.getElementById("qtable").getElementsByTagName('tbody')[0];
        var tableRows = tableBody.getElementsByTagName("tr");

        if (0 < +keyName && +keyName < 10) {
            let tableData = tableRows[+keyName - 1].getElementsByTagName("td")[0];
            let tableRow = tableData.getElementsByTagName("input")[0];
            tableRow.checked = !tableRow.checked;
            onCheck();
        }
    }

    // addCheckboxTickingShortcuts is an easy approach on using only the keyboard to toggle checkboxes in mc/sc.
    //
    // Naturally the number keys are an intuitive choice here. Unfortunately anki does capture those.
    // So the workaround is to hold the (left) 'Alt' key and then type the corresponding number to toggle the row.
    function addCheckboxTickingShortcuts() {
        document.addEventListener('keydown', tickCheckboxOnNumberKeyDown, false);
    }

    function isMobile() {
        if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
            return true;
        } else {
            return false;
        }
    }

    function run() {
        let DEFAULT_CARD_TYPE = 1; // for previewing the cards in "Manage Note Type..."

        if (isNaN(document.getElementById("Card_Type").innerHTML)) {
            document.getElementById("Card_Type").innerHTML = DEFAULT_CARD_TYPE;
        }

        if (document.getElementById("Card_Type").innerHTML != 0 && !isMobile()) {
            addCheckboxTickingShortcuts();
        }

        setTimeout(generateTable(), 1);
    }

    async function waitForReadyStateAndRun() {
        for (let i = 0; i < 100; i++) {
            if (document.readyState === "complete") {
                run();
                break;
            }
            console.log("Document not yet fully loaded (readyState: " + document.readyState + "). Retry in 0.1s.");
            await sleep(100);
        }
    }

    /*
    The following block is inspired by Glutanimate's Cloze Overlapper card template.
    The Cloze Overlapper card template is licensed under the CC BY-SA 4.0
    license (https://creativecommons.org/licenses/by-sa/4.0/).
    */
    if (document.readyState === "complete") {
        run();
    } else {
        waitForReadyStateAndRun();
    }
</script>

2) Fields screenshot:

image

3ter commented 1 year ago

There are some changes to the default template (e.g. the title is missing and you removed the question ids for 4 and 5) but I can't see how the addon should produce an error with this template.

The version proposed in #114 should alleviate this a bit by actually using the default template (which I thought already was the case... but in your case there was an error when the addon tried to save its changes). Unfortunately anki doesn't tell us what the error in the template is and instead wants us to look at the preview... which is impossible, as this is the addon trying to change the template which we never see. This would be very helpful though...

I try to think of something.

EDIT: https://faqs.ankiweb.net/card-template-has-a-problem.html

3ter commented 1 year ago

It was obvious I'm afraid. As you've removed a field which is used in the default template the template can't be changed by the addon (but it wants to, to make sure you're using the most up to date one).

I'm going to think about a sanity check in #114 to make sure fields that have been removed by the user don't show up in the template...

Current workaround for you:

This will work until the next update.

Nihilhem commented 1 year ago

It was obvious I'm afraid. As you've removed a field which is used in the default template the template can't be changed by the addon (but it wants to, to make sure you're using the most up to date one).

I'm going to think about a sanity check in #114 to make sure fields that have been removed by the user don't show up in the template...

Current workaround for you:

* Save your changes (already did that above) to note type

* Add the field `Title` back in (needed only in the fields, at the bottom is fine)

* Now the addon should be able to redo the template after the next restart of anki

* Reapply your changes to the note type (both template and fields)

This will work until the next update.

That's strange, because I don't remember deleting any fields. In fact, i didn't even know this was a possibility, and I've never opened the "Note Types" menu before. I think the reason is a deck that I had downloaded, which only had three question fields instead of five.

I've tried adding the Title field back in (only in the fields and at the bottom), saving changes, restarting Anki and saving changes again in both template and fields, but I still get an error. I don't really know what's going on 🤔

image

Debug info:
Anki 2.1.65 (aa9a734f) Python 3.9.15 Qt 6.4.3 PyQt 6.4.0
Platform: Windows-10-10.0.19045
Flags: frz=True ao=True sv=2
Add-ons, last update check: 2023-07-23 14:30:09

Caught exception:
Traceback (most recent call last):
  File "aqt.progress", line 118, in handler
  File "aqt.main", line 217, in on_window_init
  File "aqt.main", line 264, in setupProfileAfterWebviewsLoaded
  File "aqt.main", line 316, in setupProfile
  File "aqt.main", line 496, in loadProfile
  File "_aqt.hooks", line 3881, in __call__
  File "C:\Users\charl\AppData\Roaming\Anki2\addons21\1566095810\template.py", line 240, in manage_multiple_choice_note_type
    AddOrUpdateModel()
  File "C:\Users\charl\AppData\Roaming\Anki2\addons21\1566095810\template.py", line 229, in AddOrUpdateModel
    updateTemplate(mw.col)
  File "C:\Users\charl\AppData\Roaming\Anki2\addons21\1566095810\template.py", line 218, in updateTemplate
    col.models.save(model)
  File "anki.models", line 569, in save
  File "anki.models", line 552, in update
  File "anki._backend_generated", line 873, in add_or_update_notetype
  File "anki._backend", line 156, in _run_command
anki.errors.CardTypeError: Card template ⁨1⁩ in notetype '⁨AllInOne (kprim, mc, sc)⁩' has a problem.<br>See the preview for more information.
3ter commented 1 year ago

Thanks for the heads up. I've tested it with a pristine anki installation (2.1.65) and set the note type up exactly as you had. Then I only added a field with the name Title (like in your screenshot) and with the next restart the error message (that popped up before) was gone for good.

image

You're still missing Extra 1 by the way. Could that be the culprit? It is used on the back template, so this most probably is what we're looking for.

PS You can leave the Note ID of course, additional field are no problem. PPS The next version of the addon I've proposed in #114 adds all those missing fields automatically, so you don't run into that in the future.

Nihilhem commented 1 year ago

Thanks for the heads up. I've tested it with a pristine anki installation (2.1.65) and set the note type up exactly as you had. Then I only added a field with the name Title (like in your screenshot) and with the next restart the error message (that popped up before) was gone for good.

image

You're still missing Extra 1 by the way. Could that be the culprit? It is used on the back template, so this most probably is what we're looking for.

PS You can leave the Note ID of course, additional field are no problem. PPS The next version of the addon I've proposed in #114 adds all those missing fields automatically, so you don't run into that in the future.

Nice! That solved the problem. Indeed, after adding the Extra 1 field and restarting Anki, no error message is shown anymore. Thank you for your patience, your interest and your comprehensible answers.