예제 #1
0
    def _get_wrapper_attrs(self, layout):
        layout_data_key = get_layout_data_key(self.placeholder_name, layout,
                                              self.context)
        attrs = {
            "class": [
                "xt-ph", "xt-ph-edit" if self.edit else None,
                "xt-global-ph" if self.global_type else None
            ],
            "id":
            "xt-ph-%s" % layout_data_key
        }
        if self.edit:
            # Pass layout editor to editor so we can fetch
            # correct layout for editing.
            attrs["data-xt-layout-identifier"] = layout.identifier

            # We need to pass layout data key here since this is the last
            # place whe have the context available before editor.
            attrs["data-xt-layout-data-key"] = layout_data_key
            attrs["data-xt-placeholder-name"] = self.placeholder_name
            attrs[
                "data-xt-global-type"] = "global" if self.global_type else None
            attrs["title"] = _("Click to edit placeholder: %s"
                               ) % self.placeholder_name.title()
        return attrs
def test_page_layout():
    if "shuup.xtheme" not in settings.INSTALLED_APPS:
        pytest.skip("Need shuup.xtheme in INSTALLED_APPS")

    shop = factories.get_default_shop()
    theme = get_current_theme(shop)
    view_config = ViewConfig(theme=theme,
                             shop=shop,
                             view_name="PageView",
                             draft=True)
    page1_content = printable_gibberish()
    page1 = create_page(available_from=datetime.date(1917, 12, 6),
                        content=page1_content,
                        shop=shop,
                        url="test1")
    page2_content = printable_gibberish()
    page2 = create_page(available_from=datetime.date(1917, 12, 6),
                        content=page2_content,
                        shop=shop,
                        url="test2")

    placeholder_name = "cms_page"
    context = {"page": page1}
    layout = view_config.get_placeholder_layout(PageLayout,
                                                placeholder_name,
                                                context=context)
    assert isinstance(layout, PageLayout)
    assert layout.get_help_text({}) == ""  # Invalid context for help text
    assert page1.title in layout.get_help_text(context)

    # Make sure layout is empty
    serialized = layout.serialize()
    assert len(serialized["rows"]) == 0
    assert serialized["name"] == placeholder_name

    # Add custom plugin to page
    layout.begin_column({"md": 8})
    plugin_text = printable_gibberish()
    layout.add_plugin("text", {"text": plugin_text})
    view_config.save_placeholder_layout(
        get_layout_data_key(placeholder_name, layout, context), layout)
    view_config.publish()

    c = SmartClient()
    soup = c.soup(reverse("shuup:cms_page", kwargs={"url": page1.url}))
    page_content = soup.find("div", {"class": "page-content"})
    assert page1_content in page_content.text
    assert plugin_text in page_content.text

    c = SmartClient()
    soup = c.soup(reverse("shuup:cms_page", kwargs={"url": page2.url}))
    page_content = soup.find("div", {"class": "page-content"})
    assert page2_content in page_content.text
    assert plugin_text not in page_content.text
예제 #3
0
def _add_plugin_and_test_save(view_config, layout, placeholder_name, context, plugin_text=None):
    if not plugin_text:
        plugin_text = printable_gibberish()
    _add_basic_plugin(layout, plugin_text)
    view_config.save_placeholder_layout(get_layout_data_key(placeholder_name, layout, context), layout)
    view_config.publish()

    # Now refetching the layout we should get the plugin
    layout = view_config.get_placeholder_layout(layout.__class__, placeholder_name, context=context)
    assert isinstance(layout, layout.__class__)
    _assert_layout_content_for_basic_plugin(layout, placeholder_name, plugin_text)
