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.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):
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):
plugins.init()
debug.HIDDEN_SETTINGS = re.compile(
'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
import inspect
import django
from django.conf import settings
from django.db import connection
from django.contrib.auth.tokens import PasswordResetTokenGenerator
try:
from django.contrib.auth import get_user_model
@ -14,10 +19,97 @@ try:
except ImportError:
from django.db.transaction import commit_on_success
from . import app_settings, utils
user_model_label = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
from django.contrib.auth.tokens import 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.core.exceptions import ValidationError
from jsonfield import JSONField
from jwcrypto.jwk import JWKSet, InvalidJWKValue, JWK
from django_rbac.utils import get_ou_model_name
from authentic2 import compat
from . import managers
@ -89,7 +90,7 @@ class OIDCProvider(models.Model):
max_length=128,
blank=True,
verbose_name=_('scopes'))
jwkset_json = JSONField(
jwkset_json = compat.JSONField(
verbose_name=_('JSON WebKey set'),
null=True,
blank=True,

View File

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