forms.py 35.9 KB
Newer Older
1
# -*- coding: utf-8 -*-
Florian Fuchs's avatar
Florian Fuchs committed
2
# Copyright (C) 2012-2015 by the Free Software Foundation, Inc.
3
#
4
# This file is part of Postorius.
5
#
6
# Postorius is free software: you can redistribute it and/or modify it under
7 8 9 10
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
11
# Postorius is distributed in the hope that it will be useful, but WITHOUT
12 13 14 15 16
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
17
# Postorius.  If not, see <http://www.gnu.org/licenses/>.
18

Aurélien Bompard's avatar
Aurélien Bompard committed
19 20
from __future__ import absolute_import, unicode_literals

21
from django import forms
22
from django.core.urlresolvers import reverse
23
from django.core.validators import validate_email
24
from django.utils.encoding import smart_text
25
from django.utils.translation import ugettext_lazy as _
26
from django.utils.version import get_complete_version
27
from django.contrib.sites.models import Site
28

Florian Fuchs's avatar
Florian Fuchs committed
29

30 31 32 33 34 35 36 37 38
ACTION_CHOICES = (
    ("hold", _("Hold for moderation")),
    ("reject", _("Reject (with notification)")),
    ("discard", _("Discard (no notification)")),
    ("accept", _("Accept immediately (bypass other rules)")),
    ("defer", _("Default processing")),
    )


39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
class ListOfStringsField(forms.Field):
    widget = forms.widgets.Textarea

    def prepare_value(self, value):
        if isinstance(value, list):
            value = '\n'.join(value)
        return value

    def to_python(self, value):
        "Returns a list of Unicode object."
        if value.strip() in self.empty_values:
            return []
        result = []
        for line in value.splitlines():
            line = line.strip()
            if not line:
                continue
            result.append(smart_text(line))
        return result


60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
class NullBooleanRadioSelect(forms.RadioSelect):
    """
    This is necessary to detect that such a field has not been changed.
    """

    def value_from_datadict(self, data, files, name):
        value = data.get(name, None)
        return {'2': True,
                True: True,
                'True': True,
                '3': False,
                'False': False,
                False: False}.get(value, None)


75
class SiteModelChoiceField(forms.ModelChoiceField):
76

77 78
    def label_from_instance(self, obj):
            return "%s (%s)" % (obj.name, obj.domain)
79

80

81 82 83 84 85 86 87 88
def _get_web_host_help():
    # Using a function is necessary, otherwise reverse() will be called before
    # URLConfs are loaded.
    return (_('<a href="%s">Edit</a> the list of available web hosts.')
            % reverse("admin:sites_site_changelist"))


class DomainForm(forms.Form):
Florian Fuchs's avatar
Florian Fuchs committed
89
    """
90
    Add or edit a domain.
91
    """
benste's avatar
benste committed
92
    mail_host = forms.CharField(
Florian Fuchs's avatar
Florian Fuchs committed
93
        label=_('Mail Host'),
94
        error_messages={'required': _('Please enter a domain name'),
Florian Fuchs's avatar
Florian Fuchs committed
95
                        'invalid': _('Please enter a valid domain name.')},
96 97 98
        required=True,
        help_text=_('Example: domain.org'),
        )
99
    description = forms.CharField(
Florian Fuchs's avatar
Florian Fuchs committed
100 101
        label=_('Description'),
        required=False)
102
    site = SiteModelChoiceField(
103 104 105 106
        label=_('Web Host'),
        error_messages={'required': _('Please enter a domain name'),
                        'invalid': _('Please enter a valid domain name.')},
        required=True,
107
        queryset=Site.objects.order_by("name").all(),
108
        initial=lambda: Site.objects.get_current(),
109
        help_text=_get_web_host_help,
110
        )
Florian Fuchs's avatar
Florian Fuchs committed
111

benste's avatar
benste committed
112 113
    def clean_mail_host(self):
        mail_host = self.cleaned_data['mail_host']
114 115 116
        try:
            validate_email('mail@' + mail_host)
        except:
117
            raise forms.ValidationError(_("Please enter a valid domain name"))
benste's avatar
benste committed
118
        return mail_host
Florian Fuchs's avatar
Florian Fuchs committed
119

120

121 122 123
class MemberForm(forms.Form):
    """Assing a role to the member"""
    email = forms.EmailField(
124 125 126 127 128 129
        label=_('Email Address'),
        error_messages={
            'required': _('Please enter an email adddress.'),
            'invalid': _('Please enter a valid email adddress.')})


Simon Hanna's avatar
Simon Hanna committed
130
class ListNew(forms.Form):
131

132
    """
Florian Fuchs's avatar
Florian Fuchs committed
133
    Form fields to add a new list. Languages are hard coded which should
134
    be replaced by a REST lookup of available languages.
Florian Fuchs's avatar
Florian Fuchs committed
135
    """
