Skip to content

Commit

Permalink
Django 4.2 (#85)
Browse files Browse the repository at this point in the history
* django compatibility up to 5.1
  • Loading branch information
seljin authored Nov 25, 2024
1 parent d7181ee commit 0728c1d
Show file tree
Hide file tree
Showing 22 changed files with 124 additions and 94 deletions.
1 change: 0 additions & 1 deletion .venv

This file was deleted.

37 changes: 31 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ Requirements

This database wrapper work with

- python 3.6, 3.7
- django 2.0, 2.1, 2.2, 3.0, 3.1, 3.2
- python 3.9, 3.10, 3.11, 3.12
- django 4.2, 5.0, 5.1

On the api, this is tested against

- django-rest-framework 3.11, 3.12, 3.13
- dynamic-rest 2.1
- django-rest-framework 3.14, 3.15
- dynamic-rest-bse 2.4 (It's a fork because dynamic-rest is not compatible with django 4.2 at this day)


Examples
Expand Down Expand Up @@ -259,6 +259,31 @@ This database api support :
Support for ForeignKey is only available with models on the same database (api<->api) or (default<->default).
It's not possible to add a ForeignKey/ManyToMany field on a local model related to a remote model (with ApiMeta)

OAuthToken auth backend
-----------------------

grant_type is provided to the API get_token view by a GET parameter.

Recent framework updates like Django OAuth Toolkit enforce that no GET parameters are used.

Use ENFORCE_POST setting in OPTIONS of api's DATABASE :

.. code-block:: python
DATABASES = {
'default': {
...
},
'api': {
...
'OPTIONS': {
'OAUTH_URL': '/oauth2/token/',
'ENFORCE_POST': True,
}
},
}
Documentation
-------------

Expand All @@ -268,8 +293,8 @@ The full documentation is at http://django-rest-models.readthedocs.org/en/latest
Requirements
------------

- Python 2.7, 3.5
- Django >= 1.8
- Python 3.9, 3.10, 3.11, 3.12
- Django >= 4.2

Contributions and pull requests are welcome.

Expand Down
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
Django<3.3
Django<5.2
wheel
sphinx
tox
isort
flake8
requests
djangorestframework<3.14
dynamic-rest<2.2
djangorestframework<3.16
dynamic-rest-bse<2.5
-r test_requirements.txt
twine
psycopg2-binary
Pillow<11
Pillow<12
unidecode
2 changes: 1 addition & 1 deletion rest_models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__VERSION__ = '2.1.2'
__VERSION__ = '3.0.0'

try:
from rest_models.checks import register_checks
Expand Down
18 changes: 13 additions & 5 deletions rest_models/backend/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,23 @@ def get_token(self):
:return: the token from the api
:rtype: Token
"""
conn = self.databasewrapper.cursor()
params = {'grant_type': 'client_credentials'}
if self.settings_dict.get('OPTIONS', {}).get('ENFORCE_POST', False):
kw = 'data'
else:
logger.warning("Get oauth token: Using deprecated GET parameter, will be removed in future releases."
"use ENFORCE_POST to fix this (cf. djang-rest-models README)")
kw = 'params'
_kwargs = {
'auth': (self.settings_dict['USER'], self.settings_dict['PASSWORD']),
'stream': False,
kw: {'grant_type': 'client_credentials'}
}
# Get client credentials params
conn = self.databasewrapper.cursor()
response = conn.session.request(
'POST',
self.url_token,
params=params,
auth=(self.settings_dict['USER'], self.settings_dict['PASSWORD']),
stream=False
**_kwargs
)
if response.status_code != 200:
raise ProgrammingError("unable to retrive the oauth token from %s: %s" %
Expand Down
21 changes: 14 additions & 7 deletions rest_models/backend/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from django.db.models import FileField, Transform
from django.db.models.aggregates import Count
from django.db.models.base import ModelBase
from django.db.models.expressions import Col, RawSQL
from django.db.models.expressions import Col, RawSQL, Value
from django.db.models.fields.related_lookups import RelatedExact, RelatedIn
from django.db.models.lookups import Exact, In, IsNull, Lookup, Range
from django.db.models.sql.compiler import SQLCompiler as BaseSQLCompiler
Expand Down Expand Up @@ -60,8 +60,8 @@ def extract_exact_pk_value(where):
exact, isnull = where.children

if (
isinstance(exact, Exact) and isinstance(isnull, IsNull) and
exact.lhs.target == isnull.lhs.target
isinstance(exact, Exact) and isinstance(isnull, IsNull) and
exact.lhs.target == isnull.lhs.target
):
return exact
return None
Expand Down Expand Up @@ -379,7 +379,12 @@ def get_resources_for_cols(self, cols):
:return: 2 sets, the first if the alias useds, the 2nd is the set of the full path of the resources, with the
attributes
"""
resolved = [self.resolve_path(col) for col in cols if not isinstance(col, RawSQL) or col.sql != '1']
resolved = [
self.resolve_path(col)
for col in cols
if (not isinstance(col, RawSQL) or col.sql != '1')
and (not isinstance(col, Value) or col.value != 1) # skip special cases with exists()
]

return (
set(r[0] for r in resolved), # set of tuple of Alias successives
Expand Down Expand Up @@ -646,14 +651,15 @@ class SQLCompiler(BaseSQLCompiler):

META_NAME = 'meta'

def __init__(self, query, connection, using):
def __init__(self, query, connection, using, elide_empty=True):
"""
:param django.db.models.sql.query.Query query: the query
:param rest_models.backend.base.DatabaseWrapper connection: the connection
:param str using:
"""
self.query = query
self.connection = connection
self.elide_empty = elide_empty
self.using = using
self.quote_cache = {'*': '*'}
# The select, klass_info, and annotations are needed by QuerySet.iterator()
Expand Down Expand Up @@ -948,7 +954,8 @@ def response_to_table(self, responsereader, item):
resolved = [
self.query_parser.resolve_path(col)
for col, _, _ in self.select
if not isinstance(col, RawSQL) or col.sql != '1' # skip special case with exists()
if (not isinstance(col, RawSQL) or col.sql != '1')
and (not isinstance(col, Value) or col.value != 1) # skip special cases with exists()
]
if not resolved:
# nothing in select. special case in exists()
Expand Down Expand Up @@ -1226,7 +1233,7 @@ def execute_sql(self, return_id=False, chunk_size=None):
result = [result_json[get_resource_name(query.model, many=False)][opts.pk.column]]
elif django.VERSION < (3, 0):
return result
return (result, )
return (result,)


class FakeCursor(object):
Expand Down
5 changes: 1 addition & 4 deletions rest_models/backend/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ def message_from_response(response):
try:
from django.db.models import JSONField as JSONFieldLegacy
except ImportError:
try:
from django.contrib.postgres.fields import JSONField as JSONFieldLegacy
except ImportError:
pass
pass

try:
class JSONField(JSONFieldLegacy):
Expand Down
4 changes: 2 additions & 2 deletions rest_models/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
import logging
import os
import threading
from urllib.parse import unquote

import unidecode
from django.core.files.base import ContentFile
from django.core.files.storage import Storage
from django.utils.deconstruct import deconstructible
from django.utils.http import urlunquote

from rest_models.backend.connexion import get_basic_session

Expand Down Expand Up @@ -85,7 +85,7 @@ def prepare_result_from_api(self, result, cursor):
# and store the full url for later
if result is None:
return None
name = urlunquote(os.path.basename(result))
name = unquote(os.path.basename(result))
self.result_file_pool[name] = result, cursor
return name

Expand Down
4 changes: 2 additions & 2 deletions rest_models/tests/test_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def test_existing_db(self):
called = []

def tmp_exec(self_dc, args, env):
self.assertRegexpMatches(env['_resty_host'], r'http://localhost:[0-9]*/api/v2\*')
self.assertRegex(env['_resty_host'], r'http://localhost:[0-9]*/api/v2\*')
called.append(args)

DatabaseClient.execute_subprocess = tmp_exec
Expand All @@ -33,7 +33,7 @@ def test_to_run_db(self):
called = []

def tmp_exec(self_dc, args, env):
self.assertRegexpMatches(env['_resty_host'], r'http://localhost:[0-9]*/api/v2\*')
self.assertRegex(env['_resty_host'], r'http://localhost:[0-9]*/api/v2\*')
called.append(args)

DatabaseClient.execute_subprocess = tmp_exec
Expand Down
3 changes: 2 additions & 1 deletion rest_models/tests/test_restmodeltestcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ def test_track_queries(self):


class TestMockDataSample(RestModelTestCase):
databases = ['default', 'api'] + (['restaurant.json'] if POSTGIS else [])
databases = ['default', 'api']
fixtures = (['restaurant.json'] if POSTGIS else [])
database_rest_fixtures = {'api': { # api => response mocker for databasen named «api»
'menulol': [ # url menulol
{
Expand Down
24 changes: 12 additions & 12 deletions rest_models/tests/tests_queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.db.models import Q, Sum
from django.test import TestCase
from django.urls import reverse
from dynamic_rest.filters import DynamicFilterBackend
from dynamic_rest.constants import VALID_FILTER_OPERATORS

from rest_models.backend.compiler import SQLAggregateCompiler, SQLCompiler
from testapi import models as api_models
Expand Down Expand Up @@ -246,7 +246,7 @@ def test_jsonfield_create(self):
metadata={'origine': 'france', 'abattage': 2018}
)
self.assertIsNotNone(t)
self.assertEqual(t.metadata, {'origine': 'france', 'abattage': 2018})
self.assertEqual(json.loads(t.metadata), {'origine': 'france', 'abattage': 2018})
self.assertEqual(t.cost, 2)
self.assertEqual(t.name, 'lardons lux')

Expand All @@ -263,7 +263,7 @@ def test_jsonfield_update(self):
metadata={'origine': 'france', 'abattage': 2018}
)
self.assertIsNotNone(t)
self.assertEqual(t.metadata, {'origine': 'france', 'abattage': 2018})
self.assertEqual(json.loads(t.metadata), {'origine': 'france', 'abattage': 2018})
self.assertEqual(t.cost, 2)
self.assertEqual(t.name, 'lardons lux')

Expand All @@ -279,7 +279,7 @@ def test_jsonfield_update(self):


@skipIf(settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3', 'no json in sqlite')
@skipIf('year' in DynamicFilterBackend.VALID_FILTER_OPERATORS, 'skip check not compatible with current drest')
@skipIf('year' in VALID_FILTER_OPERATORS, 'skip check not compatible with current drest')
class TestJsonLookup(TestCase):
fixtures = ['data.json']

Expand Down Expand Up @@ -336,7 +336,7 @@ def test_jsonfield_lookup_deeper(self):
), [(t.pk,)])


@skipIf('year' in DynamicFilterBackend.VALID_FILTER_OPERATORS, 'skip check not compatible with current drest')
@skipIf('year' in VALID_FILTER_OPERATORS, 'skip check not compatible with current drest')
class TestQueryLookupTransform(TestCase):
fixtures = ['data.json']

Expand Down Expand Up @@ -648,7 +648,7 @@ def test_delete_obj(self):
n = api_models.Pizza.objects.count()
self.assertEqual(n, 3)
p = client_models.Pizza(pk=1)
with self.assertNumQueries(1, using='api'):
with self.assertNumQueries(2, using='api'):
p.delete()
self.assertEqual(api_models.Pizza.objects.count(), 2)
self.assertFalse(api_models.Pizza.objects.filter(pk=1).exists())
Expand All @@ -657,7 +657,7 @@ def test_delete_qs_one(self):
n = api_models.Pizza.objects.count()

self.assertEqual(n, 3)
with self.assertNumQueries(2, using='api'):
with self.assertNumQueries(3, using='api'):
client_models.Pizza.objects.filter(pk=1).delete()
self.assertEqual(api_models.Pizza.objects.count(), 2)
self.assertFalse(api_models.Pizza.objects.filter(pk=1).exists())
Expand All @@ -667,7 +667,7 @@ def test_delete_qs_one(self):
def test_delete_qs_many(self):
n = api_models.Pizza.objects.count()
self.assertEqual(n, 3)
with self.assertNumQueries(3, using='api'):
with self.assertNumQueries(4, using='api'):
client_models.Pizza.objects.filter(Q(pk__in=(1, 2))).delete()
self.assertEqual(api_models.Pizza.objects.count(), 1)
self.assertFalse(api_models.Pizza.objects.filter(pk=1).exists())
Expand All @@ -677,7 +677,7 @@ def test_delete_qs_many(self):
def test_delete_qs_many_range(self):
n = api_models.Pizza.objects.count()
self.assertEqual(n, 3)
with self.assertNumQueries(3, using='api'):
with self.assertNumQueries(4, using='api'):
client_models.Pizza.objects.filter(pk__range=(1, 2)).delete()
self.assertEqual(api_models.Pizza.objects.count(), 1)
self.assertFalse(api_models.Pizza.objects.filter(pk=1).exists())
Expand All @@ -687,7 +687,7 @@ def test_delete_qs_many_range(self):
def test_delete_qs_no_pk(self):
n = api_models.Pizza.objects.count()
self.assertEqual(n, 3)
with self.assertNumQueries(2, using='api'):
with self.assertNumQueries(3, using='api'):
client_models.Pizza.objects.filter(name='suprème').delete()
self.assertEqual(api_models.Pizza.objects.count(), 2)
self.assertFalse(api_models.Pizza.objects.filter(pk=1).exists())
Expand All @@ -697,7 +697,7 @@ def test_delete_qs_no_pk(self):
def test_delete_qs_all(self):
n = api_models.Pizza.objects.count()
self.assertEqual(n, 3)
with self.assertNumQueries(4, using='api'):
with self.assertNumQueries(5, using='api'):
client_models.Pizza.objects.all().delete()
self.assertEqual(api_models.Pizza.objects.count(), 0)
self.assertFalse(api_models.Pizza.objects.filter(pk=1).exists())
Expand All @@ -717,7 +717,7 @@ def test_manual_update(self):
res = self.client.patch(reverse('pizza-detail', kwargs={'pk': p.pk}),
data=json.dumps({'pizza': {'menu': menu2.pk}}),
content_type='application/json',
HTTP_AUTHORIZATION='Basic YWRtaW46YWRtaW4=',
headers={"authorization": 'Basic YWRtaW46YWRtaW4='}
)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.data['pizza']['menu'], menu2.pk)
Expand Down
6 changes: 3 additions & 3 deletions rest_models/tests/tests_upload_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

import logging
import os
from urllib.parse import quote
from uuid import uuid4

import unidecode
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.utils.http import urlquote

from testapi import models as apimodels
from testapp import models as clientmodels
Expand All @@ -28,8 +28,8 @@ class TestUploadDRF(TestCase):
def setUp(self):
self.img_name = str(uuid4()) + '-b--é--o--ç--é--_--b--É.png'
self.img_name2 = str(uuid4()) + '-b--é--o--ç--é--_--b--É.png'
self.img_name_quoted = urlquote(self.img_name)
self.img_name2_quoted = urlquote(self.img_name2)
self.img_name_quoted = quote(self.img_name)
self.img_name2_quoted = quote(self.img_name2)
self.img_name_cleaned = unidecode.unidecode(self.img_name)
self.img_name2_cleaned = unidecode.unidecode(self.img_name2)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
install_requires=[
'requests',
'six',
'Django<3.3',
'Django<5.2',
'unidecode',
],
packages=[
Expand Down
Loading

0 comments on commit 0728c1d

Please sign in to comment.