예제 #4
0
파일: rendering.py 프로젝트: rhocom/shuup
    def render(self):
        """
        Get this placeholder's rendered contents.

        :return: Rendered markup.
        :rtype: markupsafe.Markup
        """
        language = get_language()

        if not may_inject(self.context):
            return ""

        full_content = ""
        saved_view_config = self.view_config.saved_view_config
        for layout in self.layouts:
            cache_key = slugify(
                "shuup_xtheme_placeholders:placeholder_%s_%s_%s_%s_%s_%s" % (
                    (saved_view_config.pk if saved_view_config else ""),
                    (saved_view_config.status if saved_view_config else ""),
                    (
                        saved_view_config.modified_on.isoformat()
                        if saved_view_config and saved_view_config.modified_on
                        else ""
                    ),
                    language,
                    self.placeholder_name,
                    get_layout_data_key(self.placeholder_name, layout, self.context)
                )
            )
            layout_content = cache.get(cache_key)
            if (
                settings.SHUUP_XTHEME_USE_PLACEHOLDER_CACHE and
                saved_view_config and
                saved_view_config.status == SavedViewConfigStatus.PUBLIC and
                layout_content
            ):
                full_content += layout_content
            else:
                wrapper_start = "<div%s>" % get_html_attrs(self._get_wrapper_attrs(layout))
                buffer = []
                write = buffer.append
                self._render_layout(write, layout)
                content = "".join(buffer)
                layout_content = (
                    "%(wrapper_start)s%(content)s%(wrapper_end)s" % {
                        "wrapper_start": wrapper_start,
                        "content": content,
                        "wrapper_end": "</div>",
                    })
                cache.set(cache_key, layout_content)
                full_content += layout_content

        return Markup('<div class="placeholder-edit-wrap">%s</div>' % full_content)
예제 #5
0
    def save_default_placeholder_layout(self, placeholder_name, layout):
        """
        Save a default placeholder layout (only if no data for the PH already
        exists).

        :param placeholder_name: Placeholder name
        :type placeholder_name: str
        :param layout: Layout or layout data
        :type layout: Layout|dict
        :return: True if saved
        :rtype: bool
        """
        if not self.draft:
            return False
        if self.saved_view_config and self.saved_view_config.get_layout_data(placeholder_name) is None:
            self.save_placeholder_layout(get_layout_data_key(placeholder_name, layout, {}), layout)
            return True
        return False
예제 #6
0
파일: rendering.py 프로젝트: ruqaiya/shuup
    def _get_wrapper_attrs(self, layout):
        layout_data_key = get_layout_data_key(self.placeholder_name, layout, self.context)
        attrs = {
            "class": ["xt-ph", "xt-ph-edit" if self.edit else None, "xt-global-ph" if self.global_type else None],
            "id": "xt-ph-%s" % layout_data_key
        }
        if self.edit:
            # Pass layout editor to editor so we can fetch
            # correct layout for editing.
            attrs["data-xt-layout-identifier"] = layout.identifier

            # We need to pass layout data key here since this is the last
            # place whe have the context available before editor.
            attrs["data-xt-layout-data-key"] = layout_data_key
            attrs["data-xt-placeholder-name"] = self.placeholder_name
            attrs["data-xt-global-type"] = "global" if self.global_type else None
            attrs["title"] = _("Click to edit placeholder: %s") % self.placeholder_name.title()
        return attrs
예제 #7
0
    def save_default_placeholder_layout(self, placeholder_name, layout):
        """
        Save a default placeholder layout (only if no data for the PH already
        exists).

        :param placeholder_name: Placeholder name
        :type placeholder_name: str
        :param layout: Layout or layout data
        :type layout: Layout|dict
        :return: True if saved
        :rtype: bool
        """
        if not self.draft:
            return False
        if self.saved_view_config and self.saved_view_config.get_layout_data(placeholder_name) is None:
            self.save_placeholder_layout(get_layout_data_key(placeholder_name, layout, {}), layout)
            return True
        return False
예제 #8
0
def _add_plugin_and_test_save(view_config,
                              layout,
                              placeholder_name,
                              context,
                              plugin_text=None):
    if not plugin_text:
        plugin_text = printable_gibberish()
    _add_basic_plugin(layout, plugin_text)
    view_config.save_placeholder_layout(
        get_layout_data_key(placeholder_name, layout, context), layout)
    view_config.publish()

    # Now refetching the layout we should get the plugin
    layout = view_config.get_placeholder_layout(layout.__class__,
                                                placeholder_name,
                                                context=context)
    assert isinstance(layout, layout.__class__)
    _assert_layout_content_for_basic_plugin(layout, placeholder_name,
                                            plugin_text)