136
    listname = forms.CharField(
Florian Fuchs's avatar
Florian Fuchs committed
137 138 139 140
        label=_('List Name'),
        required=True,
        error_messages={'required': _('Please enter a name for your list.'),
                        'invalid': _('Please enter a valid list name.')})
141
    mail_host = forms.ChoiceField()
142
    list_owner = forms.EmailField(
Florian Fuchs's avatar
Florian Fuchs committed
143 144 145 146
        label=_('Inital list owner address'),
        error_messages={
            'required': _("Please enter the list owner's email address.")},
        required=True)
147
    advertised = forms.ChoiceField(
Florian Fuchs's avatar
Florian Fuchs committed
148 149 150 151 152 153
        widget=forms.RadioSelect(),
        label=_('Advertise this list?'),
        error_messages={
            'required': _("Please choose a list type.")},
        required=True,
        choices=(
Florian Fuchs's avatar
Florian Fuchs committed
154
            (True, _("Advertise this list in list index")),
Florian Fuchs's avatar
Florian Fuchs committed
155
            (False, _("Hide this list in list index"))))
156
    description = forms.CharField(
Florian Fuchs's avatar
Florian Fuchs committed
157
        label=_('Description'),
158
        required=False)
159

Florian Fuchs's avatar
Florian Fuchs committed
160 161
    def __init__(self, domain_choices, *args, **kwargs):
        super(ListNew, self).__init__(*args, **kwargs)
162
        self.fields["mail_host"] = forms.ChoiceField(
Florian Fuchs's avatar
Florian Fuchs committed
163 164 165 166 167 168
            widget=forms.Select(),
            label=_('Mail Host'),
            required=True,
            choices=domain_choices,
            error_messages={'required': _("Choose an existing Domain."),
                            'invalid': "ERROR-todo_forms.py"})
169
        if len(domain_choices) < 2:
170
            self.fields["mail_host"].help_text = _(
171
                "Site admin has not created any domains")
172
            # if len(choices) < 2:
173 174 175
            #    help_text=_("No domains available: " +
            #                "The site admin must create new domains " +
            #                "before you will be able to create a list")
Florian Fuchs's avatar
Florian Fuchs committed
176

177
    def clean_listname(self):
Florian Fuchs's avatar
Florian Fuchs committed
178 179 180
        try:
            validate_email(self.cleaned_data['listname'] + '@example.net')
        except:
181
            raise forms.ValidationError(_("Please enter a valid listname"))
182 183
        return self.cleaned_data['listname']

184
    class Meta:
185

186 187
        """
        Class to handle the automatic insertion of fieldsets and divs.
Florian Fuchs's avatar
Florian Fuchs committed
188 189 190

        To use it: add a list for each wished fieldset. The first item in
        the list should be the wished name of the fieldset, the following
191 192
        the fields that should be included in the fieldset.
        """
193 194 195 196 197
        layout = [["List Details",
                   "listname",
                   "mail_host",
                   "list_owner",
                   "description",
Florian Fuchs's avatar
Florian Fuchs committed
198 199
                   "advertised"], ]

200

Simon Hanna's avatar
Simon Hanna committed
201
class ListSubscribe(forms.Form):
202
    """Form fields to join an existing list.
203
    """
204

205
    email = forms.ChoiceField(
Simon Hanna's avatar
Simon Hanna committed
206 207 208 209 210 211
        label=_('Your email address'),
        validators=[validate_email],
        widget=forms.Select(),
        error_messages={
            'required': _('Please enter an email address.'),
            'invalid': _('Please enter a valid email address.')})
Abhilash Raj's avatar
Abhilash Raj committed
212

Simon Hanna's avatar
Simon Hanna committed
213 214
    display_name = forms.CharField(
        label=_('Your name (optional)'), required=False)
Florian Fuchs's avatar
Florian Fuchs committed
215

216 217 218 219 220
    def __init__(self, user_emails, *args, **kwargs):
        super(ListSubscribe, self).__init__(*args, **kwargs)
        self.fields['email'].choices = ((address, address)
                                        for address in user_emails)

Florian Fuchs's avatar
Florian Fuchs committed
221

222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
class ListSettingsForm(forms.Form):
    """
    Base class for list settings forms.
    """
    mlist_properties = []

    def __init__(self, *args, **kwargs):
        self._mlist = kwargs.pop('mlist')
        super(ListSettingsForm, self).__init__(*args, **kwargs)


SUBSCRIPTION_POLICY_CHOICES = (
    ('open', _('Open')),
    ('confirm', _('Confirm')),
    ('moderate', _('Moderate')),
    ('confirm_then_moderate', _('Confirm, then moderate')),
)


