]> git.parisson.com Git - django-postman.git/commitdiff
Made the code compatible with Python 2 & 3
authorPatrick Samson <pk.samson@gmail.com>
Mon, 6 Jan 2014 21:29:24 +0000 (22:29 +0100)
committerPatrick Samson <pk.samson@gmail.com>
Mon, 6 Jan 2014 21:29:24 +0000 (22:29 +0100)
CHANGELOG
docs/conf.py
docs/quickstart.rst
postman/__init__.py
postman/models.py
postman/query.py
postman/templatetags/postman_tags.py
postman/tests.py
postman/utils.py
postman/views.py
setup.py

index f7beab35bd48a335f306582c3bfccb4c1624aeb5..570c201cbcb5eae53f09ee77ab896b93c1e0156b 100644 (file)
--- 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
 ---------------------------
index 9a8fe1d6967d844b480eb291962c79f2a4c3826b..0a3aff1c34ecd49f34b56a3b3046ce90a58d70f6 100644 (file)
@@ -47,7 +47,7 @@ copyright = u'2010, Patrick Samson'
 # The short X.Y version.\r
 version = '3.0'\r
 # The full version, including alpha/beta/rc tags.\r
-release = '3.0.2'\r
+release = '3.1.0'\r
 \r
 # The language for content autogenerated by Sphinx. Refer to documentation\r
 # for a list of supported languages.\r
index 221b85edf13e8d52d4fd70175e8f706641f41e11..a5a0c5e6fe7ee2d3a49c26b2e1489a237526b225 100644 (file)
@@ -6,17 +6,19 @@ Quick start guide
 Requisites and dependances\r
 --------------------------\r
 \r
-Python version >= 2.6\r
+Python version >= 2.6 or >= 3.3\r
 \r
 Some reasons:\r
 \r
-* use of ``str.format()``\r
+* (2.6) use of ``str.format()``\r
 \r
-Django version >= 1.3\r
+Django version >= 1.4.2 on py2, >= 1.5.5 on py3\r
 \r
 Some reasons:\r
 \r
-* use of class-based views\r
+* (1.5.5/py3) Six version >= 1.4.0\r
+* (1.4.2) use of the Six library for supporting Python 2 and 3 in a single codebase\r
+* (1.3) use of class-based views\r
 \r
 Installation\r
 ------------\r
index 5f7f5b9920c5712bd9e1c2a2c3518732fe5fb39e..087d63018808fbbcb62b584119156c0d6e4966e0 100644 (file)
@@ -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
index 397b918d40dc0cd8eddddbd059b77346300ce371..6b3fc488bd1571fe190ca9839ef6988f7572d6d4 100644 (file)
@@ -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:
index a0a17b5fbff66f788c8362d694cdd1f654ff0917..4a6c522d183db57082a36b91749a6c43ff269f69 100644 (file)
@@ -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
 
index 7ca48ebac1a17f5c79d1e52475efbbc4b1643e8a..084826bc5dda6408cec22e75c3f4b506e047dc5c 100644 (file)
@@ -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 _('<me>') if value == arg else value
 
 
index b78131750c17ea178397ac8ad52b143367da3fdf..5cfc744d6ede306c7ebd86c197fa89162f499bda 100644 (file)
@@ -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/")
index 64dbacf825112621d214c553acd78f09c8d3c947..62c7dcb097a0b862584266209b8542edd9762df9 100644 (file)
@@ -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()])
index 1627ece2187a891c38c5f8f6a850e65b651d0c83..8d3a1b9956edea3e181ec12a4c09880afbff4ad5 100644 (file)
@@ -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):
index 3b60ffe1d623d13db45f40cf08fdb4356ec4b1a9..9648bc70270daebd797b96ddc50d5fc4c0b96894 100644 (file)
--- 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=[