]> git.parisson.com Git - django-postman.git/commitdiff
Convert all function-based views to class-based views 3.0.0
authorPatrick Samson <pk.samson@gmail.com>
Sun, 21 Jul 2013 18:36:23 +0000 (20:36 +0200)
committerPatrick Samson <pk.samson@gmail.com>
Sun, 21 Jul 2013 18:36:23 +0000 (20:36 +0200)
12 files changed:
CHANGELOG
docs/conf.py
docs/features.rst
docs/moderation.rst
docs/quickstart.rst
docs/views.rst
postman/__init__.py
postman/models.py
postman/test_urls.py
postman/tests.py
postman/urls.py
postman/views.py

index ef98f13c1cbdc1ea4718cca3e18f5a158c8b3521..02a64b62ab16535d09ac6f7ce265eebc16a59129 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -8,6 +8,7 @@ Version 3.0.0, July 2013
        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.
 
index 63e7ec44eaa48e3a40d3b398fb36831798a39b68..c7040779d279d6453cf1dd3f61f995c90d2024c1 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.0a1'\r
+release = '3.0.0'\r
 \r
 # The language for content autogenerated by Sphinx. Refer to documentation\r
 # for a list of supported languages.\r
index 4b4452714e9190b805568ee02acd159c0d671f5f..0167ab81caada508948d5dd7c9769919c5f62ccf 100644 (file)
@@ -35,7 +35,7 @@ you can pass the optional ``max`` parameter to the view.
 There is no parameter for a minimum number, but you can code a custom form\r
 and pass a ``min`` parameter to the recipient field (see Advanced Usage below for details).\r
 \r
-Views supporting the parameter are: ``write``, ``reply``.\r
+Views supporting the parameter are: ``WriteView``, ``ReplyView``.\r
 \r
 But this parameter does not apply to the default ``AnonymousWriteForm`` for visitors:\r
 The maximum is enforced to 1 (see Advanced Usage below for knowing how),\r
@@ -45,8 +45,9 @@ Example::
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',\r
-            {'max': 3}, name='postman_write'),\r
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',\r
+            WriteView.as_view(max=3),\r
+            name='postman_write'),\r
         # ...\r
     )\r
 \r
@@ -81,7 +82,7 @@ User filter
 If there are some situations where a user should not be a recipient, you can write a filter\r
 and pass it to the view.\r
 \r
-Views supporting a user filter are: ``write``, ``reply``.\r
+Views supporting a user filter are: ``WriteView``, ``ReplyView``.\r
 \r
 Example::\r
 \r
@@ -92,8 +93,9 @@ Example::
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',\r
-            {'user_filter': my_user_filter}, name='postman_write'),\r
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',\r
+            WriteView.as_view(user_filter=my_user_filter),\r
+            name='postman_write'),\r
         # ...\r
     )\r
 \r
@@ -139,7 +141,7 @@ If there are some situations where an exchange should not take place, you can wr
 and pass it to the view.\r
 Typical usages would be: blacklists, users that do not want solicitation from visitors.\r
 \r
-Views supporting an exchange filter are: ``write``, ``reply``.\r
+Views supporting an exchange filter are: ``WriteView``, ``ReplyView``.\r
 \r
 An example, with the django-relationships application::\r
 \r
@@ -150,8 +152,9 @@ An example, with the django-relationships application::
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',\r
-            {'exchange_filter': my_exchange_filter}, name='postman_write'),\r
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',\r
+            WriteView.as_view(exchange_filter=my_exchange_filter),\r
+            name='postman_write'),\r
         # ...\r
     )\r
 \r
@@ -242,26 +245,28 @@ Customization
 \r
 You may attach a specific channel, different from the default one, to a particular view.\r
 \r
-Views supporting an auto-complete parameter are: ``write``, ``reply``.\r
+Views supporting an auto-complete parameter are: ``WriteView``, ``ReplyView``.\r
 \r
-For the ``write`` view, the parameter is named ``autocomplete_channels`` (note the plural).\r
+For the ``WriteView`` view, the parameter is named ``autocomplete_channels`` (note the plural).\r
 It supports two variations:\r
 \r
 * a 2-tuple of channels names: the first one for authenticated users, the second for visitors.\r
   Specify ``None`` if you let the default channel name for one of the tuple parts.\r
 * a single channel name: the same for users and visitors\r
 \r
-For the ``reply`` view, the parameter is named ``autocomplete_channel`` (note the singular).\r
+For the ``ReplyView`` view, the parameter is named ``autocomplete_channel`` (note the singular).\r
 The value is the channel name.\r
 \r
 Example::\r
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',\r
-            {'autocomplete_channels': (None,'anonymous_ac')}, name='postman_write'),\r
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',\r
-            {'autocomplete_channel': 'reply_ac'}, name='postman_reply'),\r
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',\r
+            WriteView.as_view(autocomplete_channels=(None,'anonymous_ac')),\r
+            name='postman_write'),\r
+        url(r'^reply/(?P<message_id>[\d]+)/$',\r
+            ReplyView.as_view(autocomplete_channel='reply_ac'),\r
+            name='postman_reply'),\r
         # ...\r
     )\r
 \r
@@ -269,8 +274,9 @@ Example::
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',\r
-            {'autocomplete_channels': 'write_ac'}, name='postman_write'),\r
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',\r
+            WriteView.as_view(autocomplete_channels='write_ac'), \r
+            name='postman_write'),\r
         # ...\r
     )\r
 \r
index 92de994dcf84945d4d928fafc436488a3259b320..e13332473d207575e74c62f32cc55420107695e3 100644 (file)
@@ -21,7 +21,7 @@ You may automate the moderation by giving zero, one, or many auto-moderator func
 to the views.  The value of the parameter can be one single function or a sequence of\r
 functions as a tuple or a list.\r
 \r
-Views supporting an ``auto-moderators`` parameter are: ``write``, ``reply``.\r
+Views supporting an ``auto-moderators`` parameter are: ``WriteView``, ``ReplyView``.\r
 \r
 Example::\r
 \r
@@ -36,10 +36,12 @@ Example::
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',\r
-            {'auto_moderators': (mod1, mod2)}, name='postman_write'),\r
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',\r
-            {'auto_moderators': mod1}, name='postman_reply'),\r
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',\r
+            WriteView.as_view(auto_moderators=(mod1, mod2)),\r
+            name='postman_write'),\r
+        url(r'^reply/(?P<message_id>[\d]+)/$',\r
+            ReplyView.as_view(auto_moderators=mod1),\r
+            name='postman_reply'),\r
         # ...\r
     )\r
 \r
@@ -78,5 +80,3 @@ At the end of the loop, if the decision is not final, the sequence is:
 #. An average rating is computed: if greater or equal to 50, the message is accepted.\r
 #. The message is rejected. The final reason is a comma separated collection of reasons\r
    coming from moderators having returned a rating lesser than 50.\r
-\r
-\r
index 62666c8a6c32d3e7de6abb6a9e430b4ca93cc6f7..221b85edf13e8d52d4fd70175e8f706641f41e11 100644 (file)
@@ -12,11 +12,11 @@ Some reasons:
 \r
 * use of ``str.format()``\r
 \r
-Django version >= 1.2.2\r
+Django version >= 1.3\r
 \r
 Some reasons:\r
 \r
-* use of ``self.stdout`` in management commands\r
+* use of class-based views\r
 \r
 Installation\r
 ------------\r