class ListSubscriptionPolicyForm(ListSettingsForm):
    """
    List subscription policy settings.
    """
    subscription_policy = forms.ChoiceField(
        label=_('Subscription Policy'),
        choices=SUBSCRIPTION_POLICY_CHOICES,
248 249 250
        help_text=_('Open: Subscriptions are added automatically\n'
                    'Confirm: Subscribers need to confirm the subscription '
                    'using an email sent to them\n'
251 252 253 254
                    'Moderate: Moderators will have to authorize '
                    'each subscription manually.\n'
                    'Confirm then Moderate: First subscribers have to confirm,'
                    ' then a moderator '
255
                    'needs to authorize.'))
256 257 258


class ArchiveSettingsForm(ListSettingsForm):
259 260
    """
    Set the general archive policy.
261
    """
262 263
    mlist_properties = ['archivers']

264
    archive_policy_choices = (
265 266
        ("public", _("Public archives")),
        ("private", _("Private archives")),
267 268
        ("never", _("Do not archive this list")),
    )
269

270 271 272
    archive_policy = forms.ChoiceField(
        choices=archive_policy_choices,
        widget=forms.RadioSelect,
273
        label=_('Archive policy'),
274 275
        help_text=_('Policy for archiving messages for this list'),
    )
276

277 278 279
    archivers = forms.MultipleChoiceField(
        widget=forms.CheckboxSelectMultiple,
        label=_('Active archivers'),
Simon Hanna's avatar
Simon Hanna committed
280
        required=False)  # May be empty if no archivers are desired.
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297

    def __init__(self, *args, **kwargs):
        super(ArchiveSettingsForm, self).__init__(*args, **kwargs)
        self.fields['archivers'].choices = sorted(
            [(key, key) for key in sorted(self._mlist.archivers.keys())])
        if self.initial:
            self.initial['archivers'] = [
                key for key in sorted(self._mlist.archivers.keys())
                if self._mlist.archivers[key] is True]

    def clean_archivers(self):
        result = {}
        for archiver, ignore_ in self.fields['archivers'].choices:
            result[archiver] = archiver in self.cleaned_data['archivers']
        self.cleaned_data['archivers'] = result
        return result

298

299
class MessageAcceptanceForm(ListSettingsForm):
300 301 302
    """
    List messages acceptance settings.
    """
303
    acceptable_aliases = ListOfStringsField(
304 305
        label=_("Acceptable aliases"),
        required=False,
306
        help_text=_(
307 308 309 310 311 312 313 314 315 316 317
            'Alias names which qualify as explicit to or cc destination names '
            'for this list. Alternate addresses that are acceptable when '
            '`require_explicit_destination\' is enabled. This option takes a '
            'list of regular expressions, one per line, which is matched '
            'against every recipient address in the message. The matching is '
            'performed with Python\'s re.match() function, meaning they are '
            'anchored to the start of the string.'))
    administrivia = forms.BooleanField(
        widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
        required=False,
        label=_('Administrivia'),
318
        help_text=_(
319 320 321 322 323
            'Administrivia tests will check postings to see whether it\'s '
            'really meant as an administrative request (like subscribe, '
            'unsubscribe, etc), and will add it to the the administrative '
            'requests queue, notifying the administrator of the new request, '
            'in the process.'))
324
    default_member_action = forms.ChoiceField(
Florian Fuchs's avatar
Florian Fuchs committed
325
        widget=forms.RadioSelect(),
326
        label=_('Default action to take when a member posts to the list'),
Florian Fuchs's avatar
Florian Fuchs committed
327 328 329
        error_messages={
            'required': _("Please choose a default member action.")},
        required=True,
330
        choices=ACTION_CHOICES,
331
        help_text=_(
332 333 334 335
            'Default action to take when a member posts to the list.\n'
            'Hold: This holds the message for approval by the list '
            'moderators.\n'
            'Reject: this automatically rejects the message by sending a '
336
            'bounce notice to the post\'s author. The text of the bounce '
337 338 339 340 341 342
            'notice can be configured by you.\n'
            'Discard: this simply discards the message, with no notice '
            'sent to the post\'s author.\n'
            'Accept: accepts any postings without any further checks.\n'
            'Defer: default processing, run additional checks and accept '
            'the message.'))
343
    default_nonmember_action = forms.ChoiceField(
Florian Fuchs's avatar
Florian Fuchs committed
344 345
        widget=forms.RadioSelect(),
        label=_('Default action to take when a non-member posts to the'
346
                'list'),
Florian Fuchs's avatar
Florian Fuchs committed
347 348 349
        error_messages={
            'required': _("Please choose a default non-member action.")},
        required=True,
350
        choices=ACTION_CHOICES,
351
        help_text=_(
352 353 354 355
            'When a post from a non-member is received, the message\'s sender '
            'is matched against the list of explicitly accepted, held, '
            'rejected (bounced), and discarded addresses. '
            'If no match is found, then this action is taken.'))
356 357


358
class DigestSettingsForm(ListSettingsForm):
359 360 361
    """
    List digest settings.
    """
