feat: Show cards preview (#2121)

feat: Show cards preview (#2121)

diff --git a/ankihub/decks/models.py b/ankihub/decks/models.py
index 2c76bde..5016015 100644
--- a/ankihub/decks/models.py
+++ b/ankihub/decks/models.py
@@ -3,6 +3,7 @@ import re
 import uuid
 from abc import abstractmethod
 from datetime import timedelta
+from functools import cached_property
 
 import structlog
 from bs4 import BeautifulSoup
@@ -46,7 +47,7 @@ from ankihub.decks import utils
 from ankihub.decks.utils import get_no_owner_sentinel_user
 from config.settings.base import AUTH_USER_MODEL, SITE_URL
 
-from .utils import bleach_sanitize
+from .utils import NoteCardsGenerator, bleach_sanitize
 
 log = structlog.get_logger()
 
@@ -818,6 +819,96 @@ class Note(LifecycleModelMixin, models.Model):
 
         return cleaned_text
 
+    @cached_property
+    def note_type_obj(self):
+        try:
+            return NoteType.objects.get(anki_id=self.note_type_id)
+        except NoteType.DoesNotExist:
+            return None
+
+    @cached_property
+    def cards_quantity(self):
+        quantity = 0
+        for field in self.fields:
+            pattern = re.compile(r"{{c(\d+)::")
+            matches = pattern.findall(field["value"])
+            unique_matches = set(matches)
+            quantity += len(unique_matches)
+        return quantity or 1
+
+    @cached_property
+    def cards_css(self):
+        note_type = self.note_type_obj
+        return note_type.css
+
+    def get_cards(self, use_template_style=False):
+        cards_quantity = self.cards_quantity
+        cards = []
+        for i in range(cards_quantity):
+            question = self.get_question_card_html(
+                card_number=i + 1, use_template_style=use_template_style
+            )
+            answer = self.get_answer_card_html(
+                card_number=i + 1, use_template_style=use_template_style
+            )
+            cards.append(
+                (
+                    question,
+                    answer,
+                )
+            )
+        return cards
+
+    def _get_card_html(
+        self,
+        card_type,
+        card_number=1,
+        use_template_style=False,
+        show_cloze_deletion=True,
+    ):
+        card_type_map = {"answer": "afmt", "question": "qfmt"}
+        note_type = self.note_type_obj
+        templates = note_type.templates
+        first_template = templates[0]
+        template = first_template[card_type_map[card_type]]
+
+        if use_template_style:
+            template += f"\n<style>{self.cards_css}</style>"
+
+        html_string = str(BeautifulSoup(template, features="html.parser"))
+        html_string = re.sub(
+            r"<script\b[^<]*(?:(?!</script>)<[^<]*)*</script>",
+            "",
+            html_string,
+            flags=re.IGNORECASE,
+        )
+        is_answer_card = card_type == "answer"
+        return NoteCardsGenerator(
+            note=self,
+            template=html_string,
+            card_number=card_number,
+            is_answer_card=is_answer_card,
+            use_template_style=use_template_style,
+            show_cloze_deletion=show_cloze_deletion,
+        ).process()
+
+    def get_question_card_html(
+        self, card_number=1, use_template_style=False, show_cloze_deletion=True
+    ):
+        return self._get_card_html(
+            card_type="question",
+            card_number=card_number,
+            use_template_style=use_template_style,
+            show_cloze_deletion=show_cloze_deletion,
+        )
+
+    def get_answer_card_html(self, card_number=1, use_template_style=False):
+        return self._get_card_html(
+            card_type="answer",
+            card_number=card_number,
+            use_template_style=use_template_style,
+        )
+
     @hook(BEFORE_SAVE)
     def _on_before_save_extract_note_corpus(self):
         self.corpus = self.extract_corpus(self.fields)
diff --git a/ankihub/decks/tests/test_models.py b/ankihub/decks/tests/test_models.py
index c32f5eb..01c2988 100644
--- a/ankihub/decks/tests/test_models.py
+++ b/ankihub/decks/tests/test_models.py
@@ -36,6 +36,7 @@ from ankihub.decks.tests.factories import (
     NewNoteSuggestionFactory,
     NoteFactory,
     NoteTagFactory,
+    NoteTypeFactory,
     ProtectedFieldsSetFactory,
     ProtectedTagsSetFactory,
 )
@@ -488,6 +489,97 @@ class TestNote:
         assert queryset.count() == 0
         assert list(queryset) == list(Note.objects.none())
 
+    @pytest.mark.parametrize(
+        "is_answer_card, expected_response, use_template_style",
+        [
+            (False, "hello world", False),
+            (True, "hello world back", False),
+            (False, "hello world\n<style>css</style>", True),
+            (True, "hello world back\n<style>css</style>", True),
+        ],
+    )
+    def test_get_card_html(self, is_answer_card, expected_response, use_template_style):
+        templates = [{"qfmt": "hello {{Front}}", "afmt": "{{FrontSide}} back"}]
+        css = "css"
+        note_type = NoteTypeFactory(templates=templates, css=css)
+
+        note = NoteFactory(
+            fields=[{"name": "Front", "value": "world"}], note_type_id=note_type.anki_id
+        )
+
+        if is_answer_card:
+            html = note.get_answer_card_html(
+                card_number=1, use_template_style=use_template_style
+            )
+        else:
+            html = note.get_question_card_html(
+                card_number=1, use_template_style=use_template_style
+            )
+
+        assert html == expected_response
+
+    def test_note_type_obj(self):
+        templates = [{"qfmt": "hello {{Front}}", "afmt": "{{FrontSide}} back"}]
+        note_type = NoteTypeFactory(templates=templates)
+
+        note = NoteFactory(
+            fields=[{"name": "Front", "value": "world"}], note_type_id=note_type.anki_id
+        )
+        assert note.note_type_obj == note_type
+
+        note2 = note = NoteFactory(
+            fields=[{"name": "Front", "value": "world"}], note_type_id=None
+        )
+        assert note2.note_type_obj is None
+
+    def test_cards_css(self):
+        templates = [{"qfmt": "hello {{Front}}", "afmt": "{{FrontSide}} back"}]
+        css = "css"
+        note_type = NoteTypeFactory(templates=templates, css=css)
+
+        note = NoteFactory(
+            fields=[{"name": "Front", "value": "world"}], note_type_id=note_type.anki_id
+        )
+        assert note.cards_css == css
+
+    def test_cards_quantity(self):
+        templates = [{"qfmt": "hello {{Front}} ", "afmt": "{{FrontSide}} back"}]
+        note_type = NoteTypeFactory(templates=templates)
+
+        note = NoteFactory(
+            fields=[{"name": "Front", "value": "{{c1::hello}} {{c1::world}}"}],
+            note_type_id=note_type.anki_id,
+        )
+        assert note.cards_quantity == 1
+
+        note2 = NoteFactory(
+            fields=[{"name": "Front", "value": "{{c1::hello}} {{c2::world}}"}],
+            note_type_id=note_type.anki_id,
+        )
+        assert note2.cards_quantity == 2
+
+    def test_get_cards(self):
+        templates = [{"qfmt": "hello {{cloze:Front}}", "afmt": "{{FrontSide}} back"}]
+        css = "css"
+        note_type = NoteTypeFactory(templates=templates, css=css)
+
+        note = NoteFactory(
+            fields=[{"name": "Front", "value": "{{c1::test1}} {{c2::test2}}"}],
+            note_type_id=note_type.anki_id,
+        )
+        cards = note.get_cards()
+        assert len(cards) == 2
+        assert cards == [
+            (
+                'hello <span class="text-blue-500">[...]</span> test2',
+                "hello test1 test2 back",
+            ),
+            (
+                'hello test1 <span class="text-blue-500">[...]</span>',
+                "hello test1 test2 back",
+            ),
+        ]
+
 
 @pytest.mark.django_db
 class TestDeckSubscription:
diff --git a/ankihub/decks/tests/test_utils.py b/ankihub/decks/tests/test_utils.py
index 8d6d440..7e53bb2 100644
--- a/ankihub/decks/tests/test_utils.py

[... diff too long, it was truncated ...]

GitHub
sha: ac2086c494314b621b3c41cfa598088914ac61b9