Commits

Patrick Samson  committed 62fbee8

Made the code compatible with Python 2 & 3

  • Participants
  • Parent commits 92ede1f
  • Tags 3.1.0

Comments (0)

Files changed (11)

 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
 ---------------------------

File docs/conf.py

 # 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.

File docs/quickstart.rst

 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
 ------------

File postman/__init__.py

 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

File postman/models.py

 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:
     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):
         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
         ).update(read_at=now())
 
 
+@python_2_unicode_compatible
 class Message(models.Model):
     """
     A message between a User and another User or an AnonymousUser.
         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):
 
         """
         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:

File postman/query.py

 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):
         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
 

File postman/templatetags/postman_tags.py

 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,\
 
     """
     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
 
 

File postman/tests.py

     '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',
 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:
     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):
         "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):
             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')
         # 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/")

File postman/utils.py

 
 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
     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()])

File postman/views.py

 from __future__ import unicode_literals
-import urlparse
 
 from django.conf import settings
 from django.contrib import messages
 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:
     from datetime import datetime
 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))
 
 
 ########
                 'exchange_filter': self.exchange_filter,
                 'max': self.max,
                 'site': get_current_site(self.request),
-            })        
+            })
         return kwargs
 
     def get_success_url(self):
         'License :: OSI Approved :: BSD License',
         'Operating System :: OS Independent',
         'Programming Language :: Python',
+        'Programming Language :: Python :: 3',
         'Topic :: Communications :: Email',
     ],
     install_requires=[