Skip to content

Commit

Permalink
Support django 4.1 (#105)
Browse files Browse the repository at this point in the history
* Adapt url -> re_path + import path

* smart_text -> smart_str

* Use quote/unquote from urllib

* Wrap method decorators

* is_ajax() method on request no longer exists

* Add 4.0 and 4.1 to tox.ini

* Add python 3.10 as supported version

* Use 3.10 as runtime in CI for lint/tests/coverage
  • Loading branch information
HeyHugo committed Sep 26, 2022
1 parent 4defec5 commit 857fe5f
Show file tree
Hide file tree
Showing 15 changed files with 68 additions and 61 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: "3.10"
- uses: pre-commit/action@v2.0.3

tests:
Expand All @@ -29,6 +29,7 @@ jobs:
- "3.7"
- "3.8"
- "3.9"
- "3.10"

steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -57,7 +58,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: "3.10"
- uses: actions/download-artifact@v3
with:
name: coverage-files
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
default_language_version:
python: python3.9
python: python3.10
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.6
FROM python:3.10

# Install system dependencies
RUN apt-get update && apt-get install -y gettext && \
Expand Down
20 changes: 11 additions & 9 deletions djedi/admin/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from collections import defaultdict
from urllib.parse import unquote

from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.template.response import TemplateResponse
from django.utils.http import urlunquote
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
Expand All @@ -21,7 +22,7 @@


class APIView(View):
@csrf_exempt
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
if not auth.has_permission(request):
raise PermissionDenied
Expand Down Expand Up @@ -64,7 +65,7 @@ def get_post_data(self, request):
return data["data"], data["meta"]

def decode_uri(self, uri):
decoded = urlunquote(uri)
decoded = unquote(uri)

# If uri got decoded then recursive try more times until nothing more can be decoded
if decoded != uri:
Expand All @@ -77,7 +78,7 @@ def render_to_response(self, content=""):


class NodeApi(JSONResponseMixin, APIView):
@never_cache
@method_decorator(never_cache)
def get(self, request, uri):
"""
Return published node or specified version.
Expand Down Expand Up @@ -152,7 +153,7 @@ def get(self, request, uri):


class LoadApi(JSONResponseMixin, APIView):
@never_cache
@method_decorator(never_cache)
def get(self, request, uri):
"""
Load raw node source from storage.
Expand Down Expand Up @@ -182,8 +183,8 @@ def post(self, request, ext):


class NodeEditor(JSONResponseMixin, DjediContextMixin, APIView):
@never_cache
@xframe_options_exempt
@method_decorator(never_cache)
@method_decorator(xframe_options_exempt)
def get(self, request, uri):
try:
uri = self.decode_uri(uri)
Expand All @@ -199,7 +200,7 @@ def get(self, request, uri):
else:
return self.render_plugin(request, plugin_context)

@never_cache
@method_decorator(never_cache)
def post(self, request, uri):
uri = self.decode_uri(uri)
data, meta = self.get_post_data(request)
Expand All @@ -209,7 +210,8 @@ def post(self, request, uri):
context = cio.load(node.uri)
context["content"] = node.content

if request.is_ajax():
# is_ajax call?
if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest":
return self.render_to_json(context)
else:
return self.render_plugin(request, context)
Expand Down
6 changes: 3 additions & 3 deletions djedi/admin/cms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.conf.urls import include, url
from django.contrib.admin import ModelAdmin
from django.core.exceptions import PermissionDenied
from django.shortcuts import render
from django.urls import include, re_path
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import View

Expand All @@ -16,8 +16,8 @@ class Admin(ModelAdmin):

def get_urls(self):
return [
url(r"^", include("djedi.admin.urls", namespace="djedi")),
url(
re_path(r"^", include("djedi.admin.urls", namespace="djedi")),
re_path(
r"", lambda: None, name="djedi_cms_changelist"
), # Placeholder to show change link to CMS in admin
]
Expand Down
20 changes: 11 additions & 9 deletions djedi/admin/urls.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from django.conf.urls import include, url
from django.urls import include, re_path

from .api import LoadApi, NodeApi, NodeEditor, PublishApi, RenderApi, RevisionsApi
from .cms import DjediCMS

app_name = "djedi"

urlpatterns = [
url(r"^$", DjediCMS.as_view(), name="cms"),
url(r"^node/(?P<uri>.+)/editor$", NodeEditor.as_view(), name="cms.editor"),
url(r"^node/(?P<uri>.+)/load$", LoadApi.as_view(), name="api.load"),
url(r"^node/(?P<uri>.+)/publish$", PublishApi.as_view(), name="api.publish"),
url(r"^node/(?P<uri>.+)/revisions$", RevisionsApi.as_view(), name="api.revisions"),
url(r"^node/(?P<uri>.+)$", NodeApi.as_view(), name="api"),
url(r"^plugin/(?P<ext>\w+)$", RenderApi.as_view(), name="api.render"),
url(r"^api/", include("djedi.rest.urls", namespace="rest")),
re_path(r"^$", DjediCMS.as_view(), name="cms"),
re_path(r"^node/(?P<uri>.+)/editor$", NodeEditor.as_view(), name="cms.editor"),
re_path(r"^node/(?P<uri>.+)/load$", LoadApi.as_view(), name="api.load"),
re_path(r"^node/(?P<uri>.+)/publish$", PublishApi.as_view(), name="api.publish"),
re_path(
r"^node/(?P<uri>.+)/revisions$", RevisionsApi.as_view(), name="api.revisions"
),
re_path(r"^node/(?P<uri>.+)$", NodeApi.as_view(), name="api"),
re_path(r"^plugin/(?P<ext>\w+)$", RenderApi.as_view(), name="api.render"),
re_path(r"^api/", include("djedi.rest.urls", namespace="rest")),
]
4 changes: 2 additions & 2 deletions djedi/backends/django/cache/backend.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.core.cache import InvalidCacheBackendError, caches
from django.core.cache.backends.locmem import LocMemCache
from django.utils.encoding import smart_bytes, smart_text
from django.utils.encoding import smart_bytes, smart_str

from cio.backends.base import CacheBackend

Expand Down Expand Up @@ -55,7 +55,7 @@ def _decode_content(self, content):
"""
Split node string to uri and content and convert back to unicode.
"""
content = smart_text(content)
content = smart_str(content)
uri, _, content = content.partition("|")
if content == self.NONE:
content = None
Expand Down
3 changes: 2 additions & 1 deletion djedi/rest/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import simplejson as json
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
Expand Down Expand Up @@ -36,7 +37,7 @@ class NodesApi(APIView):
{uri: content, uri: content, ...}
"""

@never_cache
@method_decorator(never_cache)
def post(self, request):
# Disable caching gets in CachePipe, defaults through this api is not trusted
cio.conf.settings.configure(local=True, CACHE={"PIPE": {"CACHE_ON_GET": False}})
Expand Down
8 changes: 4 additions & 4 deletions djedi/rest/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.conf.urls import url
from django.http import Http404
from django.urls import re_path

from .api import EmbedApi, NodesApi

Expand All @@ -11,7 +11,7 @@ def not_found(*args, **kwargs):


urlpatterns = [
url(r"^$", not_found, name="api-base"),
url(r"^embed/$", EmbedApi.as_view(), name="embed"),
url(r"^nodes/$", NodesApi.as_view(), name="nodes"),
re_path(r"^$", not_found, name="api-base"),
re_path(r"^embed/$", EmbedApi.as_view(), name="embed"),
re_path(r"^nodes/$", NodesApi.as_view(), name="nodes"),
]
20 changes: 10 additions & 10 deletions djedi/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from unittest import skip

from django.urls import reverse
from django.utils.encoding import smart_text
from django.utils.encoding import smart_str

import cio.conf
from djedi.tests.base import ClientTest
Expand All @@ -11,10 +11,10 @@ class PanelTest(ClientTest):
def test_embed(self):
url = reverse("index")
response = self.client.get(url)
self.assertIn("Djedi Test", smart_text(response.content))
self.assertIn("window.DJEDI_NODES", smart_text(response.content))
self.assertIn("i18n://sv-se@foo/bar.txt", smart_text(response.content))
self.assertIn("</body>", smart_text(response.content).lower())
self.assertIn("Djedi Test", smart_str(response.content))
self.assertIn("window.DJEDI_NODES", smart_str(response.content))
self.assertIn("i18n://sv-se@foo/bar.txt", smart_str(response.content))
self.assertIn("</body>", smart_str(response.content).lower())

def test_middleware(self):
with self.settings(
Expand All @@ -27,14 +27,14 @@ def test_middleware(self):
):
url = reverse("index")
response = self.client.get(url)
self.assertNotIn("window.DJEDI_NODES", smart_text(response.content))
self.assertNotIn("window.DJEDI_NODES", smart_str(response.content))

def test_cms(self):
url = reverse("admin:djedi:cms")
response = self.client.get(url)
self.assertIn("<title>djedi cms</title>", smart_text(response.content))
self.assertNotIn("document.domain", smart_text(response.content))
self.assertNotIn("None", smart_text(response.content))
self.assertIn("<title>djedi cms</title>", smart_str(response.content))
self.assertNotIn("document.domain", smart_str(response.content))
self.assertNotIn("None", smart_str(response.content))

with cio.conf.settings(XSS_DOMAIN="foobar.se"):
response = self.client.get(url)
Expand All @@ -51,7 +51,7 @@ def test_django_admin(self): # pragma: no cover
url = reverse("admin:index")
response = self.client.get(url)
cms_url = reverse("admin:djedi:cms")
self.assertIn('<a href="%s">CMS</a>' % cms_url, smart_text(response.content))
self.assertIn('<a href="%s">CMS</a>' % cms_url, smart_str(response.content))

# Rollback patch
AdminLogNode.render = _render
16 changes: 7 additions & 9 deletions djedi/tests/test_rest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import os
from urllib.parse import quote

import simplejson as json
from django.core.files import File
from django.test import Client
from django.urls import reverse
from django.utils.encoding import smart_text
from django.utils.http import urlquote
from django.utils.encoding import smart_str

import cio
import cio.conf
Expand Down Expand Up @@ -51,9 +51,7 @@ def test_permissions(self):

class PrivateRestTest(ClientTest):
def get_api_url(self, url_name, uri):
return reverse(
"admin:djedi:" + url_name, args=[urlquote(urlquote(uri, ""), "")]
)
return reverse("admin:djedi:" + url_name, args=[quote(quote(uri, ""), "")])

def get(self, url_name, uri):
url = self.get_api_url(url_name, uri)
Expand Down Expand Up @@ -171,7 +169,7 @@ def test_delete(self):

response = self.delete("api", node.uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(smart_text(response.content), "")
self.assertEqual(smart_str(response.content), "")

with self.assertRaises(NodeDoesNotExist):
storage.get("i18n://sv-se@page/title")
Expand Down Expand Up @@ -220,7 +218,7 @@ def test_render(self):

response = self.post("api.render", "md", {"data": "# Djedi"})
assert response.status_code == 200
self.assertRenderedMarkdown(smart_text(response.content), "# Djedi")
self.assertRenderedMarkdown(smart_str(response.content), "# Djedi")

response = self.post(
"api.render",
Expand All @@ -234,7 +232,7 @@ def test_render(self):

self.assertEqual(response.status_code, 200)
self.assertEqual(
smart_text(response.content),
smart_str(response.content),
'<img alt="" height="64" src="/foo/bar.png" width="64" />',
)

Expand Down Expand Up @@ -345,7 +343,7 @@ def test_api_root_not_found(self):
def test_embed(self):
url = reverse("admin:djedi:rest:embed")
response = self.client.get(url)
html = smart_text(response.content)
html = smart_str(response.content)

self.assertIn('iframe id="djedi-cms"', html)
cms_url = "http://testserver" + reverse("admin:djedi:cms")
Expand Down
8 changes: 4 additions & 4 deletions djedi/tests/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from django.conf.urls import include, url
from django.contrib import admin
from django.shortcuts import render
from django.urls import include, re_path

admin.autodiscover()

urlpatterns = [
url(r"^$", lambda r: render(r, "index.html"), name="index"),
url(r"^adm1n/", admin.site.urls),
url(r"^djed1/", include("djedi.urls", namespace="admin")),
re_path(r"^$", lambda r: render(r, "index.html"), name="index"),
re_path(r"^adm1n/", admin.site.urls),
re_path(r"^djed1/", include("djedi.urls", namespace="admin")),
]
3 changes: 1 addition & 2 deletions djedi/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.conf.urls import include
from django.urls import path
from django.urls import include, path

app_name = "djedi"

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules",
],
Expand Down
11 changes: 7 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
[tox]
envlist = py36-django{ 22, 30, 31, 32 },
py37-django{ 22, 30, 31, 32 },
py38-django{ 22, 30, 31, 32 },
py39-django{ 22, 30, 31, 32 }
py38-django{ 22, 30, 31, 32, 40, 41 },
py39-django{ 22, 30, 31, 32, 40, 41 },
py310-django{ 22, 30, 31, 32, 40, 41 }


[testenv]
Expand All @@ -24,6 +25,8 @@ deps = six
django30: Django>=3.0,<3.1
django31: Django>=3.1,<3.2
django32: Django>=3.2,<3.3
django40: Django>=4.0,<4.1
django41: Django>=4.1,<4.2

[testenv:lcov]
passenv = COVERAGE_FILE
Expand All @@ -34,13 +37,13 @@ deps = coverage

[testenv:coverage]
skip_install = true
basepython = python3.9
basepython = python3.10
passenv = COVERAGE_FILE
commands = make coverage
deps = coverage

[testenv:lint]
skip_install = true
basepython = python3.9
basepython = python3.10
commands = make lint
deps = flake8

0 comments on commit 857fe5f

Please sign in to comment.