Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Django5 and Python 3.11,3.12 compatibility #34

Merged
merged 4 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ jobs:
strategy:
fail-fast: false
matrix:
django-version: [ "3.2", "4.2"]
python-version: [ "3.9", "3.11" ]
django-version: [ "3.2", "4.2", "5.0"]
python-version: [ "3.9", "3.10", "3.11", "3.12" ]
exclude:
- django-version: 5.0
python-version: 3.9
env:
DATABASE_URL: postgres://postgres:[email protected]:5432/adminfilters
PY_VER: ${{ matrix.python-version}}
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ def read(*parts):
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0',
'Framework :: Django :: 5.0',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Intended Audience :: Developers',
],
long_description=codecs.open('README.md', 'r').read(),
Expand Down
3 changes: 2 additions & 1 deletion src/adminfilters/autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ def __init__(self, field, request, params, model, model_admin, field_path):
self.dependants = []
self.lookup_kwarg = "%s__exact" % field_path
self.lookup_kwarg_isnull = "%s__isnull" % field_path
self.lookup_val = params.get(self.lookup_kwarg)
self._params = params
self.request = request
super().__init__(field, request, params, model, model_admin, field_path)
self.lookup_val = self.get_parameters(self.lookup_kwarg)
self.admin_site = model_admin.admin_site
self.query_string = ""
self.target_field = get_real_field(model, field_path)
Expand Down
3 changes: 3 additions & 0 deletions src/adminfilters/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django import __version__ as django_version

DJANGO_MAJOR = int(django_version.split('.')[0])
5 changes: 3 additions & 2 deletions src/adminfilters/depot/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class DepotManager(WrappperMixin, ListFilter):

def __init__(self, request, params, model, model_admin):
self.model_admin = model_admin
self._params = params.copy()
super().__init__(request, params, model, model_admin)
self.request = request
self.query_string = get_query_string(
Expand All @@ -37,8 +38,8 @@ def expected_parameters(self):
return [self.parameter_name, self.parameter_name_op]

def queryset(self, request, queryset):
filter_name = self.used_parameters.get(self.parameter_name)
operation = self.used_parameters.get(self.parameter_name_op, "add")
filter_name = self.get_parameters(self.parameter_name)
operation = self.get_parameters(self.parameter_name_op, "add")
if filter_name:
if operation == "add":
qs = get_query_string(request, {}, self.expected_parameters())
Expand Down
7 changes: 4 additions & 3 deletions src/adminfilters/dj.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ def __init__(self, request, params, model, model_admin):
self.lookup_kwarg_key = "%s__key" % self.parameter_name
self.lookup_kwarg_value = "%s__value" % self.parameter_name
self.lookup_kwarg_negated = "%s__negate" % self.parameter_name
self.lookup_field_val = params.pop(self.lookup_kwarg_key, "")
self.lookup_value_val = params.pop(self.lookup_kwarg_value, "")
self.lookup_negated_val = params.pop(self.lookup_kwarg_negated, "false")
self._params = params
self.lookup_field_val = self.get_parameters(self.lookup_kwarg_key, pop=True)
self.lookup_value_val = self.get_parameters(self.lookup_kwarg_value, pop=True)
self.lookup_negated_val = self.get_parameters(self.lookup_kwarg_negated, "false", pop=True)
self.error_message = None
self.exception = None
self.filters = None
Expand Down
17 changes: 17 additions & 0 deletions src/adminfilters/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from django.contrib.admin.options import ModelAdmin
from django.core import checks

from adminfilters.compat import DJANGO_MAJOR


class WrappperMixin:
negated = False
Expand All @@ -24,6 +26,19 @@
f"{self.model_admin.__class__.__name__} must inherit from AdminFiltersMixin"
)

def get_parameters(self, param_name, default="", multi=False, pop=False, separator=","):
if pop:
val = self._params.pop(param_name, default)
else:
val = self._params.get(param_name, default)
if val:
if DJANGO_MAJOR >= 5:
if isinstance(val, list) and not multi:
val = val[-1]

Check warning on line 37 in src/adminfilters/mixin.py

View check run for this annotation

Codecov / codecov/patch

src/adminfilters/mixin.py#L37

Added line #L37 was not covered by tests
elif multi:
val = val.split(separator)
return val