362
    digest_size_threshold = forms.DecimalField(
Florian Fuchs's avatar
Florian Fuchs committed
363
        label=_('Digest size threshold'),
364 365
        help_text=_('How big in Kb should a digest be before '
                    'it gets sent out?'))
366 367


368
class AlterMessagesForm(ListSettingsForm):
369 370 371 372 373 374 375 376 377
    """
    Alter messages list settings.
    """
    filter_content = forms.TypedChoiceField(
        coerce=lambda x: x == 'True',
        choices=((True, _('Yes')), (False, _('No'))),
        widget=forms.RadioSelect,
        required=False,
        label=_('Filter content'),
378 379
        help_text=_('Should Mailman filter the content of list traffic '
                    'according to the settings below?'))
380 381 382 383 384 385
    collapse_alternatives = forms.TypedChoiceField(
        coerce=lambda x: x == 'True',
        choices=((True, _('Yes')), (False, _('No'))),
        widget=forms.RadioSelect,
        required=False,
        label=_('Collapse alternatives'),
386 387
        help_text=_('Should Mailman collapse multipart/alternative to '
                    'its first part content?'))
388 389 390 391 392 393
    convert_html_to_plaintext = forms.TypedChoiceField(
        coerce=lambda x: x == 'True',
        choices=((True, _('Yes')), (False, _('No'))),
        widget=forms.RadioSelect,
        required=False,
        label=_('Convert html to plaintext'),
394
        help_text=_('Should Mailman convert text/html parts to plain text? '
395 396
                    'This conversion happens after MIME attachments '
                    'have been stripped.'))
397 398 399 400 401 402
    anonymous_list = forms.TypedChoiceField(
        coerce=lambda x: x == 'True',
        choices=((True, _('Yes')), (False, _('No'))),
        widget=forms.RadioSelect,
        required=False,
        label=_('Anonymous list'),
403 404 405
        help_text=_('Hide the sender of a message, '
                    'replacing it with the list address '
                    '(Removes From, Sender and Reply-To fields)'))
406 407 408 409 410
    include_rfc2369_headers = forms.TypedChoiceField(
        coerce=lambda x: x == 'True',
        choices=((True, _('Yes')), (False, _('No'))),
        widget=forms.RadioSelect,
        required=False,
Simon Hanna's avatar
Simon Hanna committed
411
        label=_('Include RFC2369 headers'),
412 413 414 415 416 417 418 419 420 421 422 423 424 425
        help_text=_(
            'Yes is highly recommended. RFC 2369 defines a set of List-* '
            'headers that are normally added to every message sent to the '
            'list membership. These greatly aid end-users who are using '
            'standards compliant mail readers. They should normally always '
            'be enabled. However, not all mail readers are standards '
            'compliant yet, and if you have a large number of members who are '
            'using non-compliant mail readers, they may be annoyed at these '
            'headers. You should first try to educate your members as to why '
            'these headers exist, and how to hide them in their mail clients. '
            'As a last resort you can disable these headers, but this is not '
            'recommended (and in fact, your ability to disable these headers '
            'may eventually go away).'))
    allow_list_posts = forms.TypedChoiceField(
426
        coerce=lambda x: x == 'True',
427 428
        choices=((True, _('Yes')), (False, _('No'))),
        widget=forms.RadioSelect,
429
        required=False,
430 431 432 433 434 435 436 437 438 439 440 441
        label=_("Include the list post header"),
        help_text=_(
            "This can be set to no for announce lists that do not wish to "
            "include the List-Post header because posting to the list is "
            "discouraged."))
    reply_to_address = forms.CharField(
        label=_('Explicit reply-to address'),
        required=False,
        help_text=_(
            'This option allows admins to set an explicit Reply-to address. '
            'It is only used if the reply-to is set to use an explicitly set '
            'header'))
442
    first_strip_reply_to = forms.TypedChoiceField(
443
        coerce=lambda x: x == 'True',
444 445
        choices=((True, _('Yes')), (False, _('No'))),
        widget=forms.RadioSelect,
446
        required=False,
447
        help_text=_(
448
            'Should any existing Reply-To: header found in the original '
449
            'message be stripped? If so, this will be done regardless of '
450
            'whether an explict Reply-To: header is added by Mailman or not.'))
