Patrick Samson avatar Patrick Samson committed bf2cf1d

Convert all function-based views to class-based views

Comments (0)

Files changed (12)

 	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.
 
 # The short X.Y version.
 version = '3.0'
 # The full version, including alpha/beta/rc tags.
-release = '3.0.0a1'
+release = '3.0.0'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.

docs/features.rst

 There is no parameter for a minimum number, but you can code a custom form
 and pass a ``min`` parameter to the recipient field (see Advanced Usage below for details).
 
-Views supporting the parameter are: ``write``, ``reply``.
+Views supporting the parameter are: ``WriteView``, ``ReplyView``.
 
 But this parameter does not apply to the default ``AnonymousWriteForm`` for visitors:
 The maximum is enforced to 1 (see Advanced Usage below for knowing how),
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',
-            {'max': 3}, name='postman_write'),
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',
+            WriteView.as_view(max=3),
+            name='postman_write'),
         # ...
     )
 
 If there are some situations where a user should not be a recipient, you can write a filter
 and pass it to the view.
 
-Views supporting a user filter are: ``write``, ``reply``.
+Views supporting a user filter are: ``WriteView``, ``ReplyView``.
 
 Example::
 
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',
-            {'user_filter': my_user_filter}, name='postman_write'),
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',
+            WriteView.as_view(user_filter=my_user_filter),
+            name='postman_write'),
         # ...
     )
 
 and pass it to the view.
 Typical usages would be: blacklists, users that do not want solicitation from visitors.
 
-Views supporting an exchange filter are: ``write``, ``reply``.
+Views supporting an exchange filter are: ``WriteView``, ``ReplyView``.
 
 An example, with the django-relationships application::
 
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',
-            {'exchange_filter': my_exchange_filter}, name='postman_write'),
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',
+            WriteView.as_view(exchange_filter=my_exchange_filter),
+            name='postman_write'),
         # ...
     )
 
 
 You may attach a specific channel, different from the default one, to a particular view.
 
-Views supporting an auto-complete parameter are: ``write``, ``reply``.
+Views supporting an auto-complete parameter are: ``WriteView``, ``ReplyView``.
 
-For the ``write`` view, the parameter is named ``autocomplete_channels`` (note the plural).
+For the ``WriteView`` view, the parameter is named ``autocomplete_channels`` (note the plural).
 It supports two variations:
 
 * a 2-tuple of channels names: the first one for authenticated users, the second for visitors.
   Specify ``None`` if you let the default channel name for one of the tuple parts.
 * a single channel name: the same for users and visitors
 
-For the ``reply`` view, the parameter is named ``autocomplete_channel`` (note the singular).
+For the ``ReplyView`` view, the parameter is named ``autocomplete_channel`` (note the singular).
 The value is the channel name.
 
 Example::
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',
-            {'autocomplete_channels': (None,'anonymous_ac')}, name='postman_write'),
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',
-            {'autocomplete_channel': 'reply_ac'}, name='postman_reply'),
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',
+            WriteView.as_view(autocomplete_channels=(None,'anonymous_ac')),
+            name='postman_write'),
+        url(r'^reply/(?P<message_id>[\d]+)/$',
+            ReplyView.as_view(autocomplete_channel='reply_ac'),
+            name='postman_reply'),
         # ...
     )
 
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',
-            {'autocomplete_channels': 'write_ac'}, name='postman_write'),
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',
+            WriteView.as_view(autocomplete_channels='write_ac'), 
+            name='postman_write'),
         # ...
     )
 

docs/moderation.rst

 to the views.  The value of the parameter can be one single function or a sequence of
 functions as a tuple or a list.
 
-Views supporting an ``auto-moderators`` parameter are: ``write``, ``reply``.
+Views supporting an ``auto-moderators`` parameter are: ``WriteView``, ``ReplyView``.
 
 Example::
 
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',
-            {'auto_moderators': (mod1, mod2)}, name='postman_write'),
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',
-            {'auto_moderators': mod1}, name='postman_reply'),
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',
+            WriteView.as_view(auto_moderators=(mod1, mod2)),
+            name='postman_write'),
+        url(r'^reply/(?P<message_id>[\d]+)/$',
+            ReplyView.as_view(auto_moderators=mod1),
+            name='postman_reply'),
         # ...
     )
 
 #. An average rating is computed: if greater or equal to 50, the message is accepted.
 #. The message is rejected. The final reason is a comma separated collection of reasons
    coming from moderators having returned a rating lesser than 50.
-
-

docs/quickstart.rst

 
 * use of ``str.format()``
 
-Django version >= 1.2.2
+Django version >= 1.3
 
 Some reasons:
 