@@ -157,11 +157,6 @@ Permute the comment tags for the lines denoted by the marks: {# dj v1.x #} in:
 \r
 * base_write.html\r
 \r
-In case you run a Django 1.2 version, perform these additional steps for any template:\r
-\r
-* Remove {% load url from future %}\r
-* Change any {% url 'XX' %} to {% url XX %}\r
-\r
 Relations between templates::\r
 \r
     base.html\r
@@ -213,18 +208,6 @@ See also :ref:`styles` for the stylesheets of views.
 \r
 For Django 1.3+, just follow the instructions related to the staticfiles app.\r
 \r
-For Django 1.2:\r
-       It's up to you to make the files visible to the URL resolver.\r
-\r
-       For example:\r
-\r
-       * Rename the path to :file:`postman/medias/`\r
-       * In a production environment, set :file:`/<MEDIA_ROOT>/postman/` as a symlink to :file:`<Postman_module>/medias/postman/`\r
-       * In a development environment (django's runserver), you can put in the URLconf, something like::\r
-\r
-               ('^' + settings.MEDIA_URL.strip('/') + r'/(?P<path>postman/.*)$', 'django.views.static.serve',\r
-                       {'document_root': os.path.join(imp.find_module('postman')[1], 'medias')}),\r
-\r
 Examples\r
 --------\r
 \r
index f1b445419867c6015ee893340f4d06651abf3adf..5364199dd66fc27989448590d2e6c5a7331be68a 100644 (file)
@@ -25,12 +25,15 @@ Examples::
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',\r
-            {'form_classes': (MyCustomWriteForm, MyCustomAnonymousWriteForm)}, name='postman_write'),\r
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',\r
-            {'form_class': MyCustomFullReplyForm}, name='postman_reply'),\r
-        url(r'^view/(?P<message_id>[\d]+)/$', 'view',\r
-            {'form_class': MyCustomQuickReplyForm}, name='postman_view'),\r
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',\r
+            WriteView.as_view(form_classes=(MyCustomWriteForm, MyCustomAnonymousWriteForm)),\r
+            name='postman_write'),\r
+        url(r'^reply/(?P<message_id>[\d]+)/$',\r
+            ReplyView.as_view(form_class=MyCustomFullReplyForm),\r
+            name='postman_reply'),\r
+        url(r'^view/(?P<message_id>[\d]+)/$',\r
+            MessageView.as_view(form_class=MyCustomQuickReplyForm),\r
+            name='postman_view'),\r
         # ...\r
     )\r
 \r
@@ -43,8 +46,9 @@ Example::
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^view/(?P<message_id>[\d]+)/$', 'view',\r
-            {'template_name': 'my_custom_view.html'}, name='postman_view'),\r
+        url(r'^view/(?P<message_id>[\d]+)/$',\r
+            MessageView.as_view(template_name='my_custom_view.html'),\r
+            name='postman_view'),\r
         # ...\r
     )\r
 \r
@@ -62,18 +66,19 @@ The default algorithm is:
 \r
 The parameter ``success_url`` is available to these views:\r
 \r
-* ``write``\r
-* ``reply``\r
-* ``archive``\r
-* ``delete``\r
-* ``undelete``\r
+* ``WriteView``\r
+* ``ReplyView``\r
+* ``ArchiveView``\r
+* ``DeleteView``\r
+* ``UndeleteView``\r
 \r
 Example::\r
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',\r
-            {'success_url': 'postman_inbox'}, name='postman_reply'),\r
+        url(r'^reply/(?P<message_id>[\d]+)/$',\r
+            ReplyView.as_view(success_url='postman_inbox'),\r
+            name='postman_reply'),\r
         # ...\r
     )\r
 \r
@@ -96,9 +101,11 @@ Examples::
 \r
     urlpatterns = patterns('postman.views',\r
         # ...\r
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',\r
-            {'formatters': (format_subject,format_body)}, name='postman_reply'),\r
-        url(r'^view/(?P<message_id>[\d]+)/$', 'view',\r
-            {'formatters': (format_subject,format_body)}, name='postman_view'),\r
+        url(r'^reply/(?P<message_id>[\d]+)/$',\r
+            ReplyView.as_view(formatters=(format_subject, format_body)),\r
+            name='postman_reply'),\r
+        url(r'^view/(?P<message_id>[\d]+)/$',\r
+            MessageView.as_view(formatters=(format_subject, format_body)),\r
+            name='postman_view'),\r
         # ...\r
     )\r
index eef3e2041ec2ac98d2bbfdc36e6eb84bf5e8bde5..0d02e1ba1a55db2bd2c3e3318b241b8d484b1b1d 100644 (file)
@@ -5,10 +5,14 @@ from __future__ import unicode_literals
 
 # following PEP 386: N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]
 VERSION = (3, 0, 0)
-PREREL = ('a', 1)
+PREREL = ()
 POST = 0
 DEV = 0
 
+# options
+OPTION_MESSAGES = 'm'
+OPTIONS = OPTION_MESSAGES  # may be extended in future
+
 
 def get_version():
     version = '.'.join(map(str, VERSION))
index fbe514b4779d59abe376528221f99b8fc2217807..cbacb82eb61b0d3b5117594b21852be4e6fecb62 100644 (file)
@@ -13,16 +13,16 @@ try:
     from django.utils.text import Truncator  # Django 1.4
 except ImportError:
     from postman.future_1_4 import Truncator
-from django.utils.translation import ugettext, ugettext_lazy as _
 try:
     from django.utils.timezone import now  # Django 1.4 aware datetimes
 except ImportError:
     from datetime import datetime
     now = datetime.now
+from django.utils.translation import ugettext, ugettext_lazy as _
 
-from postman.query import PostmanQuery
-from postman.urls import OPTION_MESSAGES
-from postman.utils import email_visitor, notify_user
+from . import OPTION_MESSAGES
+from .query import PostmanQuery
+from .utils import email_visitor, notify_user
 
 # moderation constants
 STATUS_PENDING = 'p'
index dff34ca02a44e756c78b235a490d1b25bc3f45b4..3aaca9c2cd7b85f7812b4c3d2c9acaa5466697a7 100644 (file)
@@ -9,14 +9,13 @@ try:
     from django.conf.urls import patterns, include, url  # django 1.4
 except ImportError:
     from django.conf.urls.defaults import *  # "patterns, include, url" is enough for django 1.3, "*" for django 1.2
-try:
-    from django.contrib.auth import get_user_model  # Django 1.5
-except ImportError:
-    from postman.future_1_5 import get_user_model
 from django.forms import ValidationError
 from django.views.generic.base import RedirectView
 
-from postman.urls import OPTIONS
+from . import OPTIONS
+from .views import (InboxView, SentView, ArchivesView, TrashView,
+        WriteView, ReplyView, MessageView, ConversationView,
+        ArchiveView, DeleteView, UndeleteView)
 
 
 # user_filter function set
@@ -62,67 +61,67 @@ def format_body(sender, body):
 
 postman_patterns = patterns('postman.views',
     # Basic set
-    url(r'^inbox/(?:(?P<option>'+OPTIONS+')/)?$', 'inbox', name='postman_inbox'),
-    url(r'^sent/(?:(?P<option>'+OPTIONS+')/)?$', 'sent', name='postman_sent'),
-    url(r'^archives/(?:(?P<option>'+OPTIONS+')/)?$', 'archives', name='postman_archives'),
-    url(r'^trash/(?:(?P<option>'+OPTIONS+')/)?$', 'trash', name='postman_trash'),
-    url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', name='postman_write'),
-    url(r'^reply/(?P<message_id>[\d]+)/$', 'reply', name='postman_reply'),
-    url(r'^view/(?P<message_id>[\d]+)/$', 'view', name='postman_view'),
-    url(r'^view/t/(?P<thread_id>[\d]+)/$', 'view_conversation', name='postman_view_conversation'),
-    url(r'^archive/$', 'archive', name='postman_archive'),
-    url(r'^delete/$', 'delete', name='postman_delete'),
-    url(r'^undelete/$', 'undelete', name='postman_undelete'),
+    url(r'^inbox/(?:(?P<option>'+OPTIONS+')/)?$', InboxView.as_view(), name='postman_inbox'),
+    url(r'^sent/(?:(?P<option>'+OPTIONS+')/)?$', SentView.as_view(), name='postman_sent'),
+    url(r'^archives/(?:(?P<option>'+OPTIONS+')/)?$', ArchivesView.as_view(), name='postman_archives'),
+    url(r'^trash/(?:(?P<option>'+OPTIONS+')/)?$', TrashView.as_view(), name='postman_trash'),
+    url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(), name='postman_write'),
+    url(r'^reply/(?P<message_id>[\d]+)/$', ReplyView.as_view(), name='postman_reply'),
+    url(r'^view/(?P<message_id>[\d]+)/$', MessageView.as_view(), name='postman_view'),
+    url(r'^view/t/(?P<thread_id>[\d]+)/$', ConversationView.as_view(), name='postman_view_conversation'),
+    url(r'^archive/$', ArchiveView.as_view(), name='postman_archive'),
+    url(r'^delete/$', DeleteView.as_view(), name='postman_delete'),
+    url(r'^undelete/$', UndeleteView.as_view(), name='postman_undelete'),
     (r'^$', RedirectView.as_view(url='inbox/')),
 
     # Customized set
     # 'success_url'