451
    reply_goes_to_list = forms.ChoiceField(
Florian Fuchs's avatar
Florian Fuchs committed
452 453
        label=_('Reply goes to list'),
        widget=forms.Select(),
454
        required=False,
Florian Fuchs's avatar
Florian Fuchs committed
455 456 457
        error_messages={
            'required': _("Please choose a reply-to action.")},
        choices=(
458 459 460 461
            ('no_munging', _('No Munging')),
            ('point_to_list', _('Reply goes to list')),
            ('explicit_header', _('Explicit Reply-to header set'))),
        help_text=_(
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
            'Where are replies to list messages directed? No Munging is '
            'strongly recommended for most mailing lists. \nThis option '
            'controls what Mailman does to the Reply-To: header in messages '
            'flowing through this mailing list. When set to No Munging, no '
            'Reply-To: header is '
            'added by Mailman, although if one is present in the original '
            'message, it is not stripped. Setting this value to either Reply '
            'to List or Explicit Reply causes Mailman to insert a specific '
            'Reply-To: header in all messages, overriding the header in the '
            'original message if necessary (Explicit Reply inserts the value '
            'of reply_to_address). There are many reasons not to introduce or '
            'override the Reply-To: header. One is that some posters depend '
            'on their own Reply-To: settings to convey their valid return '
            'address. Another is that modifying Reply-To: makes it much more '
            'difficult to send private replies. See `Reply-To\' Munging '
            'Considered Harmful for a general discussion of this issue. '
            'See Reply-To Munging Considered Useful for a dissenting opinion. '
            'Some mailing lists have restricted '
            'posting privileges, with a parallel list devoted to discussions. '
            'Examples are `patches\' or `checkin\' lists, where software '
            'changes are posted by a revision control system, but discussion '
            'about the changes occurs on a developers mailing list. To '
            'support these types of mailing lists, select Explicit Reply and '
            'set the Reply-To: address option to point to the parallel list.'))
486 487 488
    posting_pipeline = forms.ChoiceField(
        label=_('Pipeline'),
        widget=forms.Select(),
489
        required=False,
490 491 492
        error_messages={
            'required': _("Please choose a reply-to action.")},
        choices=(
493 494 495 496
            ('default-owner-pipeline', _('default-owner-pipeline')),
            ('default-posting-pipeline', _('default-posting-pipeline')),
            ('virgin', _('virgin'))),
        help_text=_('Type of pipeline you want to use for this mailing list'))
Florian Fuchs's avatar
Florian Fuchs committed
497

498

499
class ListAutomaticResponsesForm(ListSettingsForm):
500 501 502 503 504 505 506 507 508 509 510
    """
    List settings for automatic responses.
    """
    autorespond_choices = (
        ("respond_and_continue", _("Respond and continue processing")),
        ("respond_and_discard", _("Respond and discard message")),
        ("none", _("No automatic response")))
    autorespond_owner = forms.ChoiceField(
        choices=autorespond_choices,
        widget=forms.RadioSelect,
        label=_('Autorespond to list owner'),
511 512
        help_text=_('Should Mailman send an auto-response to '
                    'emails sent to the -owner address?'))
513 514 515 516
    autoresponse_owner_text = forms.CharField(
        label=_('Autoresponse owner text'),
        widget=forms.Textarea(),
        required=False,
517
        help_text=_('Auto-response text to send to -owner emails.'))
518 519 520 521
    autorespond_postings = forms.ChoiceField(
        choices=autorespond_choices,
        widget=forms.RadioSelect,
        label=_('Autorespond postings'),
522 523
        help_text=_('Should Mailman send an auto-response to '
                    'mailing list posters?'))
524 525 526 527
    autoresponse_postings_text = forms.CharField(
        label=_('Autoresponse postings text'),
        widget=forms.Textarea(),
        required=False,
528
        help_text=_('Auto-response text to send to mailing list posters.'))
529 530 531 532
    autorespond_requests = forms.ChoiceField(
        choices=autorespond_choices,
        widget=forms.RadioSelect,
        label=_('Autorespond requests'),
533
        help_text=_(
534 535 536 537 538 539 540 541
            'Should Mailman send an auto-response to emails sent to the '
            '-request address? If you choose yes, decide whether you want '
            'Mailman to discard the original email, or forward it on to the '
            'system as a normal mail command.'))
    autoresponse_request_text = forms.CharField(
        label=_('Autoresponse request text'),
        widget=forms.Textarea(),
        required=False,
542
        help_text=_('Auto-response text to send to -request emails.'))
543 544
    autoresponse_grace_period = forms.CharField(
        label=_('Autoresponse grace period'),
545
        help_text=_(
546 547 548 549
            'Number of days between auto-responses to either the mailing list '
            'or -request/-owner address from the same poster. Set to zero '
            '(or negative) for no grace period (i.e. auto-respond to every '
            'message).'))
550
    send_welcome_message = forms.TypedChoiceField(
Florian Fuchs's avatar
Florian Fuchs committed
551
        coerce=lambda x: x == 'True',
552
        choices=((True, _('Yes')), (False, _('No'))),
Florian Fuchs's avatar
Florian Fuchs committed
553 554
        widget=forms.RadioSelect,
        required=False,
555
        label=_('Send welcome message'),
556
        help_text=_(
557 558 559 560 561
            'Send welcome message to newly subscribed members? '
            'Turn this off only if you plan on subscribing people manually '
            'and don\'t want them to know that you did so. This option is '
            'most useful for transparently migrating lists from some other '
            'mailing list manager to Mailman.'))
