File size: 7,728 Bytes
7505a0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e64b2c
 
7505a0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import pandas as pd
from ankigen.models import Card, CardFront, CardBack
from ankigen.ui_logic import (
    cards_to_dataframe,
    dataframe_to_cards,
    update_mode_visibility,
)


def test_cards_to_dataframe_empty():
    """Test cards_to_dataframe with an empty list of cards."""
    df = cards_to_dataframe([])
    assert isinstance(df, pd.DataFrame)
    assert df.empty
    assert list(df.columns) == [
        "ID",
        "Topic",
        "Front",
        "Back",
        "Tags",
        "Card Type",
        "Explanation",
        "Example",
        "Source_URL",
    ]


def test_cards_to_dataframe_single():
    """Test cards_to_dataframe with a single card."""
    card = Card(
        front=CardFront(question="What is Python?"),
        back=CardBack(answer="A programming language", explanation="E", example="Ex"),
        metadata={
            "topic": "Programming",
            "tags": ["python", "coding"],
            "source_url": "http://python.org",
        },
        card_type="Basic",
    )
    df = cards_to_dataframe([card])
    assert len(df) == 1
    assert df.iloc[0]["ID"] == 1
    assert df.iloc[0]["Topic"] == "Programming"
    assert df.iloc[0]["Front"] == "What is Python?"
    assert df.iloc[0]["Back"] == "A programming language"
    assert df.iloc[0]["Tags"] == "python, coding"
    assert df.iloc[0]["Card Type"] == "Basic"
    assert df.iloc[0]["Source_URL"] == "http://python.org"


def test_cards_to_dataframe_multiple():
    """Test cards_to_dataframe with multiple cards having varying metadata/tags."""
    cards = [
        Card(
            front=CardFront(question="Q1"),
            back=CardBack(answer="A1", explanation="E1", example="Ex1"),
            metadata={"topic": "T1", "tags": ["tag1"]},
        ),
        Card(
            front=CardFront(question="Q2"),
            back=CardBack(answer="A2", explanation="E2", example="Ex2"),
            metadata={"tags": ["tag2", "tag3"]},
        ),
        Card(
            front=CardFront(question="Q3"),
            back=CardBack(answer="A3", explanation="E3", example="Ex3"),
            metadata={},
        ),
    ]
    df = cards_to_dataframe(cards)
    assert len(df) == 3
    assert df.iloc[0]["Topic"] == "T1"
    assert df.iloc[0]["Tags"] == "tag1"
    assert df.iloc[1]["Topic"] == "N/A"
    assert df.iloc[1]["Tags"] == "tag2, tag3"
    assert df.iloc[2]["Topic"] == "N/A"
    assert df.iloc[2]["Tags"] == ""


def test_cards_to_dataframe_no_metadata():
    """Test cards_to_dataframe with cards that have no metadata."""
    card = Card(
        front=CardFront(question="Q"),
        back=CardBack(answer="A", explanation="E", example="Ex"),
        metadata=None,
    )
    df = cards_to_dataframe([card])
    assert len(df) == 1
    assert df.iloc[0]["Topic"] == "N/A"
    assert df.iloc[0]["Tags"] == ""
    assert df.iloc[0]["Source_URL"] == ""


def test_dataframe_to_cards_empty():
    """Test dataframe_to_cards with empty dataframe and/or empty cards."""
    # Both empty
    assert dataframe_to_cards(pd.DataFrame(), []) == []

    # Empty DF, non-empty cards
    original_cards = [
        Card(
            front=CardFront(question="Q"),
            back=CardBack(answer="A", explanation="E", example="Ex"),
        )
    ]
    assert dataframe_to_cards(pd.DataFrame(), original_cards) == []


