diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..02896a1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +max_line_length = 80 +trim_trailing_whitespace = true + +[*.{html,css,js,json,xml,yaml,yml}] +indent_size = 2 + +[*.{md,ps1,sh,py,rst}] +indent_size = 4 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1a03b41..5b95660 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -92,6 +92,9 @@ stages: - script: flake8 . displayName: 'CR-QC: Static analysis (flake8)' + - script: black --check . + displayName: 'CR-QC: Format check' + - script: mypy ./wagtailcache/ displayName: 'CR-QC: Type check (mypy)' diff --git a/docs/conf.py b/docs/conf.py index 4cc3e86..a7347cc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,21 +49,23 @@ html_sidebars = { "**": [ - "about.html", # Project name, description, etc. - "searchbox.html", # Search. - "globaltoc.html", # Global table of contents. + "about.html", # Project name, description, etc. + "searchbox.html", # Search. + "globaltoc.html", # Global table of contents. "readingmodes.html", # Light/sepia/dark color schemes. - "sponsors.html", # Fancy sponsor links. + "sponsors.html", # Fancy sponsor links. ] } html_theme_options = { "description": "A fast and simple page cache for Wagtail.", - "sponsors": [{ - "href": "https://github.com/coderedcorp/wagtail-cache", - "image": "https://docs.coderedcorp.com/logo-square-red-128.png", - "note": "This project is comercially supported by CodeRed." - }], + "sponsors": [ + { + "href": "https://github.com/coderedcorp/wagtail-cache", + "image": "https://docs.coderedcorp.com/logo-square-red-128.png", + "note": "This project is comercially supported by CodeRed.", + } + ], } html_last_updated_fmt = "" diff --git a/docs/releases/index.rst b/docs/releases/index.rst index b8e56fa..064f6d1 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -14,4 +14,4 @@ Release Notes v0.3.0 v0.2.1 v0.2.0 - v0.1.0 \ No newline at end of file + v0.1.0 diff --git a/docs/releases/v1.0.2.rst b/docs/releases/v1.0.2.rst index 71c3493..37e41af 100644 --- a/docs/releases/v1.0.2.rst +++ b/docs/releases/v1.0.2.rst @@ -5,6 +5,8 @@ * Updated unit tests for Wagtail 2.12. +* Apply ``black`` formatting to codebase. + .. note:: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..24b9307 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[tool.black] +line-length = 80 +target-version = ['py36', 'py37', 'py38'] +# Regular expression of files to exclude. +exclude = ''' +/( + migrations +)/ +''' diff --git a/requirements-dev.txt b/requirements-dev.txt index 42e28f5..0e5a068 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ -e ./ -alabaster +black codespell flake8 mypy diff --git a/setup.py b/setup.py index f5fadb5..b9ba25a 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,36 @@ from setuptools import setup, find_packages from wagtailcache import __version__ -with open('README.md', encoding='utf8') as readme_file: +with open("README.md", encoding="utf8") as readme_file: readme = readme_file.read() setup( - name='wagtail-cache', + name="wagtail-cache", version=__version__, author="CodeRed LLC", - author_email='info@coderedcorp.com', - url='https://github.com/coderedcorp/wagtail-cache', + author_email="info@coderedcorp.com", + url="https://github.com/coderedcorp/wagtail-cache", description="A simple page cache for Wagtail based on the Django cache middleware.", long_description=readme, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", license="BSD license", include_package_data=True, packages=find_packages(), - install_requires=[ - 'wagtail>=2.0' - ], + install_requires=["wagtail>=2.0"], classifiers=[ - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Framework :: Django', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', - 'Framework :: Wagtail', - 'Framework :: Wagtail :: 2', + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Framework :: Django", + "Framework :: Django :: 2.0", + "Framework :: Django :: 2.1", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.0", + "Framework :: Django :: 3.1", + "Framework :: Wagtail", + "Framework :: Wagtail :: 2", ], ) diff --git a/testproject/home/models.py b/testproject/home/models.py index 8791886..c3e2e58 100644 --- a/testproject/home/models.py +++ b/testproject/home/models.py @@ -8,6 +8,7 @@ class WagtailPage(Page): cached due to the middleware, but will not have the ability to provide custom cache instructions or "smart" caching features. """ + template = "home/page.html" @@ -15,6 +16,7 @@ class CachedPage(WagtailCacheMixin, Page): """ Represents a normal use-case. """ + template = "home/page.html" @@ -23,6 +25,7 @@ class CacheControlPage(WagtailCacheMixin, Page): Page that should never cache and should generate a custom cache-control header. """ + template = "home/page.html" cache_control = "no-cache" @@ -32,6 +35,7 @@ class CallableCacheControlPage(WagtailCacheMixin, Page): Page that should never cache and should generate a custom cache-control header via a function call. """ + template = "home/page.html" def cache_control(self): diff --git a/testproject/home/tests.py b/testproject/home/tests.py index d263402..57ed45e 100644 --- a/testproject/home/tests.py +++ b/testproject/home/tests.py @@ -7,7 +7,12 @@ from wagtail.core import hooks from wagtail.core.models import PageViewRestriction -from home.models import CachedPage, CacheControlPage, CallableCacheControlPage, WagtailPage +from home.models import ( + CachedPage, + CacheControlPage, + CallableCacheControlPage, + WagtailPage, +) def hook_true(obj, is_cacheable: bool) -> bool: @@ -23,10 +28,11 @@ def hook_any(obj, is_cacheable: bool): class WagtailCacheTest(TestCase): - @classmethod def get_content_type(cls, modelname: str): - ctype, _ = ContentType.objects.get_or_create(model=modelname, app_label="home") + ctype, _ = ContentType.objects.get_or_create( + model=modelname, app_label="home" + ) return ctype @classmethod @@ -62,7 +68,9 @@ def setUpClass(cls): cls.page_wagtailpage.add_child(instance=cls.page_cachedpage) cls.page_wagtailpage.add_child(instance=cls.page_cachedpage_restricted) cls.page_wagtailpage.add_child(instance=cls.page_cachecontrolpage) - cls.page_wagtailpage.add_child(instance=cls.page_callablecachecontrolpage) + cls.page_wagtailpage.add_child( + instance=cls.page_callablecachecontrolpage + ) # Create the view restriction. cls.view_restriction = PageViewRestriction.objects.create( @@ -79,7 +87,7 @@ def setUpClass(cls): cls.skip_cache_pages = [ cls.page_cachedpage_restricted, cls.page_cachecontrolpage, - cls.page_callablecachecontrolpage + cls.page_callablecachecontrolpage, ] @classmethod @@ -127,10 +135,14 @@ def get_miss(self, url: str): """ # HEAD response = self.client.head(url) - self.assertEqual(response.get(self.header_name, None), Status.MISS.value) + self.assertEqual( + response.get(self.header_name, None), Status.MISS.value + ) # GET response = self.client.get(url) - self.assertEqual(response.get(self.header_name, None), Status.MISS.value) + self.assertEqual( + response.get(self.header_name, None), Status.MISS.value + ) return response def get_skip(self, url: str): @@ -140,17 +152,21 @@ def get_skip(self, url: str): """ # HEAD response = self.client.head(url) - self.assertEqual(response.get(self.header_name, None), Status.SKIP.value) + self.assertEqual( + response.get(self.header_name, None), Status.SKIP.value + ) self.assertTrue( - CacheControl.NOCACHE.value in response.get("Cache-Control", "") or - CacheControl.PRIVATE.value in response.get("Cache-Control", "") + CacheControl.NOCACHE.value in response.get("Cache-Control", "") + or CacheControl.PRIVATE.value in response.get("Cache-Control", "") ) # GET response = self.client.get(url) - self.assertEqual(response.get(self.header_name, None), Status.SKIP.value) + self.assertEqual( + response.get(self.header_name, None), Status.SKIP.value + ) self.assertTrue( - CacheControl.NOCACHE.value in response.get("Cache-Control", "") or - CacheControl.PRIVATE.value in response.get("Cache-Control", "") + CacheControl.NOCACHE.value in response.get("Cache-Control", "") + or CacheControl.PRIVATE.value in response.get("Cache-Control", "") ) return response @@ -176,82 +192,108 @@ def test_page_skip(self): def test_page_restricted(self): auth_url = "/_util/authenticate_with_password/%d/%d/" % ( - self.view_restriction.id, self.page_cachedpage_restricted.id + self.view_restriction.id, + self.page_cachedpage_restricted.id, + ) + response = self.client.post( + auth_url, + { + "password": "the cybers", + "return_url": self.page_cachedpage_restricted.get_url(), + }, + ) + self.assertRedirects( + response, self.page_cachedpage_restricted.get_url() ) - response = self.client.post(auth_url, { - "password": "the cybers", - "return_url": self.page_cachedpage_restricted.get_url(), - }) - self.assertRedirects(response, self.page_cachedpage_restricted.get_url()) # First get should skip cache, and also be set to private. response = self.get_skip(self.page_cachedpage_restricted.get_url()) self.assertEqual(response.status_code, 200) - self.assertEqual(response.get("Cache-Control", None), CacheControl.PRIVATE.value) + self.assertEqual( + response.get("Cache-Control", None), CacheControl.PRIVATE.value + ) # Second get should continue to skip and also be set to private. response = self.get_skip(self.page_cachedpage_restricted.get_url()) self.assertEqual(response.status_code, 200) - self.assertEqual(response.get("Cache-Control", None), CacheControl.PRIVATE.value) + self.assertEqual( + response.get("Cache-Control", None), CacheControl.PRIVATE.value + ) def test_page_404(self): # 404s should also be cached. self.get_miss("/gimme-a-404/") self.get_hit("/gimme-a-404/") - @modify_settings(MIDDLEWARE={ - "remove": 'django.contrib.auth.middleware.AuthenticationMiddleware', # noqa - }) + @modify_settings( + MIDDLEWARE={ + "remove": "django.contrib.auth.middleware.AuthenticationMiddleware", # noqa + } + ) def test_page_miss_without_auth(self): self.test_page_miss() - @modify_settings(MIDDLEWARE={ - "remove": 'django.contrib.auth.middleware.AuthenticationMiddleware', # noqa - }) + @modify_settings( + MIDDLEWARE={ + "remove": "django.contrib.auth.middleware.AuthenticationMiddleware", # noqa + } + ) def test_page_hit_without_auth(self): self.test_page_hit() - @modify_settings(MIDDLEWARE={ - "remove": 'django.contrib.auth.middleware.AuthenticationMiddleware', # noqa - }) + @modify_settings( + MIDDLEWARE={ + "remove": "django.contrib.auth.middleware.AuthenticationMiddleware", # noqa + } + ) def test_page_skip_without_auth(self): self.test_page_skip() - @modify_settings(MIDDLEWARE={ - "remove": 'django.contrib.auth.middleware.AuthenticationMiddleware', # noqa - }) + @modify_settings( + MIDDLEWARE={ + "remove": "django.contrib.auth.middleware.AuthenticationMiddleware", # noqa + } + ) def test_page_restricted_without_auth(self): self.test_page_restricted() - @modify_settings(MIDDLEWARE={ - "remove": 'django.contrib.auth.middleware.AuthenticationMiddleware', # noqa - }) + @modify_settings( + MIDDLEWARE={ + "remove": "django.contrib.auth.middleware.AuthenticationMiddleware", # noqa + } + ) def test_page_404_without_auth(self): self.test_page_404() # ---- TEST VIEWS ---------------------------------------------------------- # Views use the decorators and should work without the middleware. - @modify_settings(MIDDLEWARE={ - "remove": 'wagtailcache.cache.UpdateCacheMiddleware', # noqa - "remove": 'wagtailcache.cache.FetchFromCacheMiddleware', # noqa - }) + @modify_settings( + MIDDLEWARE={ + "remove": "wagtailcache.cache.UpdateCacheMiddleware", # noqa + "remove": "wagtailcache.cache.FetchFromCacheMiddleware", # noqa + } + ) def test_view_miss(self): # First get should miss cache. self.get_miss(reverse("cached_view")) - @modify_settings(MIDDLEWARE={ - "remove": 'wagtailcache.cache.UpdateCacheMiddleware', # noqa - "remove": 'wagtailcache.cache.FetchFromCacheMiddleware', # noqa - }) + @modify_settings( + MIDDLEWARE={ + "remove": "wagtailcache.cache.UpdateCacheMiddleware", # noqa + "remove": "wagtailcache.cache.FetchFromCacheMiddleware", # noqa + } + ) def test_view_hit(self): # First get should miss cache. self.get_miss(reverse("cached_view")) # Second get should hit cache. self.get_hit(reverse("cached_view")) - @modify_settings(MIDDLEWARE={ - "remove": 'wagtailcache.cache.UpdateCacheMiddleware', # noqa - "remove": 'wagtailcache.cache.FetchFromCacheMiddleware', # noqa - }) + @modify_settings( + MIDDLEWARE={ + "remove": "wagtailcache.cache.UpdateCacheMiddleware", # noqa + "remove": "wagtailcache.cache.FetchFromCacheMiddleware", # noqa + } + ) def test_view_skip(self): # First get should skip cache. self.get_skip(reverse("nocached_view")) @@ -309,29 +351,41 @@ def test_zero_timeout(self): def test_request_hook_true(self): # A POST should never be cached. response = self.client.post(reverse("cached_view")) - self.assertEqual(response.get(self.header_name, None), Status.SKIP.value) + self.assertEqual( + response.get(self.header_name, None), Status.SKIP.value + ) response = self.client.post(reverse("cached_view")) - self.assertEqual(response.get(self.header_name, None), Status.SKIP.value) + self.assertEqual( + response.get(self.header_name, None), Status.SKIP.value + ) + # Register hook and assert it was actually registered. hooks.register("is_request_cacheable", hook_true) hook_fns = hooks.get_hooks("is_request_cacheable") self.assertEqual(hook_fns, [hook_true]) - # Setting `is_request_cacheale=True` does not really do much, because the - # response still has the final say in whether or not the response is cached. - # The no-cache page will still not be cached due to the response. - # However a simple POST request will now be checked against the cache, - # although once again, it will probably not get cached due to the response. + + # Setting `is_request_cacheale=True` does not really do much, because + # the response still has the final say in whether or not the response is + # cached. The no-cache page will still not be cached due to the + # response. However a simple POST request will now be checked against + # the cache, although once again, it will probably not get cached due to + # the response. response = self.client.post(reverse("cached_view")) - self.assertEqual(response.get(self.header_name, None), Status.MISS.value) + self.assertEqual( + response.get(self.header_name, None), Status.MISS.value + ) response = self.client.post(reverse("cached_view")) - self.assertEqual(response.get(self.header_name, None), Status.MISS.value) + self.assertEqual( + response.get(self.header_name, None), Status.MISS.value + ) def test_request_hook_false(self): # Register hook and assert it was actually registered. hooks.register("is_request_cacheable", hook_false) hook_fns = hooks.get_hooks("is_request_cacheable") self.assertEqual(hook_fns, [hook_false]) - # The cached page should be force skipped due to the hook returning false. + # The cached page should be force skipped due to the hook returning + # false. self.get_skip(self.page_cachedpage.get_url()) self.get_skip(self.page_cachedpage.get_url()) @@ -348,7 +402,8 @@ def test_response_hook_true(self): hooks.register("is_response_cacheable", hook_true) hook_fns = hooks.get_hooks("is_response_cacheable") self.assertEqual(hook_fns, [hook_true]) - # The no-cache page should be force cached due to the hook returning true. + # The no-cache page should be force cached due to the hook returning + # true. self.get_miss(self.page_cachecontrolpage.get_url()) self.get_hit(self.page_cachecontrolpage.get_url()) @@ -357,7 +412,8 @@ def test_response_hook_false(self): hooks.register("is_response_cacheable", hook_false) hook_fns = hooks.get_hooks("is_response_cacheable") self.assertEqual(hook_fns, [hook_false]) - # The cached page should be force skipped due to the hook returning false. + # The cached page should be force skipped due to the hook returning + # false. self.get_skip(self.page_cachedpage.get_url()) self.get_skip(self.page_cachedpage.get_url()) diff --git a/testproject/testproject/settings.py b/testproject/testproject/settings.py index 418a925..7d9f068 100644 --- a/testproject/testproject/settings.py +++ b/testproject/testproject/settings.py @@ -23,78 +23,72 @@ # Application definition INSTALLED_APPS = [ - 'home', - 'wagtailcache', - - 'wagtail.contrib.forms', - 'wagtail.contrib.redirects', - 'wagtail.embeds', - 'wagtail.sites', - 'wagtail.users', - 'wagtail.snippets', - 'wagtail.documents', - 'wagtail.images', - 'wagtail.search', - 'wagtail.admin', - 'wagtail.core', - - 'modelcluster', - 'taggit', - - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "home", + "wagtailcache", + "wagtail.contrib.forms", + "wagtail.contrib.redirects", + "wagtail.embeds", + "wagtail.sites", + "wagtail.users", + "wagtail.snippets", + "wagtail.documents", + "wagtail.images", + "wagtail.search", + "wagtail.admin", + "wagtail.core", + "modelcluster", + "taggit", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] MIDDLEWARE = [ - 'wagtailcache.cache.UpdateCacheMiddleware', - - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', - - 'wagtail.contrib.redirects.middleware.RedirectMiddleware', - - 'wagtailcache.cache.FetchFromCacheMiddleware', + "wagtailcache.cache.UpdateCacheMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.security.SecurityMiddleware", + "wagtail.contrib.redirects.middleware.RedirectMiddleware", + "wagtailcache.cache.FetchFromCacheMiddleware", ] -ROOT_URLCONF = 'testproject.urls' +ROOT_URLCONF = "testproject.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - os.path.join(BASE_DIR, 'templates'), + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + os.path.join(BASE_DIR, "templates"), ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'testproject.wsgi.application' +WSGI_APPLICATION = "testproject.wsgi.application" # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -102,9 +96,9 @@ # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -117,17 +111,17 @@ # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ] -STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" -STATIC_ROOT = os.path.join(BASE_DIR, 'static') -STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATIC_URL = "/static/" -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_URL = "/media/" # Wagtail settings @@ -136,28 +130,28 @@ # Base URL to use when referring to full URLs within the Wagtail admin backend - # e.g. in notification emails. Don't include '/admin' or a trailing slash -BASE_URL = 'http://example.com' +BASE_URL = "http://example.com" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '4n5y9*ny(o(i5e^f!9=5yc8ie(z&0#^=@3*4pohw$8iv!tkcmr' +SECRET_KEY = "4n5y9*ny(o(i5e^f!9=5yc8ie(z&0#^=@3*4pohw$8iv!tkcmr" # SECURITY WARNING: define the correct hosts in production! -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" WAGTAIL_CACHE_HEADER = "X-Test-Header" CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'TIMEOUT': 90061, # 1 day, 1 hour, 1 minute, 1 second. + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "TIMEOUT": 90061, # 1 day, 1 hour, 1 minute, 1 second. + }, + "zero": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "TIMEOUT": 0, }, - 'zero': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'TIMEOUT': 0, - } } diff --git a/testproject/testproject/urls.py b/testproject/testproject/urls.py index 450b678..5661b75 100644 --- a/testproject/testproject/urls.py +++ b/testproject/testproject/urls.py @@ -9,18 +9,15 @@ from home import views urlpatterns = [ - url(r'^django-admin/', admin.site.urls), - - url(r'^admin/', include(wagtailadmin_urls)), - url(r'^documents/', include(wagtaildocs_urls)), - - url(r'^views/cached-view/', views.cached_view, name="cached_view"), - url(r'^views/nocache-view/', views.nocached_view, name="nocached_view"), - + url(r"^django-admin/", admin.site.urls), + url(r"^admin/", include(wagtailadmin_urls)), + url(r"^documents/", include(wagtaildocs_urls)), + url(r"^views/cached-view/", views.cached_view, name="cached_view"), + url(r"^views/nocache-view/", views.nocached_view, name="nocached_view"), # For anything not caught by a more specific rule above, hand over to # Wagtail's page serving mechanism. This should be the last pattern in # the list: - url(r'', include(wagtail_urls)), + url(r"", include(wagtail_urls)), ] diff --git a/wagtailcache/apps.py b/wagtailcache/apps.py index fab7fcd..0f3e66d 100644 --- a/wagtailcache/apps.py +++ b/wagtailcache/apps.py @@ -3,6 +3,6 @@ class WagtailCacheAppConfig(AppConfig): - name = 'wagtailcache' - label = 'wagtailcache' + name = "wagtailcache" + label = "wagtailcache" verbose_name = _("Wagtail Cache") diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index 97b66cf..68dbed4 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -11,7 +11,10 @@ from django.http.response import HttpResponse from django.template.response import SimpleTemplateResponse from django.utils.cache import ( - get_cache_key, get_max_age, has_vary_header, learn_cache_key, + get_cache_key, + get_max_age, + has_vary_header, + learn_cache_key, patch_response_headers, ) from django.utils.deprecation import MiddlewareMixin @@ -22,8 +25,9 @@ class CacheControl(Enum): """ - "Cache-Control" header values. + ``Cache-Control`` header values. """ + NOCACHE = "no-cache" PRIVATE = "private" @@ -32,6 +36,7 @@ class Status(Enum): """ WAGTAIL_CACHE_HEADER header values. """ + HIT = "hit" MISS = "miss" SKIP = "skip" @@ -49,7 +54,7 @@ def _patch_header(response: HttpResponse, status: Status) -> None: class FetchFromCacheMiddleware(MiddlewareMixin): """ Loads a request from the cache if it exists. - Mostly stolen from `django.middleware.cache.FetchFromCacheMiddleware`. + Mostly stolen from ``django.middleware.cache.FetchFromCacheMiddleware``. """ def __init__(self, get_response=None): @@ -64,15 +69,16 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: # Only cache GET and HEAD requests. # Don't cache requests that are previews. # Don't cache requests that have a logged in user. - # NOTE: Wagtail manually adds `is_preview` to the request object. This - # not normally part of a request object. - is_cacheable = \ - request.method in ('GET', 'HEAD') and \ - not getattr(request, 'is_preview', False) and \ - not (hasattr(request, 'user') and request.user.is_authenticated) + # NOTE: Wagtail manually adds `is_preview` to the request object. + # This is not normally part of a request object. + is_cacheable = ( + request.method in ("GET", "HEAD") + and not getattr(request, "is_preview", False) + and not (hasattr(request, "user") and request.user.is_authenticated) + ) # Allow the user to override our caching decision. - for fn in hooks.get_hooks('is_request_cacheable'): + for fn in hooks.get_hooks("is_request_cacheable"): result = fn(request, is_cacheable) if isinstance(result, bool): is_cacheable = result @@ -82,24 +88,28 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: setattr(request, "_wagtailcache_skip", True) return None # Don't bother checking the cache. - # try and get the cached GET response - cache_key = get_cache_key(request, None, 'GET', cache=self._wagcache) + # Try and get the cached GET response. + cache_key = get_cache_key(request, None, "GET", cache=self._wagcache) if cache_key is None: setattr(request, "_wagtailcache_update", True) return None # No cache information available, need to rebuild. + response = self._wagcache.get(cache_key) - # if it wasn't found and we are looking for a HEAD, try looking just for that - if response is None and request.method == 'HEAD': - cache_key = get_cache_key(request, None, 'HEAD', cache=self._wagcache) + + # If it wasn't found and a HEAD was requested, try looking for that. + if response is None and request.method == "HEAD": + cache_key = get_cache_key( + request, None, "HEAD", cache=self._wagcache + ) response = self._wagcache.get(cache_key) if response is None: setattr(request, "_wagtailcache_update", True) return None # No cache information available, need to rebuild. - # hit, return cached response + # Hit. Return cached response. setattr(request, "_wagtailcache_update", False) - # Add a response header to indicate this was a cache hit + # Add a response header to indicate this was a cache hit. _patch_header(response, Status.HIT) return response @@ -107,14 +117,16 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: class UpdateCacheMiddleware(MiddlewareMixin): """ Saves a response to the cache. - Mostly stolen from `django.middleware.cache.UpdateCacheMiddleware`. + Mostly stolen from ``django.middleware.cache.UpdateCacheMiddleware``. """ def __init__(self, get_response=None): self._wagcache = caches[wagtailcache_settings.WAGTAIL_CACHE_BACKEND] self.get_response = get_response - def process_response(self, request: WSGIRequest, response: HttpResponse) -> HttpResponse: + def process_response( + self, request: WSGIRequest, response: HttpResponse + ) -> HttpResponse: if not wagtailcache_settings.WAGTAIL_CACHE: return response @@ -132,38 +144,45 @@ def process_response(self, request: WSGIRequest, response: HttpResponse) -> Http # Do cache 200, 301, 302, 304, and 404 codes so that wagtail doesn't # have to repeatedly look up these URLs in the database. # Don't cache streaming responses. - is_cacheable = \ - CacheControl.NOCACHE.value not in response.get("Cache-Control", "") and \ - CacheControl.PRIVATE.value not in response.get("Cache-Control", "") and \ - response.status_code in (200, 301, 302, 304, 404) and \ - not response.streaming - # Don't cache 200 responses that set a user-specific cookie in response to - # a cookie-less request (e.g. CSRF tokens). + is_cacheable = ( + CacheControl.NOCACHE.value not in response.get("Cache-Control", "") + and CacheControl.PRIVATE.value + not in response.get("Cache-Control", "") + and response.status_code in (200, 301, 302, 304, 404) + and not response.streaming + ) + # Don't cache 200 responses that set a user-specific cookie in response + # to a cookie-less request (e.g. CSRF tokens). if is_cacheable and response.status_code == 200: is_cacheable = not ( - not request.COOKIES and response.cookies and has_vary_header(response, 'Cookie') + not request.COOKIES + and response.cookies + and has_vary_header(response, "Cookie") ) # Allow the user to override our caching decision. - for fn in hooks.get_hooks('is_response_cacheable'): + for fn in hooks.get_hooks("is_response_cacheable"): result = fn(response, is_cacheable) if isinstance(result, bool): is_cacheable = result # If we are not allowed to cache the response, just return. if not is_cacheable: - # Add a response header to indicate this was intentionally not cached. + # Add response header to indicate this was intentionally not cached. _patch_header(response, Status.SKIP) return response - # Try to get the timeout from the "max-age" section of the "Cache- - # Control" header before reverting to using the cache's default. + # Try to get the timeout from the ``max-age`` section of the + # ``Cache-Control`` header before reverting to using the cache's + # default. timeout = get_max_age(response) if timeout is None: timeout = self._wagcache.default_timeout patch_response_headers(response, timeout) if timeout: - cache_key = learn_cache_key(request, response, timeout, None, cache=self._wagcache) + cache_key = learn_cache_key( + request, response, timeout, None, cache=self._wagcache + ) if isinstance(response, SimpleTemplateResponse): response.add_post_render_callback( lambda r: self._wagcache.set(cache_key, r, timeout) @@ -187,10 +206,14 @@ def clear_cache() -> None: def cache_page(view_func: Callable[..., HttpResponse]): """ - Decorator that determines whether or not to cache a page or serve a cached page. + Decorator that determines whether or not to cache a page or serve a cached + page. """ + @wraps(view_func) - def _wrapped_view_func(request: WSGIRequest, *args, **kwargs) -> HttpResponse: + def _wrapped_view_func( + request: WSGIRequest, *args, **kwargs + ) -> HttpResponse: # Try to fetch an already cached page from wagtail-cache. response = FetchFromCacheMiddleware().process_request(request) if response: @@ -208,8 +231,11 @@ def nocache_page(view_func: Callable[..., HttpResponse]): """ Decorator that sets no-cache on all responses. """ + @wraps(view_func) - def _wrapped_view_func(request: WSGIRequest, *args, **kwargs) -> HttpResponse: + def _wrapped_view_func( + request: WSGIRequest, *args, **kwargs + ) -> HttpResponse: # Run the view. response = view_func(request, *args, **kwargs) # Set cache-control if wagtail-cache is enabled. @@ -229,14 +255,16 @@ def serve_password_required_response(self, request, form, action_url): """ Add a cache-control header if the page requires a password. """ - response = super().serve_password_required_response(request, form, action_url) + response = super().serve_password_required_response( + request, form, action_url + ) response["Cache-Control"] = CacheControl.PRIVATE.value return response def serve(self, request, *args, **kwargs): """ - Add a custom cache-control header, or set to private if the page is being served - behind a view restriction. + Add a custom cache-control header, or set to private if the page is + being served behind a view restriction. """ response = super().serve(request, *args, **kwargs) if self.get_view_restrictions(): diff --git a/wagtailcache/compat_backends/django_redis.py b/wagtailcache/compat_backends/django_redis.py index 28688b9..85f51f0 100644 --- a/wagtailcache/compat_backends/django_redis.py +++ b/wagtailcache/compat_backends/django_redis.py @@ -6,6 +6,7 @@ class RedisCache(BaseBackend): Extends django_redis.cache.RedisCache for compatibility with the Django cache middleware. """ + @omit_exception def set(self, *args, **kwargs): """ diff --git a/wagtailcache/icon.py b/wagtailcache/icon.py index 5cac402..f103f81 100644 --- a/wagtailcache/icon.py +++ b/wagtailcache/icon.py @@ -4,7 +4,7 @@ from django.apps import apps -if apps.is_installed('wagtailfontawesome'): - CACHE_ICON = 'fa-bolt' +if apps.is_installed("wagtailfontawesome"): + CACHE_ICON = "fa-bolt" else: - CACHE_ICON = 'cog' + CACHE_ICON = "cog" diff --git a/wagtailcache/management/commands/clear_wagtail_cache.py b/wagtailcache/management/commands/clear_wagtail_cache.py index 2f6d2c0..160a30c 100644 --- a/wagtailcache/management/commands/clear_wagtail_cache.py +++ b/wagtailcache/management/commands/clear_wagtail_cache.py @@ -5,7 +5,7 @@ class Command(BaseCommand): - help = 'Clears the cache for the entire site.' + help = "Clears the cache for the entire site." def handle(self, *args, **options): clear_cache() diff --git a/wagtailcache/templatetags/wagtailcache_tags.py b/wagtailcache/templatetags/wagtailcache_tags.py index 9b340cb..d045f02 100644 --- a/wagtailcache/templatetags/wagtailcache_tags.py +++ b/wagtailcache/templatetags/wagtailcache_tags.py @@ -13,29 +13,42 @@ def seconds_to_readable(seconds: int) -> str: Converts int seconds to a human readable string. """ if seconds <= 0: - return '{0} {1}'.format(str(seconds), _('seconds')) + return "{0} {1}".format(str(seconds), _("seconds")) mins, secs = divmod(seconds, 60) hrs, mins = divmod(mins, 60) days, hrs = divmod(hrs, 24) - pretty_time = '' + pretty_time = "" if days > 0: - pretty_time += ' {0} {1}'.format(str(days), _('days') if days > 1 else _('day')) + label = _("days") if days > 1 else _("day") + pretty_time += " {0} {1}".format(str(days), label) if hrs > 0: - pretty_time += ' {0} {1}'.format(str(hrs), _('hours') if hrs > 1 else _('hour')) + label = _("hours") if hrs > 1 else _("hour") + pretty_time += " {0} {1}".format(str(hrs), label) if mins > 0: - pretty_time += ' {0} {1}'.format(str(mins), _('minutes') if mins > 1 else _('minute')) + label = _("minutes") if mins > 1 else _("minute") + pretty_time += " {0} {1}".format(str(mins), label) if secs > 0: - pretty_time += ' {0} {1}'.format(str(secs), _('seconds') if secs > 1 else _('second')) + label = _("seconds") if secs > 1 else _("second") + pretty_time += " {0} {1}".format(str(secs), label) + return pretty_time @register.filter def get_wagtailcache_setting(value: str) -> Optional[object]: + """ + Returns a wagtailcache Django setting, or default. + """ return getattr(wagtailcache_settings, value, None) @register.simple_tag def cache_timeout() -> str: - timeout = caches[wagtailcache_settings.WAGTAIL_CACHE_BACKEND].default_timeout + """ + Returns the wagtailcache timeout in human readable format. + """ + timeout = caches[ + wagtailcache_settings.WAGTAIL_CACHE_BACKEND + ].default_timeout return seconds_to_readable(timeout) diff --git a/wagtailcache/urls.py b/wagtailcache/urls.py index 1ee7aea..890b74d 100644 --- a/wagtailcache/urls.py +++ b/wagtailcache/urls.py @@ -7,6 +7,6 @@ urlpatterns = [ - path('', index, name="index"), - path('clearcache', clear, name="clearcache"), + path("", index, name="index"), + path("clearcache", clear, name="clearcache"), ] diff --git a/wagtailcache/views.py b/wagtailcache/views.py index b322c9a..fab1b1e 100644 --- a/wagtailcache/views.py +++ b/wagtailcache/views.py @@ -14,7 +14,9 @@ def index(request): """ The wagtail-cache admin panel. """ - return render(request, 'wagtailcache/index.html', {'cache_icon': CACHE_ICON}) + return render( + request, "wagtailcache/index.html", {"cache_icon": CACHE_ICON} + ) def clear(request): diff --git a/wagtailcache/wagtail_hooks.py b/wagtailcache/wagtail_hooks.py index 3aaf46e..8efeb80 100644 --- a/wagtailcache/wagtail_hooks.py +++ b/wagtailcache/wagtail_hooks.py @@ -20,22 +20,26 @@ def is_shown(self, request): return request.user.is_superuser -@hooks.register('register_admin_urls') +@hooks.register("register_admin_urls") def register_admin_urls(): """ Registers wagtail-cache urls in the wagtail admin. """ return [ - path('cache/', include((urls, 'wagtailcache'), namespace='wagtailcache_admin')), + path( + "cache/", + include((urls, "wagtailcache"), namespace="wagtailcache_admin"), + ), ] -@hooks.register('register_settings_menu_item') +@hooks.register("register_settings_menu_item") def register_cache_menu(): """ Registers wagtail-cache settings panel in the wagtail admin. """ return CacheMenuItem( - _('Cache'), - reverse('wagtailcache_admin:index'), - classnames='icon icon-' + CACHE_ICON) + _("Cache"), + reverse("wagtailcache_admin:index"), + classnames="icon icon-" + CACHE_ICON, + )