562
    welcome_message_uri = forms.CharField(
563 564
        label=_('URI for the welcome message'),
        help_text=_(
565
            'If a welcome message is to be sent to subscribers, you can '
566
            'specify a URI that gives the text of this message.'),
567
    )
568 569 570 571 572
    goodbye_message_uri = forms.CharField(
        label=_('URI for the good bye message'),
        help_text=_(
            'If a good bye message is to be sent to unsubscribers, you can '
            'specify a URI that gives the text of this message.'),
573
    )
574
    admin_immed_notify = forms.BooleanField(
575
        widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
Florian Fuchs's avatar
Florian Fuchs committed
576
        required=False,
577
        label=_('Admin immed notify'),
578
        help_text=_(
579 580 581 582 583 584 585
            'Should the list moderators get immediate notice of new requests, '
            'as well as daily notices about collected ones? List moderators '
            '(and list administrators) are sent daily reminders of requests '
            'pending approval, like subscriptions to a moderated list, '
            'or postings that are being held for one reason or another. '
            'Setting this option causes notices to be sent immediately on the '
            'arrival of new requests as well. '))
586
    admin_notify_mchanges = forms.BooleanField(
587
        widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
Florian Fuchs's avatar
Florian Fuchs committed
588
        required=False,
589
        label=_('Notify admin of membership changes'),
590 591
        help_text=_('Should administrator get notices of '
                    'subscribes and unsubscribes?'))
592 593


594
class ListIdentityForm(ListSettingsForm):
595 596 597 598
    """
    List identity settings.
    """
    advertised = forms.TypedChoiceField(
Florian Fuchs's avatar
Florian Fuchs committed
599
        coerce=lambda x: x == 'True',
600
        choices=((True, _('Yes')), (False, _('No'))),
Florian Fuchs's avatar
Florian Fuchs committed
601
        widget=forms.RadioSelect,
602
        label=_('Show list on index page'),
603 604
        help_text=_('Choose whether to include this list '
                    'on the list of all lists'))
605 606
    description = forms.CharField(
        label=_('Description'),
607
        required=False,
608
        help_text=_(
609 610 611 612
            'This description is used when the mailing list is listed with '
            'other mailing lists, or in headers, and so forth. It should be '
            'as succinct as you can get it, while still identifying what the '
            'list is.'),
613 614 615 616
        )
    info = forms.CharField(
        label=_('Information'),
        help_text=_('A longer description of this mailing list.'),
617
        required=False,
618 619 620
        widget=forms.Textarea())
    display_name = forms.CharField(
        label=_('Display name'),
621
        required=False,
622
        help_text=_('Display name is the name shown in the web interface.')
623
    )
624
    if get_complete_version() < (1, 9):
625
        subject_prefix = forms.CharField(
Mark Sapiro's avatar
Mark Sapiro committed
626
            label=_('Subject prefix'),
627
            required=False,
628 629 630 631 632
        )
    else:
        subject_prefix = forms.CharField(
            label=_('Subject prefix'),
            strip=False,
633
            required=False,
634
        )
Florian Fuchs's avatar
Florian Fuchs committed
635 636


637 638 639 640
class ListArchiverForm(forms.Form):
    """
    Select archivers for a list.
    """
641 642 643 644
    archivers = forms.MultipleChoiceField(
        widget=forms.CheckboxSelectMultiple,
        label=_('Activate archivers for this list'))

645 646
    def __init__(self, archivers, *args, **kwargs):
        super(ListArchiverForm, self).__init__(*args, **kwargs)
647 648
        self.fields['archivers'].choices = sorted(
            [(key, key) for key in archivers.keys()])
649 650


Simon Hanna's avatar
Simon Hanna committed
651
class ListMassSubscription(forms.Form):
652 653
    """Form fields to masssubscribe users to a list.
    """
654
    emails = ListOfStringsField(
Florian Fuchs's avatar
Florian Fuchs committed
655
        label=_('Emails to mass subscribe'),
656 657 658
        help_text=_(
            'The following formats are accepted:\n'
            'jdoe@example.com\n'
659 660 661
            '&lt;jdoe@example.com&gt;\n'
            'John Doe &lt;jdoe@example.com&gt;\n'
            '"John Doe" &lt;jdoe@example.com&gt;\n'
662 663 664
            'jdoe@example.com (John Doe)\n'
            'Use the last three to associate a display name with'
            ' the address\n'),
665 666
    )

667

Simon Hanna's avatar
Simon Hanna committed
668
class ListMassRemoval(forms.Form):
669 670 671

    """Form fields to remove multiple list users.
    """