예제 #9
0
    def get_placeholder_layout(self,
                               layout_cls,
                               placeholder_name,
                               default_layout={},
                               context=None,
                               layout_data_key=None):
        """
        Get a layout object for the given placeholder.

        :param layout_cls:
        :type layout_cls:
        :param placeholder_name: The name of the placeholder to load.
        :type placeholder_name: str
        :param default_layout: Default layout configuration (either a dict or an actual Layout).
        :type default_layout: dict|Layout
        :param context: Rendering context.
        :type context: jinja2.runtime.Context
        :param layout_data_key: layout data key used for saving the layout data.
        :type layout_data_key: str
        :return: Layout.
        :rtype: Layout
        """
        svc = self.saved_view_config
        layout = layout_cls(self.theme, placeholder_name=placeholder_name)
        if not layout_data_key:
            if not layout.is_valid_context(context or {}):
                return
            layout_data_key = get_layout_data_key(placeholder_name, layout,
                                                  context)

        if svc:
            placeholder_data = svc.get_layout_data(layout_data_key)
            if placeholder_data:
                return layout.unserialize(self.theme,
                                          placeholder_data,
                                          placeholder_name=placeholder_name)

        if default_layout:
            if isinstance(default_layout, Layout):
                return default_layout
            return layout.unserialize(self.theme, default_layout)

        return layout
예제 #10
0
def test_page_layout():
    if "shuup.xtheme" not in settings.INSTALLED_APPS:
        pytest.skip("Need shuup.xtheme in INSTALLED_APPS")

    shop = factories.get_default_shop()
    theme = get_current_theme(shop)
    view_config = ViewConfig(theme=theme, shop=shop, view_name="PageView", draft=True)
    page1_content = printable_gibberish()
    page1 = create_page(available_from=datetime.date(1917, 12, 6), content=page1_content, shop=shop, url="test1")
    page2_content = printable_gibberish()
    page2 = create_page(available_from=datetime.date(1917, 12, 6), content=page2_content, shop=shop, url="test2")

    placeholder_name = "cms_page"
    context = {"page": page1}
    layout = view_config.get_placeholder_layout(PageLayout, placeholder_name, context=context)
    assert isinstance(layout, PageLayout)
    assert layout.get_help_text({}) == ""  # Invalid context for help text
    assert page1.title in layout.get_help_text(context)

    # Make sure layout is empty
    serialized = layout.serialize()
    assert len(serialized["rows"]) == 0
    assert serialized["name"] == placeholder_name

    # Add custom plugin to page
    layout.begin_column({"md": 8})
    plugin_text = printable_gibberish()
    layout.add_plugin("text", {"text": plugin_text})
    view_config.save_placeholder_layout(get_layout_data_key(placeholder_name, layout, context), layout)
    view_config.publish()

    c = SmartClient()
    soup = c.soup(reverse("shuup:cms_page", kwargs={"url": page1.url}))
    page_content = soup.find("div", {"class": "page-content"})
    assert page1_content in page_content.text
    assert plugin_text in page_content.text

    c = SmartClient()
    soup = c.soup(reverse("shuup:cms_page", kwargs={"url": page2.url}))
    page_content = soup.find("div", {"class": "page-content"})
    assert page2_content in page_content.text
    assert plugin_text not in page_content.text
예제 #11
0
    def _render_row(self, write, layout, y, row):
        """
        Render a layout row into HTML.

        :param write: Writer function
        :type write: callable
        :param y: Row Y coordinate
        :type y: int
        :param row: Row object
        :type row: shuup.xtheme.view_config.LayoutRow
        """
        row_attrs = {"class": [layout.row_class, "xt-ph-row"]}
        if self.edit:
            row_attrs["data-xt-row"] = str(y)
        write("<div%s>" % get_html_attrs(row_attrs))

        language = get_language()
        saved_view_config = self.view_config.saved_view_config

        for x, cell in enumerate(row):
            cache_key_prefix = slugify(
                "{x}_{y}_{pk}_{status}_{modified_on}_{lang}_{placeholder}_{data_key}"
                .format(
                    x=x,
                    y=y,
                    pk=(saved_view_config.pk if saved_view_config else ""),
                    status=(saved_view_config.status
                            if saved_view_config else ""),
                    modified_on=(saved_view_config.modified_on.isoformat()
                                 if saved_view_config
                                 and saved_view_config.modified_on else ""),
                    lang=language,
                    placeholder=self.placeholder_name,
                    data_key=get_layout_data_key(self.placeholder_name, layout,
                                                 self.context),
                ))
            self._render_cell(write, layout, x, cell, cache_key_prefix)

        write("</div>\n")
