diff --git a/.coveragerc b/.coveragerc index 75a24c2..671ccc9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,7 +4,6 @@ src/postorius/tests/*.py src/postorius/tests/*/*.py src/postorius/doc/*.py - src/postorius/south_migrations/*.py src/postorius/migrations/*.py [html] diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e2eb17f..2dee5a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,15 +2,15 @@ django-1.8: script: - - tox -e py27-django18 + - tox -e py27-django18-head-coverage django-1.9: script: - - tox -e py27-django19 + - tox -e py27-django19-head-coverage django-1.10: script: - - tox -e py27-django110 + - tox -e py27-django110-head-coverage pep8: script: @@ -18,5 +18,5 @@ django-latest: script: - - tox -e py27-django-latest + - tox -e py27-django-latest-head allow_failure: true diff --git a/README.rst b/README.rst index 85afdad..e21904a 100644 --- a/README.rst +++ b/README.rst @@ -35,8 +35,7 @@ ============ Postorius requires Python 2.7 or newer and mailmanclient, -the official Python bindings for GNU Mailman, it also requires -django-browserid. +the official Python bindings for GNU Mailman. The minimum Django version is 1.8. Postorius needs a running version of GNU Mailman version 3. diff --git a/example_project/.gitignore b/example_project/.gitignore index 214d9d9..1392c1d 100644 --- a/example_project/.gitignore +++ b/example_project/.gitignore @@ -1,3 +1,5 @@ postorius.db static venv +settings_local.py +logs/*.log diff --git a/example_project/logs/.keep b/example_project/logs/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/example_project/logs/.keep diff --git a/example_project/settings.py b/example_project/settings.py index 99aacd1..56f2670 100644 --- a/example_project/settings.py +++ b/example_project/settings.py @@ -39,12 +39,14 @@ SECRET_KEY = '$!-7^wl#wiifjbh)5@f7ji%x!vp7s1vzbvwt26hxv$idixq0u0' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False ADMINS = ( #('Admin', 'webmaster@example.com'), ) +SITE_ID = 1 + ALLOWED_HOSTS = [] # Mailman API credentials @@ -60,12 +62,26 @@ 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', + 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'postorius', - 'django_browserid', + 'django_mailman3', + 'django_gravatar', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.openid', + 'django_mailman3.lib.auth.fedora', + 'allauth.socialaccount.providers.github', + 'allauth.socialaccount.providers.gitlab', + 'allauth.socialaccount.providers.google', + #'allauth.socialaccount.providers.facebook', + 'allauth.socialaccount.providers.twitter', + 'allauth.socialaccount.providers.stackexchange', ) + MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -83,6 +99,7 @@ # is the only app you want to serve. ROOT_URLCONF = 'urls' + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -99,6 +116,7 @@ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'django_mailman3.context_processors.common', 'postorius.context_processors.postorius', ], }, @@ -164,18 +182,16 @@ # Example: "http://example.com/static/", "http://static.example.com/" STATIC_URL = '/static/' -LOGIN_URL = 'user_login' +LOGIN_URL = 'account_login' LOGIN_REDIRECT_URL = 'list_index' -LOGOUT_URL = 'user_logout' +LOGOUT_URL = 'account_logout' -# Use the email username as identifier, but truncate it because -# the User.username field is only 30 chars long. -def username(email): - return email.rsplit('@', 1)[0][:30] -BROWSERID_USERNAME_ALGO = username - +# From Address for emails sent to users +DEFAULT_FROM_EMAIL = 'postorius@localhost.local' +# From Address for emails sent to admins +SERVER_EMAIL = 'root@localhost.local' # Compatibility with Bootstrap 3 from django.contrib.messages import constants as messages MESSAGE_TAGS = { @@ -184,37 +200,93 @@ AUTHENTICATION_BACKENDS = ( - 'django_browserid.auth.BrowserIDBackend', 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', ) +# Django Allauth +ACCOUNT_AUTHENTICATION_METHOD = "username_email" +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_VERIFICATION = "mandatory" +ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" +ACCOUNT_UNIQUE_EMAIL = True -# From Address for emails sent to users -DEFAULT_FROM_EMAIL = 'postorius@localhost.local' -# From Address for emails sent to admins -SERVER_EMAIL = 'root@localhost.local' +SOCIALACCOUNT_PROVIDERS = { + 'openid': { + 'SERVERS': [ + dict(id='yahoo', + name='Yahoo', + openid_url='http://me.yahoo.com'), + ], + }, + 'google': { + 'SCOPE': ['profile', 'email'], + 'AUTH_PARAMS': {'access_type': 'online'}, + }, + 'facebook': { + 'METHOD': 'oauth2', + 'SCOPE': ['email'], + 'FIELDS': [ + 'email', + 'name', + 'first_name', + 'last_name', + 'locale', + 'timezone', + ], + 'VERSION': 'v2.4', + }, +} + + # These can be set to override the defaults but are not mandatory: # EMAIL_CONFIRMATION_TEMPLATE = 'postorius/address_confirmation_message.txt' # EMAIL_CONFIRMATION_SUBJECT = 'Confirmation needed' -# You can enable logging by uncommenting the following lines -# LOGGING = { -# 'version': 1, -# 'disable_existing_loggers': False, -# 'handlers': { -# 'console': { -# 'class': 'logging.StreamHandler' -# }, -# }, -# 'loggers': { -# 'django': { -# 'handlers': ['console'], -# 'level': 'INFO', -# }, -# 'django_browserid': { -# 'handlers': ['console'], -# 'level': 'DEBUG', -# }, -# }, -# } + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'simple', + }, + 'file':{ + 'level': 'INFO', + #'class': 'logging.handlers.RotatingFileHandler', + 'class': 'logging.handlers.WatchedFileHandler', + 'filename': os.path.join(BASE_DIR, 'logs', 'postorius.log'), + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console', 'file'], + 'level': 'INFO', + }, + 'django.request': { + 'handlers': ['console', 'file'], + 'level': 'ERROR', + }, + 'postorius': { + 'handlers': ['console', 'file'], + 'level': 'INFO', + }, + }, + 'formatters': { + 'simple': { + 'format': '%(levelname)s: %(message)s' + }, + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(process)d %(name)s %(message)s' + }, + }, +} + + +try: + from settings_local import * +except ImportError: + pass diff --git a/example_project/test_settings.py b/example_project/test_settings.py index 8f08d9a..d615896 100644 --- a/example_project/test_settings.py +++ b/example_project/test_settings.py @@ -26,3 +26,34 @@ MAILMAN_REST_API_URL = 'http://localhost:9001' MAILMAN_REST_API_USER = 'restadmin' MAILMAN_REST_API_PASS = 'restpass' + + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'django.request': { + 'handlers': ['console'], + 'level': 'ERROR', + }, + 'postorius': { + 'handlers': ['console'], + 'level': 'INFO', + }, + }, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(process)d %(name)s %(message)s' + }, + }, +} diff --git a/example_project/urls.py b/example_project/urls.py index 57e1492..80ff17b 100644 --- a/example_project/urls.py +++ b/example_project/urls.py @@ -18,15 +18,18 @@ from django.conf.urls import include, url - from django.contrib import admin -admin.autodiscover() - -from postorius.views import list as list_views +from django.core.urlresolvers import reverse_lazy +from django.views.generic import RedirectView urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), - url('', include('django_browserid.urls')), - url(r'^$', list_views.list_index), + url(r'^$', RedirectView.as_view( + url=reverse_lazy('postorius.views.list.list_index'), + permanent=True)), url(r'^postorius/', include('postorius.urls')), + #url(r'^hyperkitty/', include('hyperkitty.urls')), + url(r'', include('django_mailman3.urls')), + url(r'^accounts/', include('allauth.urls')), + # Django admin + url(r'^admin/', include(admin.site.urls)), ] diff --git a/setup.py b/setup.py index 2bb5baf..832ad45 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ install_requires=[ 'Django>=1.8', 'Django<1.10', - 'django-browserid', + 'django-mailman3', 'mailmanclient', ], ) diff --git a/src/postorius/__init__.py b/src/postorius/__init__.py index 04133cf..2728a65 100644 --- a/src/postorius/__init__.py +++ b/src/postorius/__init__.py @@ -16,4 +16,7 @@ # You should have received a copy of the GNU General Public License along with # Postorius. If not, see . +from __future__ import absolute_import, unicode_literals + __version__ = '1.0.2' +default_app_config = 'postorius.apps.PostoriusConfig' diff --git a/src/postorius/apps.py b/src/postorius/apps.py new file mode 100644 index 0000000..0aaf717 --- /dev/null +++ b/src/postorius/apps.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 by the Free Software Foundation, Inc. +# +# This file is part of Postorius. +# +# Postorius is free software: you can redistribute it and/or modify it under +# 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. +# +# Postorius is distributed in the hope that it will be useful, but WITHOUT +# 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 +# Postorius. If not, see . + + +from __future__ import absolute_import, unicode_literals + +from django.apps import AppConfig + + +class PostoriusConfig(AppConfig): + name = 'postorius' + verbose_name = "Postorius" diff --git a/src/postorius/auth/decorators.py b/src/postorius/auth/decorators.py index ad3aa07..b71cbb7 100644 --- a/src/postorius/auth/decorators.py +++ b/src/postorius/auth/decorators.py @@ -15,8 +15,10 @@ # # You should have received a copy of the GNU General Public License along with # Postorius. If not, see . + """Postorius view decorators.""" +from __future__ import absolute_import, unicode_literals from django.contrib.auth import authenticate, login from django.core.exceptions import PermissionDenied diff --git a/src/postorius/auth/utils.py b/src/postorius/auth/utils.py index b502e79..87356b3 100644 --- a/src/postorius/auth/utils.py +++ b/src/postorius/auth/utils.py @@ -20,17 +20,18 @@ Authentication and authorization-related utilities. """ +from __future__ import absolute_import, unicode_literals + +from allauth.account.models import EmailAddress from django.utils import six -from postorius.utils import set_other_emails from postorius.models import List def user_is_in_list_roster(user, mailing_list, roster): if not user.is_authenticated(): return False - if not hasattr(user, 'other_emails'): - set_other_emails(user) - addresses = set([user.email]) | set(user.other_emails) + addresses = set(EmailAddress.objects.filter( + user=user, verified=True).values_list("email", flat=True)) if addresses & set(getattr(mailing_list, roster)): return True # At least one address is in the roster return False diff --git a/src/postorius/context_processors.py b/src/postorius/context_processors.py index 934651f..213f68c 100644 --- a/src/postorius/context_processors.py +++ b/src/postorius/context_processors.py @@ -16,12 +16,9 @@ # You should have received a copy of the GNU General Public License along with # Postorius. If not, see . +from __future__ import absolute_import, unicode_literals + import logging - -from django.conf import settings -from django.core.urlresolvers import reverse, NoReverseMatch -from django.shortcuts import resolve_url - logger = logging.getLogger(__name__) @@ -36,19 +33,6 @@ else: template_to_extend = "postorius/base.html" - # Find the HyperKitty URL if installed - hyperkitty_url = False - if "hyperkitty" in settings.INSTALLED_APPS: - try: - hyperkitty_url = reverse("hk_root") - except NoReverseMatch: - pass - return { 'postorius_base_template': template_to_extend, - 'request': request, - 'hyperkitty_url': hyperkitty_url, - # Resolve the login and logout URLs from the settings - 'login_url': resolve_url(settings.LOGIN_URL), - 'logout_url': resolve_url(settings.LOGOUT_URL), } diff --git a/src/postorius/fieldset_forms.py b/src/postorius/fieldset_forms.py index 2b23f3a..c02b8b8 100644 --- a/src/postorius/fieldset_forms.py +++ b/src/postorius/fieldset_forms.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU General Public License along with # Postorius. If not, see . +from __future__ import absolute_import, unicode_literals + from django.forms import Form from django.utils import safestring from django.forms.forms import BoundField diff --git a/src/postorius/forms.py b/src/postorius/forms.py index 6bff4c3..d3512b2 100644 --- a/src/postorius/forms.py +++ b/src/postorius/forms.py @@ -16,12 +16,13 @@ # You should have received a copy of the GNU General Public License along with # Postorius. If not, see . +from __future__ import absolute_import, unicode_literals + from django import forms from django import __version__ from django.core.validators import validate_email from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import User from postorius.fieldset_forms import FieldsetForm @@ -68,13 +69,6 @@ required=True, help_text=_('Example: domain.org'), ) - web_host = forms.URLField( - label=_('Web Host'), - error_messages={'required': _('Please enter a host name'), - 'invalid': _('Please enter a valid host name.')}, - required=True, - help_text=_('Example: http://www.domain.org'), - ) description = forms.CharField( label=_('Description'), required=False) @@ -98,7 +92,6 @@ """ layout = [["Please enter Details", "mail_host", - "web_host", "description"]] @@ -828,35 +821,6 @@ 'the message. ')) -class AddressActivationForm(forms.Form): - email = forms.EmailField(widget=forms.TextInput( - attrs={'placeholder': 'Enter alternate email'})) - - def clean_email(self): - email = self.cleaned_data.get('email') - - # Check if the address belongs to someone else - if User.objects.filter(email=email).exists(): - raise forms.ValidationError( - _('This email is in use. Please choose another or contact' - ' the administrator'), 'error') - - return email - - -class ChangeDisplayNameForm(forms.Form): - """ - Change display name. - """ - - display_name = forms.CharField( - label=_('Display name'), - error_messages={ - 'required': _('Please enter a display name')}, - required=True - ) - - class ChangeSubscriptionForm(forms.Form): email = forms.ChoiceField() diff --git a/src/postorius/lib/__init__.py b/src/postorius/lib/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/src/postorius/lib/__init__.py +++ /dev/null diff --git a/src/postorius/lib/scrub.py b/src/postorius/lib/scrub.py deleted file mode 100644 index 86952a6..0000000 --- a/src/postorius/lib/scrub.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright (C) 2011-2012 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT 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 this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, -# USA. - -"""Cleanse a message for archiving.""" - -from __future__ import absolute_import, unicode_literals - -import os -import re -import binascii -from mimetypes import guess_all_extensions -from email.header import decode_header, make_header -from email.errors import HeaderParseError - -pre = re.compile(r'[/\\:]') -sre = re.compile(r'[^-\w.]') -dre = re.compile(r'^\.*') - -BR = '
\n' - -NEXT_PART = re.compile(r'--------------[ ]next[ ]part[ ]--------------\n') - - -def guess_extension(ctype, ext): - all_exts = guess_all_extensions(ctype, strict=False) - if ext in all_exts: - return ext - return all_exts and all_exts[0] - - -def get_charset(message, default="ascii", guess=False): - if message.get_content_charset(): - return message.get_content_charset().decode("ascii") - if message.get_charset(): - return message.get_charset().decode("ascii") - charset = default - if not guess: - return charset - text = message.get_payload(decode=True) - for encoding in ["ascii", "utf-8", "iso-8859-15"]: - try: - text.decode(encoding) - except UnicodeDecodeError: - continue - else: - charset = encoding - break - return charset - - -def oneline(s): - """Inspired by mailman.utilities.string.oneline""" - try: - h = make_header(decode_header(s)) - ustr = h.__unicode__() - return ''.join(ustr.splitlines()) - except (LookupError, UnicodeError, ValueError, HeaderParseError): - return ''.join(s.splitlines()) - - -class Scrubber(object): - def __init__(self, msg): - self.msg = msg - - def scrub(self): - attachments = [] - for part_num, part in enumerate(self.msg.walk()): - ctype = part.get_content_type() - if not isinstance(ctype, unicode): - ctype = ctype.decode("ascii") - if ctype == 'text/plain': - disposition = part.get('content-disposition') - if disposition and disposition.decode( - "ascii", "replace").strip().startswith("attachment"): - attachments.append(self.parse_attachment(part, part_num)) - part.set_payload('') - elif ctype == 'text/html': - attachments.append(self.parse_attachment(part, part_num, - filter_html=False)) - part.set_payload('') - elif ctype == 'message/rfc822': - attachments.append(self.parse_attachment(part, part_num)) - part.set_payload('') - elif part.get_payload() and not part.is_multipart(): - payload = part.get_payload(decode=True) - ctype = part.get_content_type() - if not isinstance(ctype, unicode): - ctype.decode("ascii") - if payload is None: - continue - attachments.append(self.parse_attachment(part, part_num)) - if self.msg.is_multipart(): - text = [] - for part in self.msg.walk(): - if not part.get_payload() or part.is_multipart(): - continue - partctype = part.get_content_type() - if partctype != 'text/plain' and partctype != 'text/html': - continue - try: - t = part.get_payload(decode=True) or '' - except (binascii.Error, TypeError): - t = part.get_payload() or '' - partcharset = get_charset(part, guess=True) - try: - t = t.decode(partcharset, 'replace') - except (UnicodeError, LookupError, ValueError, - AssertionError): - t = t.decode('ascii', 'replace') - if isinstance(t, basestring): - if not t.endswith('\n'): - t += '\n' - text.append(t) - - text = u"\n".join(text) - else: - text = self.msg.get_payload(decode=True) - charset = get_charset(self.msg, guess=True) - try: - text = text.decode(charset, "replace") - except (UnicodeError, LookupError, ValueError, AssertionError): - text = text.decode('ascii', 'replace') - - next_part_match = NEXT_PART.search(text) - if next_part_match: - text = text[0:next_part_match.start(0)] - - return (text, attachments) - - def parse_attachment(self, part, counter, filter_html=True): - decodedpayload = part.get_payload(decode=True) - ctype = part.get_content_type() - if not isinstance(ctype, unicode): - ctype = ctype.decode("ascii") - charset = get_charset(part, default=None, guess=False) - try: - filename = oneline(part.get_filename('')) - except (TypeError, UnicodeDecodeError): - filename = u"attachment.bin" - filename, fnext = os.path.splitext(filename) - ext = fnext or guess_extension(ctype, fnext) - if not ext: - if ctype == 'message/rfc822': - ext = '.txt' - else: - ext = '.bin' - ext = sre.sub('', ext) - if not filename: - filebase = u'attachment' - else: - parts = pre.split(filename) - filename = parts[-1] - filename = dre.sub('', filename) - filename = sre.sub('', filename) - filebase = filename - if ctype == 'message/rfc822': - submsg = part.get_payload() - decodedpayload = str(submsg) - return (counter, filebase + ext, ctype, charset, decodedpayload) diff --git a/src/postorius/management/commands/mmclient.py b/src/postorius/management/commands/mmclient.py index 69b24ac..05bb297 100644 --- a/src/postorius/management/commands/mmclient.py +++ b/src/postorius/management/commands/mmclient.py @@ -17,7 +17,7 @@ # Postorius. If not, see . from django.core.management.base import BaseCommand -from postorius import utils +from django_mailman3.lib.mailman import get_mailman_client class Command(BaseCommand): @@ -42,7 +42,7 @@ shell = code.InteractiveConsole(globals()) console_fn = shell.interact # connect to mailmanclient - client = utils.get_client() + client = get_mailman_client() # Putting client back in the global scope globals()['client'] = client # run the interpreter diff --git a/src/postorius/middleware.py b/src/postorius/middleware.py index be44670..2c1f874 100644 --- a/src/postorius/middleware.py +++ b/src/postorius/middleware.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License along with # Postorius. If not, see . +from __future__ import absolute_import, unicode_literals from postorius import utils from postorius.models import MailmanApiError @@ -23,9 +24,6 @@ class PostoriusMiddleware(object): - def process_request(self, request): - utils.set_other_emails(request.user) - def process_exception(self, request, exception): if isinstance(exception, MailmanApiError): return utils.render_api_error(request) diff --git a/src/postorius/migrations/0003_drop_addressconfirmationprofile.py b/src/postorius/migrations/0003_drop_addressconfirmationprofile.py new file mode 100644 index 0000000..0ec04a8 --- /dev/null +++ b/src/postorius/migrations/0003_drop_addressconfirmationprofile.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-08-13 09:48 + +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('postorius', '0002_auto_20160210_0721'), + ] + + operations = [ + migrations.RemoveField( + model_name='addressconfirmationprofile', + name='user', + ), + migrations.DeleteModel( + name='AddressConfirmationProfile', + ), + ] diff --git a/src/postorius/models.py b/src/postorius/models.py index 854dcc8..1ab0349 100644 --- a/src/postorius/models.py +++ b/src/postorius/models.py @@ -15,26 +15,20 @@ # # You should have received a copy of the GNU General Public License along with # Postorius. If not, see . + from __future__ import ( absolute_import, division, print_function, unicode_literals) -import uuid import logging -from datetime import datetime, timedelta from django.conf import settings from django.contrib.auth.models import User -from django.core.exceptions import ImproperlyConfigured -from django.core.mail import send_mail from django.db.models.signals import post_save -from django.core.urlresolvers import reverse from django.dispatch import receiver -from django.db import models from django.http import Http404 -from django.template.loader import render_to_string +from django_mailman3.lib.mailman import get_mailman_client from mailmanclient import MailmanConnectionError -from postorius.utils import get_client try: from urllib2 import HTTPError except ImportError: @@ -81,7 +75,7 @@ def all(self): try: - return getattr(get_client(), self.resource_name_plural) + return getattr(get_mailman_client(), self.resource_name_plural) except AttributeError: raise MailmanApiError except MailmanConnectionError as e: @@ -89,7 +83,7 @@ def get(self, *args, **kwargs): try: - method = getattr(get_client(), 'get_' + self.resource_name) + method = getattr(get_mailman_client(), 'get_' + self.resource_name) return method(*args, **kwargs) except AttributeError as e: raise MailmanApiError(e) @@ -113,7 +107,8 @@ def create(self, *args, **kwargs): try: - method = getattr(get_client(), 'create_' + self.resource_name) + method = getattr( + get_mailman_client(), 'create_' + self.resource_name) return method(*args, **kwargs) except AttributeError as e: raise MailmanApiError(e) @@ -139,7 +134,7 @@ def all(self, only_public=False): try: - objects = getattr(get_client(), self.resource_name_plural) + objects = getattr(get_mailman_client(), self.resource_name_plural) except AttributeError: raise MailmanApiError except MailmanConnectionError as e: @@ -216,87 +211,3 @@ """Member model class. """ objects = MailmanRestManager('member', 'members') - - -class AddressConfirmationProfile(models.Model): - """ - Profile model for temporarily storing an activation key to register - an email address. - """ - email = models.EmailField(unique=True) - activation_key = models.CharField(max_length=32, unique=True) - created = models.DateTimeField(auto_now=True) - user = models.ForeignKey(User) - - def save(self, *args, **kwargs): - self.activation_key = uuid.uuid4().hex - super(AddressConfirmationProfile, self).save(*args, **kwargs) - - def __unicode__(self): - return u'Address Confirmation Profile for {0}'.format(self.email) - - @property - def is_expired(self): - """ - a profile expires after 1 day by default. - This can be configured in the settings. - - >>> EMAIL_CONFIRMATION_EXPIRATION_DELTA = timedelta(days=2) - - """ - expiration_delta = getattr( - settings, 'EMAIL_CONFIRMATION_EXPIRATION_DELTA', timedelta(days=1)) - age = datetime.now().replace(tzinfo=None) - \ - self.created.replace(tzinfo=None) - return age > expiration_delta - - def send_confirmation_link(self, request, template_context=None, - template_path=None): - """ - Send out a message containing a link to activate the given address. - - The following settings are recognized: - - >>> EMAIL_CONFIRMATION_TEMPLATE = \ - 'postorius/user/address_confirmation_message.txt' - >>> EMAIL_CONFIRMATION_FROM = 'postmaster@list.org' - >>> EMAIL_CONFIRMATION_SUBJECT = 'Confirmation needed' - - :param request: The HTTP request object. - :type request: HTTPRequest - :param template_context: The context used when rendering the template. - Falls back to host url and activation link. - :type template_context: django.template.Context - """ - # Get the url string from url conf. - url = reverse('address_activation_link', - kwargs={'activation_key': self.activation_key}) - activation_link = request.build_absolute_uri(url) - # Detect the right template path, either from the param, - # the setting or the default - if not template_path: - template_path = getattr( - settings, 'EMAIL_CONFIRMATION_TEMPLATE', - 'postorius/user/address_confirmation_message.txt') - # Create a template context (if there is none) containing - # the activation_link and the host_url. - if not template_context: - template_context = {'activation_link': activation_link, - 'host_url': request.build_absolute_uri("/")} - email_subject = getattr( - settings, 'EMAIL_CONFIRMATION_SUBJECT', u'Confirmation needed') - try: - sender_address = getattr(settings, 'EMAIL_CONFIRMATION_FROM') - except AttributeError: - # settings.EMAIL_CONFIRMATION_FROM is not defined, fallback - # settings.DEFAULT_EMAIL_FROM as mentioned in the django - # docs. If that also fails, raise a `ImproperlyConfigured` Error. - try: - sender_address = getattr(settings, 'DEFAULT_FROM_EMAIL') - except AttributeError: - raise ImproperlyConfigured - - send_mail(email_subject, - render_to_string(template_path, template_context), - sender_address, - [self.email]) diff --git a/src/postorius/templates/account/base.html b/src/postorius/templates/account/base.html new file mode 100644 index 0000000..0a861ed --- /dev/null +++ b/src/postorius/templates/account/base.html @@ -0,0 +1,3 @@ +{% extends "postorius/base.html" %} + + diff --git a/src/postorius/templates/django_mailman3/base.html b/src/postorius/templates/django_mailman3/base.html new file mode 100644 index 0000000..4d545e2 --- /dev/null +++ b/src/postorius/templates/django_mailman3/base.html @@ -0,0 +1,2 @@ +{% extends "postorius/base.html" %} + diff --git a/src/postorius/templates/postorius/base.html b/src/postorius/templates/postorius/base.html index b289eb9..a6c89d3 100644 --- a/src/postorius/templates/postorius/base.html +++ b/src/postorius/templates/postorius/base.html @@ -1,18 +1,16 @@ {% load i18n %} {% load staticfiles %} +{% load gravatar %} - - {% block title %} - {% block subtitle %}{% endblock %} | Mailman/Postorius - {% endblock %} - + {% block head_title %}{{ site_name|title }}{% endblock %} + {% block additionalcss %}{% endblock %} @@ -37,20 +35,63 @@ @@ -61,7 +102,7 @@ {% for message in messages %}
{{ message }}
{% endfor %} - {% block main %}{% endblock main %} + {% block content %}{% endblock content %}