add compatibility layer for support of Django native JSONField (fixes #29193)

This commit is contained in:
Benjamin Dauvergne 2018-12-19 00:10:52 +01:00
parent b03a76dfff
commit d730dba525
5 changed files with 170 additions and 7 deletions

View File

@ -3,14 +3,61 @@ import re
from django.apps import AppConfig from django.apps import AppConfig
from django.views import debug from django.views import debug
from . import plugins from django.db import connection
from django.db.models.signals import post_migrate
from . import plugins, compat
class Authentic2Config(AppConfig): class Authentic2Config(AppConfig):
name = 'authentic2' name = 'authentic2'
verbose_name = 'Authentic2' verbose_name = 'Authentic2'
def post_migrate_update_json_column(self, sender, **kwargs):
# adapted from https://github.com/kbussell/django-jsonfield-compat/blob/4f6ac4bfaea2224559b174b6d16d846b93d125c6/jsonfield_compat/convert.py
# MIT License, kbussel
if connection.vendor != 'postgresql':
return
if compat.has_postgresql_support():
expected_type = 'JSONB'
else:
expected_type = 'TEXT'
def convert_column_to_json(model, column_name):
table_name = model._meta.db_table
with connection.cursor() as cursor:
cursor.execute(
"select data_type from information_schema.columns "
"where table_name = %s and column_name = %s;",
[table_name, column_name])
current_type = cursor.fetchone()[0].upper()
if current_type != expected_type:
print("{app}: Converting {col} to use native {type} field".format(
app=model._meta.app_label, col=column_name, type=expected_type))
cursor.execute(
"ALTER TABLE {table} ALTER COLUMN {col} "
"TYPE {type} USING {col}::{type};".format(
table=table_name, col=column_name, type=expected_type
)
)
def convert_model_json_fields(model):
json_fields = [f for f in model._meta.fields if f.__class__ == compat.JSONField]
for field in json_fields:
_, column_name = field.get_attname_column()
convert_column_to_json(model, column_name)
for model in list(sender.get_models()):
convert_model_json_fields(model)
def ready(self): def ready(self):
plugins.init() plugins.init()
debug.HIDDEN_SETTINGS = re.compile( debug.HIDDEN_SETTINGS = re.compile(
'API|TOKEN|KEY|SECRET|PASS|PROFANITIES_LIST|SIGNATURE|LDAP') 'API|TOKEN|KEY|SECRET|PASS|PROFANITIES_LIST|SIGNATURE|LDAP')
post_migrate.connect(self.post_migrate_update_json_column)

View File

@ -1,6 +1,11 @@
from datetime import datetime from datetime import datetime
import inspect
import django
from django.conf import settings from django.conf import settings
from django.db import connection
from django.contrib.auth.tokens import PasswordResetTokenGenerator
try: try:
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -14,10 +19,97 @@ try:
except ImportError: except ImportError:
from django.db.transaction import commit_on_success from django.db.transaction import commit_on_success
from . import app_settings, utils
user_model_label = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') user_model_label = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
from django.contrib.auth.tokens import PasswordResetTokenGenerator
default_token_generator = PasswordResetTokenGenerator() default_token_generator = PasswordResetTokenGenerator()
def has_postgresql_support():
if not settings.DATABASES['default'].get('NAME'):
return False
return connection.vendor == 'postgresql' and connection.pg_version > 90400
def use_django_native_field():
return has_postgresql_support() and django.VERSION >= (1, 11)
class JSONField(object):
__dj11_field = None
__jsonfield_field = None
__name = None
def __init__(self, *args, **kwargs):
self.__args = args
self.__kwargs = kwargs
if django.VERSION >= (1, 11):
from django.contrib.postgres.fields import JSONField
self.__dj11_field = JSONField(*args, **kwargs)
try:
from jsonfield.fields import JSONField
self.__jsonfield_field = JSONField(*args, **kwargs)
except ImportError:
pass
def __real_field__(self):
if use_django_native_field():
assert self.__dj11_field
return self.__dj11_field
assert self.__jsonfield_field
return self.__jsonfield_field
def __getattr__(self, key):
return getattr(self.__real_field__(), key)
def __setattr__(self, key, value):
if key.startswith('_JSONField__'):
super(JSONField, self).__setattr__(key, value)
else:
setattr(self.__real__field(), key, value)
# we need to implement contribute_to_class so that the direct
# implementation from the two sub-fields is not used directly
def contribute_to_class(self, cls, name, private_only=False, virtual_only=False, **kwargs):
assert not virtual_only and not private_only, 'virtual_only / private_only are not supported'
assert not kwargs, 'new arguments to contribute_to_class not supported'
self.__name = name
if self.__dj11_field:
self.__dj11_field.set_attributes_from_name(name)
self.__dj11_field.model = cls
if self.__jsonfield_field:
self.__jsonfield_field.set_attributes_from_name(name)
self.__jsonfield_field.model = cls
cls._meta.add_field(self)
# the next two methods are useful for compatibilit with the migration engine
# inspect is used because migration autodetector cannot recognize this class
# as a subclass of models.Field.
def deconstruct(self):
d = (self.__name, 'authentic2.compat.JSONField', self.__args, self.__kwargs)
previous_frame = inspect.currentframe().f_back
if inspect.getframeinfo(previous_frame)[2] in ('serialize', 'deep_deconstruct'):
d = d[1:]
return d
def clone(self):
from copy import copy
new = copy(self)
if self.__dj11_field:
new.__dj11_field = new.__dj11_field.clone()
if self.__jsonfield_field:
new.__jsonfield_field = new.__jsonfield_field.clone()
return new
try:
from jsonfield import fields
except ImportError:
pass
else:
# prevent django-jsonfield from modifying postgresql connection when we are
# not using it
def configure_database_connection(connection, **kwargs):
if django.VERSION < (1, 11):
fields.configure_database_connection(connection, **kwargs)
fields.connection_created.disconnect(fields.configure_database_connection)
fields.connection_created.connect(configure_database_connection)

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2018-12-18 23:05
from __future__ import unicode_literals
import authentic2.compat
import authentic2_auth_oidc.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentic2_auth_oidc', '0006_oidcprovider_claims_parameter_supported'),
]
operations = [
migrations.AlterField(
model_name='oidcprovider',
name='jwkset_json',
field=authentic2.compat.JSONField(blank=True, null=True, validators=[authentic2_auth_oidc.models.validate_jwkset], verbose_name='JSON WebKey set'),
),
]

View File

@ -6,12 +6,13 @@ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from jsonfield import JSONField
from jwcrypto.jwk import JWKSet, InvalidJWKValue, JWK from jwcrypto.jwk import JWKSet, InvalidJWKValue, JWK
from django_rbac.utils import get_ou_model_name from django_rbac.utils import get_ou_model_name
from authentic2 import compat
from . import managers from . import managers
@ -89,7 +90,7 @@ class OIDCProvider(models.Model):
max_length=128, max_length=128,
blank=True, blank=True,
verbose_name=_('scopes')) verbose_name=_('scopes'))
jwkset_json = JSONField( jwkset_json = compat.JSONField(
verbose_name=_('JSON WebKey set'), verbose_name=_('JSON WebKey set'),
null=True, null=True,
blank=True, blank=True,

View File

@ -31,6 +31,7 @@ deps =
dj111: django<2.0 dj111: django<2.0
dj111: django-tables<2.0 dj111: django-tables<2.0
pg: psycopg2-binary pg: psycopg2-binary
dj111: psycopg2-binary
coverage coverage
pytest-cov pytest-cov
pytest-django pytest-django