예제 #12
0
    def get_placeholder_layout(
            self, layout_cls, placeholder_name, default_layout={}, context=None, layout_data_key=None):
        """
        Get a layout object for the given placeholder.

        :param layout_cls:
        :type layout_cls:
        :param placeholder_name: The name of the placeholder to load.
        :type placeholder_name: str
        :param default_layout: Default layout configuration (either a dict or an actual Layout)
        :type default_layout: dict|Layout
        :param context: Rendering context
        :type context: jinja2.runtime.Context
        :param layout_data_key: layout data key used for saving the layout data.
        :type layout_data_key: str
        :return: Layout
        :rtype: Layout
        """
        svc = self.saved_view_config
        layout = layout_cls(self.theme, placeholder_name=placeholder_name)
        if not layout_data_key:
            if not layout.is_valid_context(context or {}):
                return
            layout_data_key = get_layout_data_key(placeholder_name, layout, context)

        if svc:
            placeholder_data = svc.get_layout_data(layout_data_key)
            if placeholder_data:
                return layout.unserialize(self.theme, placeholder_data, placeholder_name=placeholder_name)

        if default_layout:
            if isinstance(default_layout, Layout):
                return default_layout
            return layout.unserialize(self.theme, default_layout)

        return layout
예제 #13
0
def test_plugin():
    shop = factories.get_default_shop()
    set_current_theme(ClassicGrayTheme.identifier, shop)

    blog_one_page = Page.objects.create(
        shop=shop,
        title="Blog One",
        url="blog_one",
        available_from=(now() - timedelta(days=10)),
        available_to=(now() + timedelta(days=10)),
        content="")

    blog_two_page = Page.objects.create(
        shop=shop,
        title="Blog Two",
        url="blog_two",
        available_from=(now() - timedelta(days=10)),
        available_to=(now() + timedelta(days=10)),
        content="")
    # create 10 blog pages
    for page in [blog_one_page, blog_two_page]:
        for i in range(10):
            article = Page.objects.create(
                shop=shop,
                title="Article %d %s" % (i, page.title),
                url="blog-%d-%s" % (i, page.url),
                available_from=(now() - timedelta(days=10)),
                available_to=(now() + timedelta(days=10)),
                content="Content %d" % i,
                template_name="shuup_cms_blog/blog_page.jinja",
                parent=page)
            BlogArticle.objects.create(
                page=article,
                is_blog_article=True,
                image=factories.get_random_filer_image(),
                small_description="description %d" % i)

    # create 3 non blog post pages
    for i in range(3):
        article = Page.objects.create(
            shop=shop,
            title="Nothing %d" % i,
            url="non-%d" % i,
            available_from=(now() - timedelta(days=10)),
            available_to=(now() + timedelta(days=10)),
            content="content %i" % i)

    theme = get_current_theme(shop)
    view_config = ViewConfig(theme=theme,
                             shop=shop,
                             view_name="PageView",
                             draft=True)

    placeholder_name = "cms_page"
    context_one = {"page": blog_one_page}
    context_two = {"page": blog_two_page}

    layout_one = view_config.get_placeholder_layout(PageLayout,
                                                    placeholder_name,
                                                    context=context_one)
    layout_two = view_config.get_placeholder_layout(PageLayout,
                                                    placeholder_name,
                                                    context=context_two)

    assert isinstance(layout_one, PageLayout)
    assert isinstance(layout_two, PageLayout)

    assert layout_one.get_help_text({}) == ""
    assert layout_two.get_help_text({}) == ""

    assert blog_one_page.title in layout_two.get_help_text(context_one)
    assert blog_two_page.title in layout_two.get_help_text(context_two)

    serialized_one = layout_one.serialize()
    serialized_two = layout_two.serialize()

    assert len(serialized_one["rows"]) == 0
    assert len(serialized_two["rows"]) == 0

    assert serialized_one["name"] == placeholder_name
    assert serialized_two["name"] == placeholder_name

    layout_one.begin_column({"sm": 12})
    layout_two.begin_column({"sm": 12})

    layout_one.add_plugin(ShuupCMSBlogArticleListPlugin.identifier,
                          {"blog_page": blog_one_page.pk})
    layout_two.add_plugin(ShuupCMSBlogArticleListPlugin.identifier,
                          {"blog_page": blog_two_page.pk})

    view_config.save_placeholder_layout(
        get_layout_data_key(placeholder_name, layout_one, context_one),
        layout_one)
    view_config.save_placeholder_layout(
        get_layout_data_key(placeholder_name, layout_two, context_two),
        layout_two)
    view_config.publish()

    client = SmartClient()
    response, soup = client.response_and_soup(
        reverse("shuup:cms_page", kwargs={"url": blog_one_page.url}))
    assert response.status_code == 200
    assert len(soup.find_all("a", {"class": "article-card"})) == 10

    response, soup = client.response_and_soup(
        reverse("shuup:cms_page", kwargs={"url": blog_two_page.url}))
    assert response.status_code == 200
    assert len(soup.find_all("a", {"class": "article-card"})) == 10

    article_one = Page.objects.create(
        shop=shop,
        title="Article test",
        url="blog-test",
        available_from=(now() - timedelta(days=10)),
        available_to=(now() + timedelta(days=10)),
        content="Content test",
        template_name=
        "shuup_cms_blog/blog_page.jinja",  # Add an article without a parent
    )
    BlogArticle.objects.create(page=article_one,
                               is_blog_article=True,
                               image=factories.get_random_filer_image(),
                               small_description="description test")
    article_two = Page.objects.create(
        shop=shop,
        title="Article test 2",
        url="blog-test-2",
        available_from=(now() - timedelta(days=10)),
        available_to=(now() + timedelta(days=10)),
        content="Content test 2",
        template_name=
        "shuup_cms_blog/blog_page.jinja",  # Add an article without a parent
    )
    BlogArticle.objects.create(page=article_two,
                               is_blog_article=True,
                               image=factories.get_random_filer_image(),
                               small_description="description test 2")
    view_config = ViewConfig(theme=theme,
                             shop=shop,
                             view_name="PageView",
                             draft=True)

    # No blog page set means that only articles with no parent will be shown
    layout_two.add_plugin(ShuupCMSBlogArticleListPlugin.identifier, {})
    view_config.save_placeholder_layout(
        get_layout_data_key(placeholder_name, layout_two, context_two),
        layout_two)
    view_config.publish()

    response, soup = client.response_and_soup(
        reverse("shuup:cms_page", kwargs={"url": blog_two_page.url}))
    assert response.status_code == 200
    assert len(soup.find_all("a", {"class": "article-card"})) == 2

    response, soup = client.response_and_soup(
        reverse("shuup:cms_page", kwargs={"url": article_one.url}))
    assert response.status_code == 200
    assert len(soup.find_all("a", {"class": "article-card"})) == 1
    assert soup.find("div", {
        "class": "article-title"
    }).text == article_two.title

    article_one.soft_delete()  # Delete the article that has no parent
    article_two.soft_delete()  # Delete the article that has no parent

    response, soup = client.response_and_soup(
        reverse("shuup:cms_page", kwargs={"url": blog_two_page.url}))
    assert response.status_code == 200
    assert len(soup.find_all("a", {"class": "article-card"})) == 0

    article_with_parent = Page.objects.filter(parent=blog_one_page).first()
    response, soup = client.response_and_soup(
        reverse("shuup:cms_page", kwargs={"url": article_with_parent.url}))
    assert response.status_code == 200
    assert len(soup.find_all(
        "a",
        {"class": "article-card"})) == 9  # Assert there are 9 articles shown
    article_card_titles = soup.find_all("div", {"class": "article-title"})
    article_card_titles = [title.text for title in article_card_titles]
    # Assert that the current visited article (article_with_parent) is not visible in Related articles
    assert article_with_parent.title not in article_card_titles
