From 6145fe46a821582c0870dd37f9d4245a70362178 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 8 Nov 2024 08:13:21 +0100 Subject: [PATCH] feat: improve test coverage and use pytest --- .github/workflows/test.yml | 4 +- README.rst | 2 +- conftest.py | 123 ++++++++++++++++++ djangocms_link/fields.py | 8 ++ djangocms_link/helpers.py | 7 +- .../templatetags/djangocms_link_tags.py | 11 +- tests/requirements/base.txt | 2 + tests/settings.py | 1 - tests/test_link_dict.py | 28 +++- tests/test_models.py | 11 +- tests/utils/templates/base.html | 9 ++ tests/utils/templates/fullwidth.html | 8 ++ tests/utils/templates/page.html | 8 ++ tests/utils/urls.py | 23 ++++ 14 files changed, 226 insertions(+), 19 deletions(-) create mode 100644 conftest.py create mode 100644 tests/utils/templates/fullwidth.html create mode 100644 tests/utils/templates/page.html create mode 100644 tests/utils/urls.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9deb2109..a648a173 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,8 +42,8 @@ jobs: pip install -r tests/requirements/${{ matrix.requirements-file }} python setup.py install - - name: Run coverage - run: coverage run ./tests/settings.py + - name: Run test coverage + run: coverage run -m pytest - name: Upload Coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/README.rst b/README.rst index 3a10e5d7..6508e613 100644 --- a/README.rst +++ b/README.rst @@ -237,7 +237,7 @@ You can run tests by executing:: virtualenv env source env/bin/activate pip install -r tests/requirements.txt - python setup.py test + pytest Upgrading from version 4 or lower diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..f422ab2e --- /dev/null +++ b/conftest.py @@ -0,0 +1,123 @@ +import os +import sys + +import django +from django.conf import global_settings, settings +from django.test.utils import get_runner + +from tests.settings import HELPER_SETTINGS + + +CMS_APP = [ + "cms", + "menus", + "easy_thumbnails", + "treebeard", + "sekizai", + "djangocms_link", +] +CMS_APP_STYLE = [] +CMS_PROCESSORS = [] +CMS_MIDDLEWARE = [ + "cms.middleware.user.CurrentUserMiddleware", + "cms.middleware.page.CurrentPageMiddleware", + "cms.middleware.toolbar.ToolbarMiddleware", + "cms.middleware.language.LanguageCookieMiddleware", +] + +INSTALLED_APPS = ( + [ + "django.contrib.contenttypes", + "django.contrib.auth", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.staticfiles", + ] + + CMS_APP_STYLE + + ["django.contrib.admin", "django.contrib.messages"] + + CMS_APP +) +DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} +TEMPLATE_LOADERS = [ + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", +] +STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", +] +TEMPLATE_CONTEXT_PROCESSORS = [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", +] + CMS_PROCESSORS +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + os.path.join(os.path.dirname(__file__), "templates"), + # insert your TEMPLATE_DIRS here + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": TEMPLATE_CONTEXT_PROCESSORS, + }, + }, +] +MIDDLEWARE = [ + "django.middleware.http.ConditionalGetMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", +] + CMS_MIDDLEWARE +SITE_ID = 1 +LANGUAGE_CODE = "en" +LANGUAGES = (("en", "English"),) +STATIC_URL = "/static/" +MEDIA_URL = "/media/" +DEBUG = True +CMS_TEMPLATES = (("fullwidth.html", "Fullwidth"), ("page.html", "Normal page")) +PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) +MIGRATION_MODULES = {} +URL_CONF = "tests.utils.urls" + + +def pytest_configure(): + INSTALLED_APPS.extend(HELPER_SETTINGS.pop("INSTALLED_APPS")) + + settings.configure( + default_settings=global_settings, + **{ + **dict( + INSTALLED_APPS=INSTALLED_APPS, + TEMPLATES=TEMPLATES, + DATABASES=DATABASES, + SITE_ID=SITE_ID, + LANGUAGES=LANGUAGES, + CMS_CONFIRM_VERSION4=True, + MIGRATION_MODULES=MIGRATION_MODULES, + ROOT_URLCONF=URL_CONF, + STATIC_URL=STATIC_URL, + MEDIA_URL=MEDIA_URL, + SECRET_KEY="Secret!", + MIDDLEWARE=MIDDLEWARE, + ), + **HELPER_SETTINGS, + } + ) + django.setup() + + +if __name__ == "__main__": + pytest_configure() + + argv = ["tests"] if sys.argv is None else sys.argv + tests = argv[1:] if len(argv) > 1 else ["tests"] + TestRunner = get_runner(settings) + test_runner = TestRunner() + failures = test_runner.run_tests(tests) + sys.exit(bool(failures)) diff --git a/djangocms_link/fields.py b/djangocms_link/fields.py index 222f2343..8fee574b 100644 --- a/djangocms_link/fields.py +++ b/djangocms_link/fields.py @@ -367,6 +367,14 @@ def formfield(self, **kwargs): kwargs.setdefault("form_class", LinkFormField) return super().formfield(**kwargs) + def get_prep_value(self, value): + if isinstance(value, dict): + # Drop any cached value without changing the original value + return super().get_prep_value(dict(**{ + key: val for key, val in value.items() if key != "__cache__" + })) + return super().get_prep_value(value) + def from_db_value(self, value, expression, connection): value = super().from_db_value(value, expression, connection) return LinkDict(value) diff --git a/djangocms_link/helpers.py b/djangocms_link/helpers.py index b8362d08..df281d75 100644 --- a/djangocms_link/helpers.py +++ b/djangocms_link/helpers.py @@ -73,6 +73,7 @@ class LinkDict(dict): the url of the link. The url property is cached to avoid multiple db lookups.""" def __init__(self, initial=None, **kwargs): + anchor = kwargs.pop("anchor", None) super().__init__(**kwargs) if initial: if isinstance(initial, dict): @@ -85,8 +86,10 @@ def __init__(self, initial=None, **kwargs): self["internal_link"] = ( f"{initial._meta.app_label}.{initial._meta.model_name}:{initial.pk}" ) - if "anchor" in kwargs: - self["anchor"] = kwargs["anchor"] + self["__cache__"] = initial.get_absolute_url() + if anchor: + self["anchor"] = anchor + self["__cache__"] += anchor @property def url(self): diff --git a/djangocms_link/templatetags/djangocms_link_tags.py b/djangocms_link/templatetags/djangocms_link_tags.py index 31ea9493..764fdd3d 100644 --- a/djangocms_link/templatetags/djangocms_link_tags.py +++ b/djangocms_link/templatetags/djangocms_link_tags.py @@ -1,7 +1,6 @@ from django import template -from django.db import models -from djangocms_link.helpers import get_link +from djangocms_link.helpers import LinkDict, get_link try: @@ -22,10 +21,4 @@ def to_url(value): @register.filter def to_link(value): - if isinstance(value, File): - return {"file_link": value.pk} - elif isinstance(value, models.Model): - return { - "internal_link": f"{value._meta.app_label}.{value._meta.model_name}:{value.pk}" - } - return {"external_link": value} + return LinkDict(value) diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt index bda566a2..4dc509df 100644 --- a/tests/requirements/base.txt +++ b/tests/requirements/base.txt @@ -7,3 +7,5 @@ isort flake8 pyflakes>=2.1 django-test-migrations +pytest +pytest-django diff --git a/tests/settings.py b/tests/settings.py index 249bbfa1..52de5244 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -23,7 +23,6 @@ "easy_thumbnails.processors.filters", ), "ALLOWED_HOSTS": ["localhost"], - "DJANGOCMS_LINK_USE_SELECT2": True, "CMS_TEMPLATES": ( ("page.html", "Normal page"), ("static_placeholder.html", "Page with static placeholder"), diff --git a/tests/test_link_dict.py b/tests/test_link_dict.py index 1101e98d..3bdc504b 100644 --- a/tests/test_link_dict.py +++ b/tests/test_link_dict.py @@ -4,6 +4,7 @@ from filer.models import File from djangocms_link.helpers import LinkDict +from djangocms_link.models import Link from tests.utils.models import ThirdPartyModel @@ -46,12 +47,11 @@ def test_internal_link(self): {"internal_link": f"{obj._meta.app_label}.{obj._meta.model_name}:{obj.pk}"} ) link2 = LinkDict(obj) - link3 = LinkDict(obj, anchor="test") + link3 = LinkDict(obj, anchor="#test") - self.assertEqual(link1, link2) self.assertEqual(link1.url, obj.get_absolute_url()) self.assertEqual(link2.url, obj.get_absolute_url()) - self.assertEqual(link3.url, f"{obj.get_absolute_url()}test") + self.assertEqual(link3.url, f"{obj.get_absolute_url()}#test") self.assertEqual(link1.type, "internal_link") self.assertEqual(link2.type, "internal_link") self.assertEqual(link3.type, "internal_link") @@ -66,3 +66,25 @@ def test_link_types(self): self.assertEqual(external.type, "external_link") self.assertEqual(phone.type, "tel") self.assertEqual(mail.type, "mailto") + + def test_db_queries(self): + obj = ThirdPartyModel.objects.create( + name=get_random_string(5), path=get_random_string(5) + ) + link = LinkDict(obj) + with self.assertNumQueries(0): + self.assertEqual(link.url, obj.get_absolute_url()) + + def test_cache_no_written_to_db(self): + obj = ThirdPartyModel.objects.create( + name=get_random_string(5), path=get_random_string(5) + ) + link = Link.objects.create( + link=LinkDict(obj) + ) + self.assertEqual(link.link.url, link.link["__cache__"]) # populates cache + link.save() + + link = Link.objects.get(pk=link.pk) # load from db + + self.assertNotIn("__cache__", link.link) diff --git a/tests/test_models.py b/tests/test_models.py index 6302ef7e..ae80592f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,3 +1,5 @@ +from pprint import pprint + from django.core.exceptions import ValidationError from django.test import TestCase @@ -125,7 +127,10 @@ def test_to_link_template_tag(self): self.assertEqual(to_link(self.file), {"file_link": self.file.pk}) self.assertEqual( - to_link(self.page), {"internal_link": f"cms.page:{self.page.pk}"} + to_link(self.page), { + "internal_link": f"cms.page:{self.page.pk}", + "__cache__": self.page.get_absolute_url(), + } ) self.assertEqual( to_link("https://www.django-cms.org/#some_id"), @@ -143,3 +148,7 @@ def test_respect_link_is_optional(self): # now we allow the link to be empty instance.link_is_optional = True instance.clean() + + def test_settings(self): + from django.conf import settings + pprint(settings.__dict__) diff --git a/tests/utils/templates/base.html b/tests/utils/templates/base.html index db9d806d..da3c605e 100755 --- a/tests/utils/templates/base.html +++ b/tests/utils/templates/base.html @@ -22,3 +22,12 @@ {% show_menu 0 100 100 100 %} {% block content %} + {% endblock content %} + +{% render_block "js" %} +{% with_data "js-script" as jsset %} + {% for js in jsset %}{% endfor %} +{% end_with_data %} +{% render_block "js_end" %} + + diff --git a/tests/utils/templates/fullwidth.html b/tests/utils/templates/fullwidth.html new file mode 100644 index 00000000..e96a8f2f --- /dev/null +++ b/tests/utils/templates/fullwidth.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% load cms_tags %} + +{% block title %}{% page_attribute 'title' %}{% endblock title %} + +{% block content %} + {% placeholder "content" %} +{% endblock content %} diff --git a/tests/utils/templates/page.html b/tests/utils/templates/page.html new file mode 100644 index 00000000..e96a8f2f --- /dev/null +++ b/tests/utils/templates/page.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% load cms_tags %} + +{% block title %}{% page_attribute 'title' %}{% endblock title %} + +{% block content %} + {% placeholder "content" %} +{% endblock content %} diff --git a/tests/utils/urls.py b/tests/utils/urls.py new file mode 100644 index 00000000..3ee11096 --- /dev/null +++ b/tests/utils/urls.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django.conf.urls.i18n import i18n_patterns +from django.contrib import admin +from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.urls import include, path, re_path +from django.views.i18n import JavaScriptCatalog +from django.views.static import serve + + +admin.autodiscover() + +urlpatterns = [ + re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT, "show_indexes": True}), # NOQA + re_path(r"^jsi18n/(?P\S+?)/$", JavaScriptCatalog.as_view()), # NOQA +] +i18n_urls = [ + re_path(r"^admin/", admin.site.urls), +] + +i18n_urls.append(path("", include("cms.urls"))) # NOQA + +urlpatterns += i18n_patterns(*i18n_urls) +urlpatterns += staticfiles_urlpatterns()