Add user mailer backends support. GitLab issue #286.

Add support for creating dynamic forms.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2017-07-03 13:37:56 -04:00
parent 793fd74d7f
commit a72ac6eadb
20 changed files with 952 additions and 67 deletions

View File

@@ -6,6 +6,7 @@ from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _
from .classes import Filter, Package
@@ -83,6 +84,35 @@ class DetailForm(forms.ModelForm):
)
class DynamicFormMixin(object):
def __init__(self, *args, **kwargs):
self.schema = kwargs.pop('schema')
super(DynamicFormMixin, self).__init__(*args, **kwargs)
for field in self.schema['fields']:
field_class = import_string(field['class'])
kwargs = {
'label': field['label'],
'required': field.get('required', True),
'initial': field.get('default', None),
'help_text': field.get('help_text'),
}
kwargs.update(field.get('kwargs', {}))
self.fields[field['name']] = field_class(**kwargs)
for field, widget in self.schema.get('widgets', {}).items():
self.fields[field].widget = import_string(
widget['class']
)(**widget.get('kwargs', {}))
class DynamicForm(DynamicFormMixin, forms.Form):
pass
class DynamicModelForm(DynamicFormMixin, forms.ModelForm):
pass
class FileDisplayForm(forms.Form):
text = forms.CharField(
label='',

View File

@@ -19,10 +19,10 @@ from django_downloadview import (
)
from pure_pagination.mixins import PaginationMixin
from .forms import ChoiceForm
from .forms import ChoiceForm, DynamicForm
from .mixins import (
DeleteExtraDataMixin, ExtraContextMixin, FormExtraKwargsMixin,
MultipleObjectMixin, ObjectActionMixin,
DeleteExtraDataMixin, DynamicFormViewMixin, ExtraContextMixin,
FormExtraKwargsMixin, MultipleObjectMixin, ObjectActionMixin,
ObjectListPermissionFilterMixin, ObjectNameMixin,
ObjectPermissionCheckMixin, RedirectionMixin,
ViewPermissionCheckMixin
@@ -186,10 +186,14 @@ class ConfirmView(ObjectListPermissionFilterMixin, ObjectPermissionCheckMixin, V
return HttpResponseRedirect(self.get_success_url())
class FormView(FormExtraKwargsMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, DjangoFormView):
class FormView(ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, FormExtraKwargsMixin, DjangoFormView):
template_name = 'appearance/generic_form.html'
class DynamicFormView(DynamicFormViewMixin, FormView):
pass
class MultiFormView(DjangoFormView):
prefix = None
prefixes = {}
@@ -302,7 +306,7 @@ class SimpleView(ViewPermissionCheckMixin, ExtraContextMixin, TemplateView):
pass
class SingleObjectCreateView(ObjectNameMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, CreateView):
class SingleObjectCreateView(ObjectNameMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, FormExtraKwargsMixin, CreateView):
template_name = 'appearance/generic_form.html'
def form_valid(self, form):
@@ -344,6 +348,10 @@ class SingleObjectCreateView(ObjectNameMixin, ViewPermissionCheckMixin, ExtraCon
return HttpResponseRedirect(self.get_success_url())
class SingleObjectDynamicFormCreateView(DynamicFormViewMixin, SingleObjectCreateView):
pass
class SingleObjectDeleteView(ObjectNameMixin, DeleteExtraDataMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, DeleteView):
template_name = 'appearance/generic_confirm.html'
@@ -394,7 +402,7 @@ class SingleObjectDownloadView(ViewPermissionCheckMixin, ObjectPermissionCheckMi
VirtualFile = VirtualFile
class SingleObjectEditView(ObjectNameMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, UpdateView):
class SingleObjectEditView(ObjectNameMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, FormExtraKwargsMixin, RedirectionMixin, UpdateView):
template_name = 'appearance/generic_form.html'
def form_valid(self, form):
@@ -446,6 +454,10 @@ class SingleObjectEditView(ObjectNameMixin, ViewPermissionCheckMixin, ObjectPerm
return obj
class SingleObjectDynamicFormEditView(DynamicFormViewMixin, SingleObjectEditView):
pass
class SingleObjectListView(PaginationMixin, ViewPermissionCheckMixin, ObjectListPermissionFilterMixin, ExtraContextMixin, RedirectionMixin, ListView):
template_name = 'appearance/generic_list.html'

View File

@@ -12,10 +12,11 @@ from permissions import Permission
from acls.models import AccessControlList
from .forms import DynamicForm
__all__ = (
'DeleteExtraDataMixin', 'ExtraContextMixin', 'FormExtraKwargsMixin',
'MultipleObjectMixin', 'ObjectActionMixin',
'DeleteExtraDataMixin', 'DynamicFormViewMixin', 'ExtraContextMixin',
'FormExtraKwargsMixin', 'MultipleObjectMixin', 'ObjectActionMixin',
'ObjectListPermissionFilterMixin', 'ObjectNameMixin',
'ObjectPermissionCheckMixin', 'RedirectionMixin',
'ViewPermissionCheckMixin'
@@ -34,6 +35,15 @@ class DeleteExtraDataMixin(object):
return HttpResponseRedirect(success_url)
class DynamicFormViewMixin(object):
form_class = DynamicForm
def get_form_kwargs(self):
data = super(DynamicFormViewMixin, self).get_form_kwargs()
data.update({'schema': self.get_form_schema()})
return data
class ExtraContextMixin(object):
"""
Mixin that allows views to pass extra context to the template

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from .classes import * # NOQA
default_app_config = 'mailer.apps.MailerApp'

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.contrib import admin
from .models import LogEntry
from .models import LogEntry, UserMailer
@admin.register(LogEntry)
@@ -10,3 +10,10 @@ class LogEntryAdmin(admin.ModelAdmin):
date_hierarchy = 'datetime'
list_display = ('datetime', 'message')
readonly_fields = ('datetime', 'message')
@admin.register(UserMailer)
class UserMailerAdmin(admin.ModelAdmin):
list_display = (
'label', 'default', 'enabled', 'backend_path', 'backend_data'
)

View File

@@ -6,17 +6,26 @@ from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from acls import ModelPermission
from common import MayanAppConfig, menu_object, menu_multi_item, menu_tools
from common import (
MayanAppConfig, menu_object, menu_multi_item, menu_secondary, menu_setup,
menu_tools
)
from common.widgets import two_state_template
from mayan.celery import app
from navigation import SourceColumn
from .classes import MailerBackend
from .links import (
link_document_mailing_error_log, link_send_document_link,
link_send_document, link_send_multiple_document,
link_send_multiple_document_link
link_send_document_link, link_send_document, link_send_multiple_document,
link_send_multiple_document_link, link_system_mailer_error_log,
link_user_mailer_create, link_user_mailer_delete, link_user_mailer_edit,
link_user_mailer_list, link_user_mailer_log_list, link_user_mailer_setup,
link_user_mailer_test
)
from .permissions import (
permission_mailing_link, permission_mailing_send_document
permission_mailing_link, permission_mailing_send_document,
permission_user_mailer_delete, permission_user_mailer_edit,
permission_user_mailer_use, permission_user_mailer_view,
)
from .queues import * # NOQA
@@ -34,14 +43,34 @@ class MailerApp(MayanAppConfig):
)
LogEntry = self.get_model('LogEntry')
UserMailer = self.get_model('UserMailer')
MailerBackend.initialize()
SourceColumn(
source=LogEntry, label=_('Date and time'), attribute='datetime'
)
SourceColumn(
source=LogEntry, label=_('Message'), attribute='message'
)
SourceColumn(
source=UserMailer, label=_('Label'), attribute='label'
)
SourceColumn(
source=UserMailer, label=_('Default?'),
func=lambda context: two_state_template(
context['object'].default
)
)
SourceColumn(
source=UserMailer, label=_('Enabled?'),
func=lambda context: two_state_template(
context['object'].enabled
)
)
SourceColumn(
source=UserMailer, label=_('Label'), attribute='backend_label'
)
ModelPermission.register(
model=Document, permissions=(
@@ -49,6 +78,13 @@ class MailerApp(MayanAppConfig):
)
)
ModelPermission.register(
model=UserMailer, permissions=(
permission_user_mailer_delete, permission_user_mailer_edit,
permission_user_mailer_view, permission_user_mailer_use
)
)
app.conf.CELERY_QUEUES.append(
Queue('mailing', Exchange('mailing'), routing_key='mailing'),
)
@@ -73,4 +109,22 @@ class MailerApp(MayanAppConfig):
), sources=(Document,)
)
menu_tools.bind_links(links=(link_document_mailing_error_log,))
menu_object.bind_links(
links=(
link_user_mailer_edit, link_user_mailer_log_list,
link_user_mailer_test, link_user_mailer_delete,
), sources=(UserMailer,)
)
menu_secondary.bind_links(
links=(
link_user_mailer_list, link_user_mailer_create,
), sources=(
UserMailer, 'mailer:user_mailer_list',
'mailer:user_mailer_create'
)
)
menu_tools.bind_links(links=(link_system_mailer_error_log,))
menu_setup.bind_links(links=(link_user_mailer_setup,))

View File

@@ -0,0 +1,71 @@
from __future__ import unicode_literals
from importlib import import_module
import logging
from django.apps import apps
from django.utils import six
from django.utils.encoding import force_text
logger = logging.getLogger(__name__)
__ALL__ = ('MailerBackend',)
class MailerBackendMetaclass(type):
_registry = {}
def __new__(mcs, name, bases, attrs):
new_class = super(MailerBackendMetaclass, mcs).__new__(
mcs, name, bases, attrs
)
if not new_class.__module__ == 'mailer.classes':
mcs._registry[
'{}.{}'.format(new_class.__module__, name)
] = new_class
return new_class
class MailerBackendBase(object):
"""
Base class for the mailing backends. This class is mainly a wrapper
for other Django backends that adds a few metadata to specify the
fields it needs to be instanciated at runtime.
The fields attribute is a list of dictionaries with the format:
{
'name': '' # Field internal name
'label': '' # Label to show to users
'class': '' # Field class to use. Field classes are Python dot
paths to Django's form fields.
'initial': '' # Field initial value
'default': '' # Default value.
}
"""
class_path = '' # Dot path to the actual class that will handle the mail
fields = ()
class MailerBackend(six.with_metaclass(MailerBackendMetaclass, MailerBackendBase)):
@classmethod
def get(cls, name):
return cls._registry[name]
@classmethod
def get_all(cls):
return cls._registry
@staticmethod
def initialize():
for app in apps.get_app_configs():
try:
import_module('{}.mailers'.format(app.name))
except ImportError as exception:
if force_text(exception) != 'No module named mailers':
logger.error(
'Error importing %s mailers.py file; %s', app.name,
exception
)

View File

@@ -1,9 +1,17 @@
from __future__ import unicode_literals
import json
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from acls.models import AccessControlList
from common.forms import DynamicModelForm
from .classes import MailerBackend
from .models import UserMailer
from .permissions import permission_user_mailer_use
from .settings import (
setting_document_body_template, setting_document_subject_template,
setting_link_body_template, setting_link_subject_template
@@ -13,6 +21,7 @@ from .settings import (
class DocumentMailForm(forms.Form):
def __init__(self, *args, **kwargs):
as_attachment = kwargs.pop('as_attachment', False)
user = kwargs.pop('user', None)
super(DocumentMailForm, self).__init__(*args, **kwargs)
if as_attachment:
self.fields[
@@ -33,8 +42,70 @@ class DocumentMailForm(forms.Form):
'project_title': settings.PROJECT_TITLE,
'project_website': settings.PROJECT_WEBSITE
}
queryset = AccessControlList.objects.filter_by_access(
permission=permission_user_mailer_use, user=user,
queryset=UserMailer.objects.filter(enabled=True)
)
self.fields['user_mailer'].queryset = queryset
try:
self.fields['user_mailer'].initial = queryset.get(default=True)
except UserMailer.DoesNotExist:
pass
email = forms.EmailField(label=_('Email address'))
subject = forms.CharField(label=_('Subject'), required=False)
body = forms.CharField(
label=_('Body'), widget=forms.widgets.Textarea(), required=False
)
user_mailer = forms.ModelChoiceField(
label=_('Mailing profile'), queryset=UserMailer.objects.none()
)
class UserMailerBackendSelectionForm(forms.Form):
backend = forms.ChoiceField(choices=(), label=_('Backend'))
def __init__(self, *args, **kwargs):
super(UserMailerBackendSelectionForm, self).__init__(*args, **kwargs)
self.fields['backend'].choices = [
(
key, backend.label
) for key, backend in MailerBackend.get_all().items()
]
class UserMailerDynamicForm(DynamicModelForm):
class Meta:
fields = ('label', 'default', 'enabled', 'backend_data')
model = UserMailer
widgets = {'backend_data': forms.widgets.HiddenInput}
def __init__(self, *args, **kwargs):
result = super(UserMailerDynamicForm, self).__init__(*args, **kwargs)
if self.instance.backend_data:
for key, value in json.loads(self.instance.backend_data).items():
self.fields[key].initial = value
return result
def clean(self):
data = super(UserMailerDynamicForm, self).clean()
# Consolidate the dynamic fields into a single JSON field called
# 'backend_data'.
backend_data = {}
for field in self.schema['fields']:
backend_data[field['name']] = data.pop(
field['name'], field.get('default', None)
)
data['backend_data'] = json.dumps(backend_data)
return data
class UserMailerTestForm(forms.Form):
email = forms.EmailField(label=_('Email address'))

View File

@@ -6,7 +6,9 @@ from navigation import Link
from .permissions import (
permission_mailing_link, permission_mailing_send_document,
permission_view_error_log
permission_user_mailer_create, permission_user_mailer_delete,
permission_user_mailer_edit, permission_user_mailer_use,
permission_user_mailer_view, permission_view_error_log
)
link_send_document = Link(
@@ -23,7 +25,35 @@ link_send_multiple_document = Link(
link_send_multiple_document_link = Link(
text=_('Email link'), view='mailer:send_multiple_document_link'
)
link_document_mailing_error_log = Link(
link_system_mailer_error_log = Link(
icon='fa fa-envelope', permissions=(permission_view_error_log,),
text=_('Document mailing error log'), view='mailer:error_log',
text=_('System mailer error log'), view='mailer:system_mailer_error_log',
)
link_user_mailer_create = Link(
icon='fa fa-envelope', permissions=(permission_user_mailer_create,),
text=_('User mailer create'), view='mailer:user_mailer_backend_selection',
)
link_user_mailer_delete = Link(
args='resolved_object.pk', permissions=(permission_user_mailer_delete,),
tags='dangerous', text=_('Delete'), view='mailer:user_mailer_delete',
)
link_user_mailer_edit = Link(
args='object.pk', permissions=(permission_user_mailer_edit,),
text=_('Edit'), view='mailer:user_mailer_edit',
)
link_user_mailer_log_list = Link(
args='object.pk', permissions=(permission_user_mailer_view,),
text=_('Log'), view='mailer:user_mailer_log',
)
link_user_mailer_list = Link(
icon='fa fa-envelope', permissions=(permission_user_mailer_view,),
text=_('User mailer list'), view='mailer:user_mailer_list',
)
link_user_mailer_setup = Link(
icon='fa fa-envelope', permissions=(permission_user_mailer_view,),
text=_('User mailers'), view='mailer:user_mailer_list',
)
link_user_mailer_test = Link(
args='object.pk', permissions=(permission_user_mailer_use,),
text=_('Test'), view='mailer:user_mailer_test',
)

View File

@@ -0,0 +1,71 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from .classes import MailerBackend
__all__ = ('DjangoSMTP', 'DjangoFileBased')
class DjangoSMTP(MailerBackend):
class_path = 'django.core.mail.backends.smtp.EmailBackend'
fields = (
{
'name': 'host', 'label': _('Host'),
'class': 'django.forms.CharField', 'default': 'localhost',
'help_text': _('The host to use for sending email.'),
'kwargs': {
'max_length': 48
}, 'required': False
},
{
'name': 'port', 'label': _('Port'),
'class': 'django.forms.IntegerField', 'default': 25,
'help_text': _('Port to use for the SMTP server.'),
'required': False
},
{
'name': 'user', 'label': _('Username'),
'class': 'django.forms.CharField', 'default': '',
'help_text': _(
'Username to use for the SMTP server. If empty, '
'authentication won\'t attempted.'
), 'kwargs': {
'max_length': 48
}, 'required': False
},
{
'name': 'password', 'label': _('Password'),
'class': 'django.forms.CharField', 'default': '',
'help_text': _(
'Password to use for the SMTP server. This setting is used '
'in conjunction with the username when authenticating to '
'the SMTP server. If either of these settings is empty, '
'authentication won\'t be attempted.'
), 'kwargs': {
'max_length': 48
}, 'required': False
},
)
widgets = {
'password': {
'class': 'django.forms.widgets.PasswordInput',
'kwargs': {
'render_value': True
}
}
}
label = _('Django SMTP backend')
class DjangoFileBased(MailerBackend):
class_path = 'django.core.mail.backends.filebased.EmailBackend'
fields = (
{
'name': 'file_path', 'label': _('File path'),
'class': 'django.forms.CharField', 'kwargs': {
'max_length': 48
}
},
)
label = _('Django file based backend')

View File

@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-07-02 08:12
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mailer', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='UserMailer',
fields=[
(
'id', models.AutoField(
auto_created=True, primary_key=True, serialize=False,
verbose_name='ID'
)
),
(
'label', models.CharField(
max_length=32, unique=True, verbose_name='Label'
)
),
(
'default', models.BooleanField(
default=True, verbose_name='Default'
)
),
(
'backend_path', models.CharField(
help_text='The dotted Python path to the backend '
'class.', max_length=128, verbose_name='Backend path'
)
),
(
'backend_data', models.TextField(
blank=True, verbose_name='Backend data'
)
),
],
options={
'ordering': ('label',),
'verbose_name': 'User mailer',
'verbose_name_plural': 'User mailers',
},
),
migrations.CreateModel(
name='UserMailerLogEntry',
fields=[
(
'id', models.AutoField(
auto_created=True, primary_key=True, serialize=False,
verbose_name='ID'
)
),
(
'datetime', models.DateTimeField(
auto_now_add=True, verbose_name='Date time'
)
),
(
'message', models.TextField(
blank=True, editable=False, verbose_name='Message'
)
),
(
'user_mailer', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='mailer.UserMailer', verbose_name='User mailer'
)
),
],
options={
'ordering': ('-datetime',),
'get_latest_by': 'datetime',
'verbose_name': 'User mailer log entry',
'verbose_name_plural': 'User mailer log entries',
},
),
]

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-07-03 15:35
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mailer', '0002_usermailer_usermailerlogentry'),
]
operations = [
migrations.AddField(
model_name='usermailer',
name='enabled',
field=models.BooleanField(default=True, verbose_name='Enabled'),
),
migrations.AlterField(
model_name='usermailerlogentry',
name='user_mailer',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='error_log', to='mailer.UserMailer',
verbose_name='User mailer'
),
),
]

View File

@@ -1,8 +1,11 @@
from __future__ import unicode_literals
import json
import logging
from django.core import mail
from django.db import models
from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _
logger = logging.getLogger(__name__)
@@ -21,3 +24,110 @@ class LogEntry(models.Model):
ordering = ('-datetime',)
verbose_name = _('Log entry')
verbose_name_plural = _('Log entries')
class UserMailer(models.Model):
label = models.CharField(
max_length=32, unique=True, verbose_name=_('Label')
)
default = models.BooleanField(
default=True, help_text=_(
'If default, this mailing profile will be pre-selected on the '
'document mailing form.'
), verbose_name=_('Default')
)
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
backend_path = models.CharField(
max_length=128,
help_text=_('The dotted Python path to the backend class.'),
verbose_name=_('Backend path')
)
backend_data = models.TextField(
blank=True, verbose_name=_('Backend data')
)
class Meta:
ordering = ('label',)
verbose_name = _('User mailer')
verbose_name_plural = _('User mailers')
def __str__(self):
return self.label
def save(self, *args, **kwargs):
if self.default:
UserMailer.objects.select_for_update().exclude(pk=self.pk).update(
default=False
)
return super(UserMailer, self).save(*args, **kwargs)
def backend_label(self):
return self.get_backend().label
def get_backend(self):
return import_string(self.backend_path)
def get_connection(self):
return mail.get_connection(
backend=self.get_backend().class_path, **self.loads()
)
def loads(self):
return json.loads(self.backend_data)
def dumps(self, data):
self.backend_data = json.dumps(data)
self.save()
def send(self, **kwargs):
"""
https://docs.djangoproject.com/en/1.11/topics/email
#django.core.mail.EmailMessage
subject: The subject line of the email.
body: The body text. This should be a plain text message.
from_email: The sender's address. Both fred@example.com and Fred
<fred@example.com> forms are legal. If omitted,
the DEFAULT_FROM_EMAIL setting is used.
to: A list or tuple of recipient addresses.
bcc: A list or tuple of addresses used in the "Bcc" header when
sending the email.
connection: An email backend instance. Use this parameter if you want
to use the same connection for multiple messages. If omitted, a new
connection is created when send() is called.
attachments: A list of attachments to put on the message. These can be
either email.MIMEBase.MIMEBase instances, or (filename, content,
mimetype) triples.
headers: A dictionary of extra headers to put on the message. The
keys are the header name, values are the header values. It's up to
the caller to ensure header names and values are in the correct
format for an email message. The corresponding attribute is
extra_headers.
cc: A list or tuple of recipient addresses used in the "Cc"
header when sending the email.
reply_to: A list or tuple of recipient addresses used in the
"Reply-To" header when sending the email.
"""
with self.get_connection() as connection:
mail.EmailMessage(connection=connection, **kwargs).send()
def test(self, to):
self.send(to=to, subject=_('Test email from Mayan EDMS'))
class UserMailerLogEntry(models.Model):
user_mailer = models.ForeignKey(
UserMailer, related_name='error_log', verbose_name=_('User mailer')
)
datetime = models.DateTimeField(
auto_now_add=True, editable=False, verbose_name=_('Date time')
)
message = models.TextField(
blank=True, editable=False, verbose_name=_('Message')
)
class Meta:
get_latest_by = 'datetime'
ordering = ('-datetime',)
verbose_name = _('User mailer log entry')
verbose_name_plural = _('User mailer log entries')

View File

@@ -13,5 +13,20 @@ permission_mailing_send_document = namespace.add_permission(
name='mail_document', label=_('Send document via email')
)
permission_view_error_log = namespace.add_permission(
name='view_error_log', label=_('View document mailing error log')
name='view_error_log', label=_('View system mailing error log')
)
permission_user_mailer_create = namespace.add_permission(
name='user_mailer_create', label=_('Create an user mailer')
)
permission_user_mailer_delete = namespace.add_permission(
name='user_mailer_delete', label=_('Delete an user mailer')
)
permission_user_mailer_edit = namespace.add_permission(
name='user_mailer_edit', label=_('Edit an user mailer')
)
permission_user_mailer_view = namespace.add_permission(
name='user_mailer_view', label=_('View an user mailer')
)
permission_user_mailer_use = namespace.add_permission(
name='user_mailer_use', label=_('Use an user mailer')
)

View File

@@ -1,17 +1,25 @@
from __future__ import unicode_literals
from django.apps import apps
from django.core.mail import EmailMultiAlternatives
from documents.models import Document
from mayan.celery import app
from .models import LogEntry
@app.task(ignore_result=True)
def task_send_document(subject_text, body_text_content, sender, recipient, document_id, as_attachment=False):
def task_send_document(subject_text, body_text_content, sender, recipient, document_id, user_mailer_id, as_attachment=False):
UserMailer = apps.get_model(
app_label='mailer', model_name='UserMailer'
)
user_mailer = UserMailer.objects.get(pk=user_mailer_id)
connection = user_mailer.get_connection()
email_msg = EmailMultiAlternatives(
subject_text, body_text_content, sender, [recipient]
subject_text, body_text_content, sender, [recipient],
connection=connection,
)
if as_attachment:
@@ -24,6 +32,6 @@ def task_send_document(subject_text, body_text_content, sender, recipient, docum
try:
email_msg.send()
except Exception as exception:
LogEntry.objects.create(message=exception)
user_mailer.error_log.create(message=exception)
else:
LogEntry.objects.all().delete()
user_mailer.error_log.all().delete()

View File

@@ -0,0 +1,5 @@
from __future__ import unicode_literals
TEST_EMAIL_ADDRESS = 'test@example.com'
TEST_USER_MAILER_LABEL = 'test user mailer label'
TEST_USER_MAILER_BACKEND_PATH = 'mailer.tests.mailers.TestBackend'

View File

@@ -0,0 +1,8 @@
from __future__ import unicode_literals
from ..classes import MailerBackend
class TestBackend(MailerBackend):
class_path = 'django.core.mail.backends.locmem.EmailBackend'
label = 'Django local memory backend'

View File

@@ -2,66 +2,145 @@ from __future__ import unicode_literals
from django.core import mail
from documents.tests.test_views import GenericDocumentViewTestCase
from ..permissions import (
permission_mailing_link, permission_mailing_send_document
from documents.tests.test_views import (
GenericDocumentViewTestCase, GenericViewTestCase
)
TEST_EMAIL_ADDRESS = 'test@example.com'
from ..models import UserMailer
from ..permissions import (
permission_mailing_link, permission_mailing_send_document,
permission_user_mailer_use, permission_user_mailer_view
)
from .literals import (
TEST_EMAIL_ADDRESS, TEST_USER_MAILER_BACKEND_PATH, TEST_USER_MAILER_LABEL
)
class MailerViewsTestCase(GenericDocumentViewTestCase):
def test_mail_link_view_no_permissions(self):
self.login_user()
class MailerTestMixin(object):
def _create_user_mailer(self):
response = self.post(
'mailer:send_document_link', args=(self.document.pk,),
data={'email': TEST_EMAIL_ADDRESS},
self.user_mailer = UserMailer.objects.create(
default=True,
enabled=True,
label=TEST_USER_MAILER_LABEL,
backend_path=TEST_USER_MAILER_BACKEND_PATH,
backend_data='{}'
)
self.assertEqual(response.status_code, 302)
class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase):
def _request_document_link_send(self):
return self.post(
'mailer:send_document_link', args=(self.document.pk,),
data={
'email': TEST_EMAIL_ADDRESS,
'user_mailer': self.user_mailer.pk
},
)
def _request_document_send(self):
return self.post(
'mailer:send_document', args=(self.document.pk,),
data={
'email': TEST_EMAIL_ADDRESS,
'user_mailer': self.user_mailer.pk
},
)
def test_mail_link_view_no_permissions(self):
self._create_user_mailer()
self.login_user()
response = self._request_document_link_send()
self.assertContains(
response, 'Select a valid choice', status_code=200
)
def test_mail_link_view_with_permission(self):
self._create_user_mailer()
self.login_user()
self.grant(permission_mailing_link)
self.grant(permission_user_mailer_use)
response = self.post(
'mailer:send_document_link', args=(self.document.pk,),
data={'email': TEST_EMAIL_ADDRESS},
follow=True
)
self._request_document_link_send()
self.assertContains(
response, 'queued', status_code=200
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS])
def test_mail_document_view_no_permissions(self):
self._create_user_mailer()
self.login_user()
response = self.post(
'mailer:send_document', args=(self.document.pk,),
data={'email': TEST_EMAIL_ADDRESS},
response = self._request_document_send()
self.assertContains(
response, 'Select a valid choice', status_code=200
)
self.assertEqual(response.status_code, 302)
def test_mail_document_view_with_permission(self):
self._create_user_mailer()
self.login_user()
self.grant(permission_mailing_send_document)
self.grant(permission_user_mailer_use)
response = self.post(
'mailer:send_document', args=(self.document.pk,),
data={'email': TEST_EMAIL_ADDRESS},
follow=True
self._request_document_send()
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS])
class UserMailerViewTestCase(MailerTestMixin, GenericViewTestCase):
def _request_user_mailer_delete(self):
return self.post(
'mailer:user_mailer_delete', args=(self.user_mailer.pk,)
)
def test_user_mailer_list_view_no_permissions(self):
self._create_user_mailer()
self.login_user()
response = self.get(
'mailer:user_mailer_list',
)
self.assertNotContains(
response, text=self.user_mailer.label, status_code=200
)
def test_user_mailer_list_view_with_permissions(self):
self._create_user_mailer()
self.login_user()
self.grant(permission_user_mailer_view)
response = self.get(
'mailer:user_mailer_list',
)
self.assertContains(
response, 'queued', status_code=200
response, text=self.user_mailer.label, status_code=200
)
def test_user_mailer_delete_view_no_permissions(self):
self._create_user_mailer()
self.login_user()
self._request_user_mailer_delete()
self.assertQuerysetEqual(
UserMailer.objects.all(), (repr(self.user_mailer),)
)
def test_user_mailer_delete_view_with_permissions(self):
self._create_user_mailer()
self.login_user()
self.grant(permission_user_mailer_view)
self._request_user_mailer_delete()
self.assertNotEqual(
[UserMailer.objects.all()], [self.user_mailer]
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS])

View File

@@ -2,7 +2,12 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .views import LogEntryListView, MailDocumentLinkView, MailDocumentView
from .views import (
SystemMailerLogEntryListView, MailDocumentLinkView, MailDocumentView,
UserMailerBackendSelectionView, UserMailingCreateView,
UserMailingDeleteView, UserMailingEditView, UserMailerLogEntryListView,
UserMailerTestView, UserMailerListView
)
urlpatterns = [
url(
@@ -22,6 +27,36 @@ urlpatterns = [
name='send_multiple_document'
),
url(
r'^log/$', LogEntryListView.as_view(), name='error_log'
r'^system_mailer/log/$', SystemMailerLogEntryListView.as_view(),
name='system_mailer_error_log'
),
url(
r'^user_mailers/backend/selection',
UserMailerBackendSelectionView.as_view(),
name='user_mailer_backend_selection'
),
url(
r'^user_mailers/(?P<class_path>[a-zA-Z0-9_.]+)/create/$',
UserMailingCreateView.as_view(), name='user_mailer_create'
),
url(
r'^user_mailers/(?P<pk>\d+)/delete/$', UserMailingDeleteView.as_view(),
name='user_mailer_delete'
),
url(
r'^user_mailers/(?P<pk>\d+)/edit/$', UserMailingEditView.as_view(),
name='user_mailer_edit'
),
url(
r'^user_mailers/(?P<pk>\d+)/log/$',
UserMailerLogEntryListView.as_view(), name='user_mailer_log'
),
url(
r'^user_mailers/(?P<pk>\d+)/test/$',
UserMailerTestView.as_view(), name='user_mailer_test'
),
url(
r'^user_mailers/$', UserMailerListView.as_view(),
name='user_mailer_list'
),
]

View File

@@ -1,23 +1,37 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse, reverse_lazy
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import Context, Template
from django.utils.html import strip_tags
from django.utils.translation import ungettext, ugettext_lazy as _
from common.generics import MultipleObjectFormActionView, SingleObjectListView
from acls.models import AccessControlList
from common.generics import (
FormView, MultipleObjectFormActionView, SingleObjectDeleteView,
SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
SingleObjectListView
)
from documents.models import Document
from .forms import DocumentMailForm
from .models import LogEntry
from .classes import MailerBackend
from .forms import (
DocumentMailForm, UserMailerBackendSelectionForm, UserMailerDynamicForm,
UserMailerTestForm
)
from .models import LogEntry, UserMailer
from .permissions import (
permission_mailing_link, permission_mailing_send_document,
permission_view_error_log
permission_user_mailer_create, permission_user_mailer_delete,
permission_user_mailer_edit, permission_user_mailer_use,
permission_user_mailer_view, permission_view_error_log
)
from .tasks import task_send_document
class LogEntryListView(SingleObjectListView):
class SystemMailerLogEntryListView(SingleObjectListView):
extra_context = {
'hide_object': True,
'title': _('Document mailing error log'),
@@ -65,7 +79,8 @@ class MailDocumentView(MultipleObjectFormActionView):
def get_form_extra_kwargs(self):
return {
'as_attachment': self.as_attachment
'as_attachment': self.as_attachment,
'user': self.request.user
}
def object_action(self, form, instance):
@@ -83,13 +98,19 @@ class MailDocumentView(MultipleObjectFormActionView):
subject_template = Template(form.cleaned_data['subject'])
subject_text = strip_tags(subject_template.render(context))
AccessControlList.objects.check_access(
permissions=permission_user_mailer_use, user=self.request.user,
obj=form.cleaned_data['user_mailer']
)
task_send_document.apply_async(
args=(
subject_text, body_text_content, self.request.user.email,
form.cleaned_data['email']
), kwargs={
'document_id': instance.pk,
'as_attachment': self.as_attachment
'as_attachment': self.as_attachment,
'user_mailer_id': form.cleaned_data['user_mailer'].pk
}
)
@@ -104,3 +125,133 @@ class MailDocumentLinkView(MailDocumentView):
title = 'Email document link'
title_plural = 'Email document links'
title_document = 'Email link for document: %s'
class UserMailerBackendSelectionView(FormView):
extra_context = {
'title': _('New mailing profile backend selection'),
}
form_class = UserMailerBackendSelectionForm
view_permission = permission_user_mailer_create
def form_valid(self, form):
backend = form.cleaned_data['backend']
return HttpResponseRedirect(
reverse('mailer:user_mailer_create', args=(backend,),)
)
class UserMailingCreateView(SingleObjectDynamicFormCreateView):
form_class = UserMailerDynamicForm
post_action_redirect = reverse_lazy('mailer:user_mailer_list')
view_permission = permission_user_mailer_create
def get_backend(self):
try:
return MailerBackend.get(name=self.kwargs['class_path'])
except KeyError:
raise Http404(
'{} class not found'.format(self.kwargs['class_path'])
)
def get_extra_context(self):
return {
'title': _(
'Create a "%s" mailing profile'
) % self.get_backend().label,
}
def get_form_schema(self):
return {
'fields': self.get_backend().fields,
'widgets': getattr(self.get_backend(), 'widgets', {})
}
def get_instance_extra_data(self):
return {'backend_path': self.kwargs['class_path']}
class UserMailingDeleteView(SingleObjectDeleteView):
model = UserMailer
object_permission = permission_user_mailer_delete
post_action_redirect = reverse_lazy('mailer:user_mailer_list')
def get_extra_context(self):
return {
'title': _('Delete mailing profile: %s') % self.get_object(),
}
class UserMailingEditView(SingleObjectDynamicFormEditView):
form_class = UserMailerDynamicForm
model = UserMailer
object_permission = permission_user_mailer_edit
def form_valid(self, form):
return super(UserMailingEditView, self).form_valid(form)
def get_extra_context(self):
return {
'title': _('Edit mailing profile: %s') % self.get_object(),
}
def get_form_schema(self):
return {
'fields': self.get_object().get_backend().fields,
'widgets': getattr(self.get_object().get_backend(), 'widgets', {})
}
class UserMailerLogEntryListView(SingleObjectListView):
model = LogEntry
view_permission = permission_user_mailer_view
def get_extra_context(self):
return {
'hide_object': True,
'object': self.get_user_mailer(),
'title': _('%s error log') % self.get_user_mailer(),
}
def get_queryset(self):
return self.get_user_mailer().error_log.all()
def get_user_mailer(self):
return get_object_or_404(UserMailer, pk=self.kwargs['pk'])
class UserMailerListView(SingleObjectListView):
extra_context = {
'hide_object': True,
'title': _('Mailing profile'),
}
model = UserMailer
object_permission = permission_user_mailer_view
def get_form_schema(self):
return {'fields': self.get_backend().fields}
class UserMailerTestView(FormView):
form_class = UserMailerTestForm
def form_valid(self, form):
self.get_user_mailer().test(to=(form.cleaned_data['email'],))
return super(UserMailerTestView, self).form_valid(form=form)
def get_extra_context(self):
return {
'hide_object': True,
'object': self.get_user_mailer(),
'submit_label': _('Test'),
'title': _('Test mailing profile: %s') % self.get_user_mailer(),
}
def get_user_mailer(self):
user_mailer = get_object_or_404(UserMailer, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_user_mailer_use, user=self.request.user,
obj=user_mailer
)
return user_mailer