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

Django 4 support #223

Merged
merged 10 commits into from
Apr 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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ jobs:

strategy:
matrix:
python-version: ["3.7", "3.8"]
django-version: ["3.1.4"]
python-version: ["3.8", "3.12"]
django-version: ["3.2.25", "4.2.11"]
database-engine: ["postgres", "mysql"]

services:
Expand Down
2 changes: 1 addition & 1 deletion binder/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .json import jsondumps, JsonResponse


transaction_commit = Signal(providing_args=['changeset'])
transaction_commit = Signal()


class Changeset(models.Model):
Expand Down
4 changes: 2 additions & 2 deletions binder/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django import forms
from django.db import models
from django.db.models.fields.files import FieldFile, FileField
from django.contrib.postgres.fields import CITextField, ArrayField, JSONField, DateTimeRangeField as DTRangeField
from django.contrib.postgres.fields import CITextField, ArrayField, DateTimeRangeField as DTRangeField
from django.core import checks
from django.core.files.base import File, ContentFile
from django.core.files.images import ImageFile
Expand Down Expand Up @@ -395,7 +395,7 @@ def clean_value(self, qualifier, v):


class JSONFieldFilter(FieldFilter):
fields = [JSONField]
fields = [models.JSONField]
# TODO: Element or path-based lookup is not supported yet
allowed_qualifiers = [None, 'contains', 'contained_by', 'has_key', 'has_any_keys', 'has_keys', 'isnull']

Expand Down
16 changes: 16 additions & 0 deletions binder/plugins/my_filters/migrations/0002_migrate_jsonfield.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('my_filters', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='MyFilter',
name='params',
field=models.JSONField(),
),
]
3 changes: 1 addition & 2 deletions binder/plugins/my_filters/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.db import models
from django.contrib.postgres.fields import JSONField
from django.conf import settings

from ...models import BinderModel
Expand All @@ -13,7 +12,7 @@ class MyFilter(BinderModel):
)
view = models.TextField()
name = models.TextField()
params = JSONField()
params = models.JSONField()
default = models.BooleanField(default=False)

class Meta(BinderModel.Meta):
Expand Down
8 changes: 4 additions & 4 deletions binder/plugins/views/userview.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def reset_request(self, request):

return HttpResponse(status=204)

@never_cache
@method_decorator(never_cache)
@list_route(name='send_activation_email', unauthenticated=True)
@no_scoping_required()
def send_activation_email(self, request):
Expand Down Expand Up @@ -355,7 +355,7 @@ def send_activation_email(self, request):
return response

@method_decorator(sensitive_post_parameters())
@never_cache
@method_decorator(never_cache)
@detail_route(name='activate', unauthenticated=True)
@no_scoping_required()
def activate(self, request, pk=None):
Expand Down Expand Up @@ -406,7 +406,7 @@ def activate(self, request, pk=None):
return self.respond_with_user(request, user.id)

@method_decorator(sensitive_post_parameters())
@never_cache
@method_decorator(never_cache)
@detail_route(name='reset_password', unauthenticated=True, methods=['PUT'])
@no_scoping_required()
def reset_password(self, request, pk=None):
Expand Down Expand Up @@ -466,7 +466,7 @@ def _reset_pass_for_user(self, request, user_id, token, password):
return self.respond_with_user(request, user.id)

@method_decorator(sensitive_post_parameters())
@never_cache
@method_decorator(never_cache)
@list_route(name='change_password')
@no_scoping_required()
def change_password(self, request):
Expand Down
9 changes: 8 additions & 1 deletion binder/tests_discoverer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from django.test.runner import is_discoverable
from django.apps import apps
try:
from django.test.runner import is_discoverable
except ImportError:
# Django 4 support
from django.test.runner import try_importing
def is_discoverable(path):
is_importable, _ = try_importing(path)
return is_importable

# This prevents discoverer from loading models and views, which will
# cause all sorts of random failures.
Expand Down
2 changes: 1 addition & 1 deletion ci-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
psycopg2<2.9
psycopg2<3.0
mysqlclient
Pillow
django-request-id
Expand Down
6 changes: 3 additions & 3 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ CSRF_FAILURE_VIEW = 'binder.router.csrf_failure'
In `urls.py`, add the following:

```python
from django.conf.urls import url, include
from django.urls import re_path, include

import binder.router
import binder.views
Expand All @@ -33,8 +33,8 @@ import binder.models
router = binder.router.Router().register(binder.views.ModelView)

urlpatterns = [
url(r'^', include(router.urls)),
url(r'^', binder.views.api_catchall, name='catchall'),
re_path(r'^', include(router.urls)),
re_path(r'^', binder.views.api_catchall, name='catchall'),
]

binder.models.install_history_signal_handlers(binder.models.BinderModel)
Expand Down
6 changes: 3 additions & 3 deletions project/project/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from django.conf.urls import url, include
from django.urls import re_path, include
from django.contrib import admin

import testapp.urls

urlpatterns = [
url(r'^admin/', admin.site.urls, name='admin'),
url(r'^api/', include(testapp.urls), name='testapp'),
re_path(r'^admin/', admin.site.urls, name='admin'),
re_path(r'^api/', include(testapp.urls), name='testapp'),
]
10 changes: 3 additions & 7 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

setup(
name='django-binder',
version='1.5.0',
version='1.6.0',
package_dir={'binder': 'binder'},
packages=find_packages(),
include_package_data=True,
Expand All @@ -26,21 +26,17 @@
'Environment :: Web Environment',
'Framework :: Django',
'Framework :: Django :: 3.0',
'Framework :: Django :: 3.1',
'Framework :: Django :: 4.0',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
install_requires=[
'Django >= 3.0, < 4.0',
'Django >= 3.0, < 5.0',
'Pillow >= 3.2.0',
'django-request-id >= 1.0.0',
'requests >= 2.13.0',
Expand Down
4 changes: 2 additions & 2 deletions tests/filters/test_time_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ def setUp(self):
r = self.client.login(username='testuser', password='test')
self.assertTrue(r)

Zoo(name='Burgers Zoo', opening_time='11:00:00Z').save()
Zoo(name='Artis', opening_time='09:00:00Z').save()
Zoo(name='Burgers Zoo', opening_time='11:00:00').save()
Zoo(name='Artis', opening_time='09:00:00').save()


def test_time_filter_exact_match(self):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_none_related_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@ def test_simple_follow_related(self):

self.assertIsInstance(result[0], RelatedModel)
self.assertIsNone(result[0].reverse_fieldname)
self.assertEquals('bar', result[0].fieldname)
self.assertEqual('bar', result[0].fieldname)
self.assertEqual(BarModel, result[0].model)
46 changes: 23 additions & 23 deletions tests/test_permission_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,14 +419,14 @@ def test_cannot_delete_on_multiput_without_delete_permission(self):
}))

# This is not ok
self.assertEquals(403, res.status_code)
self.assertEqual(403, res.status_code)

content = jsonloads(res.content)
self.assertEquals('testapp.delete_city', content['required_permission'])
self.assertEqual('testapp.delete_city', content['required_permission'])

# City 2 still exists!
city2.refresh_from_db()
self.assertEquals(country, city2.country) # And belongs to the nederlands
self.assertEqual(country, city2.country) # And belongs to the nederlands

@override_settings(BINDER_PERMISSION={
'testapp.view_country': [
Expand All @@ -450,12 +450,12 @@ def test_delete_scoping_on_multiput_with_delete_permission(self):
}]
}))

self.assertEquals(200, res.status_code)
self.assertEqual(200, res.status_code)

# City 2 must not exist.
with self.assertRaises(City.DoesNotExist):
city2.refresh_from_db()
self.assertEquals(1, country.cities.count())
self.assertEqual(1, country.cities.count())

@override_settings(BINDER_PERMISSION={
'testapp.view_country': [
Expand All @@ -481,9 +481,9 @@ def test_cannot_change_on_multiput_without_change_permission(self):
}))

# This is not ok
self.assertEquals(403, res.status_code)
self.assertEqual(403, res.status_code)
content = jsonloads(res.content)
self.assertEquals('testapp.change_city', content['required_permission'])
self.assertEqual('testapp.change_city', content['required_permission'])

@override_settings(BINDER_PERMISSION={
'testapp.view_country': [
Expand All @@ -506,14 +506,14 @@ def test_cannot_delete_on_put_without_delete_permission(self):
}))

# This is not ok
self.assertEquals(res.status_code, 403)
self.assertEqual(res.status_code, 403)

content = jsonloads(res.content)
self.assertEquals('testapp.delete_city', content['required_permission'])
self.assertEqual('testapp.delete_city', content['required_permission'])

# City 2 still exists!
city2.refresh_from_db()
self.assertEquals(country, city2.country)
self.assertEqual(country, city2.country)

@override_settings(BINDER_PERMISSION={
'testapp.view_country': [
Expand All @@ -534,11 +534,11 @@ def test_can_delete_on_put_with_delete_permission(self):
'cities': [city1.pk]
}))

self.assertEquals(200, res.status_code)
self.assertEqual(200, res.status_code)

with self.assertRaises(City.DoesNotExist):
city2.refresh_from_db()
self.assertEquals(1, country.cities.count())
self.assertEqual(1, country.cities.count())

@override_settings(BINDER_PERMISSION={
'testapp.view_country': [
Expand All @@ -558,7 +558,7 @@ def test_softdelete_on_put_with_delete_permission_softdeletable(self):
'permanent_cities': []
}))

self.assertEquals(200, res.status_code)
self.assertEqual(200, res.status_code)
city1.refresh_from_db()
self.assertTrue(city1.deleted)

Expand All @@ -579,9 +579,9 @@ def test_softdelete_on_put_without_softdelete_permission_fails(self):
'permanent_cities': []
}))

self.assertEquals(res.status_code, 403)
self.assertEqual(res.status_code, 403)
content = jsonloads(res.content)
self.assertEquals('testapp.delete_permanentcity', content['required_permission'])
self.assertEqual('testapp.delete_permanentcity', content['required_permission'])

@override_settings(BINDER_PERMISSION={
'testapp.view_country': [
Expand All @@ -600,7 +600,7 @@ def test_related_object_nullable_on_delete_is_set_to_null(self):
'name': 'Belgium',
'city_states': []
}))
self.assertEquals(200, res.status_code)
self.assertEqual(200, res.status_code)

city1.refresh_from_db()

Expand All @@ -623,14 +623,14 @@ def test_related_object_nullable_on_delete_no_change_permission_not_allowed(self
'city_states': []
}))

self.assertEquals(403, res.status_code)
self.assertEqual(403, res.status_code)

content = jsonloads(res.content)
self.assertEquals('testapp.change_citystate', content['required_permission'])
self.assertEqual('testapp.change_citystate', content['required_permission'])

city1.refresh_from_db()

self.assertEquals(country.pk, country.pk)
self.assertEqual(country.pk, country.pk)

@override_settings(BINDER_PERMISSION={
'testapp.view_country': [
Expand All @@ -644,7 +644,7 @@ def test_multiput_deletions(self):
'deletions': [country.pk],
}))

self.assertEquals(200, res.status_code)
self.assertEqual(200, res.status_code)

with self.assertRaises(Country.DoesNotExist):
country.refresh_from_db()
Expand All @@ -660,7 +660,7 @@ def test_multiput_deletions_no_perm(self):
'deletions': [country.pk],
}))

self.assertEquals(403, res.status_code)
self.assertEqual(403, res.status_code)

country.refresh_from_db()

Expand All @@ -676,7 +676,7 @@ def test_multiput_with_deletions(self):
'with_deletions': {'country': [country.pk]},
}))

self.assertEquals(200, res.status_code)
self.assertEqual(200, res.status_code)

with self.assertRaises(Country.DoesNotExist):
country.refresh_from_db()
Expand All @@ -692,7 +692,7 @@ def test_multiput_with_deletions_no_perm(self):
'with_deletions': {'country': [country.pk]},
}))

self.assertEquals(403, res.status_code)
self.assertEqual(403, res.status_code)

country.refresh_from_db()

Expand Down
6 changes: 0 additions & 6 deletions tests/test_postgres_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,8 +493,6 @@ def test_value_DateTimeRangeTZ_invalid_non_json_parsable(self):
test_model = TimeTable(daterange=DateTimeTZRange(lower, upper))
test_model.save()

self.assertEqual("expected string or bytes-like object", str(te.exception))
BBooijLiewes marked this conversation as resolved.
Show resolved Hide resolved


def test_value_tuple_empty(self):
# empty is equivalent to None
Expand Down Expand Up @@ -573,8 +571,6 @@ def test_value_tuple_invalid_non_json_parsable(self):
test_model = TimeTable(daterange=(lower, upper))
test_model.save()

self.assertEqual("expected string or bytes-like object", str(te.exception))


def test_value_array_empty(self):
# empty is equivalent to None
Expand Down Expand Up @@ -653,8 +649,6 @@ def test_value_array_invalid_non_json_parsable(self):
test_model = TimeTable(daterange=[lower, upper])
test_model.save()

self.assertEqual("expected string or bytes-like object", str(te.exception))


def test_value_stringified_array(self):
tmrw = date.today() + timedelta(days=1)
Expand Down
Loading
Loading