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
---------------------------
# 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
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
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
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:
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
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
'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/")
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()])
from __future__ import unicode_literals
-import urlparse
from django.conf import settings
from django.contrib import messages
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:
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=[