672
    emails = ListOfStringsField(
673 674 675 676 677 678 679 680 681 682 683 684
        label=_('Emails to Unsubscribe'),
    )

    class Meta:

        """
        Class to define the name of the fieldsets and what should be
        included in each.
        """
        layout = [["Mass Removal", "emails"]]


685 686 687 688 689 690 691 692 693 694 695 696
class ListAddBanForm(forms.Form):
    """Ban an email address for a list."""
    email = forms.CharField(
        label=_('Add ban'),
        help_text=_(
            'You can ban a single email address or use a regular expression '
            'to match similar email addresses.'),
        error_messages={
            'required': _('Please enter an email adddress.'),
            'invalid': _('Please enter a valid email adddress.')})


697 698 699
class ListHeaderMatchForm(forms.Form):
    """Edit a list's header match."""

700 701
    HM_ACTION_CHOICES = [(None, _("Default antispam action"))] + \
                        [a for a in ACTION_CHOICES if a[0] != 'defer']
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722

    header = forms.CharField(
        label=_('Header'),
        help_text=_('Email header to filter on (case-insensitive).'),
        error_messages={
            'required': _('Please enter a header.'),
            'invalid': _('Please enter a valid header.')})
    pattern = forms.CharField(
        label=_('Pattern'),
        help_text=_('Regular expression matching the header\'s value.'),
        error_messages={
            'required': _('Please enter a pattern.'),
            'invalid': _('Please enter a valid pattern.')})
    action = forms.ChoiceField(
        label=_('Action'),
        error_messages={'invalid': _('Please enter a valid action.')},
        required=False,
        choices=HM_ACTION_CHOICES,
        help_text=_('Action to take when a header matches')
        )

723

724 725 726 727
class ListHeaderMatchFormset(forms.BaseFormSet):
    def clean(self):
        """Checks that no two header matches have the same order."""
        if any(self.errors):
728 729
            # Don't bother validating the formset unless
            # each form is valid on its own
730 731 732 733 734 735 736 737
            return
        orders = []
        for form in self.forms:
            try:
                order = form.cleaned_data['ORDER']
            except KeyError:
                continue
            if order in orders:
738 739
                raise forms.ValidationError('Header matches must have'
                                            ' distinct orders.')
740 741 742
            orders.append(order)


Simon Hanna's avatar
Simon Hanna committed
743
class UserPreferences(forms.Form):
744 745

    """
746
    Form handling the user's global, address and subscription based preferences
747
    """
748 749 750 751 752 753 754 755 756 757 758 759 760 761 762

    def __init__(self, *args, **kwargs):
        self._preferences = kwargs.pop('preferences', None)
        super(UserPreferences, self).__init__(*args, **kwargs)

    @property
    def initial(self):
        # Redirect to the preferences, this allows setting the preferences
        # after instanciation and it will also set the initial data.
        return self._preferences or {}

    @initial.setter
    def initial(self, value):
        pass

Florian Fuchs's avatar
Florian Fuchs committed
763
    choices = ((True, _('Yes')), (False, _('No')))
764 765

    delivery_mode_choices = (("regular", _('Regular')),
766 767 768
                             ("plaintext_digests", _('Plain Text Digests')),
                             ("mime_digests", _('Mime Digests')),
                             ("summary_digests", _('Summary Digests')))
769 770
    delivery_status_choices = (
        ("enabled", _('Enabled')), ("by_user", _('Disabled')))
771 772 773 774 775
    delivery_status = forms.ChoiceField(
        widget=forms.RadioSelect,
        choices=delivery_status_choices,
        required=False,
        label=_('Delivery status'),
776
        help_text=_(
777 778 779 780 781 782
            'Set this option to Enabled to receive messages posted to this '
            'mailing list. Set it to Disabled if you want to stay subscribed, '
            'but don\'t want mail delivered to you for a while (e.g. you\'re '
            'going on vacation). If you disable mail delivery, don\'t forget '
            'to re-enable it when you come back; it will not be automatically '
            're-enabled.'))
783 784 785 786 787
    delivery_mode = forms.ChoiceField(
        widget=forms.Select(),
        choices=delivery_mode_choices,
        required=False,
        label=_('Delivery mode'),
788
        help_text=_(
789 790 791 792 793 794
            'If you select summary digests , you\'ll get posts bundled '
            'together (usually one per day but possibly more on busy lists), '
            'instead of singly when they\'re sent. Your mail reader may or '
            'may not support MIME digests. In general MIME digests are '
            'preferred, but if you have a problem reading them, select '
            'plain text digests.'))
Abhilash Raj's avatar
Abhilash Raj committed
795
    receive_own_postings = forms.NullBooleanField(
796
        widget=NullBooleanRadioSelect(choices=choices),
797 798
        required=False,
        label=_('Receive own postings'),
799
        help_text=_(
800 801 802 803
            'Ordinarily, you will get a copy of every message you post to the '
            'list. If you don\'t want to receive this copy, set this option '
            'to No.'
            ))