def test_dataframe_to_cards_round_trip():
    """Test a normal round-trip from cards to dataframe and back with edits."""
    original_cards = [
        Card(
            front=CardFront(question="Original Q"),
            back=CardBack(answer="Original A", explanation="E", example="Ex"),
            metadata={"topic": "Original T", "tags": ["tag1"]},
        )
    ]
    df = cards_to_dataframe(original_cards)

    # Edit the dataframe
    df.at[0, "Front"] = "Updated Q"
    df.at[0, "Back"] = "Updated A"
    df.at[0, "Tags"] = "tag1, tag2"
    df.at[0, "Topic"] = "Updated T"
    df.at[0, "Card Type"] = "Cloze"

    updated_cards = dataframe_to_cards(df, original_cards)

    assert len(updated_cards) == 1
    assert updated_cards[0].front.question == "Updated Q"
    assert updated_cards[0].back.answer == "Updated A"
    assert updated_cards[0].metadata["tags"] == ["tag1", "tag2"]
    assert updated_cards[0].metadata["topic"] == "Updated T"
    assert updated_cards[0].card_type == "Cloze"


def test_dataframe_to_cards_out_of_bounds_id(mocker):
    """Test handling of IDs that are out of bounds for the original cards list."""
    mock_logger = mocker.patch("ankigen.ui_logic.logger")
    original_cards = [
        Card(
            front=CardFront(question="Q1"),
            back=CardBack(answer="A1", explanation="E1", example="Ex1"),
        )
    ]

    # ID 2 is out of bounds (only 1 card exists)
    df = pd.DataFrame({"ID": [2], "Front": ["Q2"]})

    result = dataframe_to_cards(df, original_cards)
    assert result == []
    mock_logger.warning.assert_called_with(
        "Card ID 2 from DataFrame is out of bounds for original_cards list."
    )


def test_dataframe_to_cards_error_handling(mocker):
    """Test handling of rows that cause errors during processing."""
    mock_logger = mocker.patch("ankigen.ui_logic.logger")
    original_cards = [
        Card(
            front=CardFront(question="Q1"),
            back=CardBack(answer="A1", explanation="E1", example="Ex1"),
        )
    ]

    # Case 1: Missing ID column (KeyError)
    # Desired behavior: the function should handle this gracefully, log an error,
    # and not raise an UnboundLocalError.
    df_missing_id = pd.DataFrame({"Front": ["Q1"]})

    result = dataframe_to_cards(df_missing_id, original_cards)

    # Expect that no cards are returned for rows missing a valid ID
    assert result == []
    mock_logger.error.assert_called()


def test_dataframe_to_cards_error_handling_recovery(mocker):
    """Test that it recovers gracefully if an error occurs after ID is parsed."""
    mock_logger = mocker.patch("ankigen.ui_logic.logger")

    # Patch the class method instead of instance because Pydantic blocks instance patching of methods
    mocker.patch(
        "ankigen.models.CardFront.model_copy",
        side_effect=AttributeError("Mock Error"),
    )

    card = Card(
        front=CardFront(question="Q1"),
        back=CardBack(answer="A1", explanation="E1", example="Ex1"),
    )
    original_cards = [card]
    df = pd.DataFrame({"ID": [1], "Front": ["New Q"]})

    result = dataframe_to_cards(df, original_cards)

    # It should catch the error, log it, and use the original card
    assert len(result) == 1
    assert result[0] is card
    assert mock_logger.error.call_count >= 1


def test_update_mode_visibility(mocker):
    """Verify update_mode_visibility returns the expected 5-tuple of gr.update() calls."""
    # Mock gr.update to return its arguments for easy verification
    mocker.patch("gradio.update", side_effect=lambda **kwargs: kwargs)

    result = update_mode_visibility("subject", "Biology")
    assert isinstance(result, tuple)
    assert len(result) == 5

    # Verify each update call
    assert result[0] == {"visible": True}
    assert result[1] == {"visible": True}
    assert result[2] == {"value": "Biology"}

    # result[3] should have a value that is an empty DataFrame with specific columns
    assert isinstance(result[3]["value"], pd.DataFrame)
    assert list(result[3]["value"].columns) == [
        "Index",
        "Topic",
        "Card_Type",
        "Question",
        "Answer",
        "Explanation",
        "Example",
        "Prerequisites",
        "Learning_Outcomes",
        "Difficulty",
    ]

    # result[4] is total_cards_html
    assert "total-cards-count" in result[4]["value"]
    assert result[4]["visible"] is False