def html_attrs(self):
classes = f"adminfilters box {self.__class__.__name__.lower()}"
if self.error_message:
Expand All @@ -46,12 +61,14 @@
class SmartListFilter(WrappperMixin, ListFilter):
def __init__(self, request, params, model, model_admin):
self.model_admin = model_admin
self._params = params
super().__init__(request, params, model, model_admin)


class SmartFieldListFilter(WrappperMixin, FieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
self.model_admin = model_admin
self._params = params
super().__init__(field, request, params, model, model_admin, field_path)


Expand Down
2 changes: 1 addition & 1 deletion src/adminfilters/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def expected_parameters(self):

def value(self):
return [
self.parameters.get(self.lookup_kwarg, ""),
self.get_parameters(self.lookup_kwarg),
]

def queryset(self, request, queryset):
Expand Down
5 changes: 3 additions & 2 deletions src/adminfilters/querystring.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ class QueryStringFilter(MediaDefinitionFilter, SmartListFilter):

def __init__(self, request, params, model, model_admin):
self.parameter_name_negated = "%s__negate" % self.parameter_name
self.lookup_field_val = params.pop(self.parameter_name, "")
self.lookup_negated_val = params.pop(self.parameter_name_negated, "false")
self._params = params
self.lookup_field_val = self.get_parameters(self.parameter_name, pop=True)
self.lookup_negated_val = self.get_parameters(self.parameter_name_negated, "false", pop=True)
self.query_string = None
self.error_message = None
self.exception = None
Expand Down
11 changes: 5 additions & 6 deletions src/adminfilters/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def __init__(self, field, request, params, model, model_admin, field_path):
self.parameters[p] = params.pop(p)

super().__init__(field, request, params, model, model_admin, field_path)
self._params = self.parameters
self.title = self._get_title()
# self.query_string = get_query_string(request, remove=self.expected_parameters())

Expand All @@ -51,8 +52,8 @@ def expected_parameters(self):

def value(self):
return [
self.parameters.get(self.lookup_kwarg, ""),
self.parameters.get(self.lookup_kwarg_negated, "") == "true"
self.get_parameters(self.lookup_kwarg),
self.get_parameters(self.lookup_kwarg_negated) == "true",
# self.parameters[self.lookup_kwarg],
# self.parameters[self.lookup_kwarg_negated] == 'true'
# self.lookup_val,
Expand Down Expand Up @@ -132,10 +133,8 @@ def placeholder(self):
return _("comma separated list of values")

def value(self):
values = self.parameters.get(self.lookup_kwarg, None)
if values is not None:
values = values.split(self.separator)
return [values, self.parameters.get(self.lookup_kwarg_negated, "") == "true"]
values = self.get_parameters(self.lookup_kwarg, None, multi=True)
return [values, self.get_parameters(self.lookup_kwarg_negated, "") == "true"]


TextFieldFilter = ValueFilter
Expand Down
1 change: 1 addition & 0 deletions src/requirements/testing.pip
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ factory-boy
flake8
isort
pdbpp
prettytable==3.9.0
psycopg2-binary
pytest<7
pytest-cov
Expand Down
3 changes: 2 additions & 1 deletion tests/demoapp/demo/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import random

import factory.fuzzy
import faker
from django.contrib.auth.models import User
from factory.base import FactoryMetaClass

Expand Down Expand Up @@ -95,7 +96,7 @@ class Meta:

class ArtistFactory(ModelFactory):
name = factory.Faker("first_name")
last_name = factory.Faker("last_name")
last_name = factory.Sequence(lambda n: f'Dummy{faker.Faker().unique.last_name()}{n}')
full_name = factory.LazyAttribute(lambda o: f"{o.last_name}, {o.name}")

country = factory.SubFactory(CountryFactory)
Expand Down
5 changes: 3 additions & 2 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ def data():
from demo.factories import ArtistFactory
from demo.management.commands.init_demo import sample_data

ArtistFactory.create_batch(20)
batch = ArtistFactory.create_batch(20)
sample_data()
return batch


class AdminSite:
Expand Down Expand Up @@ -129,6 +130,6 @@ def admin_site(live_server, selenium, data):
site = AdminSite(live_server, selenium)
site.open("/")
site.wait_for(By.LINK_TEXT, "Artists").click()
errors = site.get_errors()
errors = [err for err in site.get_errors() if err and 'favicon' not in err['message']]
assert len(errors) == 0, "\n".join(["{message}".format(**err) for err in errors])
return site
20 changes: 19 additions & 1 deletion tests/test_automplete.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import pytest
from demo.admin import ArtistModelAdmin
from demo.models import Artist, Country
from demo.urls import public_site

from adminfilters.autocomplete import AutoCompleteFilter
from adminfilters.compat import DJANGO_MAJOR


@pytest.fixture
Expand All @@ -18,14 +20,30 @@ def fixtures(db):
@pytest.mark.parametrize("value", ["c1", "c2"])
def test_filter(fixtures, value):
country = Country.objects.get(name=value)
if DJANGO_MAJOR < 5:
country_pk = country.pk
else:
country_pk = [country.pk]
f = AutoCompleteFilter(
Artist._meta.get_field("country"),
None,
{"country__exact": country, "name__isnull": ""},
{"country__exact": country_pk, "name__isnull": ""},
Artist,
public_site._registry[Artist],
"country",
)
qs = f.queryset(None, Artist.objects.all())
result = set(qs.values_list("country__name", flat=True))
assert result == {value}


def test_media():
assert AutoCompleteFilter.factory(title="Title")(
None, None, {}, Artist, ArtistModelAdmin(Artist, public_site), "last_name"
).media


def test_url():
assert AutoCompleteFilter.factory(title="Title")(
None, None, {}, Artist, ArtistModelAdmin(Artist, public_site), "last_name"
).get_url() == '/autocomplete/'
25 changes: 21 additions & 4 deletions tests/test_extra.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
from django.contrib.auth.models import Permission

from adminfilters.compat import DJANGO_MAJOR
from adminfilters.extra import PermissionPrefixFilter


def test_GenericLookupFieldFilter(db):
f = PermissionPrefixFilter

qs = f(None, {"perm": "add"}, None, None).queryset(None, Permission.objects.all())
if DJANGO_MAJOR < 5:
param = "add"
else:
param = ["add"]
qs = f(None, {"perm": param}, None, None).queryset(None, Permission.objects.all())
assert qs.first().codename.startswith("add_")

qs = f(None, {"perm": "change"}, None, None).queryset(
if DJANGO_MAJOR < 5:
param = "change"
else:
param = ["change"]
qs = f(None, {"perm": param}, None, None).queryset(
None, Permission.objects.all()
)
assert qs.first().codename.startswith("change_")

qs = f(None, {"perm": "delete"}, None, None).queryset(
if DJANGO_MAJOR < 5:
param = "delete"
else:
param = ["delete"]
qs = f(None, {"perm": param}, None, None).queryset(
None, Permission.objects.all()
)
assert qs.first().codename.startswith("delete_")

qs = f(None, {}, None, None).queryset(None, Permission.objects.all())
assert qs.exists()

qs = f(None, {"perm": "--"}, None, None).queryset(None, Permission.objects.all())
if DJANGO_MAJOR < 5:
param = "--"
else:
param = ["--"]
qs = f(None, {"perm": param}, None, None).queryset(None, Permission.objects.all())
assert not qs.exists()
7 changes: 6 additions & 1 deletion tests/test_value.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from demo.models import Artist

from adminfilters.compat import DJANGO_MAJOR
from adminfilters.filters import MultiValueFilter, ValueFilter


Expand Down Expand Up @@ -69,10 +70,14 @@ def test_factory(fixtures):
],
)
def test_MultiValueTextFieldFilter(fixtures, value, negate, expected):
if DJANGO_MAJOR < 5:
val = value
else:
val = [value]
f = MultiValueFilter(
Artist._meta.get_field("name"),
None,
{"name__in": value, "name__in__negate": str(negate).lower()},
{"name__in": val, "name__in__negate": str(negate).lower()},
None,
None,
"name",
Expand Down
7 changes: 5 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[tox]
envlist = d{32,42}-py{39,310,311}
envlist =
d{32,42}-py{39,310,311,312}
d50-py{310,311,312}
skip_missing_interpreters = true

[pytest]
Expand Down Expand Up @@ -48,9 +50,10 @@ deps =
-rsrc/requirements/testing.pip
d32: django==3.2.*
d42: django==4.2.*
d50: django==5.0.*

commands =
{posargs:py.test tests/ --create-db --selenium --cov-report=xml --cov-report=term --junitxml=pytest.xml \
{posargs:py.test tests/functional --create-db --selenium --cov-report=xml --cov-report=term --junitxml=pytest.xml \
--cov-config=tests/.coveragerc --cov adminfilters}

[testenv:package]
Expand Down
Loading