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.\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
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
\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
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
\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
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
\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
\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
\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
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
\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
#. 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
\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
\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
\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
\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
\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
\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
\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
# 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))
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'
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 . 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)
##########
########
# 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
``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.")