From: Patrick Samson Date: Mon, 6 Jan 2014 21:29:24 +0000 (+0100) Subject: Made the code compatible with Python 2 & 3 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=0a98192bb7593335ecc5e21fcf0716ed34685042;p=django-postman.git Made the code compatible with Python 2 & 3 --- diff --git a/CHANGELOG b/CHANGELOG index f7beab3..570c201 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,69 +2,73 @@ Django Postman changelog ======================== +Version 3.1.0, January 2014 +--------------------------- +* Used the 'Python 2/3 Compatible Source' strategy for a codebase compatible with Python 2 & 3 (version 3.3). + Version 3.0.2, October 2013 --------------------------- -* Rename test_urls.py to urls_for_tests.py, for adjustment with the new test discovery feature of Django 1.6. -* Fix the need for some translations to become lazy, introduced by the conversion to class-based views. -* Fix issue #36, BooleanField definition needs an explicit default value for Django 1.6. -* Fix issue #35, the app can work without the sites framework. +* Renamed test_urls.py to urls_for_tests.py, for adjustment with the new test discovery feature of Django 1.6. +* Fixed the need for some translations to become lazy, introduced by the conversion to class-based views. +* Fixed issue #36, BooleanField definition needs an explicit default value for Django 1.6. +* Fixed issue #35, the app can work without the sites framework. Version 3.0.1, August 2013 -------------------------- -* Fix issue #32, an IndexError when a Paginator is used and the folder is empty. +* Fixed issue #32, an IndexError when a Paginator is used and the folder is empty. Version 3.0.0, July 2013 ------------------------ -* !MAJOR! Redesign the DB queries for the 'by conversation' mode, +* !MAJOR! Redesigned the DB queries for the 'by conversation' mode, to fix the performances problem of issue #15. Note that the counting of messages by thread is no more global (all folders) but is now limited to the only targeted folder. -* Convert all function-based views to class-based views. -* Extend the support of django-notification from version 0.2.0 to 1.0. -* Avoid the 'Enter text to search.' help text imposed in version 1.2.5 of ajax_select. +* Converted all function-based views to class-based views. +* Extended the support of django-notification from version 0.2.0 to 1.0. +* Avoided the 'Enter text to search.' help text imposed in version 1.2.5 of ajax_select. Version 2.1.1, December 2012 ---------------------------- -* Fix issue #21, a missing unicode/str encoding migration. +* Fixed issue #21, a missing unicode/str encoding migration. Version 2.1.0, December 2012 ---------------------------- -* Make the app compatible with the new 'Custom Auth Model' feature of Django 1.5. -* Add a setting: POSTMAN_SHOW_USER_AS. -* Remove the dependency to django-pagination in the default template set. -* Add an optional auto_moderators parameter to the pm_write() API function. -* Add a template for the autocomplete of multiple recipients in version 1.2.x of django-ajax-selects. +* Made the app compatible with the new 'Custom Auth Model' feature of Django 1.5. +* Added a setting: POSTMAN_SHOW_USER_AS. +* Removed the dependency to django-pagination in the default template set. +* Added an optional auto_moderators parameter to the pm_write() API function. +* Added a template for the autocomplete of multiple recipients in version 1.2.x of django-ajax-selects. Version 2.0.0, August 2012 -------------------------- -* Add an API. -* Add a CSS example, for view.html. -* Rename the extra context variables passed to the notifier app to avoid name clash: +* Added an API. +* Added a CSS example, for view.html. +* Renamed the extra context variables passed to the notifier app to avoid name clash: pm_message and pm_action * More adjustments for Django 1.4. -* Change medias/ to static/ for conformance with django 1.3. +* Changed medias/ to static/ for conformance with django 1.3. * Adjustments for integration with version 1.2.x of django-ajax-selects, in addition to 1.1.x: - - Rename autocomplete_postman_*.html as autocomplete_postman_*_as1-1.html + - Renamed autocomplete_postman_*.html as autocomplete_postman_*_as1-1.html to make clear that they are for django-*a*jax-*s*elects app version 1.1.x. - - Replace the template variable 'is_autocompleted' (a boolean) by 'autocompleter_app' + - Replaced the template variable 'is_autocompleted' (a boolean) by 'autocompleter_app' (a dictionary with keys: 'is_active', 'name' and 'version'). -* Add this CHANGELOG file. +* Added this CHANGELOG file. Version 1.2.0, March 2012 ------------------------- -* Improve the or_me filter, in relation with issue #5. -* Improve the autopagination performance. +* Improved the or_me filter, in relation with issue #5. +* Improved the autopagination performance. * First adjustments for Django 1.4. Version 1.1.0, January 2012 --------------------------- -* Add a setting: POSTMAN_DISABLE_USER_EMAILING. +* Added a setting: POSTMAN_DISABLE_USER_EMAILING. * No need for an immediate rejection notification for a User. -* Add an ordering criteria. +* Added an ordering criteria. Version 1.0.1, January 2011 --------------------------- -* Fix issue #1. +* Fixed issue #1. Version 1.0.0, January 2011 --------------------------- diff --git a/docs/conf.py b/docs/conf.py index 9a8fe1d..0a3aff1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,7 +47,7 @@ copyright = u'2010, Patrick Samson' # The short X.Y version. version = '3.0' # The full version, including alpha/beta/rc tags. -release = '3.0.2' +release = '3.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 221b85e..a5a0c5e 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -6,17 +6,19 @@ Quick start guide Requisites and dependances -------------------------- -Python version >= 2.6 +Python version >= 2.6 or >= 3.3 Some reasons: -* use of ``str.format()`` +* (2.6) use of ``str.format()`` -Django version >= 1.3 +Django version >= 1.4.2 on py2, >= 1.5.5 on py3 Some reasons: -* use of class-based views +* (1.5.5/py3) Six version >= 1.4.0 +* (1.4.2) use of the Six library for supporting Python 2 and 3 in a single codebase +* (1.3) use of class-based views Installation ------------ diff --git a/postman/__init__.py b/postman/__init__.py index 5f7f5b9..087d630 100644 --- a/postman/__init__.py +++ b/postman/__init__.py @@ -4,7 +4,7 @@ A messaging application for Django from __future__ import unicode_literals # following PEP 386: N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN] -VERSION = (3, 0, 2) +VERSION = (3, 1, 0) PREREL = () POST = 0 DEV = 0 diff --git a/postman/models.py b/postman/models.py index 397b918..6b3fc48 100644 --- a/postman/models.py +++ b/postman/models.py @@ -10,6 +10,8 @@ from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models from django.db.models.query import QuerySet +from django.utils import six +from django.utils.encoding import force_text, python_2_unicode_compatible try: from django.utils.text import Truncator # Django 1.4 except ImportError: @@ -67,18 +69,18 @@ def get_user_representation(user): Return a User representation for display, configurable through an optional setting. """ show_user_as = getattr(settings, 'POSTMAN_SHOW_USER_AS', None) - if isinstance(show_user_as, (unicode, str)): + if isinstance(show_user_as, six.string_types): attr = getattr(user, show_user_as, None) if callable(attr): attr = attr() if attr: - return unicode(attr) + return force_text(attr) elif callable(show_user_as): try: - return unicode(show_user_as(user)) + return force_text(show_user_as(user)) except: pass - return unicode(user) # default value, or in case of empty attribute or exception + return force_text(user) # default value, or in case of empty attribute or exception class MessageManager(models.Manager): @@ -105,9 +107,11 @@ class MessageManager(models.Manager): else: qs = qs.extra(select={'count': '{0}.count'.format(qs.query.pm_alias_prefix)}) qs.query.pm_set_extra(table=( + # extra columns are always first in the SELECT query self.filter(lookups, thread_id__isnull=True).extra(select={'count': 0})\ .values_list('id', 'count').order_by(), - self.filter(lookups, thread_id__isnull=False).values('thread').annotate(id=models.Max('pk'), count=models.Count('pk'))\ + # use separate annotate() to keep control of the necessary order + self.filter(lookups, thread_id__isnull=False).values('thread').annotate(count=models.Count('pk')).annotate(id=models.Max('pk'))\ .values_list('id', 'count').order_by(), )) return qs @@ -221,6 +225,7 @@ class MessageManager(models.Manager): ).update(read_at=now()) +@python_2_unicode_compatible class Message(models.Model): """ A message between a User and another User or an AnonymousUser. @@ -256,7 +261,7 @@ class Message(models.Model): verbose_name_plural = _("messages") ordering = ['-sent_at', '-id'] - def __unicode__(self): + def __str__(self): return "{0}>{1}:{2}".format(self.obfuscated_sender, self.obfuscated_recipient, Truncator(self.subject).words(5)) def get_absolute_url(self): @@ -293,7 +298,8 @@ class Message(models.Model): """ email = self.email - digest = hashlib.md5(email + settings.SECRET_KEY).hexdigest() + data = email + settings.SECRET_KEY + digest = hashlib.md5(data.encode()).hexdigest() # encode(): py3 needs a buffer of bytes shrunken_digest = '..'.join((digest[:4], digest[-4:])) # 32 characters is too long and is useless bits = email.split('@') if len(bits) != 2: diff --git a/postman/query.py b/postman/query.py index a0a17b5..4a6c522 100644 --- a/postman/query.py +++ b/postman/query.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals -import new from types import MethodType from django.db.models.sql.query import Query +from django.utils import six class Proxy(object): @@ -17,7 +17,10 @@ class Proxy(object): target = self._target f = getattr(target, name) if isinstance(f, MethodType): - return new.instancemethod(f.im_func, self, target.__class__) + if six.PY3: + return MethodType(f.__func__, self) + else: + return MethodType(f.__func__, self, target.__class__) else: return f diff --git a/postman/templatetags/postman_tags.py b/postman/templatetags/postman_tags.py index 7ca48eb..084826b 100644 --- a/postman/templatetags/postman_tags.py +++ b/postman/templatetags/postman_tags.py @@ -11,6 +11,8 @@ from django.template import Node from django.template import TemplateSyntaxError from django.template import Library from django.template.defaultfilters import date +from django.utils import six +from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from postman.models import ORDER_BY_KEY, ORDER_BY_MAPPER, Message,\ @@ -41,10 +43,10 @@ def or_me(value, arg): """ user_model = get_user_model() - if not isinstance(value, (unicode, str)): - value = (get_user_representation if isinstance(value, user_model) else unicode)(value) - if not isinstance(arg, (unicode, str)): - arg = (get_user_representation if isinstance(arg, user_model) else unicode)(arg) + if not isinstance(value, six.string_types): + value = (get_user_representation if isinstance(value, user_model) else force_text)(value) + if not isinstance(arg, six.string_types): + arg = (get_user_representation if isinstance(arg, user_model) else force_text)(arg) return _('') if value == arg else value diff --git a/postman/tests.py b/postman/tests.py index b781317..5cfc744 100644 --- a/postman/tests.py +++ b/postman/tests.py @@ -24,7 +24,7 @@ INSTALLED_APPS = ( 'django.contrib.sessions', # 'django.contrib.sites', # is optional 'django.contrib.admin', - # 'pagination', # or use the mock + # 'pagination', # has to be before postman ; or use the mock # 'ajax_select', # is an option # 'notification', # is an option 'postman', @@ -52,8 +52,10 @@ from django.db.models import Q from django.http import QueryDict from django.template import Template, Context, TemplateSyntaxError, TemplateDoesNotExist from django.test import TestCase -from django.utils.encoding import force_unicode +from django.utils.encoding import force_text from django.utils.formats import localize +from django.utils import six +from django.utils.six.moves import reload_module try: from django.utils.timezone import now # Django 1.4 aware datetimes except ImportError: @@ -77,7 +79,7 @@ class GenericTest(TestCase): Usual generic tests. """ def test_version(self): - self.assertEqual(sys.modules['postman'].__version__, "3.0.2") + self.assertEqual(sys.modules['postman'].__version__, "3.1.0") class BaseTest(TestCase): @@ -182,14 +184,14 @@ class BaseTest(TestCase): "Reload some modules after a change in settings." clear_url_caches() try: - reload(sys.modules['postman.utils']) - reload(sys.modules['postman.fields']) - reload(sys.modules['postman.forms']) - reload(sys.modules['postman.views']) - reload(sys.modules['postman.urls']) + reload_module(sys.modules['postman.utils']) + reload_module(sys.modules['postman.fields']) + reload_module(sys.modules['postman.forms']) + reload_module(sys.modules['postman.views']) + reload_module(sys.modules['postman.urls']) except KeyError: # happens once at the setUp pass - reload(get_resolver(get_urlconf()).urlconf_module) + reload_module(get_resolver(get_urlconf()).urlconf_module) class ViewTest(BaseTest): @@ -1576,7 +1578,8 @@ class FiltersTest(BaseTest): pass # (1.2) template/__init__.py/_render_value_in_context() # (1.3) template/base.py/_render_value_in_context() - default = force_unicode(localize(dt)) + # (1.6) template/base.py/render_value_in_context() + default = force_text(localize(dt)) self.check_compact_date(dt, default, format='') self.check_compact_date(dt, default, format='one') @@ -1673,8 +1676,9 @@ class UtilsTest(BaseTest): # a property name settings.POSTMAN_SHOW_USER_AS = 'email' self.assertEqual(get_user_representation(self.user1), "foo@domain.com") - settings.POSTMAN_SHOW_USER_AS = b'email' - self.assertEqual(get_user_representation(self.user1), "foo@domain.com") + if not six.PY3: # avoid six.PY2, not available in six 1.2.0 + settings.POSTMAN_SHOW_USER_AS = b'email' # usage on PY3 is nonsense + self.assertEqual(get_user_representation(self.user1), "foo@domain.com") # a method name settings.POSTMAN_SHOW_USER_AS = 'get_absolute_url' # can't use get_full_name(), an empty string in our case self.assertEqual(get_user_representation(self.user1), "/users/foo/") diff --git a/postman/utils.py b/postman/utils.py index 64dbacf..62c7dcb 100644 --- a/postman/utils.py +++ b/postman/utils.py @@ -5,7 +5,7 @@ from textwrap import TextWrapper from django.conf import settings from django.template.loader import render_to_string -from django.utils.encoding import force_unicode +from django.utils.encoding import force_text from django.utils.translation import ugettext, ugettext_lazy as _ # make use of a favourite notifier app such as django-notification @@ -45,7 +45,7 @@ def format_body(sender, body, indent=_("> "), width=WRAP_WIDTH): Used for quoting messages in replies. """ - indent = force_unicode(indent) # join() doesn't work on lists with lazy translation objects + indent = force_text(indent) # join() doesn't work on lists with lazy translation objects ; nor startswith() wrapper = TextWrapper(width=width, initial_indent=indent, subsequent_indent=indent) # rem: TextWrapper doesn't add the indent on an empty text quote = '\n'.join([line.startswith(indent) and indent+line or wrapper.fill(line) or indent for line in body.splitlines()]) diff --git a/postman/views.py b/postman/views.py index 1627ece..8d3a1b9 100644 --- a/postman/views.py +++ b/postman/views.py @@ -1,5 +1,4 @@ from __future__ import unicode_literals -import urlparse from django.conf import settings from django.contrib import messages @@ -14,6 +13,10 @@ from django.db.models import Q from django.http import Http404 from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator +try: + from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit # Django 1.4.11, 1.5.5 +except ImportError: + from urlparse import urlsplit, urlunsplit try: from django.utils.timezone import now # Django 1.4 aware datetimes except ImportError: @@ -39,8 +42,8 @@ csrf_protect_m = method_decorator(csrf_protect) def _get_referer(request): """Return the HTTP_REFERER, if existing.""" if 'HTTP_REFERER' in request.META: - sr = urlparse.urlsplit(request.META['HTTP_REFERER']) - return urlparse.urlunsplit(('', '', sr.path, sr.query, sr.fragment)) + sr = urlsplit(request.META['HTTP_REFERER']) + return urlunsplit(('', '', sr.path, sr.query, sr.fragment)) ######## @@ -165,7 +168,7 @@ class ComposeMixin(object): 'exchange_filter': self.exchange_filter, 'max': self.max, 'site': get_current_site(self.request), - }) + }) return kwargs def get_success_url(self): diff --git a/setup.py b/setup.py index 3b60ffe..9648bc7 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 3', 'Topic :: Communications :: Email', ], install_requires=[