Abhilash Raj's avatar
Abhilash Raj committed
804
    acknowledge_posts = forms.NullBooleanField(
805
        widget=NullBooleanRadioSelect(choices=choices),
Florian Fuchs's avatar
Florian Fuchs committed
806
        required=False,
807
        label=_('Acknowledge posts'),
808
        help_text=_(
809
            'Receive acknowledgement mail when you send mail to the list?'))
Abhilash Raj's avatar
Abhilash Raj committed
810
    hide_address = forms.NullBooleanField(
811
        widget=NullBooleanRadioSelect(choices=choices),
Florian Fuchs's avatar
Florian Fuchs committed
812
        required=False,
813
        label=_('Hide address'),
814
        help_text=_(
815 816 817 818 819
            'When someone views the list membership, your email address is '
            'normally shown (in an obscured fashion to thwart spam '
            'harvesters). '
            'If you do not want your email address to show up on this '
            'membership roster at all, select Yes for this option.'))
Abhilash Raj's avatar
Abhilash Raj committed
820
    receive_list_copy = forms.NullBooleanField(
821
        widget=NullBooleanRadioSelect(choices=choices),
Florian Fuchs's avatar
Florian Fuchs committed
822
        required=False,
823
        label=_('Avoid Duplicates'),
824
        help_text=_(
825 826 827 828
            'When you are listed explicitly in the To: or Cc: headers of a '
            'list message, you can opt to not receive another copy from the '
            'mailing list. Select Yes to avoid receiving copies from the '
            'mailing list; select No to receive copies. '))
829 830

    class Meta:
831

832 833 834 835
        """
        Class to define the name of the fieldsets and what should be
        included in each.
        """
836
        layout = [["User Preferences", "acknowledge_posts", "hide_address",
Florian Fuchs's avatar
Florian Fuchs committed
837 838 839
                   "receive_list_copy", "receive_own_postings",
                   "delivery_mode", "delivery_status"]]

840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
    def save(self):
        if not self.changed_data:
            return
        for key in self.changed_data:
            if self.cleaned_data[key] is not None:
                # None: nothing set yet. Remember to remove this test
                # when Mailman accepts None as a "reset to default"
                # value.
                self._preferences[key] = self.cleaned_data[key]
        self._preferences.save()


class UserPreferencesFormset(forms.BaseFormSet):

    def __init__(self, *args, **kwargs):
        self._preferences = kwargs.pop('preferences')
        kwargs["initial"] = self._preferences
        super(UserPreferencesFormset, self).__init__(*args, **kwargs)

    def _construct_form(self, i, **kwargs):
        form = super(UserPreferencesFormset, self)._construct_form(i, **kwargs)
        form._preferences = self._preferences[i]
        return form

    def save(self):
        for form in self.forms:
            form.save()

868

Simon Hanna's avatar
Simon Hanna committed
869
class MemberModeration(forms.Form):
870 871 872 873 874 875 876 877
    """
    Form handling the member's moderation_action.
    """
    moderation_action = forms.ChoiceField(
        widget=forms.Select(),
        label=_('Moderation'),
        error_messages={
            'required': _("Please choose a moderation action.")},
878 879
        required=False,
        choices=[(None, _('List default'))] + list(ACTION_CHOICES),
880 881
        help_text=_(
            'Default action to take when this member posts to the list. '
882
            'List default -- follow the list\'s default member action. '
883 884 885 886 887 888 889 890 891 892 893 894
            'Hold -- This holds the message for approval by the list '
            'moderators. '
            'Reject -- this automatically rejects the message by sending a '
            'bounce notice to the post\'s author. The text of the bounce '
            'notice can be configured by you. '
            'Discard -- this simply discards the message, with no notice '
            'sent to the post\'s author. '
            'Accept -- accepts any postings without any further checks. '
            'Defer -- default processing, run additional checks and accept '
            'the message. '))


895 896 897 898 899 900 901 902 903 904
class ChangeSubscriptionForm(forms.Form):
    email = forms.ChoiceField()

    def __init__(self, user_emails, *args, **kwargs):
        super(ChangeSubscriptionForm, self).__init__(*args, **kwargs)
        self.fields['email'] = forms.ChoiceField(
            label=_('Select Email'),
            required=False,
            widget=forms.Select(),
            choices=((address, address) for address in user_emails))
905 906


907
class MultipleChoiceForm(forms.Form):
908

909
    class MultipleChoiceField(forms.MultipleChoiceField):
910 911 912 913

        def validate(self, value):
            pass

914
    choices = MultipleChoiceField(
915 916 917 918 919
        widget=forms.CheckboxSelectMultiple,
    )

    def clean_choices(self):
        if len(self.cleaned_data['choices']) < 1:
920
            raise forms.ValidationError(_('Make at least one selection'))
921
        return self.cleaned_data['choices']