diff --git a/src/postorius/auth/decorators.py b/src/postorius/auth/decorators.py index 0f12b55..16292e0 100644 --- a/src/postorius/auth/decorators.py +++ b/src/postorius/auth/decorators.py @@ -24,6 +24,7 @@ from postorius.models import (Domain, List, Member, MailmanUser, MailmanApiError, Mailman404Error) + def basic_auth_login(fn): def wrapper(*args, **kwargs): request = args[0] @@ -31,7 +32,8 @@ print 'already logged in' if not request.user.is_authenticated(): if request.META.has_key('HTTP_AUTHORIZATION'): - authmeth, auth = request.META['HTTP_AUTHORIZATION'].split(' ', 1) + authmeth, auth = request.META['HTTP_AUTHORIZATION'].split(' ', + 1) if authmeth.lower() == 'basic': auth = auth.strip().decode('base64') username, password = auth.split(':', 1) diff --git a/src/postorius/context_processors.py b/src/postorius/context_processors.py index ce7bf6c..d7a18f2 100644 --- a/src/postorius/context_processors.py +++ b/src/postorius/context_processors.py @@ -23,7 +23,9 @@ def postorius(request): """Add template variables to context. """ - # Use a template so that the page header/footer is suppressed when requested via AJAX + # Use a template so that the page header/footer is suppressed when + # requested via AJAX + if request.is_ajax(): template_to_extend = "postorius/base_ajax.html" else: diff --git a/src/postorius/doc/conf.py b/src/postorius/doc/conf.py index d308e7c..f49af68 100644 --- a/src/postorius/doc/conf.py +++ b/src/postorius/doc/conf.py @@ -3,7 +3,8 @@ # Postorius documentation build configuration file, created by # sphinx-quickstart on Wed Aug 17 15:43:10 2011. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its +# containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -11,7 +12,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os # add dummy settings environment variable so sphinx can import from Postorius. os.environ['DJANGO_SETTINGS_MODULE'] = 'postorius.doc.settings' @@ -22,17 +24,17 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. +# documentation root, use os.path.abspath to make it absolute, like shown here #sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- - +# -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', +# 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -73,7 +75,8 @@ # directories to ignore when looking for source files. exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. +# The reST default role (used for this markup: `text`) to use for all +# documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. @@ -94,7 +97,7 @@ #modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -174,7 +177,7 @@ htmlhelp_basename = 'postoriusdoc' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' @@ -183,10 +186,10 @@ #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ - ('index', 'postoriusweb.tex', u'Postorius Documentation', - u'Mailman Coders', 'manual'), + ('index', 'postoriusweb.tex', u'Postorius Documentation', + u'Mailman Coders', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -213,7 +216,7 @@ #latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +# -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). @@ -223,7 +226,7 @@ ] -# -- Options for Epub output --------------------------------------------------- +# -- Options for Epub output -------------------------------------------------- # Bibliographic Dublin Core info. epub_title = u'postorius' diff --git a/src/postorius/doc/news.rst b/src/postorius/doc/news.rst index ad70a9e..cd4fddc 100644 --- a/src/postorius/doc/news.rst +++ b/src/postorius/doc/news.rst @@ -49,6 +49,7 @@ * changed static file inclusion. Contributed by Richard Wackerbarth. * added delete domain feature. * url conf refactoring. Contributed by Richard Wackerbarth. +* added user deletion feature. Contributed by Varun Sharma. diff --git a/src/postorius/doc/settings.py b/src/postorius/doc/settings.py index a7e4dd1..4809130 100755 --- a/src/postorius/doc/settings.py +++ b/src/postorius/doc/settings.py @@ -16,5 +16,5 @@ # You should have received a copy of the GNU General Public License along with # Postorius. If not, see . -"""This is just an empty settings file to make Sphinx able to export +"""This is just an empty settings file to make Sphinx able to export the DJANGO_SETTINGS_MODULE env variable before building the docs.""" diff --git a/src/postorius/forms.py b/src/postorius/forms.py index e093870..db98aaa 100644 --- a/src/postorius/forms.py +++ b/src/postorius/forms.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +#-*- coding: utf-8 -*- # Copyright (C) 2012 by the Free Software Foundation, Inc. # # This file is part of Postorius. @@ -20,9 +20,11 @@ from django.core.validators import validate_email from django.utils.translation import gettext as _ from fieldset_forms import FieldsetForm +from django.forms.models import modelformset_factory class DomainNew(FieldsetForm): + """ Form field to add a new domain """ @@ -60,6 +62,7 @@ return web_host class Meta: + """ Class to handle the automatic insertion of fieldsets and divs. @@ -74,6 +77,7 @@ class NewOwnerForm(forms.Form): + """Add a list owner.""" owner_email = forms.EmailField( label=_('Email Address'), @@ -83,6 +87,7 @@ class NewModeratorForm(forms.Form): + """Add a list moderator.""" moderator_email = forms.EmailField( label=_('Email Address'), @@ -92,6 +97,7 @@ class ListNew(FieldsetForm): + """ Form fields to add a new list. Languages are hard coded which should be replaced by a REST lookup of available languages. @@ -130,13 +136,12 @@ error_messages={'required': _("Choose an existing Domain."), 'invalid': "ERROR-todo_forms.py"}) if len(domain_choices) < 2: - self.fields["mail_host"].help_text=_( + self.fields["mail_host"].help_text = _( "Site admin has not created any domains") - #if len(choices) < 2: + # if len(choices) < 2: # help_text=_("No domains available: " + # "The site admin must create new domains " + # "before you will be able to create a list") - def clean_listname(self): try: @@ -146,6 +151,7 @@ return self.cleaned_data['listname'] class Meta: + """ Class to handle the automatic insertion of fieldsets and divs. @@ -162,6 +168,7 @@ class ListSubscribe(FieldsetForm): + """Form fields to join an existing list. """ email = forms.EmailField( @@ -174,6 +181,7 @@ class ListUnsubscribe(FieldsetForm): + """Form fields to leave an existing list. """ email = forms.EmailField( @@ -185,17 +193,18 @@ class ListSettings(FieldsetForm): + """Form fields dealing with the list settings. """ choices = ((True, _('Yes')), (False, _('No'))) - #list_name = forms.CharField( + # list_name = forms.CharField( # label=_('List Name'), # required=False) host_name = forms.CharField( label=_('Domain host name'), required=False) # informational, not configurable - #fqdn_listname = forms.CharField( + # fqdn_listname = forms.CharField( # label=_('Fqdn listname'), # required=False) include_rfc2369_headers = forms.TypedChoiceField( @@ -204,15 +213,30 @@ widget=forms.RadioSelect, required=False, label= _('Include RFC2369 headers'), - 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).')) + 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( choices=choices, widget=forms.RadioSelect, 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."), + 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."), ) archive_policy_choices = ( - ("public", _("Public Archives")), + ("public", _("Public Archives")), ("private", _("Private Archives")), ("never", _("Do not archive this list")), ) @@ -230,17 +254,20 @@ choices=autorespond_choices, widget=forms.RadioSelect, label=_('Autorespond to list owner'), - help_text=('Should Mailman send an auto-response to emails sent to the -owner address?')) + help_text=( + 'Should Mailman send an auto-response to emails sent to the ' + '-owner address?')) autoresponse_owner_text = forms.CharField( label=_('Autoresponse owner text'), widget=forms.Textarea(), required=False, - help_text=('Auto-response text to send to -owner emails.')) + help_text=('Auto-response text to send to -owner emails.')) autorespond_postings = forms.ChoiceField( choices=autorespond_choices, widget=forms.RadioSelect, label=_('Autorespond postings'), - help_text=('Should Mailman send an auto-response to mailing list posters?')) + help_text=( + 'Should Mailman send an auto-response to mailing list posters?')) autoresponse_postings_text = forms.CharField( label=_('Autoresponse postings text'), widget=forms.Textarea(), @@ -250,7 +277,11 @@ choices=autorespond_choices, widget=forms.RadioSelect, label=_('Autorespond requests'), - help_text=('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.')) + help_text=( + '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(), @@ -258,38 +289,49 @@ help_text=('Auto-response text to send to -request emails.')) autoresponse_grace_period = forms.CharField( label=_('Autoresponse grace period'), - help_text=('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).')) + help_text=( + '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).')) # This doesn't make sense as a configurable, so we're leaving it out - #bounces_address = forms.EmailField( + # bounces_address = forms.EmailField( # label=_('Bounces Address'), # required=False) advertised = forms.TypedChoiceField( coerce=lambda x: x == 'True', choices=((True, _('Yes')), (False, _('No'))), widget=forms.RadioSelect, - label=_('Advertise the existance of this list?'), - help_text=('Choose whether to include this list on the list of all lists')) + label=_('Advertise the existence of this list?'), + help_text=( + 'Choose whether to include this list on the list of all lists')) filter_content = forms.TypedChoiceField( coerce=lambda x: x == 'True', choices=((True, _('Yes')), (False, _('No'))), widget=forms.RadioSelect, required=False, label=_('Filter content'), - help_text=('Should Mailman filter the content of list traffic according to the settings below?')) + help_text=( + 'Should Mailman filter the content of list traffic according to ' + 'the settings below?')) collapse_alternatives = forms.TypedChoiceField( coerce=lambda x: x == 'True', choices=((True, _('Yes')), (False, _('No'))), widget=forms.RadioSelect, required=False, label=_('Collapse alternatives'), - help_text=('Should Mailman collapse multipart/alternative to its first part content?')) + help_text=( + 'Should Mailman collapse multipart/alternative to its first part ' + 'content?')) 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'), - help_text=('Should Mailman convert text/html parts to plain text? This conversion happens after MIME attachments have been stripped.')) + help_text=( + 'Should Mailman convert text/html parts to plain text? This ' + 'conversion happens after MIME attachments have been stripped.')) action_choices = ( ("hold", _("Hold for moderator")), ("reject", _("Reject (with notification)")), @@ -303,7 +345,17 @@ 'required': _("Please choose a default member action.")}, required=True, choices=action_choices, - help_text=('Default action to take when a member posts to the list.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 to the list by default. Defer -- Defers any postings to the list by default. ')) + help_text=( + 'Default action to take when a member posts to the list. ' + '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 to the list by default. ' + 'Defer -- Defers any postings to the list by default. ')) default_nonmember_action = forms.ChoiceField( widget=forms.RadioSelect(), label=_('Default action to take when a non-member posts to the' @@ -312,27 +364,38 @@ 'required': _("Please choose a default non-member action.")}, required=True, choices=action_choices, - help_text=('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.')) + help_text=( + '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.')) description = forms.CharField( label=_('Description'), - help_text=('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.'), + help_text=( + '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.'), widget=forms.Textarea()) digest_size_threshold = forms.DecimalField( label=_('Digest size threshold'), help_text=('How big in Kb should a digest be before it gets sent out?') ) # Informational - #digest_last_sent_at = forms.IntegerField( + # digest_last_sent_at = forms.IntegerField( # label=_('Digest last sent at'), # error_messages={ # 'invalid': _('Please provide an integer.')}, # required=False) - first_strip_reply_to = forms.TypedChoiceField( + first_strip_reply_to = forms.TypedChoiceField( coerce=lambda x: x == 'False', choices=((True, _('Yes')), (False, _('No'))), widget=forms.RadioSelect, required=False, - help_text=_('Should any existing Reply-To: header found in the original message be stripped? If so, this will be done regardless of whether an explict Reply-To: header is added by Mailman or not.') + help_text=_( + 'Should any existing Reply-To: header found in the original ' + 'message be stripped? If so, this will be done regardless of ' + 'whether an explict Reply-To: header is added by Mailman or not.') ) generic_nonmember_action = forms.IntegerField( label=_('Generic nonmember action'), @@ -345,24 +408,30 @@ error_messages={'required': _('Please a domain name'), 'invalid': _('Please enter a valid domain name.')}, required=True, - help_text="The \"host_name\" is the preferred name for email to mailman-related addresses on this host, and generally should be the mail host's exchanger address, if any. This setting can be useful for selecting among alternative names of a host that has multiple addresses.") + help_text=( + "The \"host_name\" is the preferred name for email to " + "'mailman-related addresses on this host, and generally should be " + "the mail host's exchanger address, if any. This setting can be " + "useful for selecting among alternative names of a host that " + "has multiple addresses.")) # informational, not editable - #next_digest_number = forms.IntegerField( + # next_digest_number = forms.IntegerField( # label=_('Next digest number'), # error_messages={ # 'invalid': _('Please provide an integer.'), # }, # required=False, #) - #no_reply_address = forms.EmailField( + # no_reply_address = forms.EmailField( # label=_('No reply address'), # required=False, #) posting_pipeline = forms.CharField( label=_('Pipeline'), - help_text=('Type of pipeline you want to use for this mailing list') + help_text=( + 'Type of pipeline you want to use for this mailing list') ) - #post_id = forms.IntegerField( + # post_id = forms.IntegerField( # label=_('Post ID'), # error_messages={ # 'invalid': _('Please provide an integer.'), @@ -371,7 +440,7 @@ #) display_name = forms.CharField( label=_('Display name'), - help_text= ('Display name is the name shown in the web interface.') + help_text=('Display name is the name shown in the web interface.') ) subject_prefix = forms.CharField( label=_('Subject prefix'), @@ -386,14 +455,41 @@ ("no_munging", _("No Munging")), ("point_to_list", _("Reply goes to list")), ("explicit_header", _("Explicit Reply-to header set"))), - help_text=('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. ')) - reply_to_address = forms.CharField( - label= _('Explicit reply-to address'), + help_text=( + '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.')) + 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'), + 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'), ) # informational, not editable - #request_address = forms.EmailField( + # request_address = forms.EmailField( # label=_('Request address'), # required=False) send_welcome_message = forms.TypedChoiceField( @@ -402,10 +498,17 @@ widget=forms.RadioSelect, required=False, label=_('Send welcome message'), - help_text=('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.')) + help_text=( + '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.')) welcome_message_uri = forms.CharField( - label = _('URI for the welcome message'), - help_text = _('If a welcome message is to be sent to subscribers, you can specify a URI that gives the text of this message.'), + label=_('URI for the welcome message'), + help_text=_( + 'If a welcome message is to be sent to subscribers, you can ' + 'specify a URI that gives the text of this message.'), ) # tko - look this up # scheme = forms.CharField( @@ -415,52 +518,74 @@ widget=forms.Textarea(), label=_("Acceptable aliases"), required=False, - help_text=('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.')) + help_text=( + '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.')) admin_immed_notify = forms.BooleanField( widget=forms.RadioSelect(choices=choices), required=False, label=_('Admin immed notify'), - help_text=('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. ')) + help_text=( + '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. ')) admin_notify_mchanges = forms.BooleanField( widget=forms.RadioSelect(choices=choices), required=False, label=_('Notify admin of membership changes'), - help_text=('Should administrator get notices of subscribes and unsubscribes?')) + help_text=( + 'Should administrator get notices of subscribes and unsubscribes?' + )) administrivia = forms.BooleanField( widget=forms.RadioSelect(choices=choices), required=False, label=_('Administrivia'), - help_text=('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.')) + help_text=( + '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.')) anonymous_list = forms.TypedChoiceField( coerce=lambda x: x == 'True', choices=((True, _('Yes')), (False, _('No'))), widget=forms.RadioSelect, required=False, label=_('Anonymous list'), - help_text=('Hide the sender of a message, replacing it with the list address (Removes From, Sender and Reply-To fields)')) + help_text=( + 'Hide the sender of a message, replacing it with the list address ' + '(Removes From, Sender and Reply-To fields)')) # Informational field, not needed. - #created_at = forms.IntegerField( + # created_at = forms.IntegerField( # label=_('Created at'), # widget=forms.HiddenInput(), # required=False) - #join_address = forms.EmailField( + # join_address = forms.EmailField( # label=_('Join address'), # required=False) - #last_post_at = forms.IntegerField( + # last_post_at = forms.IntegerField( # label=_('Last post at'), # required=False) - #leave_address = forms.EmailField( + # leave_address = forms.EmailField( # label=_('Leave address'), # required=False) - #owner_address = forms.EmailField( + # owner_address = forms.EmailField( # label=_('Owner Address'), # required=False) - #posting_address = forms.EmailField( + # posting_address = forms.EmailField( # label=_('Posting Address'), # required=False) - - #Descriptions used in the Settings Overview Page + # Descriptions used in the Settings Overview Page section_descriptions = { "List Identity": _("Basic identity settings for the list"), "Automatic Responses": _("All options for Autoreply"), @@ -476,7 +601,7 @@ def __init__(self, visible_section, visible_option, *args, **kwargs): super(ListSettings, self).__init__(*args, **kwargs) # if settings: - # raise Exception(settings) # debug + # raise Exception(settings) # debug if visible_option: options = [] for option in self.layout: @@ -513,6 +638,7 @@ del self.fields[key] class Meta: + """Class to handle the automatic insertion of fieldsets and divs. To use it: add a list for each wished fieldset. The first item in @@ -522,31 +648,32 @@ # just a really temporary layout to see that it works. -- Anna layout = [ ["List Identity", "display_name", "mail_host", "description", - "advertised", "subject_prefix"], + "advertised", "subject_prefix"], ["Automatic Responses", "autorespond_owner", "autoresponse_owner_text", "autorespond_postings", "autoresponse_postings_text", "autorespond_requests", "autoresponse_request_text", "autoresponse_grace_period", - "send_welcome_message", + "send_welcome_message", "welcome_message_uri", "admin_immed_notify", "admin_notify_mchanges"], ["Alter Messages", "filter_content", "collapse_alternatives", "convert_html_to_plaintext", "anonymous_list", - "include_rfc2369_headers", + "include_rfc2369_headers", "allow_list_posts", "reply_goes_to_list", - "reply_to_address", - "first_strip_reply_to", - "posting_pipeline"], + "reply_to_address", + "first_strip_reply_to", + "posting_pipeline"], ["Digest", "digest_size_threshold"], ["Message Acceptance", "acceptable_aliases", "administrivia", "default_nonmember_action", "default_member_action"], ["Archives", "archive_policy"], - ] + ] class Login(FieldsetForm): + """Form fields to let the user log in. """ user = forms.EmailField( @@ -562,6 +689,7 @@ required=True) class Meta: + """ Class to define the name of the fieldsets and what should be included in each. @@ -570,6 +698,7 @@ class ListMassSubscription(FieldsetForm): + """Form fields to masssubscribe users to a list. """ emails = forms.CharField( @@ -580,6 +709,7 @@ verify_with_email = forms.BooleanField(required=False) class Meta: + """ Class to define the name of the fieldsets and what should be included in each. @@ -587,56 +717,91 @@ layout = [["Mass subscription", "emails"]] -class MembershipSettings(FieldsetForm): - """Form handling the membership settings. +class UserPreferences(FieldsetForm): + + """ + Form handling the user's global, address and subscription based preferences """ choices = ((True, _('Yes')), (False, _('No'))) - acknowledge_posts = forms.BooleanField( - widget=forms.RadioSelect(choices=choices), + + delivery_mode_choices = (("regular", _('Regular')), + ("plaintext_digests", _('Plain Text Digests')), + ("mime_digests", _('Mime Digests')), + ("summary_digests", _('Summary Digests'))) + delivery_status_choices = ( + ("enabled", _('Enabled')), ("by_user", _('Disabled'))) + delivery_status = forms.ChoiceField( + widget=forms.RadioSelect, + choices=delivery_status_choices, required=False, - label=_('Acknowledge posts')) - hide_address = forms.BooleanField( - widget=forms.RadioSelect(choices=choices), + label=_('Delivery status'), + help_text=_( + '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.')) + delivery_mode = forms.ChoiceField( + widget=forms.Select(), + choices=delivery_mode_choices, required=False, - label=_('Hide address')) - receive_list_copy = forms.BooleanField( - widget=forms.RadioSelect(choices=choices), - required=False, - label=_('Receive list copy')) + label=_('Delivery mode'), + help_text=_( + '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.')) receive_own_postings = forms.BooleanField( widget=forms.RadioSelect(choices=choices), required=False, - label=_('Receive own postings')) - delivery_mode = forms.ChoiceField( - widget=forms.Select(), - error_messages={ - 'required': _("Please choose a mode.")}, + label=_('Receive own postings'), + help_text=_( + '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.' + )) + acknowledge_posts = forms.BooleanField( + widget=forms.RadioSelect(choices=choices), required=False, - choices=( - ("", _("Please choose")), - ("delivery_mode", "some mode...")), - label=_('Delivery mode')) - delivery_status = forms.ChoiceField( - widget=forms.Select(), - error_messages={ - 'required': _("Please choose a status.")}, + label=_('Acknowledge posts'), + help_text=_( + 'Receive acknowledgement mail when you send mail to the list?')) + hide_address = forms.BooleanField( + widget=forms.RadioSelect(choices=choices), required=False, - choices=( - ("", _("Please choose")), - ("delivery_status", "some status...")), - label=_('Delivery status')) + label=_('Hide address'), + help_text=_( + '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.')) + receive_list_copy = forms.BooleanField( + widget=forms.RadioSelect(choices=choices), + required=False, + label=_('Receive list copy'), + help_text=_( + '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. ')) class Meta: + """ Class to define the name of the fieldsets and what should be included in each. """ - layout = [["Membership Settings", "acknowledge_posts", "hide_address", + layout = [["User Preferences", "acknowledge_posts", "hide_address", "receive_list_copy", "receive_own_postings", "delivery_mode", "delivery_status"]] class UserNew(FieldsetForm): + """ Form field to add a new user """ @@ -662,7 +827,7 @@ widget=forms.PasswordInput(render_value=False)) def clean(self): - cleaned_data = self.cleaned_data + cleaned_data = self.cleaned_data password = cleaned_data.get("password") password_repeat = cleaned_data.get("password_repeat") if password != password_repeat: @@ -672,8 +837,10 @@ class UserSettings(FieldsetForm): + """Form handling the user settings. """ + def __init__(self, address_choices, *args, **kwargs): """ Initialize the user settings with a field 'address' where @@ -724,6 +891,7 @@ 'invalid': _('Please enter a valid password.')}) class Meta: + """ Class to define the name of the fieldsets and what should be included in each. diff --git a/src/postorius/models.py b/src/postorius/models.py index 3c7ae1d..10a8ba3 100644 --- a/src/postorius/models.py +++ b/src/postorius/models.py @@ -67,7 +67,7 @@ raise MailmanApiError(e) except HTTPError, e: if e.code == 404: - raise Mailman404Error + raise Mailman404Error('Mailman resource could not be found.') else: raise except MailmanConnectionError, e: diff --git a/src/postorius/static/postorius/css/style.css b/src/postorius/static/postorius/css/style.css index 440698e..6423b93 100755 --- a/src/postorius/static/postorius/css/style.css +++ b/src/postorius/static/postorius/css/style.css @@ -245,12 +245,22 @@ } /* Mouseover help */ -span.more_info {border-bottom: thin dotted; background: #ffeedd;} -span.more_info:hover {text-decoration: none; background: #ffffff; z-index: 6; } -span.more_info span {position: absolute; left: -9999px; - margin: 0em; padding: 0.5em; - border-style:solid; border-color:black; border-width:1px; z-index: 6;} -span.more_info:hover span {left: 2%; background: #ffffff;} +span.more_info { + border-bottom: thin dotted; + background: #ffeedd; + font-weight: normal; +} +span.more_info:hover { + text-decoration: none; background: #ffffff; z-index: 6; +} +span.more_info span { + position: absolute; left: -9999px; + margin: 0em; padding: 0.5em; + border-style:solid; border-color:black; border-width:1px; z-index: 6; +} +span.more_info:hover span { + left: 2%; background: #ffffff; +} span.more_info span { position: absolute; left: -9999px; margin: 2em; padding: 0.5em; diff --git a/src/postorius/templates/postorius/base.html b/src/postorius/templates/postorius/base.html index 0888dc4..d2e611c 100644 --- a/src/postorius/templates/postorius/base.html +++ b/src/postorius/templates/postorius/base.html @@ -50,7 +50,7 @@
  • {{ message }}
  • {% endfor %} - {% endif %} + {% endif %} {% block main %}{% endblock main %} diff --git a/src/postorius/templates/postorius/domain_new.html b/src/postorius/templates/postorius/domain_new.html index f2c5089..d28bac0 100644 --- a/src/postorius/templates/postorius/domain_new.html +++ b/src/postorius/templates/postorius/domain_new.html @@ -4,11 +4,53 @@ {% block main %} {% include 'postorius/menu/settings_nav.html' %} -

    {% trans "Add a new Domain" %}

    -
    {% csrf_token %} - {{ form.as_p }} -
    - -
    -
    + +
    {% csrf_token %} + {{ form.non_field_errors }} +
    + Add a new Domain + +
    + {{ form.mail_host.errors }} + +
    + {{ form.mail_host}} +

    Example: domain.org

    +
    +
    + +
    + {{ form.web_host.errors }} + +
    + {{ form.web_host}} +

    Example: www.domain.org

    +
    +
    + +
    + {{ form.description.errors }} + +
    + {{ form.description}} +

    Example: Test lists will live on this domain.

    +
    +
    + +
    + {{ form.contact_address.errors }} + +
    + {{ form.contact_address}} +

    Example: postmaster@domain.org

    +
    +
    + +
    + +
    + +
    +
    + {% endblock main %} diff --git a/src/postorius/templates/postorius/errors/generic.html b/src/postorius/templates/postorius/errors/generic.html index 3716562..86031b9 100644 --- a/src/postorius/templates/postorius/errors/generic.html +++ b/src/postorius/templates/postorius/errors/generic.html @@ -5,5 +5,5 @@ {% block main %}

    {% trans 'Something went wrong' %}

    - {% if error %}
    {{ error }}
    {% endif %} + {% if error %}
    {{ error }}
    {% endif %} {% endblock %} diff --git a/src/postorius/templates/postorius/user_address_preferences.html b/src/postorius/templates/postorius/user_address_preferences.html new file mode 100644 index 0000000..96e4141 --- /dev/null +++ b/src/postorius/templates/postorius/user_address_preferences.html @@ -0,0 +1,100 @@ +{% extends postorius_base_template %} +{% load url from future %} +{% load i18n %} +{% block main %} +{% include 'postorius/menu/user_nav.html' %} + +{% if nolists %} +
    +

    {% trans "No Preferences Available" %}

    +
    +

    {% trans 'You are not yet subscribed to any lists, so you have no Mailman preferences.' %}

    +{% else %} +
    + {% csrf_token %} + {{formset.management_form}} + {{formset.non_form_errors.as_ul}} + + + + + {% for form,address in zipped_data %} + + {% endfor %} + + + + + {% for form,address in zipped_data %} + + {% endfor %} + + + + {% for form,address in zipped_data %} + + {% endfor %} + + + + {% for form,address in zipped_data %} + + {% endfor %} + + + + {% for form,address in zipped_data %} + + {% endfor %} + + + + {% for form,address in zipped_data %} + + {% endfor %} + + + + {% for form,address in zipped_data %} + + {% endfor %} + +
    Preferences {{address}}
    +

    Mail Delivery:

    +

    [More info{{ helperform.delivery_status.help_text }} + ] +

    +
    {{form.delivery_status }}
    +

    Select a Mode of Delivery:

    +

    [More info{{ helperform.delivery_mode.help_text }} + ] +

    +
    {{form.delivery_mode }}
    +

    Receive your own posts to the list?

    +

    [More info{{ helperform.receive_own_postings.help_text }} + ] +

    +
    {{form.receive_own_postings }}
    +

    Receive acknowledgement mails?

    +

    [More info{{ helperform.acknowledge_posts.help_text }} + ] +

    +
    {{form.acknowledge_posts }}
    +

    Conceal yourself from the subscriber list?

    +

    [More info{{ helperform.hide_address.help_text }} + ] +

    +
    {{form.hide_address }}
    +

    Avoid duplicate copies of messages?

    +

    [More info{{ helperform.receive_list_copy.help_text }} + ] +

    +
    {{form.receive_list_copy }}
    +
    +
    +{% endif %} +{% endblock main %} diff --git a/src/postorius/templates/postorius/user_mailmansettings.html b/src/postorius/templates/postorius/user_mailmansettings.html index 6580f79..1f442d6 100644 --- a/src/postorius/templates/postorius/user_mailmansettings.html +++ b/src/postorius/templates/postorius/user_mailmansettings.html @@ -1,116 +1,83 @@ {% extends postorius_base_template %} {% load url from future %} {% load i18n %} - {% block main %} - {% include 'postorius/menu/user_nav.html' %} - {% if nolists %} -
    -

    {% trans "No Preferences Available" %}

    -
    -

    {% trans 'You are not yet subscribed to any lists, so have no Mailman preferences.' %}

    - {% else %} - -
    -

    {% trans "Subscription Settings" %} - {{ user }}

    +{% include 'postorius/menu/user_nav.html' %} + +{% if not mm_user %} +
    +

    {% trans "No Preferences Available" %}

    - -

    Mailman display name: {{ mm_user.display_name}}

    -

    Django email: {{ user.email }}

    -

    Mailman primary email: {{ mm_user.email }}

    -

    Valid email addresses for this account:

    - - -

    WARNING: This page is currently a mockup and -anything below this line may not work

    - -

    Preferences:

    -

    preferences: {{ mm_user.preferences }} -

    user: {{ mm_user }} -

    tko -

    memberships: {{ membership_lists }} - {% for pref in mm_user.preferences %} -

  • {{ pref }}
  • - {% endfor %} - -

    List memberships:

    - {% for list in mm_user.lists %} -
  • {{ list.name }}
  • - {% endfor %} -

    Sample output: not real

    - - -

    Prefs form

    - {{ settingsform.asp }} - -

    List Preferences Overview

    -

    Sample output: not real

    - -
    {% csrf_token %} - - - - - - - - - - - - - - - - - - {% for preference in mm_user.preferences %} - {% endfor %} - - - - - - - - - - - -
    {% trans "Setting" %}{% trans "Description" %}{% trans "Set all" %}list1@example.comkumquat@example.comstarfruit@example.com
    {% trans "Mail delivery" %} ({{ preference }})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."
    {% trans "Per-Email subscriptions" %}
    anna@example.com
    +

    {% trans 'You are not yet subscribed to any lists, so you have no Mailman preferences.' %}

    +{% else %} + + {% csrf_token %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Preferences Global
    +

    Mail Delivery:

    +

    + [More info{{ settingsform.delivery_status.help_text }} + ]

    +
    {{settingsform.delivery_status }}
    +

    Select a Mode of Delivery:

    +

    + [More info{{ settingsform.delivery_mode.help_text }} + ]

    +
    {{settingsform.delivery_mode }}
    +

    Receive your own posts to the list?

    +

    + [More info{{ settingsform.receive_own_postings.help_text }} + ]

    +
    {{settingsform.receive_own_postings }}
    +

    Receive acknowledgement mails?

    +

    + [More info{{ settingsform.acknowledge_posts.help_text }} + ]

    +
    {{settingsform.acknowledge_posts }}
    +

    Conceal yourself from the subscriber list?

    +

    + [More info{{ settingsform.hide_address.help_text }} + ]

    +
    {{settingsform.hide_address }}
    +

    Avoid duplicate copies of messages?

    +

    [More info{{ settingsform.receive_list_copy.help_text }} + ]

    +
    {{settingsform.receive_list_copy }}
    +
    - {% endif %} +{% endif %} {% endblock main %} -~ diff --git a/src/postorius/templates/postorius/user_subscription_preferences.html b/src/postorius/templates/postorius/user_subscription_preferences.html new file mode 100644 index 0000000..74678db --- /dev/null +++ b/src/postorius/templates/postorius/user_subscription_preferences.html @@ -0,0 +1,50 @@ +{% extends postorius_base_template %} +{% load url from future %} +{% load i18n %} +{% block main %} +{% include 'postorius/menu/user_nav.html' %} + +{% if nolists %} +
    +

    {% trans "No Preferences Available" %}

    +
    +

    {% trans 'You are not yet subscribed to any lists, so you have no Mailman preferences.' %}

    +{% else %} +
    + {% csrf_token %} + {{formset.management_form}} + {{formset.non_form_errors.as_ul}} + + {% for form,subscription in zipped_data %} + {% if forloop.first %} + + + + {% for field in form.visible_fields %} + + {% endfor %} + + + {% endif %} + {% endfor %} + + {% for form,subscription in zipped_data %} + + + + + + + + + {% endfor %} +
    Subscription {{field.label}}

    [More info{{ field.help_text }} + ]

    {{subscription.list_id}}{{form.delivery_status}}{{form.delivery_mode}}{{form.receive_own_postings}}{{form.acknowledge_posts}}{{form.hide_address}}{{form.receive_list_copy}}
    +
    +
    +{% endif %} +{% endblock main %} diff --git a/src/postorius/templates/postorius/user_subscriptions.html b/src/postorius/templates/postorius/user_subscriptions.html index 5a52e74..6b4d8aa 100644 --- a/src/postorius/templates/postorius/user_subscriptions.html +++ b/src/postorius/templates/postorius/user_subscriptions.html @@ -20,10 +20,10 @@ {% for subscription in memberships %} - {{ subscription.fqdn_listname }} + {{ subscription.mlist }} {{ subscription.address }} {{ subscription.role }} - {{ subscription.delivery_mode }} + {{ subscription.preferences.delivery_mode }} {% endfor %} diff --git a/src/postorius/templates/postorius/users/index.html b/src/postorius/templates/postorius/users/index.html index 54979ce..d9c2ec4 100644 --- a/src/postorius/templates/postorius/users/index.html +++ b/src/postorius/templates/postorius/users/index.html @@ -16,6 +16,7 @@ {% trans 'Email' %} {% trans 'Display name' %} + @@ -29,6 +30,9 @@ {{ mm_user.display_name }} + + Delete + diff --git a/src/postorius/templates/postorius/users/new.html b/src/postorius/templates/postorius/users/new.html index c41e2ad..69ab5d6 100644 --- a/src/postorius/templates/postorius/users/new.html +++ b/src/postorius/templates/postorius/users/new.html @@ -1,4 +1,4 @@ -{% extends extend_template %} +{% extends postorius_base_template %} {% load url from future %} {% load i18n %} {% load nav_helpers %} @@ -7,11 +7,44 @@ {% if user.is_superuser %} {% users_nav 'user_new' 'Create User' %} {% endif %} + +
    {% csrf_token %} + {{ form.non_field_errors }} +
    + Create a New User + +
    + {{ form.display_name.errors }} + +
    {{ form.display_name}}
    +
    - {% csrf_token %} - {{ form.as_p }} -
    - -
    - +
    + {{ form.email.errors }} + +
    {{ form.email }}
    +
    + +
    + {{ form.password.errors }} + +
    {{ form.password }}
    +
    + +
    + {{ form.password_repeat.errors }} + +
    + {{ form.password_repeat }} +

    This must match the password typed above so we can be sure we've got the right password.

    +
    +
    + +
    + +
    + +
    + + {% endblock main %} diff --git a/src/postorius/templates/postorius/users/user_confirm_delete.html b/src/postorius/templates/postorius/users/user_confirm_delete.html new file mode 100644 index 0000000..28cec57 --- /dev/null +++ b/src/postorius/templates/postorius/users/user_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "postorius/base.html" %} +{% load url from future %} +{% load i18n %} +{% load nav_helpers %} + +{% block main %} +

    {% trans 'Confirm Deletion' %}

    +

    {% trans "Are you sure you want to permanently delete this user ?" %}

    +
    {% csrf_token %} + + {% trans "Cancel" %} +
    +{% endblock main %} diff --git a/src/postorius/tests/test_auth_decorators.py b/src/postorius/tests/test_auth_decorators.py index 2017819..0ebf9e6 100644 --- a/src/postorius/tests/test_auth_decorators.py +++ b/src/postorius/tests/test_auth_decorators.py @@ -138,7 +138,8 @@ request.user = User.objects.create_superuser('su2', 'su@sodo.org', 'pwd') return_value = dummy_function_mod_req(request, - fqdn_listname='foolist@example.org') + fqdn_listname= + 'foolist@example.org') self.assertEqual(return_value, True) @patch.object(Client, 'get_list') @@ -167,7 +168,8 @@ request.user = User.objects.create_user('les cl3', 'les@primus.org', 'pwd') return_value = dummy_function_mod_req(request, - fqdn_listname='foolist@example.org') + fqdn_listname= + 'foolist@example.org') self.assertEqual(return_value, True) @patch.object(Client, 'get_list') @@ -182,5 +184,6 @@ request.user = User.objects.create_user('les cl4', 'les@primus.org', 'pwd') return_value = dummy_function_mod_req(request, - fqdn_listname='foolist@example.org') + fqdn_listname= + 'foolist@example.org') self.assertEqual(return_value, True) diff --git a/src/postorius/urls.py b/src/postorius/urls.py index c5404f0..b365ccf 100644 --- a/src/postorius/urls.py +++ b/src/postorius/urls.py @@ -25,38 +25,52 @@ per_list_urlpatterns = patterns('postorius.views', - url(r'^members/(?P\d+)/$', - ListMembersView.as_view(), name='list_members_paged'), - url(r'^members/$', - ListMembersView.as_view(), name='list_members'), - url(r'^metrics$', - ListMetricsView.as_view(), name='list_metrics'), - url(r'^$', - ListSummaryView.as_view(), name='list_summary'), - url(r'^subscribe$', - ListSubsribeView.as_view(), name='list_subscribe'), - url(r'^unsubscribe/(?P[^/]+)$', - ListUnsubscribeView.as_view(), name='list_unsubscribe'), - url(r'^subscriptions$', - 'list_subscriptions', name='list_subscriptions'), - url(r'^mass_subscribe/$', - ListMassSubsribeView.as_view(), name='mass_subscribe'), - url(r'^delete$', - 'list_delete', name='list_delete'), - url(r'^held_messages/(?P[^/]+)/' - 'accept$', 'accept_held_message', name='accept_held_message'), - url(r'^held_messages/(?P[^/]+)/' - 'discard$', 'discard_held_message', name='discard_held_message'), - url(r'^held_messages/(?P[^/]+)/' - 'defer$', 'defer_held_message', name='defer_held_message'), - url(r'^held_messages/(?P[^/]+)/' - 'reject$', 'reject_held_message', name='reject_held_message'), - url(r'^held_messages$', - 'list_held_messages', name='list_held_messages'), - url(r'^settings/(?P[^/]+)?' - '(?:/(?P.*))?$', 'list_settings', - name='list_settings'), -) + url(r'^members/(?P\d+)/$', + ListMembersView.as_view( + ), name='list_members_paged'), + url(r'^members/$', + ListMembersView.as_view( + ), name='list_members'), + url(r'^metrics$', + ListMetricsView.as_view( + ), name='list_metrics'), + url(r'^$', + ListSummaryView.as_view( + ), name='list_summary'), + url(r'^subscribe$', + ListSubsribeView.as_view( + ), name='list_subscribe'), + url(r'^unsubscribe/(?P[^/]+)$', + ListUnsubscribeView.as_view( + ), name='list_unsubscribe'), + url(r'^subscriptions$', + 'list_subscriptions', + name='list_subscriptions'), + url(r'^mass_subscribe/$', + ListMassSubsribeView.as_view( + ), name='mass_subscribe'), + url(r'^delete$', + 'list_delete', name='list_delete'), + url(r'^held_messages/(?P[^/]+)/' + 'accept$', 'accept_held_message', + name='accept_held_message'), + url(r'^held_messages/(?P[^/]+)/' + 'discard$', 'discard_held_message', + name='discard_held_message'), + url(r'^held_messages/(?P[^/]+)/' + 'defer$', 'defer_held_message', + name='defer_held_message'), + url(r'^held_messages/(?P[^/]+)/' + 'reject$', 'reject_held_message', + name='reject_held_message'), + url(r'^held_messages$', + 'list_held_messages', + name='list_held_messages'), + url(r'^settings/(?P[^/]+)?' + '(?:/(?P.*))?$', + 'list_settings', + name='list_settings'), + ) urlpatterns = patterns( 'postorius.views', @@ -68,8 +82,13 @@ url(r'^tasks/$', 'user_tasks', name='user_tasks'), url(r'^accounts/subscriptions/$', UserSubscriptionsView.as_view(), name='user_subscriptions'), + url(r'^accounts/per-address-preferences/$', + UserAddressPreferencesView.as_view(), name='user_address_preferences'), + url(r'^accounts/per-subscription-preferences/$', + UserSubscriptionPreferencesView.as_view( + ), name='user_subscription_preferences'), url(r'^accounts/mailmansettings/$', - 'user_mailmansettings', + UserMailmanSettingsView.as_view(), name='user_mailmansettings'), # /settings/ url(r'^settings/$', 'site_settings', name="site_settings"), @@ -80,15 +99,15 @@ # /lists/ url(r'^lists/$', 'list_index', name='list_index'), url(r'^lists/new/$', 'list_new', name='list_new'), - url(r'^user_settings/$', 'user_settings', kwargs={"tab": "user"}, - name='user_settings'), - url(r'^more_info/(?P[^/]+)/(?P[^/]+)$', 'more_info_tab', name='more_info_tab'), - url(r'^lists/(?P[^/]+)/', include(per_list_urlpatterns)), + url(r'^more_info/(?P[^/]+)/(?P[^/]+)$', + 'more_info_tab', name='more_info_tab'), + url(r'^lists/(?P[^/]+)/', include(per_list_urlpatterns)), # /users/ url(r'^users/(?P\d+)/$', 'user_index', name='user_index_paged'), url(r'^users/$', 'user_index', name='user_index'), url(r'^users/new/$', 'user_new', name='user_new'), url(r'^users/(?P[^/]+)/$', UserSummaryView.as_view(), name='user_summary'), + url(r'^users/(?P\d+)/delete$', 'user_delete', name='user_delete'), url(r'^api/lists/$', 'api_list_index', name='api_list_index'), ) diff --git a/src/postorius/utils.py b/src/postorius/utils.py index a423e58..c258975 100644 --- a/src/postorius/utils.py +++ b/src/postorius/utils.py @@ -46,5 +46,6 @@ """ return render_to_response( 'postorius/errors/generic.html', - {'error': "Mailman REST API not available. Please start Mailman core."}, + {'error': "Mailman REST API not available. " + "Please start Mailman core."}, context_instance=RequestContext(request)) diff --git a/src/postorius/views/generic.py b/src/postorius/views/generic.py index 4feda1d..fb24f90 100644 --- a/src/postorius/views/generic.py +++ b/src/postorius/views/generic.py @@ -29,15 +29,16 @@ class MailmanClientMixin(object): + """Adds a mailmanclient.Client instance.""" - + def client(self): - if getattr(self._client, '_client', None) is None: + if getattr(self, '_client', None) is None: self._client = utils.get_client() return self._client - class MailingListView(TemplateView, MailmanClientMixin): + """A generic view for everything based on a mailman.client list object. @@ -80,6 +81,7 @@ class MailmanUserView(TemplateView, MailmanClientMixin): + """A generic view for everything based on a mailman.client user object. @@ -101,7 +103,7 @@ user_obj.display_name = '' user_obj.first_address = self._get_first_address(user_obj) return user_obj - + def _get_list(self, list_id): if getattr(self, 'lists', None) is None: self.lists = {} @@ -112,19 +114,14 @@ def _get_memberships(self): memberships = [] if (self.mm_user): - for a in self.mm_user.addresses: - members = self.client()._connection.call('members/find', - {'subscriber': a}) - try: - for m in members[1]['entries']: - mlist = self._get_list(m['list_id']) - memberships.append( - dict(fqdn_listname=mlist.fqdn_listname, - role=m['role'], - delivery_mode=m['delivery_mode'], - address=a)) - except KeyError: - pass + for m in self.mm_user.subscriptions: + mlist = m.list_id + memberships.append( + dict( + mlist=mlist, + role=m.role, + preferences=m.preferences, + address=m.address)) return memberships def dispatch(self, request, *args, **kwargs): @@ -139,7 +136,8 @@ self.mm_user = self._get_user(user_id) except MailmanApiError: return utils.render_api_error(request) - + + # set the template if 'template' in kwargs: self.template = kwargs['template'] diff --git a/src/postorius/views/list.py b/src/postorius/views/list.py index 141d74e..5929ca7 100644 --- a/src/postorius/views/list.py +++ b/src/postorius/views/list.py @@ -40,6 +40,7 @@ class ListMembersView(MailingListView): + """Display all members of a given list. """ @@ -98,6 +99,7 @@ class ListMetricsView(MailingListView): + """Shows common list metrics. """ @@ -109,6 +111,7 @@ class ListSummaryView(MailingListView): + """Shows common list metrics. """ @@ -122,6 +125,7 @@ class ListSubsribeView(MailingListView): + """Subscribe a mailing list.""" @method_decorator(login_required) @@ -145,6 +149,7 @@ class ListUnsubscribeView(MailingListView): + """Unsubscribe from a mailing list.""" @method_decorator(login_required) @@ -163,6 +168,7 @@ class ListMassSubsribeView(MailingListView): + """Mass subscription.""" @method_decorator(list_owner_required) @@ -227,10 +233,10 @@ choosable_domains = _get_choosable_domains(request) form = ListNew(choosable_domains, request.POST) if form.is_valid(): - #grab domain + # grab domain domain = Domain.objects.get_or_404( mail_host=form.cleaned_data['mail_host']) - #creating the list + # creating the list try: mailing_list = domain.create_list( form.cleaned_data['listname']) @@ -243,9 +249,8 @@ messages.success(request, _("List created")) return redirect("list_summary", fqdn_listname=mailing_list.fqdn_listname) - #TODO catch correct Error class: + # TODO catch correct Error class: except HTTPError, e: - messages.error(request, e) return render_to_response( 'postorius/errors/generic.html', {'error': e}, context_instance=RequestContext(request)) @@ -504,7 +509,7 @@ the_list = List.objects.get_or_404(fqdn_listname=fqdn_listname) except MailmanApiError: return utils.render_api_error(request) - #collect all Form sections for the links: + # collect all Form sections for the links: temp = ListSettings('', '') for section in temp.layout: try: @@ -524,12 +529,13 @@ list_settings.save() message = _("The list settings have been updated.") else: - message = _("Validation Error - The list settings have not been updated.") + message = _( + "Validation Error - The list settings have not been updated.") else: - #Provide a form with existing values - #create form and process layout into form.layout + # Provide a form with existing values + # create form and process layout into form.layout form = ListSettings(visible_section, visible_option, data=None) - #create a Dict of all settings which are used in the form + # create a Dict of all settings which are used in the form used_settings = {} for section in form.layout: for option in section[1:]: @@ -551,29 +557,6 @@ @login_required -def user_mailmansettings(request): - try: - the_user = MailmanUser.objects.get(address=request.user.email) - except MailmanApiError: - return utils.render_api_error(request) - except Mailman404Error: - # If we have no settings, return a "blank" settings page telling the - # user that they have no settings because they are not subscribed - # to any lists (see mailmansettings template) - return render_to_response( - 'postorius/user_mailmansettings.html', - {'nolists': 'true'}, - context_instance=RequestContext(request), - ) - - settingsform = MembershipSettings() - return render_to_response('postorius/user_mailmansettings.html', - {'mm_user': the_user, - 'settingsform': settingsform}, - context_instance=RequestContext(request)) - - -@login_required def membership_settings(request): """Display a list of all memberships. """ diff --git a/src/postorius/views/settings.py b/src/postorius/views/settings.py index 305a739..ded9730 100644 --- a/src/postorius/views/settings.py +++ b/src/postorius/views/settings.py @@ -90,6 +90,7 @@ {'form': form, 'message': message}, context_instance=RequestContext(request)) + def domain_delete(request, domain): """Deletes a domain but asks for confirmation first. """ diff --git a/src/postorius/views/user.py b/src/postorius/views/user.py index 9a6b686..aee9d9f 100644 --- a/src/postorius/views/user.py +++ b/src/postorius/views/user.py @@ -24,6 +24,7 @@ from django.conf import settings +from django.forms.formsets import formset_factory from django.contrib import messages from django.contrib.auth import logout, authenticate, login from django.contrib.auth.decorators import (login_required, @@ -48,122 +49,158 @@ from postorius.views.generic import MailingListView, MailmanUserView -@login_required -def user_settings(request, tab="membership", - template='postorius/user_settings.html', - fqdn_listname=None): - """ - Change the user or the membership settings. - The user must be logged in to be allowed to change any settings. - TODO: * add CSS to display tabs ?? - * add missing functionality in REST server and client and - change to the correct calls here - """ - member = request.user.username - message = '' - form = None - the_list = None - membership_lists = [] +class UserMailmanSettingsView(MailmanUserView): + """The logged-in user's global Mailman Preferences.""" - try: - c = utils.get_client() - if tab == "membership": - if fqdn_listname: - the_list = List.objects.get(fqdn_listname=fqdn_listname) - user_object = the_list.get_member(member) + @method_decorator(login_required) + def post(self, request): + try: + mm_user = MailmanUser.objects.get(address=request.user.email) + global_preferences_form = UserPreferences(request.POST) + if global_preferences_form.is_valid(): + preferences = mm_user.preferences + for key in global_preferences_form.fields.keys(): + preferences[ + key] = global_preferences_form.cleaned_data[key] + preferences.save() + messages.success( + request, 'Your preferences have been updated.') else: - message = ("") - for mlist in List.objects.all(): - try: - mlist.get_member(member) - membership_lists.append(mlist) - except: - pass - else: - # address_choices for the 'address' field must be a list of - # tuples of length 2 - raise Exception("") - address_choices = [[addr, addr] for addr in user_object.address] - except AttributeError, e: - return render_to_response( - 'postorius/errors/generic.html', - {'error': str(e) + "Mailman REST API not available. Please start Mailman core."}, - context_instance=RequestContext(request)) - except ValueError, e: - return render_to_response('postorius/errors/generic.html', - {'error': e}, + messages.error(request, 'Something went wrong.') + except MailmanApiError: + return utils.render_api_error(request) + except Mailman404Error as e: + messages.error(request, e.msg) + return redirect("user_mailmansettings") + + @method_decorator(login_required) + def get(self, request): + try: + mm_user = MailmanUser.objects.get(address=request.user.email) + settingsform = UserPreferences(initial=mm_user.preferences) + except MailmanApiError: + return utils.render_api_error(request) + except Mailman404Error: + mm_user = None + settingsform = None + return render_to_response('postorius/user_mailmansettings.html', + {'mm_user': mm_user, + 'settingsform': settingsform}, context_instance=RequestContext(request)) - except HTTPError, e: + + +class UserAddressPreferencesView(MailmanUserView): + """The logged-in user's address-based Mailman Preferences.""" + + @method_decorator(login_required) + def post(self, request): + try: + mm_user = MailmanUser.objects.get(address=request.user.email) + formset_class = formset_factory(UserPreferences) + formset = formset_class(request.POST) + zipped_data = zip(formset.forms, mm_user.addresses) + if formset.is_valid(): + for form, address in zipped_data: + preferences = address.preferences + for key in form.fields.keys(): + preferences[ + key] = form.cleaned_data[key] + preferences.save() + messages.success( + request, 'Your preferences have been updated.') + else: + messages.error(request, 'Something went wrong.') + except MailmanApiError: + return utils.render_api_error(request) + except HTTPError, e: + messages.error(request, e.msg) + return redirect("user_address_preferences") + + @method_decorator(login_required) + def get(self, request): + try: + helperform=UserPreferences() + mm_user = MailmanUser.objects.get(address=request.user.email) + addresses = mm_user.addresses + i = 0 + for address in addresses: + i = i + 1 + AFormset = formset_factory(UserPreferences, extra=i) + formset = AFormset() + zipped_data = zip(formset.forms, addresses) + for form, address in zipped_data: + form.initial = address.preferences + except MailmanApiError: + return utils.render_api_error(request) + except Mailman404Error: + return render_to_response( + 'postorius/user_address_preferences.html', + {'nolists': 'true'}, + context_instance=RequestContext(request)) + return render_to_response('postorius/user_address_preferences.html', + {'mm_user': mm_user, + 'addresses': addresses, + 'helperform':helperform, + 'formset': formset, + 'zipped_data': zipped_data}, + context_instance=RequestContext(request)) + + +class UserSubscriptionPreferencesView(MailmanUserView): + """The logged-in user's subscription-based Mailman Preferences.""" + + @method_decorator(login_required) + def post(self, request): + try: + mm_user = MailmanUser.objects.get(address=request.user.email) + formset_class = formset_factory(UserPreferences) + formset = formset_class(request.POST) + zipped_data = zip(formset.forms, mm_user.subscriptions) + if formset.is_valid(): + for form, subscription in zipped_data: + preferences = subscription.preferences + for key in form.fields.keys(): + preferences[key] = form.cleaned_data[key] + preferences.save() + messages.success( + request, 'Your preferences have been updated.') + else: + messages.error(request, 'Something went wrong.') + except MailmanApiError: + return utils.render_api_error(request) + except HTTPError, e: + messages.error(request, e.msg) + return redirect("user_subscription_preferences") + + @method_decorator(login_required) + def get(self, request): + try: + mm_user = MailmanUser.objects.get(address=request.user.email) + subscriptions = mm_user.subscriptions + i = len(subscriptions) + Mformset = formset_factory(UserPreferences, extra=i) + formset = Mformset() + zipped_data = zip(formset.forms, subscriptions) + for form, subscription in zipped_data: + form.initial = subscription.preferences + except MailmanApiError: + return utils.render_api_error(request) + except Mailman404Error: + return render_to_response( + 'postorius/user_subscription_preferences.html', + {'nolists': 'true'}, + context_instance=RequestContext(request)) return render_to_response( - 'postorius/errors/generic.html', - {'error': _("List ") + fqdn_listname + _(" does not exist")}, + 'postorius/user_subscription_preferences.html', + {'mm_user': mm_user, + 'subscriptions': subscriptions, + 'zipped_data': zipped_data, + 'formset': formset}, context_instance=RequestContext(request)) - #----------------------------------------------------------------- - if request.method == 'POST': - # The form enables both user and member settings. As a result - # we must find out which was the case. - raise Exception("Please fix bug prior submitting the form") - if tab == "membership": - form = MembershipSettings(request.POST) - if form.is_valid(): - member_object = c.get_member(member, request.GET["list"]) - member_object.update(request.POST) - message = "The membership settings have been updated." - else: - # the post request came from the user tab - # the 'address' field need choices as a tuple of length 2 - addr_choices = [[request.POST["address"], request.POST["address"]]] - form = UserSettings(addr_choices, request.POST) - if form.is_valid(): - user_object.update(request.POST) - # to get the full list of addresses we need to - # reinstantiate the form with all the addresses - # TODO: should return the correct settings from the DB, - # not just the address_choices (add mock data to _User - # class and make the call with 'user_object.info') - form = UserSettings(address_choices) - message = "The user settings have been updated." - - else: - if tab == "membership" and fqdn_listname: - if fqdn_listname: - # TODO : fix LP:821069 in mailman.client - the_list = List.objects.get(fqdn_listname=fqdn_listname) - member_object = the_list.get_member(member) - # TODO: add delivery_mode and deliver_status from a - # list of tuples at one point, currently we hard code - # them in forms.py - # instantiate the form with the correct member info - """ - acknowledge_posts - hide_address - receive_list_copy - receive_own_postings - delivery_mode - delivery_status - """ - data = {} - form = MembershipSettings(data) - elif tab == "user": - # TODO: should return the correct settings from the DB, - # not just the address_choices (add mock data to _User - # class and make the call with 'user_object._info') The 'language' - # field must also be added as a list of tuples with correct - # values (is currently hard coded in forms.py). - data = {} # Todo https://bugs.launchpad.net/mailman/+bug/821438 - form = UserSettings(data) - - return render_to_response(template, - {'form': form, - 'tab': tab, - 'list': the_list, - 'membership_lists': membership_lists, - 'message': message, - 'member': member}, - context_instance=RequestContext(request)) class UserSummaryView(MailmanUserView): + """Shows a summary of a user. """ @@ -179,6 +216,7 @@ class UserSubscriptionsView(MailmanUserView): + """Shows the subscriptions of a user. """ @@ -259,9 +297,9 @@ def user_profile(request, user_email=None): if not request.user.is_authenticated(): return redirect('user_login') - #try: + # try: # the_user = User.objects.get(email=user_email) - #except MailmanApiError: + # except MailmanApiError: # return utils.render_api_error(request) return render_to_response('postorius/user_profile.html', # {'mm_user': the_user}, @@ -273,20 +311,50 @@ return render_to_response('postorius/user_tasks.html', context_instance=RequestContext(request)) + @login_required def more_info_tab(request, formid=None, helpid=None, template='postorius/more_info_display.html'): """Displays more_info in new tab. """ - + if(formid == 'list_settings'): - form = ListSettings(visible_section='List Identity', visible_option='None', data=request.POST) - + form = ListSettings( + visible_section='List Identity', visible_option='None', data=request.POST) + for field in form: if field.name == helpid: help_text = field.help_text - + return render_to_response(template, - {'help_text':help_text, - 'helpid':helpid}, + {'help_text': help_text, + 'helpid': helpid}, context_instance=RequestContext(request)) - + + +@user_passes_test(lambda u: u.is_superuser) +def user_delete(request, user_id, + template='postorius/users/user_confirm_delete.html'): + """ Deletes a user upon confirmation. + """ + try: + mm_user = MailmanUser.objects.get_or_404(address=user_id) + email_id = mm_user.addresses[0] + except MailmanApiError: + return utils.render_api_error(request) + except IndexError: + email_id = '' + if request.method == 'POST': + try: + mm_user.delete() + except MailmanApiError: + return utils.render_api_error(request) + except HTTPError as e: + messages.error(request, _('The user could not be deleted:' + ' %s' % e.msg)) + return redirect("user_index") + messages.success(request, + _('The user %s has been deleted.' % email_id)) + return redirect("user_index") + return render_to_response(template, + {'user_id': user_id, 'email_id': email_id}, + context_instance=RequestContext(request))