def test_page_anonymous_layout():
    if "shuup.simple_cms" not in settings.INSTALLED_APPS:
        pytest.skip("Need shuup.simple_cms in INSTALLED_APPS")

    if "shuup.xtheme" not in settings.INSTALLED_APPS:
        pytest.skip("Need shuup.xtheme in INSTALLED_APPS")

    shop = factories.get_default_shop()
    theme = set_current_theme("shuup.themes.classic_gray", shop)
    view_config = ViewConfig(theme=theme,
                             shop=shop,
                             view_name="PageView",
                             draft=True)
    page1_content = printable_gibberish()
    page1 = create_page(available_from=datetime.date(1917, 12, 6),
                        content=page1_content,
                        shop=shop,
                        url="test1")
    page2_content = printable_gibberish()
    page2 = create_page(available_from=datetime.date(1917, 12, 6),
                        content=page2_content,
                        shop=shop,
                        url="test2")

    person = factories.create_random_person(shop=shop)
    person.user = factories.create_random_user()
    password = "******"
    person.user.set_password(password)
    person.user.save()
    person.save()

    placeholder_name = "cms_page"
    request = get_request()
    context = {"page": page1, "request": request}
    layout = view_config.get_placeholder_layout(PageAnonymousLayout,
                                                placeholder_name,
                                                context=context)
    assert isinstance(layout, PageAnonymousLayout)
    assert layout.get_help_text({}) == ""  # Invalid context for help text
    assert page1.title in layout.get_help_text(context)

    # Make sure layout is empty
    serialized = layout.serialize()
    assert len(serialized["rows"]) == 0
    assert serialized["name"] == placeholder_name

    # Add custom plugin to page
    layout.begin_column({"md": 8})
    plugin_text = printable_gibberish()
    layout.add_plugin("text", {"text": plugin_text})
    view_config.save_placeholder_layout(
        get_layout_data_key(placeholder_name, layout, context), layout)
    view_config.publish()

    c = SmartClient()
    soup = c.soup(reverse("shuup:cms_page", kwargs={"url": page1.url}))
    page_content = soup.find("div", {"class": "page-content"})
    assert page1_content in page_content.text
    assert plugin_text in page_content.text

    # Make sure content not available for page2
    c = SmartClient()
    soup = c.soup(reverse("shuup:cms_page", kwargs={"url": page2.url}))
    page_content = soup.find("div", {"class": "page-content"})
    assert page2_content in page_content.text
    assert plugin_text not in page_content.text

    # Make sure content not available for logged in users
    c = SmartClient()
    c.login(username=person.user.username, password=password)
    soup = c.soup(reverse("shuup:cms_page", kwargs={"url": page1.url}))
    page_content = soup.find("div", {"class": "page-content"})
    assert page1_content in page_content.text
    assert plugin_text not in page_content.text
