Patrick Samson avatar Patrick Samson committed de2e10f

initial code upload

Comments (0)

Files changed (57)

docs/features.rst

 An auto-complete functionality may be useful on the recipients field.
 
 To activate the option, set at least the ``arg_default`` key in the
-``POSTMAN_AUTOCOMPLETER_APP`` dictionary.  If the default ajax_select application is used,
+``POSTMAN_AUTOCOMPLETER_APP`` dictionary.  If the default ``ajax_select`` application is used,
 define a matching entry in the ``AJAX_LOOKUP_CHANNELS`` dictionary.
 
 Example::
         'arg_default': 'postman_users',
     }
 
-Support for multiple recipients is not turned on by default by django-ajax-selects.
+Support for multiple recipients is not turned on by default by `django-ajax-selects`_.
 To allow this capability, you have to pass the option ``multiple: true``.
 
+.. _`django-ajax-selects`: http://code.google.com/p/django-ajax-selects/
+
 Make your own templates, based on these two files, given as implementation examples:
 
-* postman/templates/autocomplete_postman_multiple.html
-* postman/templates/autocomplete_postman_single.html
+* :file:`postman/templates/autocomplete_postman_multiple.html`
+* :file:`postman/templates/autocomplete_postman_single.html`
 
 These examples include a correction necessary for the support of the 'multiple' option
 (in version 1.1.4 of django-ajax-selects).
 What is a message ? Roughly a piece of text, about a subject, sent by a sender to a recipient.
 Each user has access to a collection of messages, stored in folders:
 
-    | ``Inbox`` for incoming messages
-    | ``Sent`` for sent messages
-    | ``Archives`` for archived messages
-    | ``Trash`` for messages marked as deleted
+    | **Inbox** for incoming messages
+    | **Sent** for sent messages
+    | **Archives** for archived messages
+    | **Trash** for messages marked as deleted
 
 In folders, messages can be presented in two modes:
 
 
 * Autocomplete recipient field (default is 'django-ajax-selects'),
   with multiple recipient management
-* New message notification (default is 'django-notification')
-* Asynchronous mailer (default is 'django-mailer')
+* New message notification (default is `django-notification`_)
+* Asynchronous mailer (default is `django-mailer`_)
+
+.. _`django-notification`: http://github.com/jtauber/django-notification/
+.. _`django-mailer`: http://github.com/jtauber/django-mailer/
 
 Moderation
 ----------
 * to make sure that no direct contact informations are exchanged when the site is an intermediary
   and delivers services based on subscription fees.
 
-Messages are first created in a ``pending`` state. A moderator is in charge to change them to
-a ``rejected`` or ``accepted`` state.  This operation can be done in two ways:
+Messages are first created in a *pending* state. A moderator is in charge to change them to
+a *rejected* or *accepted* state.  This operation can be done in two ways:
 
 * By a person, through the Admin site. A specially simplified change view is provided,
   with one-click buttons to accept or reject the message.
 
    quickstart
    moderation
+   notification
    views
    features
    tags-filters

docs/management.rst

 
 A management command is provided for this purpose:
 
-**django-admin.py postman_cleanup**
+:command:`django-admin.py postman_cleanup`
 
 It can be run as a cron job or directly.
 
-The ``--days`` option can be used to specify the minimal number of days a message/conversation
+A :option:`--days` option can be used to specify the minimal number of days a message/conversation
 must have been marked for deletion.
 Default value is 30 days.
 
 It checks messages and conversations for possible inconsistencies, in a read-only mode.
 No change is made on the data.
 
-**django-admin.py postman_checkup**
+:command:`django-admin.py postman_checkup`
 
 It can be run directly or better as a nightly cron job.

docs/notification.rst

+Notification
+============
+
+Parties should be notified of these events:
+
+* when a message is rejected
+* when a message or a reply is received
+
+.. _for_visitors:
+
+For visitors
+------------
+An email is sent, using these templates:
+
+* :file:`postman/email_visitor_subject.txt` for the subject
+* :file:`postman/email_visitor.txt` for the body
+
+The available context variables are:
+
+* ``site``: the Site instance
+* ``object``: the Message instance
+* ``action``: 'rejection' or 'acceptance'
+
+Default templates are provided with the application. Same as for the views, you can override them,
+and design yours.
+
+For users
+---------
+If a notifier application is configured (see :ref:`optional_settings`), the following labels are used:
+
+* ``postman_rejection`` to notify the sender of the rejection
+* ``postman_message`` to notify the recipient of the reception of a message
+* ``postman_reply`` to notify the recipient of the reception of a reply
+
+Some extra context variables are passed in the call to the notifier application
+and so are available in the templates:
+
+* ``message``: the Message instance
+* ``action``: 'rejection' or 'acceptance'
+
+If no notifier application is used, an email is sent, using these templates:
+
+* :file:`postman/email_user_subject.txt` for the subject
+* :file:`postman/email_user.txt` for the body
+
+In that case, the information about context variables and templates is the same
+as in the :ref:`for_visitors` section above.

docs/quickstart.rst

 
 Add ``postman`` to the ``INSTALLED_APPS`` setting of your project.
 
-Run a ``manage.py syncdb``.
+Run a :command:`manage.py syncdb`
 
 Include the URLconf ``postman.urls`` in your project's root URL configuration.
 
 add ``postman.context_processors.inbox`` to the ``TEMPLATE_CONTEXT_PROCESSORS`` setting
 of your project.
 
-You may specify some additional configuration options in your ``settings.py``:
+You may specify some additional configuration options in your :file:`settings.py`:
 
 ``POSTMAN_DISALLOW_ANONYMOUS``
     Set it to True if you do not allow visitors to write to users.
     *Defaults to*: None.
 
     To disable the moderation feature (no control, no filter):
+
     * Set this option to True
     * Do not provide any auto-moderation functions
 
 * {{ MEDIA_URL }}css/jquery.autocomplete.css
 * {{ MEDIA_URL }}css/indicator.gif
 
-The ``postman/base.html`` template extends a ``base.html`` site template,
+The :file:`postman/base.html` template extends a :file:`base.html` site template,
 in which some blocks are expected:
 
 * title: in <html><head><title>, at least for a part of the entire title string
 
 Medias
 ~~~~~~
-A CSS file is provided with the application, for the Admin site: ``postman/css/admin.css``.
+A CSS file is provided with the application, for the Admin site: :file:`postman/css/admin.css`.
 It is not obligatory but makes the display more confortable.
 
-The file is provided under ``postman/medias/``. It's up to you to make it visible to the URL resolver.
+The file is provided under :file:`postman/medias/`. It's up to you to make it visible to the URL resolver.
 
 For example:
 