-    url(r'^write_sent/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'success_url': 'postman_sent'}, name='postman_write_with_success_url_to_sent'),
-    url(r'^reply_sent/(?P<message_id>[\d]+)/$', 'reply', {'success_url': 'postman_sent'}, name='postman_reply_with_success_url_to_sent'),
-    url(r'^archive_arch/$', 'archive', {'success_url': 'postman_archives'}, name='postman_archive_with_success_url_to_archives'),
-    url(r'^delete_arch/$', 'delete', {'success_url': 'postman_archives'}, name='postman_delete_with_success_url_to_archives'),
-    url(r'^undelete_arch/$', 'undelete', {'success_url': 'postman_archives'}, name='postman_undelete_with_success_url_to_archives'),
+    url(r'^write_sent/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(success_url='postman_sent'), name='postman_write_with_success_url_to_sent'),
+    url(r'^reply_sent/(?P<message_id>[\d]+)/$', ReplyView.as_view(success_url='postman_sent'), name='postman_reply_with_success_url_to_sent'),
+    url(r'^archive_arch/$', ArchiveView.as_view(success_url='postman_archives'), name='postman_archive_with_success_url_to_archives'),
+    url(r'^delete_arch/$', DeleteView.as_view(success_url='postman_archives'), name='postman_delete_with_success_url_to_archives'),
+    url(r'^undelete_arch/$', UndeleteView.as_view(success_url='postman_archives'), name='postman_undelete_with_success_url_to_archives'),
     # 'max'
-    url(r'^write_max/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'max': 1}, name='postman_write_with_max'),
-    url(r'^reply_max/(?P<message_id>[\d]+)/$', 'reply', {'max': 1}, name='postman_reply_with_max'),
+    url(r'^write_max/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(max=1), name='postman_write_with_max'),
+    url(r'^reply_max/(?P<message_id>[\d]+)/$', ReplyView.as_view(max=1), name='postman_reply_with_max'),
     # 'user_filter' on write
-    url(r'^write_user_filter_reason/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'user_filter': user_filter_reason}, name='postman_write_with_user_filter_reason'),
-    url(r'^write_user_filter_no_reason/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'user_filter': user_filter_no_reason}, name='postman_write_with_user_filter_no_reason'),
-    url(r'^write_user_filter_false/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'user_filter': user_filter_false}, name='postman_write_with_user_filter_false'),
-    url(r'^write_user_filter_exception/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'user_filter': user_filter_exception}, name='postman_write_with_user_filter_exception'),
+    url(r'^write_user_filter_reason/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(user_filter=user_filter_reason), name='postman_write_with_user_filter_reason'),
+    url(r'^write_user_filter_no_reason/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(user_filter=user_filter_no_reason), name='postman_write_with_user_filter_no_reason'),
+    url(r'^write_user_filter_false/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(user_filter=user_filter_false), name='postman_write_with_user_filter_false'),
+    url(r'^write_user_filter_exception/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(user_filter=user_filter_exception), name='postman_write_with_user_filter_exception'),
     # 'user_filter' on reply
-    url(r'^reply_user_filter_reason/(?P<message_id>[\d]+)/$', 'reply', {'user_filter': user_filter_reason}, name='postman_reply_with_user_filter_reason'),
-    url(r'^reply_user_filter_no_reason/(?P<message_id>[\d]+)/$', 'reply', {'user_filter': user_filter_no_reason}, name='postman_reply_with_user_filter_no_reason'),
-    url(r'^reply_user_filter_false/(?P<message_id>[\d]+)/$', 'reply', {'user_filter': user_filter_false}, name='postman_reply_with_user_filter_false'),
-    url(r'^reply_user_filter_exception/(?P<message_id>[\d]+)/$', 'reply', {'user_filter': user_filter_exception}, name='postman_reply_with_user_filter_exception'),
+    url(r'^reply_user_filter_reason/(?P<message_id>[\d]+)/$', ReplyView.as_view(user_filter=user_filter_reason), name='postman_reply_with_user_filter_reason'),
+    url(r'^reply_user_filter_no_reason/(?P<message_id>[\d]+)/$', ReplyView.as_view(user_filter=user_filter_no_reason), name='postman_reply_with_user_filter_no_reason'),
+    url(r'^reply_user_filter_false/(?P<message_id>[\d]+)/$', ReplyView.as_view(user_filter=user_filter_false), name='postman_reply_with_user_filter_false'),
+    url(r'^reply_user_filter_exception/(?P<message_id>[\d]+)/$', ReplyView.as_view(user_filter=user_filter_exception), name='postman_reply_with_user_filter_exception'),
     # 'exchange_filter' on write
-    url(r'^write_exch_filter_reason/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'exchange_filter': exch_filter_reason}, name='postman_write_with_exch_filter_reason'),
-    url(r'^write_exch_filter_no_reason/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'exchange_filter': exch_filter_no_reason}, name='postman_write_with_exch_filter_no_reason'),
-    url(r'^write_exch_filter_false/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'exchange_filter': exch_filter_false}, name='postman_write_with_exch_filter_false'),
-    url(r'^write_exch_filter_exception/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'exchange_filter': exch_filter_exception}, name='postman_write_with_exch_filter_exception'),
+    url(r'^write_exch_filter_reason/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(exchange_filter=exch_filter_reason), name='postman_write_with_exch_filter_reason'),
+    url(r'^write_exch_filter_no_reason/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(exchange_filter=exch_filter_no_reason), name='postman_write_with_exch_filter_no_reason'),
+    url(r'^write_exch_filter_false/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(exchange_filter=exch_filter_false), name='postman_write_with_exch_filter_false'),
+    url(r'^write_exch_filter_exception/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(exchange_filter=exch_filter_exception), name='postman_write_with_exch_filter_exception'),
     # 'exchange_filter' on reply
-    url(r'^reply_exch_filter_reason/(?P<message_id>[\d]+)/$', 'reply', {'exchange_filter': exch_filter_reason}, name='postman_reply_with_exch_filter_reason'),
-    url(r'^reply_exch_filter_no_reason/(?P<message_id>[\d]+)/$', 'reply', {'exchange_filter': exch_filter_no_reason}, name='postman_reply_with_exch_filter_no_reason'),
-    url(r'^reply_exch_filter_false/(?P<message_id>[\d]+)/$', 'reply', {'exchange_filter': exch_filter_false}, name='postman_reply_with_exch_filter_false'),
-    url(r'^reply_exch_filter_exception/(?P<message_id>[\d]+)/$', 'reply', {'exchange_filter': exch_filter_exception}, name='postman_reply_with_exch_filter_exception'),
+    url(r'^reply_exch_filter_reason/(?P<message_id>[\d]+)/$', ReplyView.as_view(exchange_filter=exch_filter_reason), name='postman_reply_with_exch_filter_reason'),
+    url(r'^reply_exch_filter_no_reason/(?P<message_id>[\d]+)/$', ReplyView.as_view(exchange_filter=exch_filter_no_reason), name='postman_reply_with_exch_filter_no_reason'),
+    url(r'^reply_exch_filter_false/(?P<message_id>[\d]+)/$', ReplyView.as_view(exchange_filter=exch_filter_false), name='postman_reply_with_exch_filter_false'),
+    url(r'^reply_exch_filter_exception/(?P<message_id>[\d]+)/$', ReplyView.as_view(exchange_filter=exch_filter_exception), name='postman_reply_with_exch_filter_exception'),
     # 'auto_moderators'
-    url(r'^write_moderate/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'auto_moderators': (moderate_as_51,moderate_as_48)}, name='postman_write_moderate'),
-    url(r'^reply_moderate/(?P<message_id>[\d]+)/$', 'reply', {'auto_moderators': (moderate_as_51,moderate_as_48)}, name='postman_reply_moderate'),
+    url(r'^write_moderate/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(auto_moderators=(moderate_as_51,moderate_as_48)), name='postman_write_moderate'),
+    url(r'^reply_moderate/(?P<message_id>[\d]+)/$', ReplyView.as_view(auto_moderators=(moderate_as_51,moderate_as_48)), name='postman_reply_moderate'),
     # 'formatters'
-    url(r'^reply_formatters/(?P<message_id>[\d]+)/$', 'reply', {'formatters': (format_subject,format_body)}, name='postman_reply_formatters'),
-    url(r'^view_formatters/(?P<message_id>[\d]+)/$', 'view', {'formatters': (format_subject,format_body)}, name='postman_view_formatters'),
+    url(r'^reply_formatters/(?P<message_id>[\d]+)/$', ReplyView.as_view(formatters=(format_subject, format_body)), name='postman_reply_formatters'),
+    url(r'^view_formatters/(?P<message_id>[\d]+)/$', MessageView.as_view(formatters=(format_subject, format_body)), name='postman_view_formatters'),
     # auto-complete
-    url(r'^write_ac/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'autocomplete_channels': ('postman_multiple_as1-1', None)}, name='postman_write_auto_complete'),
-    url(r'^reply_ac/(?P<message_id>[\d]+)/$', 'reply', {'autocomplete_channel': 'postman_multiple_as1-1'}, name='postman_reply_auto_complete'),
+    url(r'^write_ac/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(autocomplete_channels=('postman_multiple_as1-1', None)), name='postman_write_auto_complete'),
+    url(r'^reply_ac/(?P<message_id>[\d]+)/$', ReplyView.as_view(autocomplete_channel='postman_multiple_as1-1'), name='postman_reply_auto_complete'),
     # 'template_name'
-    url(r'^inbox_template/(?:(?P<option>'+OPTIONS+')/)?$', 'inbox', {'template_name': 'postman/fake.html'}, name='postman_inbox_template'),
-    url(r'^sent_template/(?:(?P<option>'+OPTIONS+')/)?$', 'sent', {'template_name': 'postman/fake.html'}, name='postman_sent_template'),
-    url(r'^archives_template/(?:(?P<option>'+OPTIONS+')/)?$', 'archives', {'template_name': 'postman/fake.html'}, name='postman_archives_template'),
-    url(r'^trash_template/(?:(?P<option>'+OPTIONS+')/)?$', 'trash', {'template_name': 'postman/fake.html'}, name='postman_trash_template'),
-    url(r'^write_template/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', {'template_name': 'postman/fake.html'}, name='postman_write_template'),
-    url(r'^reply_template/(?P<message_id>[\d]+)/$', 'reply', {'template_name': 'postman/fake.html'}, name='postman_reply_template'),
-    url(r'^view_template/(?P<message_id>[\d]+)/$', 'view', {'template_name': 'postman/fake.html'}, name='postman_view_template'),
-    url(r'^view_template/t/(?P<thread_id>[\d]+)/$', 'view_conversation', {'template_name': 'postman/fake.html'}, name='postman_view_conversation_template'),
+    url(r'^inbox_template/(?:(?P<option>'+OPTIONS+')/)?$', InboxView.as_view(template_name='postman/fake.html'), name='postman_inbox_template'),
+    url(r'^sent_template/(?:(?P<option>'+OPTIONS+')/)?$', SentView.as_view(template_name='postman/fake.html'), name='postman_sent_template'),
+    url(r'^archives_template/(?:(?P<option>'+OPTIONS+')/)?$', ArchivesView.as_view(template_name='postman/fake.html'), name='postman_archives_template'),
+    url(r'^trash_template/(?:(?P<option>'+OPTIONS+')/)?$', TrashView.as_view(template_name='postman/fake.html'), name='postman_trash_template'),
+    url(r'^write_template/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(template_name='postman/fake.html'), name='postman_write_template'),
+    url(r'^reply_template/(?P<message_id>[\d]+)/$', ReplyView.as_view(template_name='postman/fake.html'), name='postman_reply_template'),
+    url(r'^view_template/(?P<message_id>[\d]+)/$', MessageView.as_view(template_name='postman/fake.html'), name='postman_view_template'),
+    url(r'^view_template/t/(?P<thread_id>[\d]+)/$', ConversationView.as_view(template_name='postman/fake.html'), name='postman_view_conversation_template'),
 )
 
 urlpatterns = patterns('',
index 3aec90a4224fab47484d4f0a34215a9e4c5f7311..b7055bb198899749b96ee931da08872a79066d02 100644 (file)
@@ -53,22 +53,22 @@ from django.template import Template, Context, TemplateSyntaxError, TemplateDoes
 from django.test import TestCase
 from django.utils.encoding import force_unicode
 from django.utils.formats import localize
-from django.utils.translation import deactivate
 try:
     from django.utils.timezone import now  # Django 1.4 aware datetimes
 except ImportError:
     from datetime import datetime
     now = datetime.now
+from django.utils.translation import deactivate
 
-from postman.api import pm_broadcast, pm_write
+from . import OPTION_MESSAGES
+from .api import pm_broadcast, pm_write
 # because of reload()'s, do "from postman.fields import CommaSeparatedUserField" just before needs
 # because of reload()'s, do "from postman.forms import xxForm" just before needs
-from postman.models import ORDER_BY_KEY, ORDER_BY_MAPPER, Message, PendingMessage,\
-    STATUS_PENDING, STATUS_ACCEPTED, STATUS_REJECTED,\
-    get_order_by, get_user_representation
-from postman.urls import OPTION_MESSAGES
+from .models import ORDER_BY_KEY, ORDER_BY_MAPPER, Message, PendingMessage,\
+        STATUS_PENDING, STATUS_ACCEPTED, STATUS_REJECTED,\
+        get_order_by, get_user_representation
 # because of reload()'s, do "from postman.utils import notification" just before needs
-from postman.utils import format_body, format_subject
+from .utils import format_body, format_subject
 
 
 class GenericTest(TestCase):
@@ -76,7 +76,7 @@ class GenericTest(TestCase):
     Usual generic tests.
     """
     def test_version(self):
-        self.assertEqual(sys.modules['postman'].__version__, "3.0.0a1")
+        self.assertEqual(sys.modules['postman'].__version__, "3.0.0")
 
 
 class BaseTest(TestCase):
@@ -734,7 +734,7 @@ class ViewTest(BaseTest):
 
         # not a POST
         response = self.client.get(url, data)
-        self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.status_code, 405)
         # not yours
         self.assert_(self.client.login(username='baz', password='pass'))
         response = self.client.post(url, data)
@@ -758,7 +758,7 @@ class ViewTest(BaseTest):
 
         # not a POST
         response = self.client.get(url, data)
-        self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.status_code, 405)
         # not yours
         self.assert_(self.client.login(username='baz', password='pass'))
         response = self.client.post(url, data)
index b9a63cc8c513d4008f39f48a5c2424cbbd0121fd..b34c741d17bab600a23398b0048aed5d8bc2c32b 100644 (file)
@@ -8,21 +8,21 @@ Otherwise you may customize the behavior by passing extra parameters.
 
 Recipients Max
 --------------
-Views supporting the parameter are: ``write``, ``reply``.
+Views supporting the parameter are: ``WriteView``, ``ReplyView``.
 Example::
-    ..., {'max': 3}, name='postman_write'),
+    ...View.as_view(max=3), name='postman_write'),
 See also the ``POSTMAN_DISALLOW_MULTIRECIPIENTS`` setting
 
 User filter
 -----------
-Views supporting a user filter are: ``write``, ``reply``.
+Views supporting a user filter are: ``WriteView``, ``ReplyView``.
 Example::
     def my_user_filter(user):
         if user.get_profile().is_absent:
             return "is away"
         return None
     ...
-    ..., {'user_filter': my_user_filter}, name='postman_write'),
+    ...View.as_view(user_filter=my_user_filter), name='postman_write'),
 
 function interface:
 In: a User instance
@@ -30,14 +30,14 @@ Out: None, False, '', 'a reason', or ValidationError
 
 Exchange filter
 ---------------
-Views supporting an exchange filter are: ``write``, ``reply``.
+Views supporting an exchange filter are: ``WriteView``, ``ReplyView``.
 Example::
     def my_exchange_filter(sender, recipient, recipients_list):
         if recipient.relationships.exists(sender, RelationshipStatus.objects.blocking()):
             return "has blacklisted you"
         return None
     ...
-    ..., {'exchange_filter': my_exchange_filter}, name='postman_write'),
+    ...View.as_view(exchange_filter=my_exchange_filter), name='postman_write'),
 
 function interface:
 In:
@@ -48,15 +48,15 @@ Out: None, False, '', 'a reason', or ValidationError
 
 Auto-complete field
 -------------------
-Views supporting an auto-complete parameter are: ``write``, ``reply``.
+Views supporting an auto-complete parameter are: ``WriteView``, ``ReplyView``.
 Examples::
-    ..., {'autocomplete_channels': (None,'anonymous_ac')}, name='postman_write'),
-    ..., {'autocomplete_channels': 'write_ac'}, name='postman_write'),
-    ..., {'autocomplete_channel': 'reply_ac'}, name='postman_reply'),
+    ...View.as_view(autocomplete_channels=(None,'anonymous_ac')), name='postman_write'),
+    ...View.as_view(autocomplete_channels='write_ac'), name='postman_write'),
+    ...View.as_view(autocomplete_channel='reply_ac'), name='postman_reply'),
 
 Auto moderators
 ---------------
-Views supporting an ``auto-moderators`` parameter are: ``write``, ``reply``.
+Views supporting an ``auto-moderators`` parameter are: ``WriteView``, ``ReplyView``.
 Example::
     def mod1(message):
         # ...
@@ -66,8 +66,8 @@ Example::
         return None
     mod2.default_reason = 'mod2 default reason'
     ...
-    ..., {'auto_moderators': (mod1, mod2)}, name='postman_write'),
-    ..., {'auto_moderators': mod1}, name='postman_reply'),
+    ...View.as_view(auto_moderators=(mod1, mod2)), name='postman_write'),
+    ...View.as_view(auto_moderators=mod1), name='postman_reply'),
 
 function interface:
 In: ``message``: a Message instance
@@ -77,37 +77,40 @@ Out: rating or (rating, "reason")
 Others
 ------
 Refer to documentation.
-    ..., {'form_classes': (MyCustomWriteForm, MyCustomAnonymousWriteForm)}, name='postman_write'),
-    ..., {'form_class': MyCustomFullReplyForm}, name='postman_reply'),
-    ..., {'form_class': MyCustomQuickReplyForm}, name='postman_view'),
-    ..., {'template_name': 'my_custom_view.html'}, name='postman_view'),
-    ..., {'success_url': 'postman_inbox'}, name='postman_reply'),
-    ..., {'formatters': (format_subject,format_body)}, name='postman_reply'),
-    ..., {'formatters': (format_subject,format_body)}, name='postman_view'),
+    ...View.as_view(form_classes=(MyCustomWriteForm, MyCustomAnonymousWriteForm)), name='postman_write'),
+    ...View.as_view(form_class=MyCustomFullReplyForm), name='postman_reply'),
+    ...View.as_view(form_class=MyCustomQuickReplyForm), name='postman_view'),
+    ...View.as_view(template_name='my_custom_view.html'), name='postman_view'),
+    ...View.as_view(success_url='postman_inbox'), name='postman_reply'),
+    ...View.as_view(formatters=(format_subject, format_body)), name='postman_reply'),
+    ...View.as_view(formatters=(format_subject, format_body)), name='postman_view'),
 
 """
 from __future__ import unicode_literals
 
 try:
-    from django.conf.urls import patterns, include, url  # django 1.4
+    from django.conf.urls import patterns, url  # django 1.4
 except ImportError:
-    from django.conf.urls.defaults import patterns, include, url  # django 1.3
+    from django.conf.urls.defaults import patterns, url  # django 1.3
 from django.views.generic.base import RedirectView
 
-OPTION_MESSAGES = 'm'
-OPTIONS = OPTION_MESSAGES
+from . import OPTIONS
+from .views import (InboxView, SentView, ArchivesView, TrashView,
+        WriteView, ReplyView, MessageView, ConversationView,
+        ArchiveView, DeleteView, UndeleteView)
+
 
 urlpatterns = patterns('postman.views',
-    url(r'^inbox/(?:(?P<option>'+OPTIONS+')/)?$', 'inbox', name='postman_inbox'),
-    url(r'^sent/(?:(?P<option>'+OPTIONS+')/)?$', 'sent', name='postman_sent'),
-    url(r'^archives/(?:(?P<option>'+OPTIONS+')/)?$', 'archives', name='postman_archives'),
-    url(r'^trash/(?:(?P<option>'+OPTIONS+')/)?$', 'trash', name='postman_trash'),
-    url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write', name='postman_write'),
-    url(r'^reply/(?P<message_id>[\d]+)/$', 'reply', name='postman_reply'),
-    url(r'^view/(?P<message_id>[\d]+)/$', 'view', name='postman_view'),
-    url(r'^view/t/(?P<thread_id>[\d]+)/$', 'view_conversation', name='postman_view_conversation'),
-    url(r'^archive/$', 'archive', name='postman_archive'),
-    url(r'^delete/$', 'delete', name='postman_delete'),
-    url(r'^undelete/$', 'undelete', name='postman_undelete'),
+    url(r'^inbox/(?:(?P<option>'+OPTIONS+')/)?$', InboxView.as_view(), name='postman_inbox'),
+    url(r'^sent/(?:(?P<option>'+OPTIONS+')/)?$', SentView.as_view(), name='postman_sent'),
+    url(r'^archives/(?:(?P<option>'+OPTIONS+')/)?$', ArchivesView.as_view(), name='postman_archives'),
+    url(r'^trash/(?:(?P<option>'+OPTIONS+')/)?$', TrashView.as_view(), name='postman_trash'),
+    url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', WriteView.as_view(), name='postman_write'),
+    url(r'^reply/(?P<message_id>[\d]+)/$', ReplyView.as_view(), name='postman_reply'),
+    url(r'^view/(?P<message_id>[\d]+)/$', MessageView.as_view(), name='postman_view'),
+    url(r'^view/t/(?P<thread_id>[\d]+)/$', ConversationView.as_view(), name='postman_view_conversation'),
+    url(r'^archive/$', ArchiveView.as_view(), name='postman_archive'),
+    url(r'^delete/$', DeleteView.as_view(), name='postman_delete'),
+    url(r'^undelete/$', UndeleteView.as_view(), name='postman_undelete'),
     (r'^$', RedirectView.as_view(url='inbox/')),
 )
index 511d91cc28ee1bc2aa29703f153fc1c59c740ba9..2dceef555febbdc9f8195b7daadf7f36929d3bbf 100644 (file)
@@ -11,20 +11,25 @@ except ImportError:
 from django.core.urlresolvers import reverse
 from django.db.models import Q
 from django.http import Http404
-from django.shortcuts import render_to_response, get_object_or_404, redirect
-from django.template import RequestContext
-from django.utils.translation import ugettext as _
+from django.shortcuts import get_object_or_404, redirect
+from django.utils.decorators import method_decorator
 try:
     from django.utils.timezone import now  # Django 1.4 aware datetimes
 except ImportError:
     from datetime import datetime
     now = datetime.now
+from django.utils.translation import ugettext as _
+from django.views.decorators.csrf import csrf_protect
+from django.views.generic import FormView, TemplateView, View
+
+from . import OPTION_MESSAGES
+from .fields import autocompleter_app
+from .forms import WriteForm, AnonymousWriteForm, QuickReplyForm, FullReplyForm
+from .models import Message, get_order_by
+from .utils import format_subject, format_body
 
-from postman.fields import autocompleter_app
-from postman.forms import WriteForm, AnonymousWriteForm, QuickReplyForm, FullReplyForm
-from postman.models import Message, get_order_by
-from postman.urls import OPTION_MESSAGES
-from postman.utils import format_subject, format_body
+login_required_m = method_decorator(login_required)
+csrf_protect_m = method_decorator(csrf_protect)
 
 
 ##########
@@ -40,85 +45,102 @@ def _get_referer(request):
 ########
 # Views
 ########
-def _folder(request, folder_name, view_name, option, template_name):
+class FolderMixin(object):
     """Code common to the folders."""
-    kwargs = {}
-    if option:
-        kwargs.update(option=option)
-    order_by = get_order_by(request.GET)
-    if order_by:
-        kwargs.update(order_by=order_by)
-    msgs = getattr(Message.objects, folder_name)(request.user, **kwargs)
-    return render_to_response(template_name, {
-        'pm_messages': msgs,  # avoid 'messages', already used by contrib.messages
-        'by_conversation': option is None,
-        'by_message': option == OPTION_MESSAGES,
-        'by_conversation_url': reverse(view_name),
-        'by_message_url': reverse(view_name, args=[OPTION_MESSAGES]),
-        'current_url': request.get_full_path(),
-        'gets': request.GET,  # useful to postman_order_by template tag
-        }, context_instance=RequestContext(request))
-
-
-@login_required
-def inbox(request, option=None, template_name='postman/inbox.html'):
+    http_method_names = ['get']
+
+    @login_required_m
+    def dispatch(self, *args, **kwargs):
+        return super(FolderMixin, self).dispatch(*args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        context = super(FolderMixin, self).get_context_data(**kwargs)
+        params = {}
+        option = kwargs.get('option')
+        if option:
+            params['option'] = option
+        order_by = get_order_by(self.request.GET)
+        if order_by:
+            params['order_by'] = order_by
+        msgs = getattr(Message.objects, self.folder_name)(self.request.user, **params)
+        context.update({
+            'pm_messages': msgs,  # avoid 'messages', already used by contrib.messages
+            'by_conversation': option is None,
+            'by_message': option == OPTION_MESSAGES,
+            'by_conversation_url': reverse(self.view_name),
+            'by_message_url': reverse(self.view_name, args=[OPTION_MESSAGES]),
+            'current_url': self.request.get_full_path(),
+            'gets': self.request.GET,  # useful to postman_order_by template tag
+        })
+        return context
+
+
+class InboxView(FolderMixin, TemplateView):
     """
     Display the list of received messages for the current user.
 
-    Optional arguments:
+    Optional URLconf name-based argument:
         ``option``: display option:
             OPTION_MESSAGES to view all messages
             default to None to view only the last message for each conversation
+    Optional URLconf configuration attribute:
         ``template_name``: the name of the template to use
 
     """
-    return _folder(request, 'inbox', 'postman_inbox', option, template_name)
+    # for FolderMixin:
+    folder_name = 'inbox'
+    view_name = 'postman_inbox'
+    # for TemplateView:
+    template_name = 'postman/inbox.html'
 
 
-@login_required
-def sent(request, option=None, template_name='postman/sent.html'):
+class SentView(FolderMixin, TemplateView):
     """
     Display the list of sent messages for the current user.
 
-    Optional arguments: refer to inbox()
+    Optional arguments and attributes: refer to InboxView.
 
     """
-    return _folder(request, 'sent', 'postman_sent', option, template_name)
+    # for FolderMixin:
+    folder_name = 'sent'
+    view_name = 'postman_sent'
+    # for TemplateView:
+    template_name = 'postman/sent.html'
 
 
-@login_required
-def archives(request, option=None, template_name='postman/archives.html'):
+class ArchivesView(FolderMixin, TemplateView):
     """
     Display the list of archived messages for the current user.
 
-    Optional arguments: refer to inbox()
+    Optional arguments and attributes: refer to InboxView.
 
     """
-    return _folder(request, 'archives', 'postman_archives', option, template_name)
+    # for FolderMixin:
+    folder_name = 'archives'
+    view_name = 'postman_archives'
+    # for TemplateView:
+    template_name = 'postman/archives.html'
 
 
-@login_required
-def trash(request, option=None, template_name='postman/trash.html'):
+class TrashView(FolderMixin, TemplateView):
     """
     Display the list of deleted messages for the current user.
 
-    Optional arguments: refer to inbox()
+    Optional arguments and attributes: refer to InboxView.
 
     """
-    return _folder(request, 'trash', 'postman_trash', option, template_name)
+    # for FolderMixin:
+    folder_name = 'trash'
+    view_name = 'postman_trash'
+    # for TemplateView:
+    template_name = 'postman/trash.html'
 
 
-def write(request, recipients=None, form_classes=(WriteForm, AnonymousWriteForm), autocomplete_channels=None,
-        template_name='postman/write.html', success_url=None,
-        user_filter=None, exchange_filter=None, max=None, auto_moderators=[]):
+class ComposeMixin(object):
     """
-    Display a form to compose a message.
+    Code common to the write and reply views.
 
-    Optional arguments:
-        ``recipients``: a colon-separated list of usernames
-        ``form_classes``: a 2-tuple of form classes
-        ``autocomplete_channels``: a channel name or a 2-tuple of names
-        ``template_name``: the name of the template to use
+    Optional attributes:
         ``success_url``: where to redirect to after a successful POST
         ``user_filter``: a filter for recipients
         ``exchange_filter``: a filter for exchanges between a sender and a recipient
@@ -126,193 +148,271 @@ def write(request, recipients=None, form_classes=(WriteForm, AnonymousWriteForm)
         ``auto_moderators``: a list of auto-moderation functions
 
     """
-    user = request.user
-    form_class = form_classes[0] if user.is_authenticated() else form_classes[1]
-    if isinstance(autocomplete_channels, tuple) and len(autocomplete_channels) == 2:
-        channel = autocomplete_channels[user.is_anonymous()]
-    else:
-        channel = autocomplete_channels
-    next_url = _get_referer(request)
-    if request.method == 'POST':
-        form = form_class(request.POST, sender=user, channel=channel,
-            user_filter=user_filter,
-            exchange_filter=exchange_filter,
-            max=max)
-        if form.is_valid():
-            is_successful = form.save(auto_moderators=auto_moderators)
-            if is_successful:
-                messages.success(request, _("Message successfully sent."), fail_silently=True)
-            else:
-                messages.warning(request, _("Message rejected for at least one recipient."), fail_silently=True)
-            return redirect(request.GET.get('next', success_url or next_url or 'postman_inbox'))
-    else:
-        initial = dict(request.GET.items())  # allow optional initializations by query string
-        if recipients:
-            # order_by() is not mandatory, but: a) it doesn't hurt; b) it eases the test suite
-            # and anyway the original ordering cannot be respected.
-            user_model = get_user_model()
-            usernames = list(user_model.objects.values_list(user_model.USERNAME_FIELD, flat=True).filter(
-                is_active=True,
-                **{'{0}__in'.format(user_model.USERNAME_FIELD): [r.strip() for r in recipients.split(':') if r and not r.isspace()]}
-            ).order_by(user_model.USERNAME_FIELD))
-            if usernames:
-                initial.update(recipients=', '.join(usernames))
-        form = form_class(initial=initial, channel=channel)
-    return render_to_response(template_name, {
-        'form': form,
-        'autocompleter_app': autocompleter_app,
-        'next_url': request.GET.get('next', next_url),
-        }, context_instance=RequestContext(request))
-if getattr(settings, 'POSTMAN_DISALLOW_ANONYMOUS', False):
-    write = login_required(write)
-
-
-@login_required
-def reply(request, message_id, form_class=FullReplyForm, formatters=(format_subject,format_body), autocomplete_channel=None,
-        template_name='postman/reply.html', success_url=None,
-        user_filter=None, exchange_filter=None, max=None, auto_moderators=[]):
+    http_method_names = ['get', 'post']
+    success_url = None
+    user_filter = None
+    exchange_filter = None
+    max = None
+    auto_moderators = []
+
+    def get_form_kwargs(self):
+        kwargs = super(ComposeMixin, self).get_form_kwargs()
+        if self.request.method == 'POST':
+            kwargs.update({
+                'sender': self.request.user,
+                'user_filter': self.user_filter,
+                'exchange_filter': self.exchange_filter,
+                'max': self.max,
+            })        
+        return kwargs
+
+    def get_success_url(self):
+        return self.request.GET.get('next') or self.success_url or _get_referer(self.request) or 'postman_inbox'
+
+    def form_valid(self, form):
+        params = {'auto_moderators': self.auto_moderators}
+        if hasattr(self, 'parent'):  # only in the ReplyView case
+            params['parent'] = self.parent
+        is_successful = form.save(**params)
+        if is_successful:
+            messages.success(self.request, _("Message successfully sent."), fail_silently=True)
+        else:
+            messages.warning(self.request, _("Message rejected for at least one recipient."), fail_silently=True)
+        return redirect(self.get_success_url())
+
+    def get_context_data(self, **kwargs):
+        context = super(ComposeMixin, self).get_context_data(**kwargs)
+        context.update({
+            'autocompleter_app': autocompleter_app,
+            'next_url': self.request.GET.get('next') or _get_referer(self.request),
+        })
+        return context
+
+
+class WriteView(ComposeMixin, FormView):
+    """
+    Display a form to compose a message.
+
+    Optional URLconf name-based argument:
+        ``recipients``: a colon-separated list of usernames
+    Optional attributes:
+        ``form_classes``: a 2-tuple of form classes
+        ``autocomplete_channels``: a channel name or a 2-tuple of names
+        ``template_name``: the name of the template to use
+        + those of ComposeMixin
+
+    """
+    form_classes = (WriteForm, AnonymousWriteForm)
+    autocomplete_channels = None
+    template_name = 'postman/write.html'
+
+    @csrf_protect_m
+    def dispatch(self, *args, **kwargs):
+        if getattr(settings, 'POSTMAN_DISALLOW_ANONYMOUS', False):
+            return login_required(super(WriteView, self).dispatch)(*args, **kwargs)
+        return super(WriteView, self).dispatch(*args, **kwargs)
+
+    def get_form_class(self):
+        return self.form_classes[0] if self.request.user.is_authenticated() else self.form_classes[1]
+
+    def get_initial(self):
+        initial = super(WriteView, self).get_initial()
+        if self.request.method == 'GET':
+            initial.update(self.request.GET.items())  # allow optional initializations by query string
+            recipients = self.kwargs.get('recipients')
+            if recipients:
+                # order_by() is not mandatory, but: a) it doesn't hurt; b) it eases the test suite
+                # and anyway the original ordering cannot be respected.
+                user_model = get_user_model()
+                usernames = list(user_model.objects.values_list(user_model.USERNAME_FIELD, flat=True).filter(
+                    is_active=True,
+                    **{'{0}__in'.format(user_model.USERNAME_FIELD): [r.strip() for r in recipients.split(':') if r and not r.isspace()]}
+                ).order_by(user_model.USERNAME_FIELD))
+                if usernames:
+                    initial['recipients'] = ', '.join(usernames)
+        return initial
+
+    def get_form_kwargs(self):
+        kwargs = super(WriteView, self).get_form_kwargs()
+        if isinstance(self.autocomplete_channels, tuple) and len(self.autocomplete_channels) == 2:
+            channel = self.autocomplete_channels[self.request.user.is_anonymous()]
+        else:
+            channel = self.autocomplete_channels
+        kwargs['channel'] = channel
+        return kwargs
+
+
+class ReplyView(ComposeMixin, FormView):
     """
     Display a form to compose a reply.
 
-    Optional arguments:
+    Optional attributes:
         ``form_class``: the form class to use
         ``formatters``: a 2-tuple of functions to prefill the subject and body fields
         ``autocomplete_channel``: a channel name
         ``template_name``: the name of the template to use
-        ``success_url``: where to redirect to after a successful POST
-        ``user_filter``: a filter for recipients
-        ``exchange_filter``: a filter for exchanges between a sender and a recipient
-        ``max``: an upper limit for the recipients number
-        ``auto_moderators``: a list of auto-moderation functions
+        + those of ComposeMixin
 
     """
-    user = request.user
-    perms = Message.objects.perms(user)
-    parent = get_object_or_404(Message, perms, pk=message_id)
-    initial = parent.quote(*formatters)
-    next_url = _get_referer(request)
-    if request.method == 'POST':
-        post = request.POST.copy()
-        if 'subject' not in post:  # case of the quick reply form
-            post['subject'] = initial['subject']
-        form = form_class(post, sender=user, recipient=parent.sender or parent.email,
-            channel=autocomplete_channel,
-            user_filter=user_filter,
-            exchange_filter=exchange_filter,
-            max=max)
-        if form.is_valid():
-            is_successful = form.save(parent=parent, auto_moderators=auto_moderators)
-            if is_successful:
-                messages.success(request, _("Message successfully sent."), fail_silently=True)
-            else:
-                messages.warning(request, _("Message rejected for at least one recipient."), fail_silently=True)
-            return redirect(request.GET.get('next', success_url or next_url or 'postman_inbox'))
-    else:
-        initial.update(request.GET.items())  # allow overwriting of the defaults by query string
-        form = form_class(initial=initial, channel=autocomplete_channel)
-    return render_to_response(template_name, {
-        'form': form,
-        'recipient': parent.obfuscated_sender,
-        'autocompleter_app': autocompleter_app,
-        'next_url': request.GET.get('next', next_url),
-        }, context_instance=RequestContext(request))
-
-
-def _view(request, filter, form_class=QuickReplyForm, formatters=(format_subject,format_body),
-        template_name='postman/view.html'):
+    form_class = FullReplyForm
+    formatters = (format_subject, format_body)
+    autocomplete_channel = None
+    template_name = 'postman/reply.html'
+
+    @csrf_protect_m
+    @login_required_m
+    def dispatch(self, request, message_id, *args, **kwargs):
+        perms = Message.objects.perms(request.user)
+        self.parent = get_object_or_404(Message, perms, pk=message_id)
+        return super(ReplyView, self).dispatch(request,*args, **kwargs)
+
+    def get_initial(self):
+        self.initial = self.parent.quote(*self.formatters)  # will also be partially used in get_form_kwargs()
+        if self.request.method == 'GET':
+            self.initial.update(self.request.GET.items())  # allow overwriting of the defaults by query string
+        return self.initial
+
+    def get_form_kwargs(self):
+        kwargs = super(ReplyView, self).get_form_kwargs()
+        kwargs['channel'] = self.autocomplete_channel
+        if self.request.method == 'POST':
+            if 'subject' not in kwargs['data']:  # case of the quick reply form
+                post = kwargs['data'].copy()  # self.request.POST is immutable
+                post['subject'] = self.initial['subject']
+                kwargs['data'] = post
+            kwargs['recipient'] = self.parent.sender or self.parent.email
+        return kwargs
+
+    def get_context_data(self, **kwargs):
+        context = super(ReplyView, self).get_context_data(**kwargs)
+        context['recipient'] = self.parent.obfuscated_sender
+        return context
+
+
+class DisplayMixin(object):
     """
     Code common to the by-message and by-conversation views.
 
-    Optional arguments:
+    Optional attributes:
         ``form_class``: the form class to use
         ``formatters``: a 2-tuple of functions to prefill the subject and body fields
         ``template_name``: the name of the template to use
 
     """
-    user = request.user
-    msgs = Message.objects.thread(user, filter)
-    if msgs:
-        Message.objects.set_read(user, filter)
+    http_method_names = ['get']
+    form_class = QuickReplyForm
+    formatters = (format_subject, format_body)
+    template_name = 'postman/view.html'
+
+    @login_required_m
+    def dispatch(self, *args, **kwargs):
+        return super(DisplayMixin, self).dispatch(*args, **kwargs)
+
+    def get(self, request, *args, **kwargs):
+        user = request.user
+        self.msgs = Message.objects.thread(user, self.filter)
+        if not self.msgs:
+            raise Http404
+        Message.objects.set_read(user, self.filter)
+        return super(DisplayMixin, self).get(request, *args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        context = super(DisplayMixin, self).get_context_data(**kwargs)
+        user = self.request.user
         # are all messages archived ?
-        for m in msgs:
+        for m in self.msgs:
             if not getattr(m, ('sender' if m.sender == user else 'recipient') + '_archived'):
                 archived = False
                 break
         else:
             archived = True
-        # look for the more recent received message (and non-deleted to comply with the future perms() control), if any
-        for m in reversed(msgs):
+        # look for the most recent received message (and non-deleted to comply with the future perms() control), if any
+        for m in reversed(self.msgs):
             if m.recipient == user and not m.recipient_deleted_at:
                 received = m
                 break
         else:
             received = None
-        return render_to_response(template_name, {
-            'pm_messages': msgs,
+        context.update({
+            'pm_messages': self.msgs,
             'archived': archived,
             'reply_to_pk': received.pk if received else None,
-            'form': form_class(initial=received.quote(*formatters)) if received else None,
-            'next_url': request.GET.get('next', reverse('postman_inbox')),
-            }, context_instance=RequestContext(request))
-    raise Http404
+            'form': self.form_class(initial=received.quote(*self.formatters)) if received else None,
+            'next_url': self.request.GET.get('next') or reverse('postman_inbox'),
+        })
+        return context
 
 
-@login_required
-def view(request, message_id, *args, **kwargs):
+class MessageView(DisplayMixin, TemplateView):
     """Display one specific message."""
-    return _view(request, Q(pk=message_id), *args, **kwargs)
 
+    def get(self, request, message_id, *args, **kwargs):
+        self.filter = Q(pk=message_id)
+        return super(MessageView, self).get(request, *args, **kwargs)
 
-@login_required
-def view_conversation(request, thread_id, *args, **kwargs):
+
+class ConversationView(DisplayMixin, TemplateView):
     """Display a conversation."""
-    return _view(request, Q(thread=thread_id), *args, **kwargs)
+
+    def get(self, request, thread_id, *args, **kwargs):
+        self.filter = Q(thread=thread_id)
+        return super(ConversationView, self).get(request, *args, **kwargs)
 
 
-def _update(request, field_bit, success_msg, field_value=None, success_url=None):
+class UpdateMessageMixin(object):
     """
     Code common to the archive/delete/undelete actions.
 
-    Arguments:
+    Attributes:
         ``field_bit``: a part of the name of the field to update
         ``success_msg``: the displayed text in case of success
-    Optional arguments:
+    Optional attributes:
         ``field_value``: the value to set in the field
         ``success_url``: where to redirect to after a successful POST
 
     """
-    if not request.method == 'POST':
-        raise Http404
-    next_url = _get_referer(request) or 'postman_inbox'
-    pks = request.POST.getlist('pks')
-    tpks = request.POST.getlist('tpks')
-    if pks or tpks:
-        user = request.user
-        filter = Q(pk__in=pks) | Q(thread__in=tpks)
-        recipient_rows = Message.objects.as_recipient(user, filter).update(**{'recipient_{0}'.format(field_bit): field_value})
-        sender_rows = Message.objects.as_sender(user, filter).update(**{'sender_{0}'.format(field_bit): field_value})
-        if not (recipient_rows or sender_rows):
-            raise Http404  # abnormal enough, like forged ids
-        messages.success(request, success_msg, fail_silently=True)
-        return redirect(request.GET.get('next', success_url or next_url))
-    else:
-        messages.warning(request, _("Select at least one object."), fail_silently=True)
-        return redirect(next_url)
-
-
-@login_required
-def archive(request, *args, **kwargs):
+    http_method_names = ['post']
+    field_value = None
+    success_url = None
+
+    @csrf_protect_m
+    @login_required_m
+    def dispatch(self, *args, **kwargs):
+        return super(UpdateMessageMixin, self).dispatch(*args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        next_url = _get_referer(request) or 'postman_inbox'
+        pks = request.POST.getlist('pks')
+        tpks = request.POST.getlist('tpks')
+        if pks or tpks:
+            user = request.user
+            filter = Q(pk__in=pks) | Q(thread__in=tpks)
+            recipient_rows = Message.objects.as_recipient(user, filter).update(**{'recipient_{0}'.format(self.field_bit): self.field_value})
+            sender_rows = Message.objects.as_sender(user, filter).update(**{'sender_{0}'.format(self.field_bit): self.field_value})
+            if not (recipient_rows or sender_rows):
+                raise Http404  # abnormal enough, like forged ids
+            messages.success(request, self.success_msg, fail_silently=True)
+            return redirect(request.GET.get('next') or self.success_url or next_url)
+        else:
+            messages.warning(request, _("Select at least one object."), fail_silently=True)
+            return redirect(next_url)
+
+
+class ArchiveView(UpdateMessageMixin, View):
     """Mark messages/conversations as archived."""
-    return _update(request, 'archived', _("Messages or conversations successfully archived."), True, *args, **kwargs)
+    field_bit = 'archived'
+    success_msg = _("Messages or conversations successfully archived.")
+    field_value = True
 
 
-@login_required
-def delete(request, *args, **kwargs):
+class DeleteView(UpdateMessageMixin, View):
     """Mark messages/conversations as deleted."""
-    return _update(request, 'deleted_at', _("Messages or conversations successfully deleted."), now(), *args, **kwargs)
+    field_bit = 'deleted_at'
+    success_msg = _("Messages or conversations successfully deleted.")
+    field_value = now()
 
 
-@login_required
-def undelete(request, *args, **kwargs):
+class UndeleteView(UpdateMessageMixin, View):
     """Revert messages/conversations from marked as deleted."""
-    return _update(request, 'deleted_at', _("Messages or conversations successfully recovered."), *args, **kwargs)
+    field_bit = 'deleted_at'
+    success_msg = _("Messages or conversations successfully recovered.")