-* use of ``self.stdout`` in management commands
+* use of class-based views
 
 Installation
 ------------
 
 * base_write.html
 
-In case you run a Django 1.2 version, perform these additional steps for any template:
-
-* Remove {% load url from future %}
-* Change any {% url 'XX' %} to {% url XX %}
-
 Relations between templates::
 
     base.html
 
 For Django 1.3+, just follow the instructions related to the staticfiles app.
 
-For Django 1.2:
-	It's up to you to make the files visible to the URL resolver.
-
-	For example:
-
-	* Rename the path to :file:`postman/medias/`
-	* In a production environment, set :file:`/<MEDIA_ROOT>/postman/` as a symlink to :file:`<Postman_module>/medias/postman/`
-	* In a development environment (django's runserver), you can put in the URLconf, something like::
-
-		('^' + settings.MEDIA_URL.strip('/') + r'/(?P<path>postman/.*)$', 'django.views.static.serve',
-			{'document_root': os.path.join(imp.find_module('postman')[1], 'medias')}),
-
 Examples
 --------
 
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$', 'write',
-            {'form_classes': (MyCustomWriteForm, MyCustomAnonymousWriteForm)}, name='postman_write'),
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',
-            {'form_class': MyCustomFullReplyForm}, name='postman_reply'),
-        url(r'^view/(?P<message_id>[\d]+)/$', 'view',
-            {'form_class': MyCustomQuickReplyForm}, name='postman_view'),
+        url(r'^write/(?:(?P<recipients>[\w.@+-:]+)/)?$',
+            WriteView.as_view(form_classes=(MyCustomWriteForm, MyCustomAnonymousWriteForm)),
+            name='postman_write'),
+        url(r'^reply/(?P<message_id>[\d]+)/$',
+            ReplyView.as_view(form_class=MyCustomFullReplyForm),
+            name='postman_reply'),
+        url(r'^view/(?P<message_id>[\d]+)/$',
+            MessageView.as_view(form_class=MyCustomQuickReplyForm),
+            name='postman_view'),
         # ...
     )
 
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^view/(?P<message_id>[\d]+)/$', 'view',
-            {'template_name': 'my_custom_view.html'}, name='postman_view'),
+        url(r'^view/(?P<message_id>[\d]+)/$',
+            MessageView.as_view(template_name='my_custom_view.html'),
+            name='postman_view'),
         # ...
     )
 
 
 The parameter ``success_url`` is available to these views:
 
-* ``write``
-* ``reply``
-* ``archive``
-* ``delete``
-* ``undelete``
+* ``WriteView``
+* ``ReplyView``
+* ``ArchiveView``
+* ``DeleteView``
+* ``UndeleteView``
 
 Example::
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',
-            {'success_url': 'postman_inbox'}, name='postman_reply'),
+        url(r'^reply/(?P<message_id>[\d]+)/$',
+            ReplyView.as_view(success_url='postman_inbox'),
+            name='postman_reply'),
         # ...
     )
 
 
     urlpatterns = patterns('postman.views',
         # ...
-        url(r'^reply/(?P<message_id>[\d]+)/$', 'reply',
-            {'formatters': (format_subject,format_body)}, name='postman_reply'),
-        url(r'^view/(?P<message_id>[\d]+)/$', 'view',
-            {'formatters': (format_subject,format_body)}, name='postman_view'),
+        url(r'^reply/(?P<message_id>[\d]+)/$',
+            ReplyView.as_view(formatters=(format_subject, format_body)),
+            name='postman_reply'),
+        url(r'^view/(?P<message_id>[\d]+)/$',
+            MessageView.as_view(formatters=(format_subject, format_body)),
+            name='postman_view'),
         # ...
     )

postman/__init__.py

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

postman/models.py

     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'

postman/test_urls.py

     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
 
 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('',
 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):
     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):
 
         # 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)
 
         # 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)
 
 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
 
 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:
 
 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):
         # ...
         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
 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/')),
 )
 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 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
+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
+
+login_required_m = method_decorator(login_required)
+csrf_protect_m = method_decorator(csrf_protect)
 
 
 ##########
 ########
 # 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))
+    http_method_names = ['get']
 
+    @login_required_m
+    def dispatch(self, *args, **kwargs):
+        return super(FolderMixin, self).dispatch(*args, **kwargs)
 
-@login_required
-def inbox(request, option=None, template_name='postman/inbox.html'):
+    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
         ``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)
+    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
 
-@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=[]):
+    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))
+    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 _view(request, filter, form_class=QuickReplyForm, formatters=(format_subject,format_body),
-        template_name='postman/view.html'):
+    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)
+    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)
 
-@login_required
-def archive(request, *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.")
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.