-* In a production environment, set /<MEDIA_URL>/postman/ as a symlink to <Postman_module>/medias/postman/
+* In a production environment, set :file:`/<MEDIA_URL>/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',
 Examples
 --------
 
-``settings.py``::
+:file:`settings.py`::
 
     INSTALLED_APPS = (
         # ...
         # 'arg_default': 'postman_friends', # no default, mandatory to enable the feature
     # } # default is {}
 
-``urls.py``::
+:file:`urls.py`::
 
     (r'^messages/', include('postman.urls')),

docs/tags-filters.rst

 
     {% load postman_tags %}
 
-Here are the other special libraries in the ``postman/templatetags/`` directory,
+Here are the other special libraries in the :file:`postman/templatetags/` directory,
 that are not intended for your site design:
 
-* ``postman_admin_modify.py``: a library exclusively designed for a customized change_form
+* :file:`postman_admin_modify.py`: a library exclusively designed for a customized change_form
   template used in the Admin site for the moderation of pending messages.
 
-* ``pagination_tags_for_tests.py``: a mock of the django-pagination application template tags,
+* :file:`pagination_tags_for_tests.py`: a mock of the django-pagination application template tags,
   only usable for the test suite in case the real application is not installed.
-  To rename to ``pagination_tags.py`` during the test session.
+  To rename to :file:`pagination_tags.py` during the test session.
 
 Tags
 ----
 
 styles
 ------
-Here is a sample of some CSS rules, usable for ``postman/views.html``::
+Here is a sample of some CSS rules, usable for :file:`postman/views.html`::
 
     .pm_message.pm_deleted             { text-decoration: line-through; }
     .pm_message.pm_deleted .pm_body    { display: none; }

postman/__init__.py

+"""A messaging application for Django"""
+
+# following PEP 386: N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]
+VERSION = (1, 0, 0)
+PREREL = ()
+POST = 0
+DEV = 0
+
+def get_version():
+    version = '.'.join(map(str, VERSION))
+    if PREREL:
+        version += PREREL[0] + '.'.join(map(str, PREREL[1:]))
+    if POST:
+        version += ".post" + str(POST)
+    if DEV:
+        version += ".dev" + str(DEV)
+    return version
+
+__version__ = get_version()
+from django import forms
+from django.contrib import admin
+from django.db import transaction
+from django.utils.translation import ugettext, ugettext_lazy as _
+
+from postman.models import Message, PendingMessage
+
+class MessageAdminForm(forms.ModelForm):
+    class Meta:
+        model = Message
+    class Media:
+        css = { "all": ("postman/css/admin.css",) }
+
+    def clean(self):
+        """Check data validity and coherence."""
+        cleaned_data = super(MessageAdminForm, self).clean()
+        sender = cleaned_data.get('sender')
+        recipient = cleaned_data.get('recipient')
+        email = cleaned_data.get('email')
+        errors = []
+        if not sender and not recipient:
+            errors.append(ugettext("Sender and Recipient cannot be both undefined."))
+            if 'sender' in cleaned_data:
+                del cleaned_data['sender']
+            if 'recipient' in cleaned_data:
+                del cleaned_data['recipient']
+        elif sender and recipient:
+            if email:
+                errors.append(ugettext("Visitor's email is in excess."))
+                if 'email' in cleaned_data:
+                    del cleaned_data['email']
+        else:
+            if not email:
+                errors.append(ugettext("Visitor's email is missing."))
+                if 'email' in cleaned_data:
+                    del cleaned_data['email']
+        sent_at = cleaned_data.get('sent_at')
+        read_at = cleaned_data.get('read_at')
+        if read_at and read_at < sent_at:
+            errors.append(ugettext("Reading date must be later to sending date."))
+            if 'read_at' in cleaned_data:
+                del cleaned_data['read_at']
+        sender_deleted_at = cleaned_data.get('sender_deleted_at')
+        if sender_deleted_at and sender_deleted_at < sent_at:
+            errors.append(ugettext("Deletion date by sender must be later to sending date."))
+            if 'sender_deleted_at' in cleaned_data:
+                del cleaned_data['sender_deleted_at']
+        recipient_deleted_at = cleaned_data.get('recipient_deleted_at')
+        if recipient_deleted_at and recipient_deleted_at < sent_at:
+            errors.append(ugettext("Deletion date by recipient must be later to sending date."))
+            if 'recipient_deleted_at' in cleaned_data:
+                del cleaned_data['recipient_deleted_at']
+        replied_at = cleaned_data.get('replied_at')
+        obj = self.instance
+        if replied_at:
+            len_begin = len(errors)
+            if replied_at < sent_at:
+                errors.append(ugettext("Response date must be later to sending date."))
+            if not read_at:
+                errors.append(ugettext("The message cannot be replied without having been read."))
+            elif replied_at < read_at:
+                errors.append(ugettext("Response date must be later to reading date."))
+            if not obj.get_replies_count():
+                errors.append(ugettext("Response date cannot be set without at least one reply."))
+            if not obj.thread_id:
+                errors.append(ugettext("The message cannot be replied without being in a conversation."))
+            if len(errors) > len_begin:
+                if 'replied_at' in cleaned_data:
+                    del cleaned_data['replied_at']
+        # if obj.parent_id and not obj.thread_id:# can't be set by the form
+        if errors:
+            raise forms.ValidationError(errors)
+
+        self.initial_status = obj.moderation_status
+        return cleaned_data
+
+class MessageAdmin(admin.ModelAdmin):
+    form = MessageAdminForm
+    search_fields = ('subject', 'body')
+    date_hierarchy = 'sent_at'
+    list_display = ('subject', 'admin_sender', 'admin_recipient', 'sent_at', 'moderation_status')
+    list_filter = ('moderation_status', )
+    fieldsets = (
+        (None, {'fields': (
+            ('sender', 'recipient', 'email'),
+            'sent_at',
+            )}),
+        (_('Message'), {'fields': (
+            'subject',
+            'body',
+            ('parent', 'thread'),
+            )}),
+        (_('Dates'), {'classes': ('collapse', ), 'fields': (
+            ('read_at', 'replied_at'),
+            ('sender_archived', 'recipient_archived'),
+            ('sender_deleted_at', 'recipient_deleted_at'),
+            )}),
+        (_('Moderation'), {'fields': (
+            ('moderation_status', 'moderation_date', 'moderation_by'),
+            'moderation_reason',
+            )}),
+    )
+    readonly_fields = (
+        'parent', 'thread', # no reason to change, and anyway too many objects
+        'moderation_date', 'moderation_by', # automatically set at status change
+    )
+    radio_fields = {'moderation_status': admin.VERTICAL}
+
+    def queryset(self, request):
+        """
+        Add a custom select_related() to avoid a bunch of queries for users
+        in the 'change list' admin view.
+
+        Setting 'list_select_related = True' is not efficient as the default
+        select_related() does not follow foreign keys that have null=True.
+
+        """
+        return super(MessageAdmin, self).queryset(request).select_related('sender', 'recipient')
+
+    @transaction.commit_on_success
+    def save_model(self, request, obj, form, change):
+        """
+        Add some actions around the save.
+
+        Before the save, adjust some constrained fields.
+        After the save, update related objects and notify parties if needed.
+
+        """
+        obj.clean_moderation(form.initial_status, request.user)
+        obj.clean_for_visitor()
+        super(MessageAdmin, self).save_model(request, obj, form, change)
+        obj.update_parent(form.initial_status)
+        obj.notify_users(form.initial_status)
+
+class PendingMessageAdminForm(forms.ModelForm):
+    class Meta:
+        model = PendingMessage
+    class Media:
+        css = { "all": ("postman/css/admin.css",) }
+
+    def clean(self):
+        """Set status according to the button used to submit."""
+        cleaned_data = super(PendingMessageAdminForm, self).clean()
+        obj = self.instance
+        self.initial_status = obj.moderation_status
+        # look for for button names provided by custom admin/postman/pendingmessage/change_form.html
+        if '_saveasaccepted' in self.data:
+            obj.set_accepted()
+        elif '_saveasrejected' in self.data:
+            obj.set_rejected()
+        return cleaned_data
+
+class PendingMessageAdmin(MessageAdmin):
+    form = PendingMessageAdminForm
+    search_fields = ()
+    date_hierarchy = None
+    actions = None
+    list_display = ('subject', 'admin_sender', 'admin_recipient', 'sent_at')
+    list_filter = ()
+    fieldsets = (
+        (None, {'fields': (
+            'admin_sender', 'admin_recipient', 'sent_at',
+            )}),
+        (_('Message'), {'fields': (
+            'subject',
+            'body',
+            )}),
+        (_('Moderation'), {'fields': (
+            'moderation_reason',
+            )}),
+    )
+    readonly_fields = ('admin_sender', 'admin_recipient')
+
+    def has_add_permission(self, request):
+        "Adding is impossible"
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        "Deleting is impossible"
+        return False
+
+admin.site.register(Message, MessageAdmin)
+admin.site.register(PendingMessage, PendingMessageAdmin)

postman/context_processors.py

+from postman.models import Message
+
+def inbox(request):
+    """Provide the count of unread messages for an authenticated user."""
+    if request.user.is_authenticated():
+        return {'postman_unread_count': Message.objects.inbox_unread_count(request.user)}
+    else:
+        return {}

postman/fields.py

+"""
+Custom fields.
+"""
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
+from django.core.validators import EMPTY_VALUES
+from django.forms.fields import CharField
+from django.utils.translation import ugettext_lazy as _
+
+class BasicCommaSeparatedUserField(CharField):
+    """
+    An internal base class for CommaSeparatedUserField.
+
+    This class is not intended to be used directly in forms.
+    Use CommaSeparatedUserField instead,
+    to benefit from the auto-complete fonctionality if available.
+
+    """
+    default_error_messages = {
+        'unknown': _("Some usernames are unknown or no more active: {users}."),
+        'max': _("Ensure this value has at most {limit_value} distinct items (it has {show_value})."),
+        'min': _("Ensure this value has at least {limit_value} distinct items (it has {show_value})."),
+        'filtered': _("Some usernames are rejected: {users}."),
+        'filtered_user': _("{user.username}"),
+        'filtered_user_with_reason': _("{user.username} ({reason})"),
+    }
+
+    def __init__(self, max=None, min=None, user_filter=None, *args, **kwargs):
+        self.max, self.min, self.user_filter = max, min, user_filter
+        label = kwargs.get('label')
+        if isinstance(label, tuple):
+            self.pluralized_labels = label
+            kwargs.update(label=label[max == 1])
+        super(BasicCommaSeparatedUserField, self).__init__(*args, **kwargs)
+
+    def set_max(self, max):
+        """Supersede the max value and ajust accordingly the label."""
+        pluralized_labels = getattr(self, 'pluralized_labels', None)
+        if pluralized_labels:
+            self.label = pluralized_labels[max == 1]
+        self.max = max
+
+    def to_python(self, value):
+        """Normalize data to an unordered list of distinct, non empty, whitespace-stripped strings."""
+        value = super(BasicCommaSeparatedUserField, self).to_python(value)
+        if value in EMPTY_VALUES: # Return an empty list if no useful input was given.
+            return []
+        return list(set([name.strip() for name in value.split(',') if name and not name.isspace()]))
+
+    def validate(self, value):
+        """Check the limits."""
+        super(BasicCommaSeparatedUserField, self).validate(value)
+        if value in EMPTY_VALUES:
+            return
+        count = len(value)
+        if self.max and count > self.max:
+            raise ValidationError(self.error_messages['max'].format(limit_value=self.max, show_value=count))
+        if self.min and count < self.min:
+            raise ValidationError(self.error_messages['min'].format(limit_value=self.min, show_value=count))
+
+    def clean(self, value):
+        """Check names are valid and filter them."""
+        names = super(BasicCommaSeparatedUserField, self).clean(value)
+        if not names:
+            return []
+        users = list(User.objects.filter(is_active=True, username__in=names))
+        unknown_names = set(names) ^ set([u.username for u in users])
+        errors = []
+        if unknown_names:
+            errors.append(self.error_messages['unknown'].format(users=', '.join(unknown_names)))
+        if self.user_filter:
+            filtered_names = []
+            for u in users[:]:
+                try:
+                    reason = self.user_filter(u)
+                    if reason is not None:
+                        users.remove(u)
+                        filtered_names.append(
+                            self.error_messages[
+                                'filtered_user_with_reason' if reason else 'filtered_user'
+                            ].format(user=u, reason=reason)
+                        )
+                except ValidationError, e:
+                    users.remove(u)
+                    errors.extend(e.messages)
+            if filtered_names:
+                errors.append(self.error_messages['filtered'].format(users=', '.join(filtered_names)))
+        if errors:
+            raise ValidationError(errors)
+        return users
+
+d = getattr(settings, 'POSTMAN_AUTOCOMPLETER_APP', {})
+app_name = d.get('name', 'ajax_select')
+field_name = d.get('field', 'AutoCompleteField')
+arg_name = d.get('arg_name', 'channel')
+arg_default = d.get('arg_default') # the minimum to declare to enable the feature
+
+if app_name in settings.INSTALLED_APPS and arg_default:
+    # does something like "from ajax_select.fields import AutoCompleteField"
+    auto_complete_field = getattr(__import__(app_name + '.fields', globals(), locals(), [field_name]), field_name)
+    is_autocompleted = True
+
+    class CommaSeparatedUserField(BasicCommaSeparatedUserField, auto_complete_field):
+        def __init__(self, *args, **kwargs):
+            if not args and arg_name not in kwargs:
+                kwargs.update([(arg_name,arg_default)])
+            super(CommaSeparatedUserField, self).__init__(*args, **kwargs)
+
+        def set_arg(self, value):
+            """Same as it is done in ajax_select.fields.py for Fields and Widgets."""
+            if hasattr(self, arg_name):
+                setattr(self, arg_name, value)
+            if hasattr(self.widget, arg_name):
+                setattr(self.widget, arg_name, value)
+
+else:
+    CommaSeparatedUserField = BasicCommaSeparatedUserField
+    is_autocompleted = False
+"""
+You may define your own custom forms, based or inspired by the following ones.
+
+Examples of customization:
+    recipients = CommaSeparatedUserField(label=("Recipients", "Recipient"),
+        min=2,
+        max=5,
+        user_filter=my_user_filter,
+        channel='my_channel',
+    )
+    can_overwrite_limits = False
+    exchange_filter = staticmethod(my_exchange_filter)
+
+"""
+from django import forms
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.db import transaction
+from django.utils.translation import ugettext, ugettext_lazy as _
+
+from postman.fields import CommaSeparatedUserField
+from postman.models import Message
+from postman.utils import WRAP_WIDTH
+
+class BaseWriteForm(forms.ModelForm):
+    """The base class for other forms."""
+    class Meta:
+        model = Message
+        fields = ('body',)
+        widgets = {
+            # for better confort, ensure a 'cols' of at least
+            # the 'width' of the body quote formatter.
+            'body': forms.Textarea(attrs={'cols': WRAP_WIDTH, 'rows': 12}),
+        }
+
+    error_css_class = 'error'
+    required_css_class = 'required'
+
+    def __init__(self, *args, **kwargs):
+        sender = kwargs.pop('sender', None)
+        exchange_filter = kwargs.pop('exchange_filter', None)
+        user_filter = kwargs.pop('user_filter', None)
+        max = kwargs.pop('max', None)
+        channel = kwargs.pop('channel', None)
+        super(BaseWriteForm, self).__init__(*args, **kwargs)
+
+        self.instance.sender = sender if (sender and sender.is_authenticated()) else None
+        if exchange_filter:
+            self.exchange_filter = exchange_filter
+        if 'recipients' in self.fields:
+            if user_filter and hasattr(self.fields['recipients'], 'user_filter'):
+                self.fields['recipients'].user_filter = user_filter
+
+            if getattr(settings, 'POSTMAN_DISALLOW_MULTIRECIPIENTS', False):
+                max = 1
+            if max is not None and hasattr(self.fields['recipients'], 'set_max') \
+            and getattr(self, 'can_overwrite_limits', True):
+                self.fields['recipients'].set_max(max)
+
+            if channel and hasattr(self.fields['recipients'], 'set_arg'):
+                self.fields['recipients'].set_arg(channel)
+
+    error_messages = {
+        'filtered': _("Writing to some users is not possible: {users}."),
+        'filtered_user': _("{user.username}"),
+        'filtered_user_with_reason': _("{user.username} ({reason})"),
+    }
+    def clean_recipients(self):
+        """Check no filter prohibit the exchange."""
+        recipients = self.cleaned_data['recipients']
+        exchange_filter = getattr(self, 'exchange_filter', None)
+        if exchange_filter:
+            errors = []
+            filtered_names = []
+            recipients_list = recipients[:]
+            for u in recipients_list:
+                try:
+                    reason = exchange_filter(self.instance.sender, u, recipients_list)
+                    if reason is not None:
+                        recipients.remove(u)
+                        filtered_names.append(
+                            self.error_messages[
+                                'filtered_user_with_reason' if reason else 'filtered_user'
+                            ].format(user=u, reason=reason)
+                        )
+                except forms.ValidationError, e:
+                    recipients.remove(u)
+                    errors.extend(e.messages)
+            if filtered_names:
+                errors.append(self.error_messages['filtered'].format(users=', '.join(filtered_names)))
+            if errors:
+                raise forms.ValidationError(errors)
+        return recipients
+
+    @transaction.commit_on_success
+    def save(self, recipient=None, parent=None, auto_moderators=[]):
+        """
+        Save as many messages as there are recipients.
+
+        Additional actions:
+        - If it's a reply, build a conversation
+        - Call auto-moderators
+        - Notify parties if needed
+
+        Return False if one of the messages is rejected.
+
+        """
+        recipients = self.cleaned_data.get('recipients', [])
+        if parent and not parent.thread_id: # at the very first reply, make it a conversation
+            parent.thread = parent
+            parent.save()
+            # but delay the setting of parent.replied_at to the moderation step
+        if parent:
+            self.instance.parent = parent
+            self.instance.thread_id = parent.thread_id
+        initial_moderation = self.instance.get_moderation()
+        initial_dates = self.instance.get_dates()
+        initial_status = self.instance.moderation_status
+        if recipient:
+            if isinstance(recipient, User) and recipient in recipients:
+                recipients.remove(recipient)
+            recipients.insert(0, recipient)
+        is_successful = True
+        for r in recipients:
+            if isinstance(r, User):
+                self.instance.recipient = r
+            else:
+                self.instance.recipient = None
+                self.instance.email = r
+            self.instance.pk = None # force_insert=True is not accessible from here
+            self.instance.auto_moderate(auto_moderators)
+            self.instance.clean_moderation(initial_status)
+            self.instance.clean_for_visitor()
+            m = super(BaseWriteForm, self).save()
+            if self.instance.is_rejected():
+                is_successful = False
+            self.instance.update_parent(initial_status)
+            self.instance.notify_users(initial_status)
+            # some resets for next reuse
+            if not isinstance(r, User):
+                self.instance.email = ''
+            self.instance.set_moderation(*initial_moderation)
+            self.instance.set_dates(*initial_dates)
+        return is_successful
+
+class WriteForm(BaseWriteForm):
+    """The form for an authenticated user, to compose a message."""
+    recipients = CommaSeparatedUserField(label=(_("Recipients"), _("Recipient")))
+
+    class Meta(BaseWriteForm.Meta):
+        fields = ('recipients', 'subject', 'body')
+
+class AnonymousWriteForm(BaseWriteForm):
+    """The form for an anonymous user, to compose a message."""
+    # The 'max' customization should not be permitted here.
+    # The features available to anonymous users should be kept to the strict minimum.
+    can_overwrite_limits = False
+
+    email = forms.EmailField(label=_("Email"))
+    recipients = CommaSeparatedUserField(label=(_("Recipients"), _("Recipient")), max=1) # one recipient is enough
+
+    class Meta(BaseWriteForm.Meta):
+        fields = ('email', 'recipients', 'subject', 'body')
+
+class BaseReplyForm(BaseWriteForm):
+    """The base class for a reply to a message."""
+    def __init__(self, *args, **kwargs):
+        recipient = kwargs.pop('recipient', None)
+        super(BaseReplyForm, self).__init__(*args, **kwargs)
+        self.recipient = recipient
+
+    def clean(self):
+        """Check that the recipient is correctly initialized."""
+        if not self.recipient:
+            raise forms.ValidationError(ugettext("Undefined recipient."))
+        return super(BaseReplyForm, self).clean()
+
+    def save(self, *args, **kwargs):
+        return super(BaseReplyForm, self).save(self.recipient, *args, **kwargs)
+
+class QuickReplyForm(BaseReplyForm):
+    """
+    The form to use in the view of a message or a conversation, for a quick reply.
+
+    The recipient is imposed and a default value for the subject will be provided.
+
+    """
+    pass
+
+allow_copies = not getattr(settings, 'POSTMAN_DISALLOW_COPIES_ON_REPLY', False)
+class FullReplyForm(BaseReplyForm):
+    """The complete reply form."""
+    if allow_copies:
+        recipients = CommaSeparatedUserField(label=(_("Additional recipients"), _("Additional recipient")), required=False)
+
+    class Meta(BaseReplyForm.Meta):
+        fields = (['recipients'] if allow_copies else []) + ['subject', 'body']
Add a comment to this file

postman/locale/de/LC_MESSAGES/django.mo

Binary file modified.

postman/locale/de/LC_MESSAGES/django.po

 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-# 
+#
 msgid ""
 msgstr ""
 "Project-Id-Version: django-postman\n"
-"Report-Msgid-Bugs-To: http://bitbucket.org/psam/django-postman/issues\n"
-"POT-Creation-Date: 2010-12-24 18:38+0100\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-12-27 14:44+0100\n"
 "PO-Revision-Date: 2010-12-25 11:36+0000\n"
 "Last-Translator: psam <maxcom@laposte.net>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 msgstr ""
 
 #: .\admin.py:66
-msgid "The message cannot be replied without being in a thread."
+msgid "The message cannot be replied without being in a conversation."
 msgstr ""
 
 #: .\admin.py:88 .\admin.py:157 .\templates\postman\view.html.py:5
 msgstr ""
 
 #: .\fields.py:23
-msgid "Ensure this value has at most {limit_value} distinct items (it has {show_value})."
+msgid ""
+"Ensure this value has at most {limit_value} distinct items (it has "
+"{show_value})."
 msgstr ""
 
 #: .\fields.py:24
-msgid "Ensure this value has at least {limit_value} distinct items (it has {show_value})."
+msgid ""
+"Ensure this value has at least {limit_value} distinct items (it has "
+"{show_value})."
 msgstr ""
 
 #: .\fields.py:25
 msgid "Writing to some users is not possible: {users}."
 msgstr ""
 
-#: .\forms.py:149 .\forms.py:161
+#: .\forms.py:148 .\forms.py:160
 msgid "Recipients"
 msgstr ""
 
-#: .\forms.py:149 .\forms.py:161 .\templates\postman\base_folder.html.py:26
+#: .\forms.py:148 .\forms.py:160 .\templates\postman\base_folder.html.py:26
 #: .\templates\postman\reply.html.py:4
 msgid "Recipient"
 msgstr "Empfänger"
 
-#: .\forms.py:160
+#: .\forms.py:159
 msgid "Email"
 msgstr "E-Mail"
 
-#: .\forms.py:176
+#: .\forms.py:175
 msgid "Undefined recipient."
 msgstr ""
 
-#: .\forms.py:195
+#: .\forms.py:194
 msgid "Additional recipients"
 msgstr ""
 
-#: .\forms.py:195
+#: .\forms.py:194
 msgid "Additional recipient"
 msgstr ""
 
 msgid "body"
 msgstr "inhalt"
 
-#: .\models.py:199 .\models.py:282
+#: .\models.py:199 .\models.py:281
 msgid "sender"
 msgstr "absender"
 
-#: .\models.py:200 .\models.py:306
+#: .\models.py:200 .\models.py:305
 msgid "recipient"
 msgstr "empfänger"
 
+#: .\models.py:201
+msgid "visitor"
+msgstr "besucher"
+
 #: .\models.py:202
-msgid "visitor"
-msgstr ""
-
-#: .\models.py:203
 msgid "parent message"
 msgstr "Übergeordnete nachricht"
 
-#: .\models.py:204
+#: .\models.py:203
 msgid "root message"
 msgstr ""
 
-#: .\models.py:205
+#: .\models.py:204
 msgid "sent at"
 msgstr "gesendet am"
 
-#: .\models.py:206
+#: .\models.py:205
 msgid "read at"
 msgstr "gelesen am"
 
-#: .\models.py:207
+#: .\models.py:206
 msgid "replied at"
 msgstr "beantwortet am"
 
+#: .\models.py:207
+msgid "archived by sender"
+msgstr "vom absender archiviert"
+
 #: .\models.py:208
-msgid "archived by sender"
-msgstr ""
+msgid "archived by recipient"
+msgstr "vom empfänger archiviert"
 
 #: .\models.py:209
-msgid "archived by recipient"
-msgstr ""
-
-#: .\models.py:210
 msgid "deleted by sender at"
 msgstr "vom absender gelöscht am"
 
-#: .\models.py:211
+#: .\models.py:210
 msgid "deleted by recipient at"
 msgstr "vom empfänger gelöscht am"
 
-#: .\models.py:213
+#: .\models.py:212
 msgid "status"
 msgstr "status"
 
-#: .\models.py:215
+#: .\models.py:214
 msgid "moderator"
 msgstr ""
 
-#: .\models.py:216
+#: .\models.py:215
 msgid "moderated at"
 msgstr ""
 
-#: .\models.py:217
+#: .\models.py:216
 msgid "rejection reason"
 msgstr ""
 
-#: .\models.py:222
+#: .\models.py:221
 msgid "message"
 msgstr "nachricht"
 
-#: .\models.py:223
+#: .\models.py:222
 msgid "messages"
 msgstr "nachrichten"
 
-#: .\models.py:334
+#: .\models.py:333
 msgid "Undefined sender."
 msgstr ""
 
-#: .\models.py:478
+#: .\models.py:473
 msgid "pending message"
 msgstr ""
 
-#: .\models.py:479
+#: .\models.py:474
 msgid "pending messages"
 msgstr ""
 
 msgid "Message rejected for at least one recipient."
 msgstr ""
 
-#: .\views.py:277
+#: .\views.py:276
 msgid "Select at least one object."
 msgstr ""
 
-#: .\views.py:283
-msgid "Message(s) or thread(s) successfully archived."
-msgstr ""
+#: .\views.py:282
+msgid "Messages or conversations successfully archived."
+msgstr "Nachrichten oder Konversationen erfolgreich archiviert."
 
-#: .\views.py:288
-msgid "Message(s) or thread(s) successfully deleted."
-msgstr ""
+#: .\views.py:287
+msgid "Messages or conversations successfully deleted."
+msgstr "Nachrichten oder Konversationen erfolgreich gelöscht."
 
-#: .\views.py:293
-msgid "Message(s) or thread(s) successfully recovered."
-msgstr ""
+#: .\views.py:292
+msgid "Messages or conversations successfully recovered."
+msgstr "Nachrichten oder Konversationen erfolgreich wiederhergestellt."
 
 #: .\management\__init__.py:14
 msgid "Message Rejected"
 
 #: .\templates\postman\archives.html.py:3
 msgid "Archived Messages"
-msgstr ""
+msgstr "Archivierte Nachrichten"
 
 #: .\templates\postman\archives.html.py:7
-msgid "Messages in this folder will never be removed. You can use this folder for long term storage."
+msgid ""
+"Messages in this folder will never be removed. You can use this folder for "
+"long term storage."
 msgstr ""
 
 #: .\templates\postman\base.html.py:3
 
 #: .\templates\postman\base.html.py:9
 msgid "Archives"
-msgstr ""
+msgstr "Archiven"
 
 #: .\templates\postman\base.html.py:10
 msgid "Trash"
 msgstr "Sorry, diese Seite ist ungültig."
 
 #: .\templates\postman\base_folder.html.py:12
-msgid "by thread"
-msgstr ""
+msgid "by conversation"
+msgstr "nach Konversation"
 
 #: .\templates\postman\base_folder.html.py:13
 msgid "by message"
-msgstr ""
+msgstr "nach Nachricht"
 
 #: .\templates\postman\base_folder.html.py:17
 #: .\templates\postman\view.html.py:22
 #: .\templates\postman\base_folder.html.py:18
 #: .\templates\postman\view.html.py:23
 msgid "Archive"
-msgstr ""
+msgstr "Archivieren"
 
 #: .\templates\postman\base_folder.html.py:19
 msgid "Undelete"
 
 #: .\templates\postman\email_user.txt.py:1
 msgid "Dear user,"
-msgstr ""
+msgstr "Sehr geehrter Benutzer,"
 
 #: .\templates\postman\email_user.txt.py:3
 #: .\templates\postman\email_visitor.txt.py:3
 #: .\templates\postman\email_visitor.txt.py:10
 #, python-format
 msgid "On %(date)s, you sent a message to the user '%(sender)s'."
-msgstr "Am %(date)s, du hast eine Nachricht an den Benutzer '%(sender)s' gesendet."
+msgstr ""
+"Am %(date)s, du hast eine Nachricht an den Benutzer '%(sender)s' gesendet."
 
 #: .\templates\postman\email_user.txt.py:10
 msgid "Your correspondent has given you an answer."
 #: .\templates\postman\email_user.txt.py:13
 #, python-format
 msgid "You have received a message from the user '%(sender)s'."
-msgstr "Du hast eine Nachricht von den Benutzer  '%(sender)s' erhalten."
+msgstr "Du hast eine Nachricht von den Benutzer '%(sender)s' erhalten."
 
 #: .\templates\postman\email_user.txt.py:16
 #: .\templates\postman\email_visitor.txt.py:14
 #: .\templates\postman\email_visitor_subject.txt.py:1
 #, python-format
 msgid "Message \"%(subject)s\" on the site %(sitename)s"
-msgstr ""
+msgstr "Nachricht \"%(subject)s\" auf der site %(sitename)s"
 
 #: .\templates\postman\email_visitor.txt.py:1
 msgid "Dear visitor,"
 msgstr "Gelöschte Nachrichten"
 
 #: .\templates\postman\trash.html.py:10
-msgid "Messages in this folder can be removed from time to time. For long term storage, use instead the archive folder."
+msgid ""
+"Messages in this folder can be removed from time to time. For long term "
+"storage, use instead the archive folder."
 msgstr ""
 
 #: .\templates\postman\view.html.py:5
-msgid "Thread"
-msgstr ""
+msgid "Conversation"
+msgstr "Konversation"
 
 #: .\templates\postman\view.html.py:13
 msgid ":"
 msgid "Back"
 msgstr "Zurück"
 
-#: .\templatetags\postman_tags.py:34
+#: .\templatetags\postman_tags.py:35
 msgid "<me>"
-msgstr "<ich>"
+msgstr "<Ich>"

postman/locale/en/LC_MESSAGES/django.po

 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2010-12-27 13:35+0100\n"
+"POT-Creation-Date: 2010-12-27 14:21+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 msgstr ""
 
 #: .\views.py:282
-msgid "Message(s) or conversation(s) successfully archived."
+msgid "Messages or conversations successfully archived."
 msgstr ""
 
 #: .\views.py:287
-msgid "Message(s) or conversation(s) successfully deleted."
+msgid "Messages or conversations successfully deleted."
 msgstr ""
 
 #: .\views.py:292
-msgid "Message(s) or conversation(s) successfully recovered."
+msgid "Messages or conversations successfully recovered."
 msgstr ""
 
 #: .\management\__init__.py:14
Add a comment to this file

postman/locale/es/LC_MESSAGES/django.mo

Binary file modified.

postman/locale/es/LC_MESSAGES/django.po

 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-# 
+#
 msgid ""
 msgstr ""
 "Project-Id-Version: django-postman\n"
-"Report-Msgid-Bugs-To: http://bitbucket.org/psam/django-postman/issues\n"
-"POT-Creation-Date: 2010-12-24 18:38+0100\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-12-27 15:16+0100\n"
 "PO-Revision-Date: 2010-12-25 15:29+0000\n"
 "Last-Translator: psam <maxcom@laposte.net>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 msgstr ""
 
 #: .\admin.py:66
-msgid "The message cannot be replied without being in a thread."
+msgid "The message cannot be replied without being in a conversation."
 msgstr ""
 
 #: .\admin.py:88 .\admin.py:157 .\templates\postman\view.html.py:5
 msgstr ""
 
 #: .\fields.py:23
-msgid "Ensure this value has at most {limit_value} distinct items (it has {show_value})."
+msgid ""
+"Ensure this value has at most {limit_value} distinct items (it has "
+"{show_value})."
 msgstr ""
 
 #: .\fields.py:24
-msgid "Ensure this value has at least {limit_value} distinct items (it has {show_value})."
+msgid ""
+"Ensure this value has at least {limit_value} distinct items (it has "
+"{show_value})."
 msgstr ""
 
 #: .\fields.py:25
 msgid "Writing to some users is not possible: {users}."
 msgstr ""
 
-#: .\forms.py:149 .\forms.py:161
+#: .\forms.py:148 .\forms.py:160
 msgid "Recipients"
 msgstr "Destinatarios"
 
-#: .\forms.py:149 .\forms.py:161 .\templates\postman\base_folder.html.py:26
+#: .\forms.py:148 .\forms.py:160 .\templates\postman\base_folder.html.py:26
 #: .\templates\postman\reply.html.py:4
 msgid "Recipient"
 msgstr "Destinatario"
 
-#: .\forms.py:160
+#: .\forms.py:159
 msgid "Email"
-msgstr "Correo electrónico"
+msgstr "Correo"
 
-#: .\forms.py:176
+#: .\forms.py:175
 msgid "Undefined recipient."
 msgstr ""
 
-#: .\forms.py:195
+#: .\forms.py:194
 msgid "Additional recipients"
 msgstr ""
 
-#: .\forms.py:195
+#: .\forms.py:194
 msgid "Additional recipient"
 msgstr ""
 
 msgid "body"
 msgstr "contenido"
 
-#: .\models.py:199 .\models.py:282
+#: .\models.py:199 .\models.py:281
 msgid "sender"
 msgstr "emisor"
 
-#: .\models.py:200 .\models.py:306
+#: .\models.py:200 .\models.py:305
 msgid "recipient"
 msgstr "destinatario"
 
-#: .\models.py:202
+#: .\models.py:201
 msgid "visitor"
 msgstr ""
 
-#: .\models.py:203
+#: .\models.py:202
 msgid "parent message"
 msgstr "mensaje padre"
 
-#: .\models.py:204
+#: .\models.py:203
 msgid "root message"
 msgstr ""
 
-#: .\models.py:205
+#: .\models.py:204
 msgid "sent at"
 msgstr "enviado a"
 
-#: .\models.py:206
+#: .\models.py:205
 msgid "read at"
 msgstr "leído a"
 
-#: .\models.py:207
+#: .\models.py:206
 msgid "replied at"
 msgstr "respondido a"
 
-#: .\models.py:208
+#: .\models.py:207
 msgid "archived by sender"
 msgstr ""
 
-#: .\models.py:209
+#: .\models.py:208
 msgid "archived by recipient"
 msgstr ""
 
-#: .\models.py:210
+#: .\models.py:209
 msgid "deleted by sender at"
 msgstr ""
 
-#: .\models.py:211
+#: .\models.py:210
 msgid "deleted by recipient at"
 msgstr ""
 
-#: .\models.py:213
+#: .\models.py:212
 msgid "status"
 msgstr ""
 
-#: .\models.py:215
+#: .\models.py:214
 msgid "moderator"
 msgstr ""
 
-#: .\models.py:216
+#: .\models.py:215
 msgid "moderated at"
 msgstr ""
 
-#: .\models.py:217
+#: .\models.py:216
 msgid "rejection reason"
 msgstr ""
 
-#: .\models.py:222
+#: .\models.py:221
 msgid "message"
 msgstr "mensaje"
 
-#: .\models.py:223
+#: .\models.py:222
 msgid "messages"
 msgstr "mensajes"
 
-#: .\models.py:334
+#: .\models.py:333
 msgid "Undefined sender."
 msgstr ""
 
-#: .\models.py:478
+#: .\models.py:473
 msgid "pending message"
 msgstr ""
 
-#: .\models.py:479
+#: .\models.py:474
 msgid "pending messages"
 msgstr ""
 
 msgid "Message rejected for at least one recipient."
 msgstr ""
 
-#: .\views.py:277
+#: .\views.py:276
 msgid "Select at least one object."
 msgstr ""
 
-#: .\views.py:283
-msgid "Message(s) or thread(s) successfully archived."
-msgstr ""
+#: .\views.py:282
+msgid "Messages or conversations successfully archived."
+msgstr "Mensajes o conversaciones archivado con éxito."
 
-#: .\views.py:288
-msgid "Message(s) or thread(s) successfully deleted."
-msgstr ""
+#: .\views.py:287
+msgid "Messages or conversations successfully deleted."
+msgstr "Mensajes o conversaciones eliminado con éxito."
 
-#: .\views.py:293
-msgid "Message(s) or thread(s) successfully recovered."
-msgstr ""
+#: .\views.py:292
+msgid "Messages or conversations successfully recovered."
+msgstr "Mensajes o conversaciones recuperado con éxito."
 
 #: .\management\__init__.py:14
 msgid "Message Rejected"
 
 #: .\templates\postman\archives.html.py:3
 msgid "Archived Messages"
-msgstr ""
+msgstr "Mensajes archivados"
 
 #: .\templates\postman\archives.html.py:7
-msgid "Messages in this folder will never be removed. You can use this folder for long term storage."
+msgid ""
+"Messages in this folder will never be removed. You can use this folder for "
+"long term storage."
 msgstr ""
 
 #: .\templates\postman\base.html.py:3
 
 #: .\templates\postman\base.html.py:6
 msgid "Inbox"
-msgstr "Bandeja de entrada"
+msgstr "Recibidos"
 
 #: .\templates\postman\base.html.py:7 .\templates\postman\sent.html.py:3
 msgid "Sent Messages"
 
 #: .\templates\postman\base.html.py:9
 msgid "Archives"
-msgstr ""
+msgstr "Archivos"
 
 #: .\templates\postman\base.html.py:10
 msgid "Trash"
 msgstr ""
 
 #: .\templates\postman\base_folder.html.py:12
-msgid "by thread"
-msgstr ""
+msgid "by conversation"
+msgstr "por conversación"
 
 #: .\templates\postman\base_folder.html.py:13
 msgid "by message"
 #: .\templates\postman\base_folder.html.py:18
 #: .\templates\postman\view.html.py:23
 msgid "Archive"
-msgstr ""
+msgstr "Archivar"
 
 #: .\templates\postman\base_folder.html.py:19
 msgid "Undelete"
 msgstr "Mensajes eliminados"
 
 #: .\templates\postman\trash.html.py:10
-msgid "Messages in this folder can be removed from time to time. For long term storage, use instead the archive folder."
+msgid ""
+"Messages in this folder can be removed from time to time. For long term "
+"storage, use instead the archive folder."
 msgstr ""
 
 #: .\templates\postman\view.html.py:5
-msgid "Thread"
-msgstr ""
+msgid "Conversation"
+msgstr "Conversación"
 
 #: .\templates\postman\view.html.py:13
 msgid ":"
 msgid "Back"
 msgstr "Volver"
 
-#: .\templatetags\postman_tags.py:34
+#: .\templatetags\postman_tags.py:35
 msgid "<me>"
-msgstr "<>"
+msgstr "<usuario>"
Add a comment to this file

postman/locale/fr/LC_MESSAGES/django.mo

Binary file modified.

postman/locale/fr/LC_MESSAGES/django.po

 msgstr ""
 "Project-Id-Version: django-postman 1.0.0\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2010-12-27 13:36+0100\n"
+"POT-Creation-Date: 2010-12-27 14:21+0100\n"
 "PO-Revision-Date: 2010-12-15 17:19+0100\n"
 "Last-Translator: Patrick Samson <maxcom@laposte.net>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 msgstr "Sélectionner au moins un objet."
 
 #: .\views.py:282
-msgid "Message(s) or conversation(s) successfully archived."
-msgstr "Message(s) ou conversation(s) archivé(s) avec succès."
+msgid "Messages or conversations successfully archived."
+msgstr "Messages ou conversations archivés avec succès."
 
 #: .\views.py:287
-msgid "Message(s) or conversation(s) successfully deleted."
-msgstr "Message(s) ou conversation(s) supprimé(s) avec succès."
+msgid "Messages or conversations successfully deleted."
+msgstr "Messages ou conversations supprimés avec succès."
 
 #: .\views.py:292
-msgid "Message(s) or conversation(s) successfully recovered."
-msgstr "Message(s) ou conversation(s) restauré(s) avec succès."
+msgid "Messages or conversations successfully recovered."
+msgstr "Messages ou conversations restaurés avec succès."
 
 #: .\management\__init__.py:14
 msgid "Message Rejected"
Add a comment to this file

postman/locale/it/LC_MESSAGES/django.mo

Binary file modified.

postman/locale/it/LC_MESSAGES/django.po

 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-# 
+#
 msgid ""
 msgstr ""
 "Project-Id-Version: django-postman\n"
-"Report-Msgid-Bugs-To: http://bitbucket.org/psam/django-postman/issues\n"
-"POT-Creation-Date: 2010-12-24 18:38+0100\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-12-27 15:44+0100\n"
 "PO-Revision-Date: 2010-12-25 14:39+0000\n"
 "Last-Translator: psam <maxcom@laposte.net>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 msgstr ""
 
 #: .\admin.py:66
-msgid "The message cannot be replied without being in a thread."
+msgid "The message cannot be replied without being in a conversation."
 msgstr ""
 
 #: .\admin.py:88 .\admin.py:157 .\templates\postman\view.html.py:5
 msgstr ""
 
 #: .\fields.py:23
-msgid "Ensure this value has at most {limit_value} distinct items (it has {show_value})."
+msgid ""
+"Ensure this value has at most {limit_value} distinct items (it has "
+"{show_value})."
 msgstr ""
 
 #: .\fields.py:24
-msgid "Ensure this value has at least {limit_value} distinct items (it has {show_value})."
+msgid ""
+"Ensure this value has at least {limit_value} distinct items (it has "
+"{show_value})."
 msgstr ""
 
 #: .\fields.py:25
 msgid "Writing to some users is not possible: {users}."
 msgstr "Scrivi per alcuni utenti non è possibile: {users}."
 
-#: .\forms.py:149 .\forms.py:161
+#: .\forms.py:148 .\forms.py:160
 msgid "Recipients"
 msgstr "Destinatari"
 
-#: .\forms.py:149 .\forms.py:161 .\templates\postman\base_folder.html.py:26
+#: .\forms.py:148 .\forms.py:160 .\templates\postman\base_folder.html.py:26
 #: .\templates\postman\reply.html.py:4
 msgid "Recipient"
 msgstr "Destinatario"
 
-#: .\forms.py:160
+#: .\forms.py:159
 msgid "Email"
-msgstr "E-mail"
+msgstr "Posta"
 
-#: .\forms.py:176
+#: .\forms.py:175
 msgid "Undefined recipient."
 msgstr ""
 
-#: .\forms.py:195
+#: .\forms.py:194
 msgid "Additional recipients"
 msgstr "Altri destinatari"
 
-#: .\forms.py:195
+#: .\forms.py:194
 msgid "Additional recipient"
 msgstr "Ulteriori destinatario"
 
 msgid "body"
 msgstr "contenuto"
 
-#: .\models.py:199 .\models.py:282
+#: .\models.py:199 .\models.py:281
 msgid "sender"
 msgstr "mittente"
 
-#: .\models.py:200 .\models.py:306
+#: .\models.py:200 .\models.py:305
 msgid "recipient"
 msgstr "destinatario"
 
-#: .\models.py:202
+#: .\models.py:201
 msgid "visitor"
 msgstr "visitatore"
 
-#: .\models.py:203
+#: .\models.py:202
 msgid "parent message"
 msgstr ""
 
-#: .\models.py:204
+#: .\models.py:203
 msgid "root message"
 msgstr ""
 
-#: .\models.py:205
+#: .\models.py:204
 msgid "sent at"
 msgstr "inviato il"
 
-#: .\models.py:206
+#: .\models.py:205
 msgid "read at"
 msgstr "letto il"
 
-#: .\models.py:207
+#: .\models.py:206
 msgid "replied at"
 msgstr "risposto il"
 
+#: .\models.py:207
+msgid "archived by sender"
+msgstr "archiviato dal mittente"
+
 #: .\models.py:208
-msgid "archived by sender"
-msgstr ""
+msgid "archived by recipient"
+msgstr "archiviato dal destinatario"
 
 #: .\models.py:209
-msgid "archived by recipient"
-msgstr ""
-
-#: .\models.py:210
 msgid "deleted by sender at"
 msgstr "cancellati dal mittente il"
 
-#: .\models.py:211
+#: .\models.py:210
 msgid "deleted by recipient at"
 msgstr "cancellati dal destinatario il"
 
-#: .\models.py:213
+#: .\models.py:212
 msgid "status"
 msgstr ""
 
-#: .\models.py:215
+#: .\models.py:214
 msgid "moderator"
 msgstr ""
 
-#: .\models.py:216
+#: .\models.py:215
 msgid "moderated at"
 msgstr ""
 
-#: .\models.py:217
+#: .\models.py:216
 msgid "rejection reason"
 msgstr ""
 
-#: .\models.py:222
+#: .\models.py:221
 msgid "message"
 msgstr "messaggio"
 
-#: .\models.py:223
+#: .\models.py:222
 msgid "messages"
 msgstr "messaggi"
 
-#: .\models.py:334
+#: .\models.py:333
 msgid "Undefined sender."
 msgstr ""
 
-#: .\models.py:478
+#: .\models.py:473
 msgid "pending message"
 msgstr ""
 
-#: .\models.py:479
+#: .\models.py:474
 msgid "pending messages"
 msgstr ""
 
 msgid "Message rejected for at least one recipient."
 msgstr ""
 
-#: .\views.py:277
+#: .\views.py:276
 msgid "Select at least one object."
 msgstr ""
 
-#: .\views.py:283
-msgid "Message(s) or thread(s) successfully archived."
-msgstr ""
+#: .\views.py:282
+msgid "Messages or conversations successfully archived."
+msgstr "Messaggi o conversazioni archiviati con successo."
 
-#: .\views.py:288
-msgid "Message(s) or thread(s) successfully deleted."
-msgstr ""
+#: .\views.py:287
+msgid "Messages or conversations successfully deleted."
+msgstr "Messaggi o conversazioni eliminato con successo."
 
-#: .\views.py:293
-msgid "Message(s) or thread(s) successfully recovered."
-msgstr ""
+#: .\views.py:292
+msgid "Messages or conversations successfully recovered."
+msgstr "Messaggi o conversazioni recuperati con successo."
 
 #: .\management\__init__.py:14
 msgid "Message Rejected"
 
 #: .\templates\postman\archives.html.py:3
 msgid "Archived Messages"
-msgstr ""
+msgstr "Messaggi archiviati"
 
 #: .\templates\postman\archives.html.py:7
-msgid "Messages in this folder will never be removed. You can use this folder for long term storage."
+msgid ""
+"Messages in this folder will never be removed. You can use this folder for "
+"long term storage."
 msgstr ""
 
 #: .\templates\postman\base.html.py:3
 
 #: .\templates\postman\base.html.py:6
 msgid "Inbox"
-msgstr ""
+msgstr "Posta in arrivo"
 
 #: .\templates\postman\base.html.py:7 .\templates\postman\sent.html.py:3
 msgid "Sent Messages"
 
 #: .\templates\postman\base.html.py:9
 msgid "Archives"
-msgstr ""
+msgstr "Archivi"
 
 #: .\templates\postman\base.html.py:10
 msgid "Trash"
 msgstr "Spiacenti, questo numero di pagina non valida."
 
 #: .\templates\postman\base_folder.html.py:12
-msgid "by thread"
-msgstr "di conversazione"
+msgid "by conversation"
+msgstr "dal conversazione"
 
 #: .\templates\postman\base_folder.html.py:13
 msgid "by message"
 #: .\templates\postman\base_folder.html.py:17
 #: .\templates\postman\view.html.py:22
 msgid "Delete"
-msgstr "Cancella"
+msgstr "Elimina"
 
 #: .\templates\postman\base_folder.html.py:18
 #: .\templates\postman\view.html.py:23
 msgid "Archive"
-msgstr ""
+msgstr "Archivia"
 
 #: .\templates\postman\base_folder.html.py:19
 msgid "Undelete"
 
 #: .\templates\postman\email_user.txt.py:1
 msgid "Dear user,"
-msgstr ""
+msgstr "Caro utente,"
 
 #: .\templates\postman\email_user.txt.py:3
 #: .\templates\postman\email_visitor.txt.py:3
 #: .\templates\postman\email_visitor_subject.txt.py:1
 #, python-format
 msgid "Message \"%(subject)s\" on the site %(sitename)s"
-msgstr ""
+msgstr "Messaggio \"%(subject)s\" sul sito %(sitename)s"
 
 #: .\templates\postman\email_visitor.txt.py:1
 msgid "Dear visitor,"
 
 #: .\templates\postman\trash.html.py:3
 msgid "Deleted Messages"
-msgstr "Messaggi cancellati"
+msgstr "Messaggi eliminati"
 
 #: .\templates\postman\trash.html.py:10
-msgid "Messages in this folder can be removed from time to time. For long term storage, use instead the archive folder."
+msgid ""
+"Messages in this folder can be removed from time to time. For long term "
+"storage, use instead the archive folder."
 msgstr ""
 
 #: .\templates\postman\view.html.py:5
-msgid "Thread"
-msgstr ""
+msgid "Conversation"
+msgstr "Conversazione"
 
 #: .\templates\postman\view.html.py:13
 msgid ":"
 msgid "Back"
 msgstr "Indietro"
 
-#: .\templatetags\postman_tags.py:34
+#: .\templatetags\postman_tags.py:35
 msgid "<me>"
-msgstr "<i>"
+msgstr "<me>"
Add a comment to this file

postman/locale/nl/LC_MESSAGES/django.mo

Binary file added.

postman/locale/nl/LC_MESSAGES/django.po

+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# 
+msgid ""
+msgstr ""
+"Project-Id-Version: django-postman\n"
+"Report-Msgid-Bugs-To: http://bitbucket.org/psam/django-postman/issues\n"
+"POT-Creation-Date: 2010-12-27 14:21+0100\n"
+"PO-Revision-Date: 2010-12-27 15:10+0000\n"
+"Last-Translator: psam <maxcom@laposte.net>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+#: .\admin.py:22
+msgid "Sender and Recipient cannot be both undefined."
+msgstr ""
+
+#: .\admin.py:29
+msgid "Visitor's email is in excess."
+msgstr ""
+
+#: .\admin.py:34
+msgid "Visitor's email is missing."
+msgstr ""
+
+#: .\admin.py:40
+msgid "Reading date must be later to sending date."
+msgstr ""
+
+#: .\admin.py:45
+msgid "Deletion date by sender must be later to sending date."
+msgstr ""
+
+#: .\admin.py:50
+msgid "Deletion date by recipient must be later to sending date."
+msgstr ""
+
+#: .\admin.py:58
+msgid "Response date must be later to sending date."
+msgstr ""
+
+#: .\admin.py:60
+msgid "The message cannot be replied without having been read."
+msgstr ""
+
+#: .\admin.py:62
+msgid "Response date must be later to reading date."
+msgstr ""
+
+#: .\admin.py:64
+msgid "Response date cannot be set without at least one reply."
+msgstr ""
+
+#: .\admin.py:66
+msgid "The message cannot be replied without being in a conversation."
+msgstr ""
+
+#: .\admin.py:88 .\admin.py:157 .\templates\postman\view.html.py:5
+msgid "Message"
+msgstr "Bericht"
+
+#: .\admin.py:93
+msgid "Dates"
+msgstr "Data"
+
+#: .\admin.py:98 .\admin.py:161
+msgid "Moderation"
+msgstr ""
+
+#: .\fields.py:22
+msgid "Some usernames are unknown or no more active: {users}."
+msgstr ""
+
+#: .\fields.py:23
+msgid "Ensure this value has at most {limit_value} distinct items (it has {show_value})."
+msgstr ""
+
+#: .\fields.py:24
+msgid "Ensure this value has at least {limit_value} distinct items (it has {show_value})."
+msgstr ""
+
+#: .\fields.py:25
+msgid "Some usernames are rejected: {users}."
+msgstr ""
+
+#: .\fields.py:26 .\forms.py:65
+msgid "{user.username}"
+msgstr "{user.username}"
+
+#: .\fields.py:27 .\forms.py:66
+msgid "{user.username} ({reason})"
+msgstr "{user.username} ({reason})"
+
+#: .\forms.py:64
+msgid "Writing to some users is not possible: {users}."
+msgstr ""
+
+#: .\forms.py:148 .\forms.py:160
+msgid "Recipients"
+msgstr "Ontvangers"
+
+#: .\forms.py:148 .\forms.py:160 .\templates\postman\base_folder.html.py:26
+#: .\templates\postman\reply.html.py:4
+msgid "Recipient"
+msgstr "Ontvanger"
+
+#: .\forms.py:159
+msgid "Email"
+msgstr "E-mail"
+
+#: .\forms.py:175
+msgid "Undefined recipient."
+msgstr ""
+
+#: .\forms.py:194
+msgid "Additional recipients"
+msgstr ""
+
+#: .\forms.py:194
+msgid "Additional recipient"
+msgstr ""
+
+#: .\models.py:19
+msgid "Pending"
+msgstr ""
+
+#: .\models.py:20
+msgid "Accepted"
+msgstr ""
+
+#: .\models.py:21 .\templates\postman\view.html.py:13
+msgid "Rejected"
+msgstr ""
+
+#: .\models.py:197
+msgid "subject"
+msgstr "onderwerp"
+
+#: .\models.py:198
+msgid "body"
+msgstr "inhoud"
+
+#: .\models.py:199 .\models.py:281
+msgid "sender"
+msgstr "verstuurder"
+
+#: .\models.py:200 .\models.py:305
+msgid "recipient"
+msgstr "ontvanger"
+
+#: .\models.py:201
+msgid "visitor"
+msgstr "bezoeker"
+
+#: .\models.py:202
+msgid "parent message"
+msgstr "hoofdbericht"
+
+#: .\models.py:203
+msgid "root message"
+msgstr ""
+
+#: .\models.py:204
+msgid "sent at"
+msgstr "verstuurd op"
+
+#: .\models.py:205
+msgid "read at"
+msgstr "gelezen op"
+
+#: .\models.py:206
+msgid "replied at"
+msgstr "beantwoord op"
+
+#: .\models.py:207
+msgid "archived by sender"
+msgstr ""
+
+#: .\models.py:208
+msgid "archived by recipient"
+msgstr ""
+
+#: .\models.py:209
+msgid "deleted by sender at"
+msgstr ""
+
+#: .\models.py:210
+msgid "deleted by recipient at"
+msgstr ""
+
+#: .\models.py:212
+msgid "status"
+msgstr ""
+
+#: .\models.py:214
+msgid "moderator"
+msgstr ""
+
+#: .\models.py:215
+msgid "moderated at"
+msgstr ""
+
+#: .\models.py:216
+msgid "rejection reason"