diff --git a/setup.py b/setup.py index f3ed6d5..5e8edec 100644 --- a/setup.py +++ b/setup.py @@ -106,13 +106,11 @@ setup( 'Programming Language :: Python', ], install_requires=[ - 'django<2.3', + 'django<3.3', 'isodate', - 'psycopg2<2.9', 'jsonschema', 'gadjo', - 'six', - 'djangorestframework>=3.3,<3.10', + 'djangorestframework>=3.9,<3.13', 'pytz', 'python-dateutil', 'django-admin-rangefilter', diff --git a/tests/test_commands.py b/tests/test_commands.py index 9bbef6e..3da5ba1 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import io import mock import pytest import sys @@ -9,12 +10,11 @@ from zoo.zoo_nanterre import utils from django.core.management import call_command from django.db.transaction import atomic -from django.utils.six import StringIO def get_output_of_command(command, *args, **kwargs): old_stdout = sys.stdout - output = sys.stdout = StringIO() + output = sys.stdout = io.StringIO() call_command(command, *args, **kwargs) sys.stdout = old_stdout return output.getvalue() diff --git a/tests/test_nanterre.py b/tests/test_nanterre.py index 0295de3..b6da781 100644 --- a/tests/test_nanterre.py +++ b/tests/test_nanterre.py @@ -5,14 +5,13 @@ import datetime import isodate import requests import threading +import urllib.parse import pytest import httmock from django.urls import reverse -from django.utils.encoding import force_text from django.utils.http import urlencode -from django.utils.six.moves.urllib import parse as urlparse from django.utils.timezone import now 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') if more: assert 'cookie' in response.json - parsed = urlparse.urlparse(response.json['more']) + parsed = urllib.parse.urlparse(response.json['more']) 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 count == qs.count() 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)) 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 len(req_content['fragments']) == 3 assert req_content['fragments'][0]['type'] == 'maj-adresse' diff --git a/tests/test_nanterre_doublons.py b/tests/test_nanterre_doublons.py index 0d6ed69..4e33434 100644 --- a/tests/test_nanterre_doublons.py +++ b/tests/test_nanterre_doublons.py @@ -1,8 +1,8 @@ import copy +import urllib from django.urls import reverse 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_data.models import Log, Entity @@ -43,8 +43,8 @@ def test_list_doublons(nanterre_classic_family, app): assert response.json['err'] == 0 assert 'more' in response.json assert 'cookie' in response.json - assert response.json['cookie'] == urlparse.parse_qs( - urlparse.urlparse( + assert response.json['cookie'] == urllib.parse.parse_qs( + urllib.parse.urlparse( response.json['more']).query)['cookie'][0] assert len(response.json['data']) >= 10 assert response.json['data'][0]['id'] == d.id diff --git a/tests/test_nanterre_fragments.py b/tests/test_nanterre_fragments.py index 5296409..26988be 100644 --- a/tests/test_nanterre_fragments.py +++ b/tests/test_nanterre_fragments.py @@ -4,7 +4,6 @@ import json import httmock from django.urls import reverse -from django.utils.encoding import force_text from zoo.zoo_nanterre.fragments import Synchronization from zoo.models import Job @@ -29,7 +28,7 @@ def test_synchro_full(app, nanterre_classic_family): @httmock.urlmatch() def technocarte_ok(url, request): - request_bodies.append(json.loads(force_text(request.body))) + request_bodies.append(json.loads(request.body)) return httmock.response( 200, [ { @@ -228,7 +227,7 @@ def test_infor(app, nanterre_classic_family): @httmock.urlmatch() def infor_ok(url, request): - request_bodies.append(json.loads(force_text(request.body))) + request_bodies.append(json.loads(request.body)) return httmock.response( 200, { 'http_code': 200, @@ -277,7 +276,7 @@ def test_infor(app, nanterre_classic_family): @httmock.urlmatch() def infor_nok(url, request): - request_bodies.append(json.loads(force_text(request.body))) + request_bodies.append(json.loads(request.body)) return httmock.response( 200, { 'http_code': 500, diff --git a/tests/test_nanterre_synchronize_federations.py b/tests/test_nanterre_synchronize_federations.py index 207e7b2..6dc83e1 100644 --- a/tests/test_nanterre_synchronize_federations.py +++ b/tests/test_nanterre_synchronize_federations.py @@ -1,10 +1,7 @@ # -*- coding: utf-8 -*- import csv - -from django.utils import six -from django.utils.encoding import force_bytes -from django.utils.six import StringIO +import io 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'Nouvel import') 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 = response.form.submit().follow() assert len(response.pyquery('table#result-list tbody tr')) == 1 response = response.click('Rapport') def check_csv_response(csv_response): - if six.PY3: - reader = csv.DictReader(StringIO(csv_response.text)) - reader.fieldnames = reader.reader.__next__() - else: - reader = csv.DictReader(StringIO(csv_response.content)) - reader.fieldnames = reader.reader.next() + reader = csv.DictReader(io.StringIO(csv_response.text)) + reader.fieldnames = reader.reader.__next__() rows = list(reader) def rows_by_action(action): diff --git a/tox.ini b/tox.ini index b7c7a01..db393d2 100644 --- a/tox.ini +++ b/tox.ini @@ -18,11 +18,12 @@ setenv = usedevelop = true deps = dj22: django<2.3 + dj22: psycopg2-binary<2.9 + dj22: djangorestframework>=3.9.2,<3.10 dj32: django>=3.2,<3.3 - djangorestframework>=3.9.2,<3.10 + dj32: psycopg2-binary pip>8 pytest-flakes - pg: psycopg2<2.9 coverage pytest-cov pytest-django diff --git a/zoo/urls.py b/zoo/urls.py index 2521604..9e435fb 100644 --- a/zoo/urls.py +++ b/zoo/urls.py @@ -14,30 +14,15 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -"""zoo URL Configuration - -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.urls import re_path, include from django.contrib import admin from .views import login, logout urlpatterns = [ - url(r'^admin/', admin.site.urls), - url(r'^demo/', include('zoo.zoo_demo.urls')), - url(r'^rsu/', include('zoo.zoo_nanterre.urls')), - url(r'^logout/$', logout, name='logout'), - url(r'^login/$', login, name='auth_login'), + re_path(r'^admin/', admin.site.urls), + re_path(r'^demo/', include('zoo.zoo_demo.urls')), + re_path(r'^rsu/', include('zoo.zoo_nanterre.urls')), + re_path(r'^logout/$', logout, name='logout'), + re_path(r'^login/$', login, name='auth_login'), ] diff --git a/zoo/utils.py b/zoo/utils.py index b49853c..3e36386 100644 --- a/zoo/utils.py +++ b/zoo/utils.py @@ -16,8 +16,6 @@ import unicodedata -from django.utils.encoding import force_text - from rest_framework.views import exception_handler from rest_framework.response import Response @@ -33,8 +31,8 @@ def rest_exception_handler(exc, context): raise response = Response({ 'err': 1, - 'exc_class': force_text(exc.__class__), - 'exc_value': force_text(exc), + 'exc_class': str(exc.__class__), + 'exc_value': str(exc), }) response.status = 400 return response diff --git a/zoo/zoo_data/__init__.py b/zoo/zoo_data/__init__.py index a0ea79b..4db6c87 100644 --- a/zoo/zoo_data/__init__.py +++ b/zoo/zoo_data/__init__.py @@ -14,4 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -default_app_config = 'zoo.zoo_data.apps.ZooDataConfig' +import django + +if django.VERSION < (3, 2): + default_app_config = 'zoo.zoo_data.apps.ZooDataConfig' diff --git a/zoo/zoo_data/apps.py b/zoo/zoo_data/apps.py index 8884f0a..14177f4 100644 --- a/zoo/zoo_data/apps.py +++ b/zoo/zoo_data/apps.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from django.apps import AppConfig -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class ZooDataConfig(AppConfig): diff --git a/zoo/zoo_data/lookups.py b/zoo/zoo_data/lookups.py index 7693de3..cf01358 100644 --- a/zoo/zoo_data/lookups.py +++ b/zoo/zoo_data/lookups.py @@ -16,87 +16,40 @@ 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 try: - from django.contrib.postgres.fields.jsonb import KeyTransform, KeyTransformTextLookupMixin + from django.db.models.fields.json import KeyTransform, KeyTransformTextLookupMixin except ImportError: - # backport from Django 2.x - 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) + from django.contrib.postgres.fields.jsonb import KeyTransform, KeyTransformTextLookupMixin class Unaccent(Transform): lookup_name = 'unaccent' function = 'immutable_unaccent' + output_field = CharField() -TextField.register_lookup(Unaccent) +CharField.register_lookup(Unaccent) +CharField.register_lookup(Lower) class Normalize(Transform): lookup_name = 'normalize' function = 'immutable_normalize' + output_field = CharField() -TextField.register_lookup(Normalize) +CharField.register_lookup(Normalize) class Date(Transform): lookup_name = 'timestamp' function = 'immutable_date' - _output_field = DateField() + output_field = DateField() -TextField.register_lookup(Date) +CharField.register_lookup(Date) class JSONUnaccent(KeyTransformTextLookupMixin, Unaccent): diff --git a/zoo/zoo_data/models.py b/zoo/zoo_data/models.py index 758f97d..cf4c58a 100644 --- a/zoo/zoo_data/models.py +++ b/zoo/zoo_data/models.py @@ -24,22 +24,20 @@ import datetime from django.db import models, connection from django.db.models import F, Value +from django.db.models.functions import Lower from django.db.models.query import QuerySet, Q from django.core.exceptions import ValidationError from django.urls import reverse -from django.utils.encoding import force_text -from django.utils.six import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.utils.timezone import now from django.contrib.postgres.fields import JSONField 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 -@python_2_unicode_compatible class Transaction(models.Model): created = models.DateTimeField( auto_now_add=True, @@ -54,7 +52,7 @@ class Transaction(models.Model): null=True) def __str__(self): - return force_text(self.id) + return str(self.id) @classmethod def get_transaction(self): @@ -77,7 +75,7 @@ class EntityQuerySet(QuerySet): for key, value in kwargs.items(): filters.append(Q(**{ 'content__' + key + '__unaccent__lower__trigram_similar': - Lower(Unaccent(Value(value))), + Unaccent(Lower(Value(value))), })) qs = qs.filter(functools.reduce(__or__, filters)) expressions = [] @@ -93,7 +91,6 @@ class EntityQuerySet(QuerySet): return qs -@python_2_unicode_compatible class CommonData(models.Model): def clean(self): if self.schema: @@ -103,7 +100,7 @@ class CommonData(models.Model): raise ValidationError({'content': e}) def __str__(self): - return force_text(self.id) + return str(self.id) class Meta: abstract = True @@ -255,7 +252,6 @@ class JobQuerySet(QuerySet): return self.filter(**{'content__$classpath': class_path}) -@python_2_unicode_compatible class Job(models.Model): '''Store synchronization messages sent to applications''' SCHEDULER_STEP = 60 * 5 # 5 minutes @@ -334,7 +330,7 @@ class Job(models.Model): job.state = cls.STATE_UNRECOVERABLE_ERROR error = job.content.setdefault('error', {}) error['code'] = 'internal-server-error' - error['exc_detail'] = force_text(e) + error['exc_detail'] = str(e) error['exc_tb'] = traceback.format_exc() job.get_logger().exception('exception during job %s', job.admin_url) job.save() @@ -366,7 +362,7 @@ class Job(models.Model): url = self.admin_url self.get_logger().exception('exception during job %s', url) 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['$classpath'] = self.get_classpath(action) self.save() diff --git a/zoo/zoo_data/search.py b/zoo/zoo_data/search.py index c7dc555..f5ee6a1 100644 --- a/zoo/zoo_data/search.py +++ b/zoo/zoo_data/search.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.db.models import Func, Value +from django.db.models import Func, Value, CharField class Unaccent(Func): @@ -27,11 +27,6 @@ class Normalize(Func): arity = 1 -class Lower(Func): - function = 'LOWER' - arity = 1 - - class JSONRef(Func): function = '' arg_joiner = '->' @@ -46,6 +41,7 @@ class JSONTextRef(Func): function = '' arg_joiner = '->>' arity = 2 + output_field = CharField() def __init__(self, *expressions, **extra): jsonb = expressions[0] diff --git a/zoo/zoo_data/static/js/jsoneditor.min.js b/zoo/zoo_data/static/js/jsoneditor.min.js index e396783..7e251f9 100644 --- a/zoo/zoo_data/static/js/jsoneditor.min.js +++ b/zoo/zoo_data/static/js/jsoneditor.min.js @@ -856,4 +856,4 @@ c&&c.destroy(), c=new f(this.get(0),a),this.data("jsoneditor",c), // Setup event listeners c.on("change",function(){b.trigger("change")}),c.on("ready",function(){b.trigger("ready")}))}return this}}}(),window.JSONEditor=f}(); -//# sourceMappingURL=jsoneditor.min.js.map \ No newline at end of file +//# sourceMappingURL=jsoneditor.min.js.map diff --git a/zoo/zoo_demo/urls.py b/zoo/zoo_demo/urls.py index e2afad8..f812b23 100644 --- a/zoo/zoo_demo/urls.py +++ b/zoo/zoo_demo/urls.py @@ -14,11 +14,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.conf.urls import url +from django.urls import re_path from .views import schemas, schema urlpatterns = [ - url(r'^schemas/$', schemas, name='demo-schemas'), - url(r'^schemas/(?P\w*)/$', schema, name='demo-schema'), + re_path(r'^schemas/$', schemas, name='demo-schemas'), + re_path(r'^schemas/(?P\w*)/$', schema, name='demo-schema'), ] diff --git a/zoo/zoo_meta/__init__.py b/zoo/zoo_meta/__init__.py index 2177cd5..94ca905 100644 --- a/zoo/zoo_meta/__init__.py +++ b/zoo/zoo_meta/__init__.py @@ -14,4 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -default_app_config = 'zoo.zoo_meta.apps.ZooMetaAppConfig' +import django + +if django.VERSION < (3, 2): + default_app_config = 'zoo.zoo_meta.apps.ZooMetaAppConfig' diff --git a/zoo/zoo_meta/apps.py b/zoo/zoo_meta/apps.py index 7b80476..97e894a 100644 --- a/zoo/zoo_meta/apps.py +++ b/zoo/zoo_meta/apps.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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.apps import AppConfig diff --git a/zoo/zoo_meta/management/commands/zoo-rebuild-indexes.py b/zoo/zoo_meta/management/commands/zoo-rebuild-indexes.py index e55e553..209d27f 100644 --- a/zoo/zoo_meta/management/commands/zoo-rebuild-indexes.py +++ b/zoo/zoo_meta/management/commands/zoo-rebuild-indexes.py @@ -16,7 +16,6 @@ from __future__ import print_function from django.core.management.base import BaseCommand -from django.utils.encoding import force_text from zoo.zoo_meta.models import EntitySchema @@ -25,7 +24,7 @@ class Command(BaseCommand): def handle(self, *args, **options): for schema in EntitySchema.objects.all(): if options['verbosity'] >= 1: - print('Rebuilding index for', force_text(schema), end=' ') + print('Rebuilding index for', schema, end=' ') schema.rebuild_indexes() if options['verbosity'] >= 1: print(' Done.') diff --git a/zoo/zoo_meta/models.py b/zoo/zoo_meta/models.py index 7d273c6..0daa570 100644 --- a/zoo/zoo_meta/models.py +++ b/zoo/zoo_meta/models.py @@ -18,8 +18,7 @@ from hashlib import md5 from django.apps import apps from django.db import models, connection -from django.utils.encoding import force_bytes, force_text -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.postgres.fields import JSONField @@ -74,7 +73,7 @@ class CommonSchema(models.Model): def rebuild_string_index(self, cursor, table, 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) ' ' gin_trgm_ops) WHERE schema_id = %s' % (key, self.id, table, expr, self.id)) cursor.execute(sql) @@ -84,7 +83,7 @@ class CommonSchema(models.Model): def rebuild_string_date_time_index(self, cursor, table, 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) ' 'WHERE schema_id = %s' % (key, self.id, table, expr, self.id)) cursor.execute(sql) @@ -99,7 +98,7 @@ class CommonSchema(models.Model): else: 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_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)' @@ -147,8 +146,8 @@ class CommonSchema(models.Model): try: return eval(self.caption_template, {}, value.content) except Exception as e: - return force_text(e) - return force_text(value.id) + return str(e) + return str(value.id) class Meta: abstract = True diff --git a/zoo/zoo_nanterre/__init__.py b/zoo/zoo_nanterre/__init__.py index a870879..1ac4c1f 100644 --- a/zoo/zoo_nanterre/__init__.py +++ b/zoo/zoo_nanterre/__init__.py @@ -14,4 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -default_app_config = 'zoo.zoo_nanterre.apps.ZooNanterreConfig' +import django + +if django.VERSION < (3, 2): + default_app_config = 'zoo.zoo_nanterre.apps.ZooNanterreConfig' diff --git a/zoo/zoo_nanterre/api_views.py b/zoo/zoo_nanterre/api_views.py index f8eafc2..ae44090 100644 --- a/zoo/zoo_nanterre/api_views.py +++ b/zoo/zoo_nanterre/api_views.py @@ -32,7 +32,6 @@ from django.db.models.query import Q from django.db.transaction import non_atomic_requests, atomic from django.urls import reverse from django.http import Http404, HttpResponse -from django.utils.encoding import force_text from django.utils.timezone import now from django.utils.http import urlencode @@ -135,7 +134,7 @@ class TransactionalView(APIView): content = { 'request': self.request.data, 'status_code': 500, - '$exc_detail': force_text(exc), + '$exc_detail': str(exc), '$exc_tb': traceback.format_exc(), } self.transaction.content = content @@ -2055,7 +2054,7 @@ class FalsePositiveView(DoublonActionView): except AssertionError as e: return Response({ 'err': 1, - 'errors': force_text(e), + 'errors': str(e), }, status=500) @@ -2096,7 +2095,7 @@ class DedupView(DoublonActionView): except AssertionError as e: return Response({ 'err': 1, - 'errors': force_text(e), + 'errors': str(e), }, status=500) dedup = DedupView.as_view() diff --git a/zoo/zoo_nanterre/apps.py b/zoo/zoo_nanterre/apps.py index 645df1f..854e785 100644 --- a/zoo/zoo_nanterre/apps.py +++ b/zoo/zoo_nanterre/apps.py @@ -19,8 +19,8 @@ import functools from django.apps import AppConfig -from django.conf.urls import url -from django.utils.translation import ugettext_lazy as _ +from django.urls import re_path +from django.utils.translation import gettext_lazy as _ from django.db.models.signals import post_migrate @@ -93,14 +93,14 @@ class ZooNanterreConfig(AppConfig): urls = [] for desc in descs: - urls.append(url( + urls.append(re_path( r'^synchronize-federations/%s$' % desc['re'], model_admin.admin_site.admin_view( getattr(views, 'synchronize_federations' + desc['view'])), kwargs={'model_admin': model_admin}, name='synchronize-federations' + desc['name'], )) - urls.append(url( + urls.append(re_path( r'^inactive/', model_admin.admin_site.admin_view( getattr(views, 'inactive_index')), diff --git a/zoo/zoo_nanterre/forms.py b/zoo/zoo_nanterre/forms.py index 4393ae1..d0ae0c5 100644 --- a/zoo/zoo_nanterre/forms.py +++ b/zoo/zoo_nanterre/forms.py @@ -15,8 +15,7 @@ # along with this program. If not, see . from django import forms -from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError from .utils import PersonSearch, get_applications, get_application @@ -63,13 +62,15 @@ class SynchronizeFederationsForm(forms.Form): def clean_csv_uploaded(self): csv_uploaded = self.cleaned_data['csv_uploaded'] errors = [] + csv_uploaded.seek(0) for i, line in enumerate(csv_uploaded): try: - force_text(line).encode('ascii') - # works with pyhton2 and 3 - except (UnicodeEncodeError, UnicodeDecodeError) as e: + line.decode('ascii') + except UnicodeError as e: errors.append(_(u'non-ASCII character on line {0} and column {1}').format( i + 1, e.start + 1)) + # restore file state + csv_uploaded.seek(0) if errors: raise ValidationError(errors) return csv_uploaded diff --git a/zoo/zoo_nanterre/fragments.py b/zoo/zoo_nanterre/fragments.py index 795f7b4..bc228b8 100644 --- a/zoo/zoo_nanterre/fragments.py +++ b/zoo/zoo_nanterre/fragments.py @@ -5,7 +5,6 @@ import datetime import requests from requests.exceptions import RequestException -from django.utils.encoding import force_text from django.utils.timezone import now from django.conf import settings from django.db import DatabaseError @@ -204,7 +203,7 @@ class FragmentBuilder(object): error_detail = u'erreur réseau/SSL ou expiration' self.error = { 'code': 'transport-error', - 'detail': force_text(e), + 'detail': str(e), } state = self.state_on_network_error else: diff --git a/zoo/zoo_nanterre/management/commands/rsu-duplicates.py b/zoo/zoo_nanterre/management/commands/rsu-duplicates.py index ad43518..1b7cd16 100644 --- a/zoo/zoo_nanterre/management/commands/rsu-duplicates.py +++ b/zoo/zoo_nanterre/management/commands/rsu-duplicates.py @@ -23,7 +23,6 @@ import datetime import django from django.core.management.base import BaseCommand, CommandParser -from django.utils.six import python_2_unicode_compatible from django.utils.timezone import now 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 -@python_2_unicode_compatible class Table(object): def __init__(self, names): self.size = len(names) diff --git a/zoo/zoo_nanterre/models.py b/zoo/zoo_nanterre/models.py index d01d32b..263aedf 100644 --- a/zoo/zoo_nanterre/models.py +++ b/zoo/zoo_nanterre/models.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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.db import models diff --git a/zoo/zoo_nanterre/qf.py b/zoo/zoo_nanterre/qf.py index 44f81ce..5153288 100644 --- a/zoo/zoo_nanterre/qf.py +++ b/zoo/zoo_nanterre/qf.py @@ -8,7 +8,6 @@ from zoo.models import Job import requests -from django.utils.encoding import force_text from django.utils.timezone import now from django.conf import settings @@ -64,7 +63,7 @@ class QF(object): if response: for qf in response: 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: qf['annee_imposition'] = 'inconnue' return response, error @@ -148,7 +147,7 @@ class QF(object): else: return response[0], None 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): federation = individu.content['cles_de_federation'].get('implicit') @@ -168,8 +167,8 @@ class QF(object): if isinstance(response, list): return response, None else: - return None, (u'Implicit lire-quotient-familial réponse invalide: %r' - % force_text(response)[:1024]) + return None, ('Implicit lire-quotient-familial réponse invalide: %r' + % str(response)[:1024]) def editer_carte(self, individu, id_qf): federation = individu.content['cles_de_federation'].get('implicit') diff --git a/zoo/zoo_nanterre/saga.py b/zoo/zoo_nanterre/saga.py index ff77f1c..d83af41 100644 --- a/zoo/zoo_nanterre/saga.py +++ b/zoo/zoo_nanterre/saga.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- import datetime import decimal +import urllib.parse import xml.etree.ElementTree as ET from django.conf import settings -from django.utils import six, timezone -from django.utils.six.moves.urllib import parse as urlparse +from django.utils import timezone import requests @@ -36,13 +36,13 @@ class Saga(object): @property def creance_url(self): - return urlparse.urljoin( + return urllib.parse.urljoin( self.url, '/%s/services/etat_facture_creance_literal' % self.base_uri) @property def paiement_url(self): - return urlparse.urljoin( + return urllib.parse.urljoin( self.url, '/%s/services/paiement_internet_ws_literal' % self.base_uri) @@ -194,7 +194,7 @@ class Saga(object): {urlretour_synchrone} ''' 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) tree, error = self.soap_call( self.paiement_url, body, 'TransactionReturn', diff --git a/zoo/zoo_nanterre/synchronize_federations.py b/zoo/zoo_nanterre/synchronize_federations.py index cdfe4ca..bd5dc27 100644 --- a/zoo/zoo_nanterre/synchronize_federations.py +++ b/zoo/zoo_nanterre/synchronize_federations.py @@ -17,15 +17,13 @@ # along with this program. If not, see . import csv +import io from django.core.files.storage import default_storage from django.urls import reverse from django.conf import settings from django.db import DatabaseError 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_data.models import Job, Entity, Transaction, Log @@ -106,15 +104,12 @@ class SynchronizeFederationsImport(object): self.report('report') def report(self, target): - output_file = StringIO() + output_file = io.StringIO() writer = csv.writer(output_file) writer.writerow(['RSU ID', 'prenoms', 'nom de naissance', 'nom d\'usage', 'application', 'federation', 'action']) for action in self.actions: - if six.PY3: - action = [v for v in action] - else: - action = [force_bytes(v) for v in action] + action = [v for v in action] writer.writerow(action) setattr(self.action, target + '_csv_filename', self.action.csv_filename + '-report.csv') diff --git a/zoo/zoo_nanterre/urls.py b/zoo/zoo_nanterre/urls.py index a212105..2994510 100644 --- a/zoo/zoo_nanterre/urls.py +++ b/zoo/zoo_nanterre/urls.py @@ -14,74 +14,74 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.conf.urls import url +from django.urls import re_path from .views import demo, search, import_control from . import api_views urlpatterns = [ - url(r'^demo/$', demo, name='demo'), - url(r'^demo/search/$', search, name='demo'), - url(r'^import_control/$', import_control, name='demo'), - url(r'^search/$', api_views.search, name='rsu-api-search'), - url(r'^individu/(?P[-\w]+)/$', api_views.reseau, name='rsu-api-reseau'), - url(r'^individu/(?P[-\w]+)/suppression/$', api_views.suppression_individu, + re_path(r'^demo/$', demo, name='demo'), + re_path(r'^demo/search/$', search, name='demo'), + re_path(r'^import_control/$', import_control, name='demo'), + re_path(r'^search/$', api_views.search, name='rsu-api-search'), + re_path(r'^individu/(?P[-\w]+)/$', api_views.reseau, name='rsu-api-reseau'), + re_path(r'^individu/(?P[-\w]+)/suppression/$', api_views.suppression_individu, name='rsu-api-suppression-individu'), - url(r'^individu/(?P[-\w]+)/liste/$', api_views.reseau_liste, + re_path(r'^individu/(?P[-\w]+)/liste/$', api_views.reseau_liste, name='rsu-api-reseau-liste'), - url(r'^individu/(?P[-\w]+)/journal/$', api_views.journal, name='rsu-api-journal'), - url(r'^individu/(?P[-\w]+)/declaration-responsabilite-legale/$', + re_path(r'^individu/(?P[-\w]+)/journal/$', api_views.journal, name='rsu-api-journal'), + re_path(r'^individu/(?P[-\w]+)/declaration-responsabilite-legale/$', api_views.declaration_responsabilite_legale, name='rsu-api-declaration-responsabilite-legale'), - url(r'^individu/(?P[-\w]+)/declaration-adresse-principale/$', + re_path(r'^individu/(?P[-\w]+)/declaration-adresse-principale/$', api_views.declaration_adresse_principale, name='rsu-api-declaration-adresse-principale'), - url(r'^individu/(?P[-\w]+)/changement-de-situation-maritale/$', + re_path(r'^individu/(?P[-\w]+)/changement-de-situation-maritale/$', api_views.changement_de_situation_maritale, name='rsu-api-changement-de-situation-maritale'), - url(r'^individu/(?P[-\w]+)/separation/$', + re_path(r'^individu/(?P[-\w]+)/separation/$', api_views.separation, name='rsu-api-separation'), - url(r'^individu/(?P[-\w]+)/declaration-de-deces/$', + re_path(r'^individu/(?P[-\w]+)/declaration-de-deces/$', api_views.declaration_de_deces, name='rsu-api-declaration-de-deces'), - url(r'^individu/(?P[-\w]+)/(?P[-\w]+)/' + re_path(r'^individu/(?P[-\w]+)/(?P[-\w]+)/' 'suppression-lien-de-responsabilite/$', api_views.suppression_lien_de_responsabilite, name='rsu-api-suppression-lien-de-responsabilite'), - url(r'^individu/$', api_views.create_individu, name='rsu-api-create-individu'), - url(r'^individu/(?P[-\w]+)/federation/(?P\w+)/$', api_views.federation, + re_path(r'^individu/$', api_views.create_individu, name='rsu-api-create-individu'), + re_path(r'^individu/(?P[-\w]+)/federation/(?P\w+)/$', api_views.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'), - url(r'^synchronisation/$', api_views.synchronization, + re_path(r'^synchronisation/$', api_views.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'), - 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'), - url(r'^saga/tiers/(?P\w+)/(?P[-\w]+)/$', api_views.saga_tiers, + re_path(r'^saga/tiers/(?P\w+)/(?P[-\w]+)/$', api_views.saga_tiers, name='rsu-api-saga-tiers'), - url(r'^saga/(?P[-\w]+)/factures/$', api_views.saga_factures, + re_path(r'^saga/(?P[-\w]+)/factures/$', api_views.saga_factures, name='rsu-api-saga-factures'), - url(r'^saga/(?P[-\w]+)/transaction/$', api_views.saga_transaction, + re_path(r'^saga/(?P[-\w]+)/transaction/$', api_views.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'), - url(r'^qf/simuler/$', api_views.qf_simuler, + re_path(r'^qf/simuler/$', api_views.qf_simuler, name='rsu-api-qf-simuler'), - url(r'^qf/(?P[-\w]+)/$', api_views.qf_calculer, + re_path(r'^qf/(?P[-\w]+)/$', api_views.qf_calculer, name='rsu-api-qf-calculer'), - url(r'^qf/(?P[-\w]+)/editer-carte/(?P\w+)/$', api_views.qf_editer_carte, + re_path(r'^qf/(?P[-\w]+)/editer-carte/(?P\w+)/$', api_views.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'), - url(r'^doublons/(?P[0-9 ]+)/$', api_views.doublon, + re_path(r'^doublons/(?P[0-9 ]+)/$', api_views.doublon, name='rsu-api-doublon'), - url(r'^doublons/(?P[0-9 ]+)/false-positive/$', api_views.false_positive, + re_path(r'^doublons/(?P[0-9 ]+)/false-positive/$', api_views.false_positive, name='rsu-api-doublon-false-positive'), - url(r'^doublons/(?P[0-9 ]+)/dedup/$', api_views.dedup, + re_path(r'^doublons/(?P[0-9 ]+)/dedup/$', api_views.dedup, name='rsu-api-doublon-dedup'), ] diff --git a/zoo/zoo_nanterre/utils.py b/zoo/zoo_nanterre/utils.py index 105136f..35405ed 100644 --- a/zoo/zoo_nanterre/utils.py +++ b/zoo/zoo_nanterre/utils.py @@ -38,13 +38,12 @@ import psycopg2 from django.conf import settings from django.contrib.postgres.search import TrigramDistance 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 import transaction from django.contrib.auth.hashers import make_password from django.http import HttpResponse -from django.utils import six from django.utils.timezone import now, make_aware from django.utils.encoding import force_bytes @@ -542,7 +541,7 @@ class PersonSearch(object): Coalesce( JSONTextRef(F('content'), 'nom_d_usage'), JSONTextRef(F('content'), 'nom_de_naissance'), - Value(' ') + Value(' '), ), Value(' '), JSONTextRef(F('content'), 'prenoms')) @@ -653,7 +652,7 @@ def integrity_check(): def upper_dict(d): '''Transform all string values in d to uppercase''' for key, value in d.items(): - if isinstance(value, six.text_type): + if isinstance(value, str): d[key] = value.upper() @@ -1283,18 +1282,11 @@ def individu_caption(individu): def csv_export_response(rows, filename): - if six.PY3: - with io.StringIO(newline='') as f: - writer = csv.writer(f) - for row in rows: - writer.writerow(map(str, row)) - 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') + with io.StringIO(newline='') as f: + writer = csv.writer(f) + for row in rows: + writer.writerow(map(str, row)) + r = HttpResponse(f.getvalue(), content_type='text/csv') r['Content-Disposition'] = 'attachment; filename="%s"' % filename return r diff --git a/zoo/zoo_nanterre/views.py b/zoo/zoo_nanterre/views.py index 967775f..bb8a853 100644 --- a/zoo/zoo_nanterre/views.py +++ b/zoo/zoo_nanterre/views.py @@ -30,7 +30,6 @@ from django.db import connection from django.conf import settings from django.core.cache import cache from django.utils.timezone import now -from django.utils import six from django.contrib.auth.decorators import permission_required from django.contrib import messages @@ -117,9 +116,7 @@ def synchronize_federations_report(request, job_id, model_admin, *args, **kwargs if not report: raise Http404('no report') with report: - text_report = report - if six.PY3: - text_report = io.TextIOWrapper(text_report, encoding='utf-8') + text_report = io.TextIOWrapper(report, encoding='utf-8') reader = csv.reader(text_report) next(reader) 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: if not report: raise Http404('no report') - text_report = report - if six.PY3: - text_report = io.TextIOWrapper(text_report, encoding='utf-8') + text_report = io.TextIOWrapper(report, encoding='utf-8') reader = csv.reader(text_report) next(reader) actions = [row for row in reader if row[6] != 'KEEP']