wip/74843-compatibilite-django #1

Merged
bdauvergne merged 11 commits from wip/74843-compatibilite-django into main 2023-02-27 10:38:51 +01:00
33 changed files with 144 additions and 241 deletions

View File

@ -106,13 +106,11 @@ setup(
'Programming Language :: Python', 'Programming Language :: Python',
], ],
install_requires=[ install_requires=[
'django<2.3', 'django<3.3',
'isodate', 'isodate',
'psycopg2<2.9',
'jsonschema', 'jsonschema',
'gadjo', 'gadjo',
'six', 'djangorestframework>=3.9,<3.13',
'djangorestframework>=3.3,<3.10',
'pytz', 'pytz',
'python-dateutil', 'python-dateutil',
'django-admin-rangefilter', 'django-admin-rangefilter',

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import io
import mock import mock
import pytest import pytest
import sys import sys
@ -9,12 +10,11 @@ from zoo.zoo_nanterre import utils
from django.core.management import call_command from django.core.management import call_command
from django.db.transaction import atomic from django.db.transaction import atomic
from django.utils.six import StringIO
def get_output_of_command(command, *args, **kwargs): def get_output_of_command(command, *args, **kwargs):
old_stdout = sys.stdout old_stdout = sys.stdout
output = sys.stdout = StringIO() output = sys.stdout = io.StringIO()
call_command(command, *args, **kwargs) call_command(command, *args, **kwargs)
sys.stdout = old_stdout sys.stdout = old_stdout
return output.getvalue() return output.getvalue()

View File

@ -5,14 +5,13 @@ import datetime
import isodate import isodate
import requests import requests
import threading import threading
import urllib.parse
import pytest import pytest
import httmock import httmock
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.timezone import now from django.utils.timezone import now
from zoo.zoo_data.models import Entity, Relation, Log, Job from zoo.zoo_data.models import Entity, Relation, Log, Job
@ -693,9 +692,9 @@ def test_create_individu(settings, transactional_db, app, app_noauth, rsu_schema
more = response.json.get('more') more = response.json.get('more')
if more: if more:
assert 'cookie' in response.json assert 'cookie' in response.json
parsed = urlparse.urlparse(response.json['more']) parsed = urllib.parse.urlparse(response.json['more'])
query = parsed.query query = parsed.query
assert urlparse.parse_qs(query)['cookie'] == [response.json['cookie']] assert urllib.parse.parse_qs(query)['cookie'] == [response.json['cookie']]
assert sorted(d['id'] for d in all_data) == sorted(qs.values_list('id', flat=True)) assert sorted(d['id'] for d in all_data) == sorted(qs.values_list('id', flat=True))
assert count == qs.count() assert count == qs.count()
assert 'more' not in response.json assert 'more' not in response.json
@ -1294,7 +1293,7 @@ def test_passage_a_la_majorite(db, settings, nanterre_classic_family, freezer):
Job.redo(timestamp=now() + datetime.timedelta(seconds=20)) Job.redo(timestamp=now() + datetime.timedelta(seconds=20))
assert len(requests) == 1 assert len(requests) == 1
req_content = json.loads(force_text(requests[0].body)) req_content = json.loads(requests[0].body)
assert req_content['metadonnees']['service'] == 'passage-majorite' assert req_content['metadonnees']['service'] == 'passage-majorite'
assert len(req_content['fragments']) == 3 assert len(req_content['fragments']) == 3
assert req_content['fragments'][0]['type'] == 'maj-adresse' assert req_content['fragments'][0]['type'] == 'maj-adresse'

View File

@ -1,8 +1,8 @@
import copy import copy
import urllib
from django.urls import reverse from django.urls import reverse
from django.core.management import call_command from django.core.management import call_command
from django.utils.six.moves.urllib import parse as urlparse
from zoo.zoo_nanterre.models import Duplicate from zoo.zoo_nanterre.models import Duplicate
from zoo.zoo_data.models import Log, Entity from zoo.zoo_data.models import Log, Entity
@ -43,8 +43,8 @@ def test_list_doublons(nanterre_classic_family, app):
assert response.json['err'] == 0 assert response.json['err'] == 0
assert 'more' in response.json assert 'more' in response.json
assert 'cookie' in response.json assert 'cookie' in response.json
assert response.json['cookie'] == urlparse.parse_qs( assert response.json['cookie'] == urllib.parse.parse_qs(
urlparse.urlparse( urllib.parse.urlparse(
response.json['more']).query)['cookie'][0] response.json['more']).query)['cookie'][0]
assert len(response.json['data']) >= 10 assert len(response.json['data']) >= 10
assert response.json['data'][0]['id'] == d.id assert response.json['data'][0]['id'] == d.id

View File

@ -4,7 +4,6 @@ import json
import httmock import httmock
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_text
from zoo.zoo_nanterre.fragments import Synchronization from zoo.zoo_nanterre.fragments import Synchronization
from zoo.models import Job from zoo.models import Job
@ -29,7 +28,7 @@ def test_synchro_full(app, nanterre_classic_family):
@httmock.urlmatch() @httmock.urlmatch()
def technocarte_ok(url, request): def technocarte_ok(url, request):
request_bodies.append(json.loads(force_text(request.body))) request_bodies.append(json.loads(request.body))
return httmock.response( return httmock.response(
200, [ 200, [
{ {
@ -228,7 +227,7 @@ def test_infor(app, nanterre_classic_family):
@httmock.urlmatch() @httmock.urlmatch()
def infor_ok(url, request): def infor_ok(url, request):
request_bodies.append(json.loads(force_text(request.body))) request_bodies.append(json.loads(request.body))
return httmock.response( return httmock.response(
200, { 200, {
'http_code': 200, 'http_code': 200,
@ -277,7 +276,7 @@ def test_infor(app, nanterre_classic_family):
@httmock.urlmatch() @httmock.urlmatch()
def infor_nok(url, request): def infor_nok(url, request):
request_bodies.append(json.loads(force_text(request.body))) request_bodies.append(json.loads(request.body))
return httmock.response( return httmock.response(
200, { 200, {
'http_code': 500, 'http_code': 500,

View File

@ -1,10 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import csv import csv
import io
from django.utils import six
from django.utils.encoding import force_bytes
from django.utils.six import StringIO
from webtest import Upload from webtest import Upload
@ -26,19 +23,15 @@ def test_synchronize_federations(settings, app, nanterre_classic_family, admin):
response = response.click(u'Synchroniser les fédérations') response = response.click(u'Synchroniser les fédérations')
response = response.click(u'Nouvel import') response = response.click(u'Nouvel import')
response.form.set('app_id', 'technocarte') response.form.set('app_id', 'technocarte')
content = force_bytes('\n'.join(map(str, [f['kevin'].id + 1000, f['marie'].id + 1000, '99999']))) content = ('\n'.join(map(str, [f['kevin'].id + 1000, f['marie'].id + 1000, '99999']))).encode()
response.form.set('csv_uploaded', Upload('federations.csv', content, 'application/octet-stream')) response.form.set('csv_uploaded', Upload('federations.csv', content, 'application/octet-stream'))
response = response.form.submit().follow() response = response.form.submit().follow()
assert len(response.pyquery('table#result-list tbody tr')) == 1 assert len(response.pyquery('table#result-list tbody tr')) == 1
response = response.click('Rapport') response = response.click('Rapport')
def check_csv_response(csv_response): def check_csv_response(csv_response):
if six.PY3: reader = csv.DictReader(io.StringIO(csv_response.text))
reader = csv.DictReader(StringIO(csv_response.text)) reader.fieldnames = reader.reader.__next__()
reader.fieldnames = reader.reader.__next__()
else:
reader = csv.DictReader(StringIO(csv_response.content))
reader.fieldnames = reader.reader.next()
rows = list(reader) rows = list(reader)
def rows_by_action(action): def rows_by_action(action):

View File

@ -18,11 +18,12 @@ setenv =
usedevelop = true usedevelop = true
deps = deps =
dj22: django<2.3 dj22: django<2.3
dj22: psycopg2-binary<2.9
dj22: djangorestframework>=3.9.2,<3.10
dj32: django>=3.2,<3.3 dj32: django>=3.2,<3.3
djangorestframework>=3.9.2,<3.10 dj32: psycopg2-binary
pip>8 pip>8
pytest-flakes pytest-flakes
pg: psycopg2<2.9
coverage coverage
pytest-cov pytest-cov
pytest-django pytest-django

View File

@ -14,30 +14,15 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
"""zoo URL Configuration from django.urls import re_path, include
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url, include
from django.contrib import admin from django.contrib import admin
from .views import login, logout from .views import login, logout
urlpatterns = [ urlpatterns = [
url(r'^admin/', admin.site.urls), re_path(r'^admin/', admin.site.urls),
url(r'^demo/', include('zoo.zoo_demo.urls')), re_path(r'^demo/', include('zoo.zoo_demo.urls')),
url(r'^rsu/', include('zoo.zoo_nanterre.urls')), re_path(r'^rsu/', include('zoo.zoo_nanterre.urls')),
url(r'^logout/$', logout, name='logout'), re_path(r'^logout/$', logout, name='logout'),
url(r'^login/$', login, name='auth_login'), re_path(r'^login/$', login, name='auth_login'),
] ]

View File

@ -16,8 +16,6 @@
import unicodedata import unicodedata
from django.utils.encoding import force_text
from rest_framework.views import exception_handler from rest_framework.views import exception_handler
from rest_framework.response import Response from rest_framework.response import Response
@ -33,8 +31,8 @@ def rest_exception_handler(exc, context):
raise raise
response = Response({ response = Response({
'err': 1, 'err': 1,
'exc_class': force_text(exc.__class__), 'exc_class': str(exc.__class__),
'exc_value': force_text(exc), 'exc_value': str(exc),
}) })
response.status = 400 response.status = 400
return response return response

View File

@ -14,4 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
default_app_config = 'zoo.zoo_data.apps.ZooDataConfig' import django
if django.VERSION < (3, 2):
default_app_config = 'zoo.zoo_data.apps.ZooDataConfig'

View File

@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class ZooDataConfig(AppConfig): class ZooDataConfig(AppConfig):

View File

@ -16,87 +16,40 @@
import django import django
from django.db.models import Transform, TextField, DateField from django.db.models import Transform, CharField, DateField
from django.db.models.functions import Lower
from django.contrib.postgres.fields import jsonb from django.contrib.postgres.fields import jsonb
try: try:
from django.contrib.postgres.fields.jsonb import KeyTransform, KeyTransformTextLookupMixin from django.db.models.fields.json import KeyTransform, KeyTransformTextLookupMixin
except ImportError: except ImportError:
# backport from Django 2.x from django.contrib.postgres.fields.jsonb import KeyTransform, KeyTransformTextLookupMixin
class KeyTransform(Transform):
operator = '->'
nested_operator = '#>'
def __init__(self, key_name, *args, **kwargs):
super(KeyTransform, self).__init__(*args, **kwargs)
self.key_name = key_name
def as_sql(self, compiler, connection):
key_transforms = [self.key_name]
previous = self.lhs
while isinstance(previous, KeyTransform):
key_transforms.insert(0, previous.key_name)
previous = previous.lhs
lhs, params = compiler.compile(previous)
if len(key_transforms) > 1:
return "(%s %s %%s)" % (lhs, self.nested_operator), [key_transforms] + params
try:
int(self.key_name)
except ValueError:
lookup = "'%s'" % self.key_name
else:
lookup = "%s" % self.key_name
return "(%s %s %s)" % (lhs, self.operator, lookup), params
jsonb.KeyTransform = KeyTransform
class KeyTextTransform(KeyTransform):
operator = '->>'
nested_operator = '#>>'
_output_field = TextField()
class KeyTransformTextLookupMixin(object):
"""
Mixin for combining with a lookup expecting a text lhs from a JSONField
key lookup. Make use of the ->> operator instead of casting key values to
text and performing the lookup on the resulting representation.
"""
def __init__(self, key_transform, *args, **kwargs):
assert isinstance(key_transform, KeyTransform)
key_text_transform = KeyTextTransform(
key_transform.key_name, *key_transform.source_expressions, **key_transform.extra
)
super(KeyTransformTextLookupMixin, self).__init__(key_text_transform, *args, **kwargs)
class Lower(Transform):
lookup_name = 'lower'
function = 'LOWER'
TextField.register_lookup(Lower)
class Unaccent(Transform): class Unaccent(Transform):
lookup_name = 'unaccent' lookup_name = 'unaccent'
function = 'immutable_unaccent' function = 'immutable_unaccent'
output_field = CharField()
TextField.register_lookup(Unaccent) CharField.register_lookup(Unaccent)
CharField.register_lookup(Lower)
class Normalize(Transform): class Normalize(Transform):
lookup_name = 'normalize' lookup_name = 'normalize'
function = 'immutable_normalize' function = 'immutable_normalize'
output_field = CharField()
TextField.register_lookup(Normalize) CharField.register_lookup(Normalize)
class Date(Transform): class Date(Transform):
lookup_name = 'timestamp' lookup_name = 'timestamp'
function = 'immutable_date' function = 'immutable_date'
_output_field = DateField() output_field = DateField()
TextField.register_lookup(Date) CharField.register_lookup(Date)
class JSONUnaccent(KeyTransformTextLookupMixin, Unaccent): class JSONUnaccent(KeyTransformTextLookupMixin, Unaccent):

View File

@ -24,22 +24,20 @@ import datetime
from django.db import models, connection from django.db import models, connection
from django.db.models import F, Value from django.db.models import F, Value
from django.db.models.functions import Lower
from django.db.models.query import QuerySet, Q from django.db.models.query import QuerySet, Q
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_text from django.utils.translation import gettext_lazy as _
from django.utils.six import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import now from django.utils.timezone import now
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.contrib.postgres.search import TrigramDistance from django.contrib.postgres.search import TrigramDistance
from .search import Unaccent, Lower, JSONTextRef from .search import Unaccent, JSONTextRef
from zoo.zoo_meta.validators import schema_validator from zoo.zoo_meta.validators import schema_validator
@python_2_unicode_compatible
class Transaction(models.Model): class Transaction(models.Model):
created = models.DateTimeField( created = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
@ -54,7 +52,7 @@ class Transaction(models.Model):
null=True) null=True)
def __str__(self): def __str__(self):
return force_text(self.id) return str(self.id)
@classmethod @classmethod
def get_transaction(self): def get_transaction(self):
@ -77,7 +75,7 @@ class EntityQuerySet(QuerySet):
for key, value in kwargs.items(): for key, value in kwargs.items():
filters.append(Q(**{ filters.append(Q(**{
'content__' + key + '__unaccent__lower__trigram_similar': 'content__' + key + '__unaccent__lower__trigram_similar':
Lower(Unaccent(Value(value))), Unaccent(Lower(Value(value))),
})) }))
qs = qs.filter(functools.reduce(__or__, filters)) qs = qs.filter(functools.reduce(__or__, filters))
expressions = [] expressions = []
@ -93,7 +91,6 @@ class EntityQuerySet(QuerySet):
return qs return qs
@python_2_unicode_compatible
class CommonData(models.Model): class CommonData(models.Model):
def clean(self): def clean(self):
if self.schema: if self.schema:
@ -103,7 +100,7 @@ class CommonData(models.Model):
raise ValidationError({'content': e}) raise ValidationError({'content': e})
def __str__(self): def __str__(self):
return force_text(self.id) return str(self.id)
class Meta: class Meta:
abstract = True abstract = True
@ -255,7 +252,6 @@ class JobQuerySet(QuerySet):
return self.filter(**{'content__$classpath': class_path}) return self.filter(**{'content__$classpath': class_path})
@python_2_unicode_compatible
class Job(models.Model): class Job(models.Model):
'''Store synchronization messages sent to applications''' '''Store synchronization messages sent to applications'''
SCHEDULER_STEP = 60 * 5 # 5 minutes SCHEDULER_STEP = 60 * 5 # 5 minutes
@ -334,7 +330,7 @@ class Job(models.Model):
job.state = cls.STATE_UNRECOVERABLE_ERROR job.state = cls.STATE_UNRECOVERABLE_ERROR
error = job.content.setdefault('error', {}) error = job.content.setdefault('error', {})
error['code'] = 'internal-server-error' error['code'] = 'internal-server-error'
error['exc_detail'] = force_text(e) error['exc_detail'] = str(e)
error['exc_tb'] = traceback.format_exc() error['exc_tb'] = traceback.format_exc()
job.get_logger().exception('exception during job %s', job.admin_url) job.get_logger().exception('exception during job %s', job.admin_url)
job.save() job.save()
@ -366,7 +362,7 @@ class Job(models.Model):
url = self.admin_url url = self.admin_url
self.get_logger().exception('exception during job %s', url) self.get_logger().exception('exception during job %s', url)
self.state = self.STATE_UNRECOVERABLE_ERROR self.state = self.STATE_UNRECOVERABLE_ERROR
self.content['$exc_detail'] = force_text(e) self.content['$exc_detail'] = str(e)
self.content['$exc_tb'] = traceback.format_exc() self.content['$exc_tb'] = traceback.format_exc()
self.content['$classpath'] = self.get_classpath(action) self.content['$classpath'] = self.get_classpath(action)
self.save() self.save()

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.db.models import Func, Value from django.db.models import Func, Value, CharField
class Unaccent(Func): class Unaccent(Func):
@ -27,11 +27,6 @@ class Normalize(Func):
arity = 1 arity = 1
class Lower(Func):
function = 'LOWER'
arity = 1
class JSONRef(Func): class JSONRef(Func):
function = '' function = ''
arg_joiner = '->' arg_joiner = '->'
@ -46,6 +41,7 @@ class JSONTextRef(Func):
function = '' function = ''
arg_joiner = '->>' arg_joiner = '->>'
arity = 2 arity = 2
output_field = CharField()
def __init__(self, *expressions, **extra): def __init__(self, *expressions, **extra):
jsonb = expressions[0] jsonb = expressions[0]

View File

@ -856,4 +856,4 @@ c&&c.destroy(),
c=new f(this.get(0),a),this.data("jsoneditor",c), c=new f(this.get(0),a),this.data("jsoneditor",c),
// Setup event listeners // Setup event listeners
c.on("change",function(){b.trigger("change")}),c.on("ready",function(){b.trigger("ready")}))}return this}}}(),window.JSONEditor=f}(); c.on("change",function(){b.trigger("change")}),c.on("ready",function(){b.trigger("ready")}))}return this}}}(),window.JSONEditor=f}();
//# sourceMappingURL=jsoneditor.min.js.map //# sourceMappingURL=jsoneditor.min.js.map

View File

@ -14,11 +14,11 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf.urls import url from django.urls import re_path
from .views import schemas, schema from .views import schemas, schema
urlpatterns = [ urlpatterns = [
url(r'^schemas/$', schemas, name='demo-schemas'), re_path(r'^schemas/$', schemas, name='demo-schemas'),
url(r'^schemas/(?P<name>\w*)/$', schema, name='demo-schema'), re_path(r'^schemas/(?P<name>\w*)/$', schema, name='demo-schema'),
] ]

View File

@ -14,4 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
default_app_config = 'zoo.zoo_meta.apps.ZooMetaAppConfig' import django
if django.VERSION < (3, 2):
default_app_config = 'zoo.zoo_meta.apps.ZooMetaAppConfig'

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db.models.signals import post_migrate, post_save from django.db.models.signals import post_migrate, post_save
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -16,7 +16,6 @@
from __future__ import print_function from __future__ import print_function
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils.encoding import force_text
from zoo.zoo_meta.models import EntitySchema from zoo.zoo_meta.models import EntitySchema
@ -25,7 +24,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
for schema in EntitySchema.objects.all(): for schema in EntitySchema.objects.all():
if options['verbosity'] >= 1: if options['verbosity'] >= 1:
print('Rebuilding index for', force_text(schema), end=' ') print('Rebuilding index for', schema, end=' ')
schema.rebuild_indexes() schema.rebuild_indexes()
if options['verbosity'] >= 1: if options['verbosity'] >= 1:
print(' Done.') print(' Done.')

View File

@ -18,8 +18,7 @@ from hashlib import md5
from django.apps import apps from django.apps import apps
from django.db import models, connection from django.db import models, connection
from django.utils.encoding import force_bytes, force_text from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
@ -74,7 +73,7 @@ class CommonSchema(models.Model):
def rebuild_string_index(self, cursor, table, path): def rebuild_string_index(self, cursor, table, path):
expr = 'immutable_normalize((content%s))' % self.path_to_sql_expr(path) expr = 'immutable_normalize((content%s))' % self.path_to_sql_expr(path)
key = md5(force_bytes(expr)).hexdigest()[:8] key = md5(expr.encode()).hexdigest()[:8]
sql = ('CREATE INDEX zoo_entity_%s_gin_%s_dynamic_idx ON %s USING gin ((%s) ' sql = ('CREATE INDEX zoo_entity_%s_gin_%s_dynamic_idx ON %s USING gin ((%s) '
' gin_trgm_ops) WHERE schema_id = %s' % (key, self.id, table, expr, self.id)) ' gin_trgm_ops) WHERE schema_id = %s' % (key, self.id, table, expr, self.id))
cursor.execute(sql) cursor.execute(sql)
@ -84,7 +83,7 @@ class CommonSchema(models.Model):
def rebuild_string_date_time_index(self, cursor, table, path): def rebuild_string_date_time_index(self, cursor, table, path):
expr = 'immutable_date(content%s)' % self.path_to_sql_expr(path) expr = 'immutable_date(content%s)' % self.path_to_sql_expr(path)
key = md5(force_bytes(expr)).hexdigest()[:8] key = md5(expr.encode()).hexdigest()[:8]
sql = ('CREATE INDEX zoo_entity_%s_%s_dynamic_idx ON %s (%s) ' sql = ('CREATE INDEX zoo_entity_%s_%s_dynamic_idx ON %s (%s) '
'WHERE schema_id = %s' % (key, self.id, table, expr, self.id)) 'WHERE schema_id = %s' % (key, self.id, table, expr, self.id))
cursor.execute(sql) cursor.execute(sql)
@ -99,7 +98,7 @@ class CommonSchema(models.Model):
else: else:
raise NotImplementedError(self) raise NotImplementedError(self)
key = md5(force_bytes(expr)).hexdigest()[:8] key = md5(expr.encode()).hexdigest()[:8]
gin_sql = ('CREATE INDEX zoo_entity_%s_gin_%s_dynamic_idx ON %s USING gin ((%s) ' gin_sql = ('CREATE INDEX zoo_entity_%s_gin_%s_dynamic_idx ON %s USING gin ((%s) '
'gin_trgm_ops) WHERE schema_id = %s' % (key, self.id, table, expr, self.id)) 'gin_trgm_ops) WHERE schema_id = %s' % (key, self.id, table, expr, self.id))
gist_sql = ('CREATE INDEX zoo_entity_%s_gist_%s_dynamic_idx ON %s USING gist ((%s)' gist_sql = ('CREATE INDEX zoo_entity_%s_gist_%s_dynamic_idx ON %s USING gist ((%s)'
@ -147,8 +146,8 @@ class CommonSchema(models.Model):
try: try:
return eval(self.caption_template, {}, value.content) return eval(self.caption_template, {}, value.content)
except Exception as e: except Exception as e:
return force_text(e) return str(e)
return force_text(value.id) return str(value.id)
class Meta: class Meta:
abstract = True abstract = True

View File

@ -14,4 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
default_app_config = 'zoo.zoo_nanterre.apps.ZooNanterreConfig' import django
if django.VERSION < (3, 2):
default_app_config = 'zoo.zoo_nanterre.apps.ZooNanterreConfig'

View File

@ -32,7 +32,6 @@ from django.db.models.query import Q
from django.db.transaction import non_atomic_requests, atomic from django.db.transaction import non_atomic_requests, atomic
from django.urls import reverse from django.urls import reverse
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.utils.encoding import force_text
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.http import urlencode from django.utils.http import urlencode
@ -135,7 +134,7 @@ class TransactionalView(APIView):
content = { content = {
'request': self.request.data, 'request': self.request.data,
'status_code': 500, 'status_code': 500,
'$exc_detail': force_text(exc), '$exc_detail': str(exc),
'$exc_tb': traceback.format_exc(), '$exc_tb': traceback.format_exc(),
} }
self.transaction.content = content self.transaction.content = content
@ -2055,7 +2054,7 @@ class FalsePositiveView(DoublonActionView):
except AssertionError as e: except AssertionError as e:
return Response({ return Response({
'err': 1, 'err': 1,
'errors': force_text(e), 'errors': str(e),
}, status=500) }, status=500)
@ -2096,7 +2095,7 @@ class DedupView(DoublonActionView):
except AssertionError as e: except AssertionError as e:
return Response({ return Response({
'err': 1, 'err': 1,
'errors': force_text(e), 'errors': str(e),
}, status=500) }, status=500)
dedup = DedupView.as_view() dedup = DedupView.as_view()

View File

@ -19,8 +19,8 @@
import functools import functools
from django.apps import AppConfig from django.apps import AppConfig
from django.conf.urls import url from django.urls import re_path
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
@ -93,14 +93,14 @@ class ZooNanterreConfig(AppConfig):
urls = [] urls = []
for desc in descs: for desc in descs:
urls.append(url( urls.append(re_path(
r'^synchronize-federations/%s$' % desc['re'], r'^synchronize-federations/%s$' % desc['re'],
model_admin.admin_site.admin_view( model_admin.admin_site.admin_view(
getattr(views, 'synchronize_federations' + desc['view'])), getattr(views, 'synchronize_federations' + desc['view'])),
kwargs={'model_admin': model_admin}, kwargs={'model_admin': model_admin},
name='synchronize-federations' + desc['name'], name='synchronize-federations' + desc['name'],
)) ))
urls.append(url( urls.append(re_path(
r'^inactive/', r'^inactive/',
model_admin.admin_site.admin_view( model_admin.admin_site.admin_view(
getattr(views, 'inactive_index')), getattr(views, 'inactive_index')),

View File

@ -15,8 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django import forms from django import forms
from django.utils.encoding import force_text from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from .utils import PersonSearch, get_applications, get_application from .utils import PersonSearch, get_applications, get_application
@ -63,13 +62,15 @@ class SynchronizeFederationsForm(forms.Form):
def clean_csv_uploaded(self): def clean_csv_uploaded(self):
csv_uploaded = self.cleaned_data['csv_uploaded'] csv_uploaded = self.cleaned_data['csv_uploaded']
errors = [] errors = []
csv_uploaded.seek(0)
for i, line in enumerate(csv_uploaded): for i, line in enumerate(csv_uploaded):
try: try:
force_text(line).encode('ascii') line.decode('ascii')
# works with pyhton2 and 3 except UnicodeError as e:
except (UnicodeEncodeError, UnicodeDecodeError) as e:
errors.append(_(u'non-ASCII character on line {0} and column {1}').format( errors.append(_(u'non-ASCII character on line {0} and column {1}').format(
i + 1, e.start + 1)) i + 1, e.start + 1))
# restore file state
csv_uploaded.seek(0)
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
return csv_uploaded return csv_uploaded

View File

@ -5,7 +5,6 @@ import datetime
import requests import requests
from requests.exceptions import RequestException from requests.exceptions import RequestException
from django.utils.encoding import force_text
from django.utils.timezone import now from django.utils.timezone import now
from django.conf import settings from django.conf import settings
from django.db import DatabaseError from django.db import DatabaseError
@ -204,7 +203,7 @@ class FragmentBuilder(object):
error_detail = u'erreur réseau/SSL ou expiration' error_detail = u'erreur réseau/SSL ou expiration'
self.error = { self.error = {
'code': 'transport-error', 'code': 'transport-error',
'detail': force_text(e), 'detail': str(e),
} }
state = self.state_on_network_error state = self.state_on_network_error
else: else:

View File

@ -23,7 +23,6 @@ import datetime
import django import django
from django.core.management.base import BaseCommand, CommandParser from django.core.management.base import BaseCommand, CommandParser
from django.utils.six import python_2_unicode_compatible
from django.utils.timezone import now from django.utils.timezone import now
from zoo.zoo_nanterre.utils import individu_caption from zoo.zoo_nanterre.utils import individu_caption
@ -31,7 +30,6 @@ from zoo.zoo_nanterre.duplicates import find_duplicates
from zoo.zoo_nanterre.models import Duplicate from zoo.zoo_nanterre.models import Duplicate
@python_2_unicode_compatible
class Table(object): class Table(object):
def __init__(self, names): def __init__(self, names):
self.size = len(names) self.size = len(names)

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.db import models from django.db import models

View File

@ -8,7 +8,6 @@ from zoo.models import Job
import requests import requests
from django.utils.encoding import force_text
from django.utils.timezone import now from django.utils.timezone import now
from django.conf import settings from django.conf import settings
@ -64,7 +63,7 @@ class QF(object):
if response: if response:
for qf in response: for qf in response:
try: try:
qf['annee_imposition'] = force_text(int(re.findall(r'(\d+)', qf['libelle'])[0]) - 1) qf['annee_imposition'] = str(int(re.findall(r'(\d+)', qf['libelle'])[0]) - 1)
except Exception: except Exception:
qf['annee_imposition'] = 'inconnue' qf['annee_imposition'] = 'inconnue'
return response, error return response, error
@ -148,7 +147,7 @@ class QF(object):
else: else:
return response[0], None return response[0], None
else: else:
return None, u'Implicit calcul-qf réponse invalide: %r' % force_text(response)[:1024] return None, 'Implicit calcul-qf réponse invalide: %r' % str(response)[:1024]
def lire_quotient_familial(self, individu, date_de_reference): def lire_quotient_familial(self, individu, date_de_reference):
federation = individu.content['cles_de_federation'].get('implicit') federation = individu.content['cles_de_federation'].get('implicit')
@ -168,8 +167,8 @@ class QF(object):
if isinstance(response, list): if isinstance(response, list):
return response, None return response, None
else: else:
return None, (u'Implicit lire-quotient-familial réponse invalide: %r' return None, ('Implicit lire-quotient-familial réponse invalide: %r'
% force_text(response)[:1024]) % str(response)[:1024])
def editer_carte(self, individu, id_qf): def editer_carte(self, individu, id_qf):
federation = individu.content['cles_de_federation'].get('implicit') federation = individu.content['cles_de_federation'].get('implicit')

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime import datetime
import decimal import decimal
import urllib.parse
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from django.conf import settings from django.conf import settings
from django.utils import six, timezone from django.utils import timezone
from django.utils.six.moves.urllib import parse as urlparse
import requests import requests
@ -36,13 +36,13 @@ class Saga(object):
@property @property
def creance_url(self): def creance_url(self):
return urlparse.urljoin( return urllib.parse.urljoin(
self.url, self.url,
'/%s/services/etat_facture_creance_literal' % self.base_uri) '/%s/services/etat_facture_creance_literal' % self.base_uri)
@property @property
def paiement_url(self): def paiement_url(self):
return urlparse.urljoin( return urllib.parse.urljoin(
self.url, self.url,
'/%s/services/paiement_internet_ws_literal' % self.base_uri) '/%s/services/paiement_internet_ws_literal' % self.base_uri)
@ -194,7 +194,7 @@ class Saga(object):
<urlretour_synchrone>{urlretour_synchrone}</urlretour_synchrone> <urlretour_synchrone>{urlretour_synchrone}</urlretour_synchrone>
</Transaction>''' </Transaction>'''
assert factures, u'factures ne doit pas être vide' assert factures, u'factures ne doit pas être vide'
id_facture = u'--'.join(six.text_type(facture.num) for facture in factures) id_facture = u'--'.join(str(facture.num) for facture in factures)
montant = sum(facture.reste_a_payer for facture in factures) montant = sum(facture.reste_a_payer for facture in factures)
tree, error = self.soap_call( tree, error = self.soap_call(
self.paiement_url, body, 'TransactionReturn', self.paiement_url, body, 'TransactionReturn',

View File

@ -17,15 +17,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import csv import csv
import io
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.db import DatabaseError from django.db import DatabaseError
from django.db.transaction import atomic from django.db.transaction import atomic
from django.utils import six
from django.utils.encoding import force_bytes
from django.utils.six import StringIO
from zoo.zoo_meta.models import EntitySchema from zoo.zoo_meta.models import EntitySchema
from zoo.zoo_data.models import Job, Entity, Transaction, Log from zoo.zoo_data.models import Job, Entity, Transaction, Log
@ -106,15 +104,12 @@ class SynchronizeFederationsImport(object):
self.report('report') self.report('report')
def report(self, target): def report(self, target):
output_file = StringIO() output_file = io.StringIO()
writer = csv.writer(output_file) writer = csv.writer(output_file)
writer.writerow(['RSU ID', 'prenoms', 'nom de naissance', writer.writerow(['RSU ID', 'prenoms', 'nom de naissance',
'nom d\'usage', 'application', 'federation', 'action']) 'nom d\'usage', 'application', 'federation', 'action'])
for action in self.actions: for action in self.actions:
if six.PY3: action = [v for v in action]
action = [v for v in action]
else:
action = [force_bytes(v) for v in action]
writer.writerow(action) writer.writerow(action)
setattr(self.action, target + '_csv_filename', setattr(self.action, target + '_csv_filename',
self.action.csv_filename + '-report.csv') self.action.csv_filename + '-report.csv')

View File

@ -14,74 +14,74 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf.urls import url from django.urls import re_path
from .views import demo, search, import_control from .views import demo, search, import_control
from . import api_views from . import api_views
urlpatterns = [ urlpatterns = [
url(r'^demo/$', demo, name='demo'), re_path(r'^demo/$', demo, name='demo'),
url(r'^demo/search/$', search, name='demo'), re_path(r'^demo/search/$', search, name='demo'),
url(r'^import_control/$', import_control, name='demo'), re_path(r'^import_control/$', import_control, name='demo'),
url(r'^search/$', api_views.search, name='rsu-api-search'), re_path(r'^search/$', api_views.search, name='rsu-api-search'),
url(r'^individu/(?P<identifier>[-\w]+)/$', api_views.reseau, name='rsu-api-reseau'), re_path(r'^individu/(?P<identifier>[-\w]+)/$', api_views.reseau, name='rsu-api-reseau'),
url(r'^individu/(?P<identifier>[-\w]+)/suppression/$', api_views.suppression_individu, re_path(r'^individu/(?P<identifier>[-\w]+)/suppression/$', api_views.suppression_individu,
name='rsu-api-suppression-individu'), name='rsu-api-suppression-individu'),
url(r'^individu/(?P<identifier>[-\w]+)/liste/$', api_views.reseau_liste, re_path(r'^individu/(?P<identifier>[-\w]+)/liste/$', api_views.reseau_liste,
name='rsu-api-reseau-liste'), name='rsu-api-reseau-liste'),
url(r'^individu/(?P<identifier>[-\w]+)/journal/$', api_views.journal, name='rsu-api-journal'), re_path(r'^individu/(?P<identifier>[-\w]+)/journal/$', api_views.journal, name='rsu-api-journal'),
url(r'^individu/(?P<identifier>[-\w]+)/declaration-responsabilite-legale/$', re_path(r'^individu/(?P<identifier>[-\w]+)/declaration-responsabilite-legale/$',
api_views.declaration_responsabilite_legale, api_views.declaration_responsabilite_legale,
name='rsu-api-declaration-responsabilite-legale'), name='rsu-api-declaration-responsabilite-legale'),
url(r'^individu/(?P<identifier>[-\w]+)/declaration-adresse-principale/$', re_path(r'^individu/(?P<identifier>[-\w]+)/declaration-adresse-principale/$',
api_views.declaration_adresse_principale, api_views.declaration_adresse_principale,
name='rsu-api-declaration-adresse-principale'), name='rsu-api-declaration-adresse-principale'),
url(r'^individu/(?P<identifier>[-\w]+)/changement-de-situation-maritale/$', re_path(r'^individu/(?P<identifier>[-\w]+)/changement-de-situation-maritale/$',
api_views.changement_de_situation_maritale, api_views.changement_de_situation_maritale,
name='rsu-api-changement-de-situation-maritale'), name='rsu-api-changement-de-situation-maritale'),
url(r'^individu/(?P<identifier>[-\w]+)/separation/$', re_path(r'^individu/(?P<identifier>[-\w]+)/separation/$',
api_views.separation, name='rsu-api-separation'), api_views.separation, name='rsu-api-separation'),
url(r'^individu/(?P<identifier>[-\w]+)/declaration-de-deces/$', re_path(r'^individu/(?P<identifier>[-\w]+)/declaration-de-deces/$',
api_views.declaration_de_deces, name='rsu-api-declaration-de-deces'), api_views.declaration_de_deces, name='rsu-api-declaration-de-deces'),
url(r'^individu/(?P<identifier>[-\w]+)/(?P<identifier_enfant>[-\w]+)/' re_path(r'^individu/(?P<identifier>[-\w]+)/(?P<identifier_enfant>[-\w]+)/'
'suppression-lien-de-responsabilite/$', 'suppression-lien-de-responsabilite/$',
api_views.suppression_lien_de_responsabilite, api_views.suppression_lien_de_responsabilite,
name='rsu-api-suppression-lien-de-responsabilite'), name='rsu-api-suppression-lien-de-responsabilite'),
url(r'^individu/$', api_views.create_individu, name='rsu-api-create-individu'), re_path(r'^individu/$', api_views.create_individu, name='rsu-api-create-individu'),
url(r'^individu/(?P<identifier>[-\w]+)/federation/(?P<application>\w+)/$', api_views.federation, re_path(r'^individu/(?P<identifier>[-\w]+)/federation/(?P<application>\w+)/$', api_views.federation,
name='rsu-api-federation'), name='rsu-api-federation'),
url(r'^declaration-union/$', api_views.declaration_union, re_path(r'^declaration-union/$', api_views.declaration_union,
name='rsu-api-declaration-union'), name='rsu-api-declaration-union'),
url(r'^synchronisation/$', api_views.synchronization, re_path(r'^synchronisation/$', api_views.synchronization,
name='rsu-api-synchronization'), name='rsu-api-synchronization'),
url(r'^saga/retour-asynchrone/$', api_views.saga_retour_asynchrone, re_path(r'^saga/retour-asynchrone/$', api_views.saga_retour_asynchrone,
name='rsu-api-saga-retour-asynchrone'), name='rsu-api-saga-retour-asynchrone'),
url(r'^saga/retour-synchrone/$', api_views.saga_retour_synchrone, re_path(r'^saga/retour-synchrone/$', api_views.saga_retour_synchrone,
name='rsu-api-saga-retour-synchrone'), name='rsu-api-saga-retour-synchrone'),
url(r'^saga/tiers/(?P<application>\w+)/(?P<identifier>[-\w]+)/$', api_views.saga_tiers, re_path(r'^saga/tiers/(?P<application>\w+)/(?P<identifier>[-\w]+)/$', api_views.saga_tiers,
name='rsu-api-saga-tiers'), name='rsu-api-saga-tiers'),
url(r'^saga/(?P<identifier>[-\w]+)/factures/$', api_views.saga_factures, re_path(r'^saga/(?P<identifier>[-\w]+)/factures/$', api_views.saga_factures,
name='rsu-api-saga-factures'), name='rsu-api-saga-factures'),
url(r'^saga/(?P<identifier>[-\w]+)/transaction/$', api_views.saga_transaction, re_path(r'^saga/(?P<identifier>[-\w]+)/transaction/$', api_views.saga_transaction,
name='rsu-api-saga-transaction'), name='rsu-api-saga-transaction'),
url(r'^qf/lire-quotients-valides/$', api_views.qf_lire_quotiens_valides, re_path(r'^qf/lire-quotients-valides/$', api_views.qf_lire_quotiens_valides,
name='rsu-api-qf-lire-quotients-valides'), name='rsu-api-qf-lire-quotients-valides'),
url(r'^qf/simuler/$', api_views.qf_simuler, re_path(r'^qf/simuler/$', api_views.qf_simuler,
name='rsu-api-qf-simuler'), name='rsu-api-qf-simuler'),
url(r'^qf/(?P<identifier>[-\w]+)/$', api_views.qf_calculer, re_path(r'^qf/(?P<identifier>[-\w]+)/$', api_views.qf_calculer,
name='rsu-api-qf-calculer'), name='rsu-api-qf-calculer'),
url(r'^qf/(?P<identifier>[-\w]+)/editer-carte/(?P<id_qf>\w+)/$', api_views.qf_editer_carte, re_path(r'^qf/(?P<identifier>[-\w]+)/editer-carte/(?P<id_qf>\w+)/$', api_views.qf_editer_carte,
name='rsu-api-qf-editer-carte'), name='rsu-api-qf-editer-carte'),
url(r'^doublons/$', api_views.doublons, re_path(r'^doublons/$', api_views.doublons,
name='rsu-api-doublons'), name='rsu-api-doublons'),
url(r'^doublons/(?P<doublon_id>[0-9 ]+)/$', api_views.doublon, re_path(r'^doublons/(?P<doublon_id>[0-9 ]+)/$', api_views.doublon,
name='rsu-api-doublon'), name='rsu-api-doublon'),
url(r'^doublons/(?P<doublon_id>[0-9 ]+)/false-positive/$', api_views.false_positive, re_path(r'^doublons/(?P<doublon_id>[0-9 ]+)/false-positive/$', api_views.false_positive,
name='rsu-api-doublon-false-positive'), name='rsu-api-doublon-false-positive'),
url(r'^doublons/(?P<doublon_id>[0-9 ]+)/dedup/$', api_views.dedup, re_path(r'^doublons/(?P<doublon_id>[0-9 ]+)/dedup/$', api_views.dedup,
name='rsu-api-doublon-dedup'), name='rsu-api-doublon-dedup'),
] ]

View File

@ -38,13 +38,12 @@ import psycopg2
from django.conf import settings from django.conf import settings
from django.contrib.postgres.search import TrigramDistance from django.contrib.postgres.search import TrigramDistance
from django.db import connection from django.db import connection
from django.db.models import Q, F, Value, ExpressionWrapper, CharField, When, Case from django.db.models import Q, F, Value, ExpressionWrapper, CharField, When, Case, CharField
from django.db.models.functions import Least, Greatest, Coalesce, Concat from django.db.models.functions import Least, Greatest, Coalesce, Concat
from django.db import transaction from django.db import transaction
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.http import HttpResponse from django.http import HttpResponse
from django.utils import six
from django.utils.timezone import now, make_aware from django.utils.timezone import now, make_aware
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@ -542,7 +541,7 @@ class PersonSearch(object):
Coalesce( Coalesce(
JSONTextRef(F('content'), 'nom_d_usage'), JSONTextRef(F('content'), 'nom_d_usage'),
JSONTextRef(F('content'), 'nom_de_naissance'), JSONTextRef(F('content'), 'nom_de_naissance'),
Value(' ') Value(' '),
), ),
Value(' '), Value(' '),
JSONTextRef(F('content'), 'prenoms')) JSONTextRef(F('content'), 'prenoms'))
@ -653,7 +652,7 @@ def integrity_check():
def upper_dict(d): def upper_dict(d):
'''Transform all string values in d to uppercase''' '''Transform all string values in d to uppercase'''
for key, value in d.items(): for key, value in d.items():
if isinstance(value, six.text_type): if isinstance(value, str):
d[key] = value.upper() d[key] = value.upper()
@ -1283,18 +1282,11 @@ def individu_caption(individu):
def csv_export_response(rows, filename): def csv_export_response(rows, filename):
if six.PY3: with io.StringIO(newline='') as f:
with io.StringIO(newline='') as f: writer = csv.writer(f)
writer = csv.writer(f) for row in rows:
for row in rows: writer.writerow(map(str, row))
writer.writerow(map(str, row)) r = HttpResponse(f.getvalue(), content_type='text/csv')
r = HttpResponse(f.getvalue(), content_type='text/csv')
else:
with io.BytesIO() as f:
writer = csv.writer(f)
for row in rows:
writer.writerow(map(force_bytes, row))
r = HttpResponse(f.getvalue(), content_type='text/csv')
r['Content-Disposition'] = 'attachment; filename="%s"' % filename r['Content-Disposition'] = 'attachment; filename="%s"' % filename
return r return r

View File

@ -30,7 +30,6 @@ from django.db import connection
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.utils.timezone import now from django.utils.timezone import now
from django.utils import six
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib import messages from django.contrib import messages
@ -117,9 +116,7 @@ def synchronize_federations_report(request, job_id, model_admin, *args, **kwargs
if not report: if not report:
raise Http404('no report') raise Http404('no report')
with report: with report:
text_report = report text_report = io.TextIOWrapper(report, encoding='utf-8')
if six.PY3:
text_report = io.TextIOWrapper(text_report, encoding='utf-8')
reader = csv.reader(text_report) reader = csv.reader(text_report)
next(reader) next(reader)
actions = [row for row in reader if row[6] != 'KEEP'] actions = [row for row in reader if row[6] != 'KEEP']
@ -152,9 +149,7 @@ def synchronize_federations_apply_report(request, job_id, model_admin, *args, **
with report: with report:
if not report: if not report:
raise Http404('no report') raise Http404('no report')
text_report = report text_report = io.TextIOWrapper(report, encoding='utf-8')
if six.PY3:
text_report = io.TextIOWrapper(text_report, encoding='utf-8')
reader = csv.reader(text_report) reader = csv.reader(text_report)
next(reader) next(reader)
actions = [row for row in reader if row[6] != 'KEEP'] actions = [row for row in reader if row[6] != 'KEEP']