diff --git a/.travis.yml b/.travis.yml index 99b064c..939202b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,9 +23,26 @@ python: - "3.6" env: - - TOXENV=py27-1.8.X,py34-1.8.X,py35-1.8.X - - TOXENV=py27-1.10.X,py34-1.10.X,py35-1.10.X - - TOXENV=py27-1.11.X,py34-1.11.X,py36-1.11.X + - TOXENV=py27-1.8.X + - TOXENV=py34-1.8.X + - TOXENV=py35-1.8.X + + - TOXENV=py27-1.10.X + - TOXENV=py34-1.10.X + - TOXENV=py35-1.10.X + + - TOXENV=py27-1.11.X + - TOXENV=py34-1.11.X + - TOXENV=py35-1.11.X + - TOXENV=py36-1.11.X + + - TOXENV=py34-2.0.X + - TOXENV=py35-2.0.X + - TOXENV=py36-2.0.X + + - TOXENV=py35-2.1.X + - TOXENV=py36-2.1.X + - TOXENV=coverage - TOXENV=docs - TOXENV=qunit @@ -41,9 +58,6 @@ install: script: - tox -branches: - only: - - master after_success: - coveralls diff --git a/README.rst b/README.rst index 166884e..4b146c7 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ Features Installation -------------------------------------- -django-scribbler requires Django 1.8, 1.10, or 1.11, and Python 2.7 or >= 3.4. +django-scribbler requires Django 1.8, 1.10, 1.11, or 2.0, and Python 2.7 or >= 3.4. To install from PyPi:: diff --git a/package.json b/package.json index 0625714..06d9919 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,9 @@ }, "dependencies": { "backbone": ">=1.2 <1.3", - "codemirror": ">=5.10 <6.0", + "codemirror": "^5.40.0", "jquery": ">=2.2 <2.3", - "jshint": "^2.9.5", + "jshint": "^2.9.6", "less": "^2.7.3", "phantomjs-prebuilt": "^2.1.16", "underscore": ">=1.8 <1.9" diff --git a/runtests.py b/runtests.py index 6654597..dd02eab 100755 --- a/runtests.py +++ b/runtests.py @@ -1,11 +1,27 @@ #!/usr/bin/env python import sys import os +from optparse import OptionParser import django from django import VERSION as django_version from django.conf import settings +MIDDLEWARES=( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +class DisableMigrations(object): + def __contains__(self, item): + return True + + def __getitem__(self, item): + return 'notmigrations' + if not settings.configured: settings.configure( @@ -22,13 +38,8 @@ 'django.contrib.staticfiles', 'scribbler', ), - MIDDLEWARE_CLASSES=( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - ), + MIDDLEWARE_CLASSES=MIDDLEWARES, + MIDDLEWARE=MIDDLEWARES, SITE_ID=1, SECRET_KEY='super-secret', @@ -68,7 +79,7 @@ # https://docs.djangoproject.com/en/1.11/ref/settings/#migration-modules 'scribbler': 'scribbler.tests.migrations' if django_version < (1, 9) else None, 'dayslog': 'dayslog.tests.migrations' if django_version < (1, 9) else None, - }, + } if django_version >= (1, 9) else DisableMigrations(), MEDIA_ROOT='', MEDIA_URL='/media/', STATIC_ROOT='', @@ -80,18 +91,23 @@ from django.test.utils import get_runner -def runtests(): +def runtests(*test_args, **kwargs): if django_version < (1, 11): # Try lots of ports until we find one we can use os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost:8099-9999' if hasattr(django, 'setup'): django.setup() + if not test_args: + test_args = ['scribbler', ] TestRunner = get_runner(settings) test_runner = TestRunner(verbosity=1, interactive=True, failfast=False) - failures = test_runner.run_tests(['scribbler', ]) + failures = test_runner.run_tests(test_args) sys.exit(failures) if __name__ == '__main__': - runtests() + parser = OptionParser() + + (options, args) = parser.parse_args() + runtests(*args) diff --git a/scribbler/conf.py b/scribbler/conf.py index 2ebd155..ebce048 100644 --- a/scribbler/conf.py +++ b/scribbler/conf.py @@ -15,7 +15,7 @@ def default_cache_key(slug, url): "Construct a cache key for a given slug/url pair." - sha = hashlib.sha1('{0}#{1}'.format(url, slug).encode('ascii')) + sha = hashlib.sha1('{0}#{1}'.format(url, slug).encode('utf8')) return '{0}:{1}'.format(CACHE_PREFIX, sha.hexdigest()) diff --git a/scribbler/forms.py b/scribbler/forms.py index 98090c1..287d255 100644 --- a/scribbler/forms.py +++ b/scribbler/forms.py @@ -5,9 +5,16 @@ from django import forms from django.db.models import ObjectDoesNotExist, FieldDoesNotExist -from django.template import StringOrigin -from django.core.urlresolvers import reverse +try: + from django.template import Origin +except ImportError: # Django<2.0 + from django.template import StringOrigin as Origin +try: + from django.urls import reverse +except ImportError: # Django<2.0 + from django.core.urlresolvers import reverse from django.contrib.contenttypes.models import ContentType +from django.utils.encoding import force_text from .models import Scribble @@ -17,7 +24,7 @@ class ScribbleFormMixin(object): def clean_content(self): content = self.cleaned_data.get('content', '') if content: - origin = StringOrigin(content) + origin = Origin(content) try: from django.template.debug import DebugLexer, DebugParser @@ -27,7 +34,7 @@ def clean_content(self): from django.template import Template # Try to create a Template try: - template = Template(template_string=origin) + template = Template(template_string=force_text(content), origin=origin) # This is an error with creating the template except Exception as e: self.exc_info = { diff --git a/scribbler/models.py b/scribbler/models.py index b9a2ccf..f0787df 100644 --- a/scribbler/models.py +++ b/scribbler/models.py @@ -5,6 +5,10 @@ from django.db import models from django.db.models.signals import post_save, post_delete, pre_save from django.dispatch import receiver +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse try: from django.utils.six import PY3 except ImportError: @@ -35,16 +39,14 @@ def __str__(self): class Meta(object): unique_together = ('slug', 'url') - @models.permalink def get_save_url(self): if self.pk: - return ('edit-scribble', (), {'scribble_id': self.pk}) + return reverse('edit-scribble', kwargs={'scribble_id': self.pk}) else: - return ('create-scribble', (), {}) + return reverse('create-scribble') - @models.permalink def get_delete_url(self): - return ('delete-scribble', (), {'scribble_id': self.pk}) + return reverse('delete-scribble', kwargs={'scribble_id': self.pk}) @receiver(post_save, sender=Scribble) diff --git a/scribbler/templatetags/scribbler_tags.py b/scribbler/templatetags/scribbler_tags.py index c2b660e..af17c46 100644 --- a/scribbler/templatetags/scribbler_tags.py +++ b/scribbler/templatetags/scribbler_tags.py @@ -14,6 +14,16 @@ from scribbler.views import build_scribble_context +try: + TOKEN_VAR = template_base.TokenType.VAR + TOKEN_BLOCK = template_base.TokenType.BLOCK + TOKEN_COMMENT = template_base.TokenType.COMMENT +except AttributeError: + TOKEN_VAR = template_base.TOKEN_VAR + TOKEN_BLOCK = template_base.TOKEN_BLOCK + TOKEN_COMMENT = template_base.TOKEN_COMMENT + + register = template.Library() @@ -88,24 +98,25 @@ def render(self, context): return wrapper_template.render(context_data, request) + def rebuild_template_string(tokens): "Reconstruct the original template from a list of tokens." result = '' for token in tokens: value = token.contents - if token.token_type == template_base.TOKEN_VAR: + if token.token_type == TOKEN_VAR: value = '{0} {1} {2}'.format( template_base.VARIABLE_TAG_START, value, template_base.VARIABLE_TAG_END, ) - elif token.token_type == template_base.TOKEN_BLOCK: + elif token.token_type == TOKEN_BLOCK: value = '{0} {1} {2}'.format( template_base.BLOCK_TAG_START, value, template_base.BLOCK_TAG_END, ) - elif token.token_type == template_base.TOKEN_COMMENT: + elif token.token_type == TOKEN_COMMENT: value = '{0} {1} {2}'.format( template_base.COMMENT_TAG_START, value, diff --git a/scribbler/tests/base.py b/scribbler/tests/base.py index 15009d1..1f3801d 100644 --- a/scribbler/tests/base.py +++ b/scribbler/tests/base.py @@ -5,12 +5,12 @@ import string from django.contrib.auth.models import User -from django.test import TestCase +from django.test import TransactionTestCase from scribbler.models import Scribble -class ScribblerDataTestCase(TestCase): +class ScribblerDataTestCase(TransactionTestCase): "Base test case for creating scribbler models." def get_random_string(self, length=10): diff --git a/scribbler/tests/jinja2.py b/scribbler/tests/jinja2.py index 6c2db3a..ad28cd3 100644 --- a/scribbler/tests/jinja2.py +++ b/scribbler/tests/jinja2.py @@ -1,7 +1,10 @@ from __future__ import absolute_import # Python 2 only from django.contrib.staticfiles.storage import staticfiles_storage -from django.core.urlresolvers import reverse +try: + from django.urls import reverse +except ImportError: # Django<2.0 + from django.core.urlresolvers import reverse from jinja2 import Environment diff --git a/scribbler/tests/migrations/__init__.py b/scribbler/tests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scribbler/tests/test_templatetags.py b/scribbler/tests/test_templatetags.py index 43f65d8..1a82408 100644 --- a/scribbler/tests/test_templatetags.py +++ b/scribbler/tests/test_templatetags.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + "Test for template tags." from __future__ import unicode_literals @@ -12,6 +14,16 @@ from .base import ScribblerDataTestCase from scribbler.conf import CACHE_TIMEOUT +class UnicodeURLTestCase(ScribblerDataTestCase): + "Test, that unicode characters in url got cached" + + def testUnicodeURL(self): + scribble = self.create_scribble( + url='/foo/čřžžýü', slug='sidebar', + content='

Scribble content.

' + ) + self.assertEquals(scribble.url, "/foo/čřžžýü") + class RenderScribbleTestCase(ScribblerDataTestCase): "Tag to render a scribble for the current page." @@ -70,6 +82,16 @@ def test_default_rendering(self): result = self.render_template_tag(slug='"sidebar"') self.assertTrue('

Default.

' in result) + def test_unicode_rendering(self): + "Render with unicode defaults when no scribbles exist." + # On Django>=1.9 ScribbleFormMixin.clean_content directly uses django.template.Template + # and also uses force_text that may fail for non-string objects that have __str__ with + # unicode output. + self.scribble.delete() + unicode_default = '

\u0422\u0435\u043a\u0441\u0442.

' + result = self.render_template_tag(slug='"sidebar"', default=unicode_default) + self.assertTrue(unicode_default in result) + def test_no_slug_given(self): "Slug is required by the tag." self.assertRaises(TemplateSyntaxError, self.render_template_tag, slug='') diff --git a/scribbler/tests/test_views.py b/scribbler/tests/test_views.py index a7ff5f2..ac6f507 100644 --- a/scribbler/tests/test_views.py +++ b/scribbler/tests/test_views.py @@ -9,7 +9,10 @@ from django.contrib.auth.models import Permission, User from django.contrib.contenttypes.models import ContentType -from django.core.urlresolvers import reverse +try: + from django.urls import reverse +except ImportError: # Django<2.0 + from django.core.urlresolvers import reverse from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.test import override_settings @@ -469,4 +472,5 @@ def test_editor(self): action = ActionChains(self.browser) action.send_keys(Keys.F11) action.perform() + self.browser.implicitly_wait(10) self.assertTrue(self.browser.find_element_by_class_name("CodeMirror-fullscreen")) diff --git a/scribbler/tests/urls.py b/scribbler/tests/urls.py index 1806430..4fd1b87 100644 --- a/scribbler/tests/urls.py +++ b/scribbler/tests/urls.py @@ -1,26 +1,36 @@ from django.conf.urls import include, url, handler404, handler500 from django.http import HttpResponseNotFound, HttpResponseServerError from django.contrib.auth import views as auth_views -from django.views.i18n import javascript_catalog +try: + from django.views.i18n import JavaScriptCatalog + javascript_catalog = JavaScriptCatalog.as_view() +except ImportError: # Django<1.10 + from django.views.i18n import javascript_catalog handler404 = 'scribbler.tests.urls.test_404' handler500 = 'scribbler.tests.urls.test_500' -def test_404(request): +def test_404(request, exception=None): return HttpResponseNotFound() -def test_500(request): +def test_500(request, exception=None): return HttpResponseServerError() + js_info_dict = { 'packages': ('scribbler', ), } +try: + login_url = url(r'^test/', auth_views.LoginView.as_view(template_name='test.html')) +except AttributeError: + login_url = url(r'^test/', auth_views.login, {'template_name': 'test.html'}) + urlpatterns = [ url(r'^scribble/', include('scribbler.urls')), url(r'^jsi18n/$', javascript_catalog, js_info_dict, name='jsi18n'), - url(r'^test/', auth_views.login, {'template_name': 'test.html'}), + login_url, ] diff --git a/scribbler/views.py b/scribbler/views.py index 5d5fbbf..1a1005c 100644 --- a/scribbler/views.py +++ b/scribbler/views.py @@ -29,7 +29,7 @@ def build_scribble_context(scribble): @require_POST def preview_scribble(request, ct_pk): "Render scribble content or return error information." - if not request.user.is_authenticated(): + if not request.user.is_authenticated: return HttpResponseForbidden() content_type = get_object_or_404(ContentType, pk=ct_pk) change_scribble = '{0}.change_{1}'.format( @@ -83,7 +83,7 @@ def preview_scribble(request, ct_pk): @require_POST def create_edit_scribble(request, scribble_id=None): "Create a new Scribble or edit an existing one." - if not request.user.is_authenticated(): + if not request.user.is_authenticated: return HttpResponseForbidden() if scribble_id is not None: scribble = get_object_or_404(Scribble, pk=scribble_id) @@ -107,7 +107,7 @@ def create_edit_scribble(request, scribble_id=None): @require_POST def edit_scribble_field(request, ct_pk, instance_pk, field_name): - if not request.user.is_authenticated(): + if not request.user.is_authenticated: return HttpResponseForbidden() content_type = get_object_or_404(ContentType, pk=ct_pk) perm_name = '{0}.change_{1}'.format(content_type.app_label, content_type.model) @@ -133,7 +133,7 @@ def edit_scribble_field(request, ct_pk, instance_pk, field_name): @require_POST def delete_scribble(request, scribble_id): "Delete an existing scribble." - if not request.user.is_authenticated(): + if not request.user.is_authenticated: return HttpResponseForbidden() scribble = get_object_or_404(Scribble, pk=scribble_id) if not request.user.has_perm('scribbler.delete_scribble'): diff --git a/tox.ini b/tox.ini index b1a8314..571109b 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,16 @@ # 1.8: 2.7, 3.2, 3.3, 3.4, 3.5 # 1.10: 2.7, 3.4, 3.5 # 1.11: 2.7, 3.4, 3.5, 3.6 +# 2.0: 3.4, 3.5, 3.6 [tox] -envlist = {py27,py34,py35}-1.8.X,{py27,py34,py35}-1.10.X,{py27,py34,py36}-1.11.X,coverage,docs,qunit +envlist = + {py27,py34,py35}-1.8.X, + {py27,py34,py35}-1.10.X, + {py27,py34,py35,py36}-1.11.X, + {py34,py35,py36}-{2.0}.X, + {py35,py36}-{2.1}.X, + coverage,docs,qunit [testenv] passenv = TRAVIS DISPLAY @@ -18,6 +25,8 @@ deps = 1.8.X: Django>=1.8,<1.9 1.10.X: Django>=1.10,<1.11 1.11.X: Django>=1.11,<2.0 + 2.0.X: Django>=2.0,<2.1 + 2.1.X: Django>=2.1,<2.2 Jinja2 selenium whitelist_externals = make