예제 #15
0
def test_plugin():
    shop = factories.get_default_shop()
    set_current_theme(ClassicGrayTheme.identifier, shop)

    blog_page = Page.objects.create(shop=shop,
                                    title="Blog",
                                    url="blog",
                                    available_from=(now() -
                                                    timedelta(days=10)),
                                    available_to=(now() + timedelta(days=10)),
                                    content="")

    # create 10 blog pages
    for i in range(10):
        article = Page.objects.create(
            shop=shop,
            title="Article %d" % i,
            url="blog-%d" % i,
            available_from=(now() - timedelta(days=10)),
            available_to=(now() + timedelta(days=10)),
            content="Content %d" % i,
            template_name="shuup_cms_blog/blog_page.jinja")
        BlogArticle.objects.create(page=article,
                                   is_blog_article=True,
                                   image=factories.get_random_filer_image(),
                                   small_description="description %d" % i)

    # create 3 non blog post pages
    for i in range(3):
        article = Page.objects.create(
            shop=shop,
            title="Nothing %d" % i,
            url="non-%d" % i,
            available_from=(now() - timedelta(days=10)),
            available_to=(now() + timedelta(days=10)),
            content="content %i" % i)

    theme = get_current_theme(shop)
    view_config = ViewConfig(theme=theme,
                             shop=shop,
                             view_name="PageView",
                             draft=True)

    placeholder_name = "cms_page"
    context = {"page": blog_page}
    layout = view_config.get_placeholder_layout(PageLayout,
                                                placeholder_name,
                                                context=context)
    assert isinstance(layout, PageLayout)
    assert layout.get_help_text({}) == ""
    assert blog_page.title in layout.get_help_text(context)
    serialized = layout.serialize()
    assert len(serialized["rows"]) == 0
    assert serialized["name"] == placeholder_name

    layout.begin_column({"sm": 12})
    layout.add_plugin(ShuupCMSBlogArticleListPlugin.identifier, {})
    view_config.save_placeholder_layout(
        get_layout_data_key(placeholder_name, layout, context), layout)
    view_config.publish()

    client = SmartClient()
    response, soup = client.response_and_soup(
        reverse("shuup:cms_page", kwargs={"url": blog_page.url}))
    assert response.status_code == 200
    assert len(soup.find_all("a", {"class": "article-card"})) == 10