general: update for compatibility with django 2.2 (#41626)

This commit is contained in:
Frédéric Péters 2020-04-12 11:05:05 +02:00
parent d340a2bc7e
commit e01c978bec
20 changed files with 90 additions and 68 deletions

View File

@ -45,8 +45,10 @@ class Migration(migrations.Migration):
name='Subscription', name='Subscription',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('category', models.ForeignKey(verbose_name='category', to='corbo.Category')), ('category', models.ForeignKey(verbose_name='category',
('user', models.ForeignKey(verbose_name='user', blank=True, to=settings.AUTH_USER_MODEL, null=True)), to='corbo.Category', on_delete=models.CASCADE)),
('user', models.ForeignKey(verbose_name='user', blank=True,
to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
], ],
options={ options={
}, },
@ -57,7 +59,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('identifier', models.CharField(help_text='ex.: email, mobile phone number, jabber id', max_length=128, verbose_name='identifier', blank=True)), ('identifier', models.CharField(help_text='ex.: email, mobile phone number, jabber id', max_length=128, verbose_name='identifier', blank=True)),
('subscription', models.ForeignKey(to='corbo.Subscription')), ('subscription', models.ForeignKey(to='corbo.Subscription', on_delete=models.CASCADE)),
], ],
options={ options={
}, },
@ -70,7 +72,8 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='announce', model_name='announce',
name='category', name='category',
field=models.ForeignKey(verbose_name='category', to='corbo.Category'), field=models.ForeignKey(verbose_name='category',
to='corbo.Category', on_delete=models.CASCADE),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -18,7 +18,8 @@ class Migration(migrations.Migration):
('channel', models.CharField(max_length=32, verbose_name='channel', choices=[(b'sms', 'SMS'), (b'email', 'Email')])), ('channel', models.CharField(max_length=32, verbose_name='channel', choices=[(b'sms', 'SMS'), (b'email', 'Email')])),
('time', models.DateTimeField(auto_now_add=True, verbose_name='sent time')), ('time', models.DateTimeField(auto_now_add=True, verbose_name='sent time')),
('result', models.TextField(verbose_name='result', blank=True)), ('result', models.TextField(verbose_name='result', blank=True)),
('announce', models.ForeignKey(verbose_name='announce', to='corbo.Announce')), ('announce', models.ForeignKey(verbose_name='announce',
to='corbo.Announce', on_delete=models.CASCADE)),
], ],
options={ options={
'ordering': ('-time',), 'ordering': ('-time',),

View File

@ -33,7 +33,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='subscription', model_name='subscription',
name='category', name='category',
field=models.ForeignKey(verbose_name='Category', to='corbo.Category'), field=models.ForeignKey(verbose_name='Category', to='corbo.Category', on_delete=models.CASCADE),
preserve_default=True, preserve_default=True,
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(

View File

@ -65,7 +65,8 @@ class Category(models.Model):
class Announce(models.Model): class Announce(models.Model):
category = models.ForeignKey('Category', verbose_name=_('category')) category = models.ForeignKey('Category', verbose_name=_('category'),
on_delete=models.CASCADE)
title = models.CharField(_('title'), max_length=256, title = models.CharField(_('title'), max_length=256,
help_text=_('maximum 256 characters')) help_text=_('maximum 256 characters'))
identifier = models.CharField(max_length=256, null=True, blank=True) identifier = models.CharField(max_length=256, null=True, blank=True)
@ -150,7 +151,7 @@ class Announce(models.Model):
class Broadcast(models.Model): class Broadcast(models.Model):
announce = models.ForeignKey(Announce, verbose_name=_('announce')) announce = models.ForeignKey(Announce, verbose_name=_('announce'), on_delete=models.CASCADE)
deliver_time = models.DateTimeField(_('Deliver time'), null=True) deliver_time = models.DateTimeField(_('Deliver time'), null=True)
delivery_count = models.IntegerField(_('Delivery count'), default=0) delivery_count = models.IntegerField(_('Delivery count'), default=0)
@ -187,7 +188,7 @@ class Broadcast(models.Model):
class Subscription(models.Model): class Subscription(models.Model):
category = models.ForeignKey('Category', verbose_name=_('Category')) category = models.ForeignKey('Category', verbose_name=_('Category'), on_delete=models.CASCADE)
uuid = models.CharField(_('User identifier'), max_length=128, blank=True) uuid = models.CharField(_('User identifier'), max_length=128, blank=True)
identifier = models.CharField(_('identifier'), max_length=128, blank=True, identifier = models.CharField(_('identifier'), max_length=128, blank=True,
help_text=_('ex.: mailto, ...')) help_text=_('ex.: mailto, ...'))

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.core.urlresolvers import reverse from django.urls import reverse
from django.forms.utils import flatatt from django.forms.utils import flatatt
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -24,10 +24,12 @@ from django.utils.translation import get_language
import ckeditor.widgets import ckeditor.widgets
def ckeditor_render(self, name, value, attrs=None): def ckeditor_render(self, name, value, attrs=None, renderer=None):
if value is None: if value is None:
value = '' value = ''
final_attrs = {'name': name} final_attrs = {'name': name}
if getattr(self, 'attrs', None):
final_attrs.update(self.attrs)
if attrs: if attrs:
final_attrs.update(attrs) final_attrs.update(attrs)
if 'filebrowserUploadUrl' not in self.config: if 'filebrowserUploadUrl' not in self.config:

View File

@ -42,12 +42,11 @@ INSTALLED_APPS = (
'rest_framework', 'rest_framework',
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
) )

View File

@ -8,7 +8,7 @@ from django.views.decorators.cache import never_cache
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from .urls_utils import decorated_includes, manager_required from .urls_utils import decorated_includes, manager_required
from .views import homepage, atom, unsubscribe, unsubscription_done, login, logout from .views import homepage, atom, unsubscribe, unsubscription_done, LoginView, LogoutView
from .manage_urls import urlpatterns as manage_urls from .manage_urls import urlpatterns as manage_urls
from .api_urls import urlpatterns as api_urls from .api_urls import urlpatterns as api_urls
@ -21,14 +21,14 @@ urlpatterns = [
url(r'^atom$', atom, name='atom'), url(r'^atom$', atom, name='atom'),
url(r'^manage/', decorated_includes(manager_required, url(r'^manage/', decorated_includes(manager_required,
include(manage_urls))), include(manage_urls))),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', admin.site.urls),
url(r'^api/', include(api_urls)), url(r'^api/', include(api_urls)),
url(r'^unsubscribe/done/$', unsubscription_done, url(r'^unsubscribe/done/$', unsubscription_done,
name='unsubscription_done'), name='unsubscription_done'),
url(r'^unsubscribe/(?P<unsubscription_token>[\w:-]+)$', unsubscribe, url(r'^unsubscribe/(?P<unsubscription_token>[\w:-]+)$', unsubscribe,
name='unsubscribe'), name='unsubscribe'),
url(r'^logout/$', logout, name='auth_logout'), url(r'^logout/$',LogoutView.as_view(), name='auth_logout'),
url(r'^login/$', login, name='auth_login'), url(r'^login/$', LoginView.as_view(), name='auth_login'),
url(r'^ckeditor/upload/', staff_member_required(ckeditor_views.upload), url(r'^ckeditor/upload/', staff_member_required(ckeditor_views.upload),
name='ckeditor_upload'), name='ckeditor_upload'),
url(r'^ckeditor/browse/', never_cache(staff_member_required(ckeditor_views.browse)), url(r'^ckeditor/browse/', never_cache(staff_member_required(ckeditor_views.browse)),

View File

@ -16,43 +16,52 @@
# Decorating URL includes, <https://djangosnippets.org/snippets/2532/> # Decorating URL includes, <https://djangosnippets.org/snippets/2532/>
import django
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
class DecoratedURLPattern(RegexURLPattern): if django.VERSION < (2, 0, 0):
from django.urls.resolvers import RegexURLPattern as URLPattern
from django.urls.resolvers import RegexURLResolver as URLResolver
else:
from django.urls.resolvers import URLPattern, URLResolver
class DecoratedURLPattern(URLPattern):
def resolve(self, *args, **kwargs): def resolve(self, *args, **kwargs):
result = super(DecoratedURLPattern, self).resolve(*args, **kwargs) result = super(DecoratedURLPattern, self).resolve(*args, **kwargs)
if result: if result:
result.func = self._decorate_with(result.func) result.func = self._decorate_with(result.func)
return result return result
class DecoratedRegexURLResolver(RegexURLResolver):
class DecoratedURLResolver(URLResolver):
def resolve(self, *args, **kwargs): def resolve(self, *args, **kwargs):
result = super(DecoratedRegexURLResolver, self).resolve(*args, **kwargs) result = super(DecoratedURLResolver, self).resolve(*args, **kwargs)
if result: if result:
result.func = self._decorate_with(result.func) result.func = self._decorate_with(result.func)
return result return result
def decorated_includes(func, includes, *args, **kwargs): def decorated_includes(func, includes, *args, **kwargs):
urlconf_module, app_name, namespace = includes urlconf_module, app_name, namespace = includes
for item in urlconf_module: for item in urlconf_module:
if isinstance(item, RegexURLPattern): if isinstance(item, URLResolver):
item.__class__ = DecoratedURLResolver
else:
item.__class__ = DecoratedURLPattern item.__class__ = DecoratedURLPattern
item._decorate_with = func item._decorate_with = func
elif isinstance(item, RegexURLResolver):
item.__class__ = DecoratedRegexURLResolver
item._decorate_with = func
return urlconf_module, app_name, namespace return urlconf_module, app_name, namespace
def manager_required(function=None, login_url=None): def manager_required(function=None, login_url=None):
def check_manager(user): def check_manager(user):
if user and user.is_staff: if user and user.is_staff:
return True return True
if user and not user.is_anonymous(): if user and not user.is_anonymous:
raise PermissionDenied() raise PermissionDenied()
# As the last resort, show the login form # As the last resort, show the login form
return False return False

View File

@ -27,7 +27,7 @@ from django.conf import settings
from django.template import loader from django.template import loader
from django.utils.translation import activate from django.utils.translation import activate
from django.core.files.storage import DefaultStorage from django.core.files.storage import DefaultStorage
from django.core.urlresolvers import reverse from django.urls import reverse
from django.core import signing from django.core import signing
from django.utils.six.moves.urllib import parse as urlparse from django.utils.six.moves.urllib import parse as urlparse

View File

@ -4,11 +4,12 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core import signing from django.core import signing
from django.utils import timezone from django.utils import timezone
from django.core.urlresolvers import reverse from django.urls import reverse
from django.views.generic import CreateView, UpdateView, DeleteView, \ from django.views.generic import CreateView, UpdateView, DeleteView, \
ListView, TemplateView, RedirectView, DetailView, FormView ListView, TemplateView, RedirectView, DetailView, FormView
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.utils.decorators import method_decorator
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.feedgenerator import Atom1Feed as DjangoAtom1Feed from django.utils.feedgenerator import Atom1Feed as DjangoAtom1Feed
from django.utils.http import quote from django.utils.http import quote
@ -18,6 +19,7 @@ from django.contrib.auth import views as auth_views
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ngettext from django.utils.translation import ngettext
from django.views.decorators.cache import never_cache
from . import models from . import models
from .forms import AnnounceForm, CategoryForm, SubscriptionsImportForm, \ from .forms import AnnounceForm, CategoryForm, SubscriptionsImportForm, \
@ -30,24 +32,23 @@ except ImportError:
get_idps = lambda: [] get_idps = lambda: []
def login(request, *args, **kwargs): class LoginView(auth_views.LoginView):
if any(get_idps()): def get(self, request, *args, **kwargs):
if 'next' not in request.GET: if any(get_idps()):
return HttpResponseRedirect(resolve_url('mellon_login')) if not 'next' in request.GET:
return HttpResponseRedirect(resolve_url('mellon_login') + '?next=' + return HttpResponseRedirect(resolve_url('mellon_login'))
quote(request.GET.get('next'))) return HttpResponseRedirect(
return auth_views.login(request, *args, **kwargs) resolve_url('mellon_login') + '?next=' + quote(request.GET.get('next'))
)
return super(LoginView, self).get(request, *args, **kwargs)
def logout(request, next_page=None): class LogoutView(auth_views.LogoutView):
if any(get_idps()): @method_decorator(never_cache)
return HttpResponseRedirect(resolve_url('mellon_logout')) def dispatch(self, request, *args, **kwargs):
auth_logout(request) if any(get_idps()):
if next_page is not None: return HttpResponseRedirect(resolve_url('mellon_logout'))
next_page = resolve_url(next_page) return super(LogoutView, self).dispatch(request, *args, **kwargs)
else:
next_page = '/'
return HttpResponseRedirect(next_page)
class HomepageView(RedirectView): class HomepageView(RedirectView):

View File

@ -83,7 +83,7 @@ class PickerWidgetMixin(object):
super(PickerWidgetMixin, self).__init__(attrs, format=self.format) super(PickerWidgetMixin, self).__init__(attrs, format=self.format)
def render(self, name, value, attrs=None): def render(self, name, value, attrs=None, renderer=None):
final_attrs = self.build_attrs(attrs) final_attrs = self.build_attrs(attrs)
rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs) rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs)

View File

@ -109,9 +109,9 @@ setup(
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2',
], ],
install_requires=['django>1.7, <1.12', install_requires=['django>1.7, <2.3',
'django-ckeditor<4.5.4', 'django-ckeditor<4.5.4',
'djangorestframework>=3.3,<3.7', 'djangorestframework>=3.3,<3.8',
'html2text', 'html2text',
'gadjo', 'gadjo',
'emails', 'emails',

View File

@ -1,2 +1,4 @@
# Add corbo hobo agent # Add corbo hobo agent
INSTALLED_APPS = ('corbo.hobo_agent', 'hobo.agent.common') + INSTALLED_APPS INSTALLED_APPS = ('corbo.hobo_agent', 'hobo.agent.common') + INSTALLED_APPS
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = ['rest_framework.authentication.BasicAuthentication']

View File

@ -6,7 +6,7 @@ import tempfile
from django.core.files.storage import DefaultStorage from django.core.files.storage import DefaultStorage
from django.utils import timezone from django.utils import timezone
from django.core.urlresolvers import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.test import override_settings from django.test import override_settings

View File

@ -4,7 +4,7 @@ import urllib
from uuid import uuid4 from uuid import uuid4
from django.core.urlresolvers import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.text import slugify from django.utils.text import slugify
@ -50,7 +50,7 @@ def user():
def test_get_newsletters(app, categories, announces, user): def test_get_newsletters(app, categories, announces, user):
resp = app.get(reverse('newsletters'), status=403) resp = app.get(reverse('newsletters'), status=(401, 403))
app.authorization = ('Basic', ('john.doe', 'password')) app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.get(reverse('newsletters')) resp = app.get(reverse('newsletters'))
data = resp.json data = resp.json
@ -65,9 +65,9 @@ def test_get_newsletters(app, categories, announces, user):
def test_get_subscriptions(app, categories, announces, user): def test_get_subscriptions(app, categories, announces, user):
resp = app.get(reverse('subscriptions'), status=403) resp = app.get(reverse('subscriptions'), status=(401, 403))
uuid = str(uuid4()) uuid = str(uuid4())
resp = app.get(reverse('subscriptions'), params={'uuid': uuid}, status=403) resp = app.get(reverse('subscriptions'), params={'uuid': uuid}, status=(401, 403))
app.authorization = ('Basic', ('john.doe', 'password')) app.authorization = ('Basic', ('john.doe', 'password'))
for identifier, name in channel_choices: for identifier, name in channel_choices:
@ -105,7 +105,7 @@ def test_update_subscriptions(app, categories, announces, user):
def test_delete_subscriptions(app, categories, announces, user): def test_delete_subscriptions(app, categories, announces, user):
params = urlencode({'uuid': str(uuid4())}) params = urlencode({'uuid': str(uuid4())})
subscriptions_url = reverse('subscriptions') + '?' + params subscriptions_url = reverse('subscriptions') + '?' + params
resp = app.delete(subscriptions_url, status=403) resp = app.delete(subscriptions_url, status=(401, 403))
app.authorization = ('Basic', ('john.doe', 'password')) app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.delete(subscriptions_url) resp = app.delete(subscriptions_url)
if resp.json['data']: if resp.json['data']:
@ -119,7 +119,7 @@ def test_simple_email_subscription(app, categories, user):
url = '/api/subscribe/?uuid=%s&email=john@example.net' % uuid url = '/api/subscribe/?uuid=%s&email=john@example.net' % uuid
# anonymous # anonymous
resp = app.post_json(url, params=payload, status=403) resp = app.post_json(url, params=payload, status=(401, 403))
assert resp.json['detail'] == 'Authentication credentials were not provided.' assert resp.json['detail'] == 'Authentication credentials were not provided.'
# authenticated # authenticated

View File

@ -7,7 +7,7 @@ import mock
import random import random
import requests import requests
from django.core.urlresolvers import reverse from django.urls import reverse
from django.core import mail, signing from django.core import mail, signing
from django.utils import timezone from django.utils import timezone
from django.core.files.storage import DefaultStorage from django.core.files.storage import DefaultStorage

View File

@ -2,6 +2,7 @@ import uuid
import pytest import pytest
import django
from django.db import connection from django.db import connection
from django.db.migrations.executor import MigrationExecutor from django.db.migrations.executor import MigrationExecutor
@ -9,6 +10,8 @@ pytestmark = pytest.mark.django_db
def test_subscription_sms_identifier_format_migration(): def test_subscription_sms_identifier_format_migration():
if django.VERSION >= (2, 0, 0):
pytest.skip('NotSupportedError')
executor = MigrationExecutor(connection) executor = MigrationExecutor(connection)
app = 'corbo' app = 'corbo'
migrate_from = [(app, '0009_auto_20170120_1533')] migrate_from = [(app, '0009_auto_20170120_1533')]

View File

@ -3,7 +3,7 @@ import mock
import os import os
import pytest import pytest
from django.core.urlresolvers import reverse from django.urls import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import override_settings from django.test import override_settings

View File

@ -4,7 +4,7 @@ import pytest
from webtest import Upload from webtest import Upload
from django.utils.text import slugify from django.utils.text import slugify
from django.core.urlresolvers import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from corbo.models import Category, Subscription from corbo.models import Category, Subscription

19
tox.ini
View File

@ -1,6 +1,6 @@
[tox] [tox]
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/corbo/{env:BRANCH_NAME:} toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/corbo/{env:BRANCH_NAME:}
envlist = py2-coverage-django111,py3-django111 envlist = py2-coverage-django111,py3-django111,py3-django22
[testenv] [testenv]
usedevelop = usedevelop =
@ -10,16 +10,17 @@ setenv =
CORBO_SETTINGS_FILE=tests/settings.py CORBO_SETTINGS_FILE=tests/settings.py
coverage: COVERAGE=--junitxml=test_results.xml --cov-report xml --cov-report html --cov=corbo/ --cov-config .coveragerc coverage: COVERAGE=--junitxml=test_results.xml --cov-report xml --cov-report html --cov=corbo/ --cov-config .coveragerc
deps = deps =
django>=1.11,<1.12 django111: django>=1.11,<1.12
django22: django>=2.2,<2.3
http://git.entrouvert.org/hobo.git/snapshot/hobo-master.tar.gz http://git.entrouvert.org/hobo.git/snapshot/hobo-master.tar.gz
pytest-cov pytest-cov
pytest-django>=3.1.1,<3.4.6 pytest-django
pytest>=3.0.4 pytest
django-webtest<1.9.3 django-webtest
django-ckeditor<4.5.3 git+http://git.entrouvert.org/debian/django-ckeditor.git
djangorestframework>=3.3,<3.7 djangorestframework>=3.3,<3.8
pylint==1.4.0 pylint
astroid==1.3.2 astroid
mock mock
commands = commands =
py.test {env:COVERAGE:} {posargs:tests/} py.test {env:COVERAGE:} {posargs:tests/}