feat: add feature to edit email settings, basic message preview
This commit is contained in:
parent
3035d1f58a
commit
95ff87fbf3
6
docs/api/wuttaweb.emails.rst
Normal file
6
docs/api/wuttaweb.emails.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttaweb.emails``
|
||||
===================
|
||||
|
||||
.. automodule:: wuttaweb.emails
|
||||
:members:
|
6
docs/api/wuttaweb.views.email.rst
Normal file
6
docs/api/wuttaweb.views.email.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttaweb.views.email``
|
||||
========================
|
||||
|
||||
.. automodule:: wuttaweb.views.email
|
||||
:members:
|
|
@ -35,6 +35,7 @@ the narrative docs are pretty scant. That will eventually change.
|
|||
api/wuttaweb.db
|
||||
api/wuttaweb.db.continuum
|
||||
api/wuttaweb.db.sess
|
||||
api/wuttaweb.emails
|
||||
api/wuttaweb.forms
|
||||
api/wuttaweb.forms.base
|
||||
api/wuttaweb.forms.schema
|
||||
|
@ -54,6 +55,7 @@ the narrative docs are pretty scant. That will eventually change.
|
|||
api/wuttaweb.views.base
|
||||
api/wuttaweb.views.batch
|
||||
api/wuttaweb.views.common
|
||||
api/wuttaweb.views.email
|
||||
api/wuttaweb.views.essential
|
||||
api/wuttaweb.views.master
|
||||
api/wuttaweb.views.people
|
||||
|
|
|
@ -43,9 +43,12 @@ log = logging.getLogger(__name__)
|
|||
class WebAppProvider(AppProvider):
|
||||
"""
|
||||
The :term:`app provider` for WuttaWeb. This adds some methods to
|
||||
the :term:`app handler`, which are specific to web apps.
|
||||
the :term:`app handler`, which are specific to web apps. It also
|
||||
registers some :term:`email templates <email template>` for the
|
||||
app, etc.
|
||||
"""
|
||||
email_templates = 'wuttaweb:email/templates'
|
||||
email_modules = ['wuttaweb.emails']
|
||||
email_templates = ['wuttaweb:email-templates']
|
||||
|
||||
def get_web_handler(self, **kwargs):
|
||||
"""
|
||||
|
|
48
src/wuttaweb/emails.py
Normal file
48
src/wuttaweb/emails.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# wuttaweb -- Web App for Wutta Framework
|
||||
# Copyright © 2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
# Wutta Framework 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.
|
||||
#
|
||||
# Wutta Framework 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
|
||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
:term:`Email Settings <email setting>` for WuttaWeb
|
||||
"""
|
||||
|
||||
from wuttjamaican.email import EmailSetting
|
||||
|
||||
|
||||
class feedback(EmailSetting):
|
||||
"""
|
||||
Sent when user submits feedback via the web app.
|
||||
"""
|
||||
default_subject = "User Feedback"
|
||||
|
||||
def sample_data(self):
|
||||
""" """
|
||||
model = self.app.model
|
||||
person = model.Person(full_name="Barney Rubble")
|
||||
user = model.User(username='barney', person=person)
|
||||
return {
|
||||
'user': user,
|
||||
'user_name': str(person),
|
||||
'user_url': '#',
|
||||
'referrer': 'http://example.com/',
|
||||
'client_ip': '127.0.0.1',
|
||||
'message': "This app is cool but needs a new feature.\n\nAllow me to describe...",
|
||||
}
|
|
@ -30,9 +30,11 @@ import uuid as _uuid
|
|||
import colander
|
||||
import sqlalchemy as sa
|
||||
|
||||
from wuttjamaican.db.model import Person
|
||||
from wuttjamaican.conf import parse_list
|
||||
|
||||
from wuttaweb.db import Session
|
||||
from wuttaweb.forms import widgets
|
||||
from wuttjamaican.db.model import Person
|
||||
|
||||
|
||||
class WuttaDateTime(colander.DateTime):
|
||||
|
@ -569,5 +571,36 @@ class FileDownload(colander.String):
|
|||
return widgets.FileDownloadWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class EmailRecipients(colander.String):
|
||||
"""
|
||||
Custom schema type for :term:`email setting` recipient fields
|
||||
(``To``, ``Cc``, ``Bcc``).
|
||||
"""
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if appstruct is colander.null:
|
||||
return colander.null
|
||||
|
||||
return '\n'.join(parse_list(appstruct))
|
||||
|
||||
def deserialize(self, node, cstruct):
|
||||
""" """
|
||||
if cstruct is colander.null:
|
||||
return colander.null
|
||||
|
||||
values = [value for value in parse_list(cstruct)
|
||||
if value]
|
||||
return ', '.join(values)
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
"""
|
||||
Constructs a default widget for the field.
|
||||
|
||||
:returns: Instance of
|
||||
:class:`~wuttaweb.forms.widgets.EmailRecipientsWidget`.
|
||||
"""
|
||||
return widgets.EmailRecipientsWidget(**kwargs)
|
||||
|
||||
|
||||
# nb. colanderalchemy schema overrides
|
||||
sa.DateTime.__colanderalchemy_config__ = {'typ': WuttaDateTime}
|
||||
|
|
|
@ -51,6 +51,8 @@ from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
|
|||
DateTimeInputWidget, MoneyInputWidget)
|
||||
from webhelpers2.html import HTML
|
||||
|
||||
from wuttjamaican.conf import parse_list
|
||||
|
||||
from wuttaweb.db import Session
|
||||
from wuttaweb.grids import Grid
|
||||
|
||||
|
@ -423,6 +425,41 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget):
|
|||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class EmailRecipientsWidget(TextAreaWidget):
|
||||
"""
|
||||
Widget for :term:`email setting` recipient fields (``To``, ``Cc``,
|
||||
``Bcc``).
|
||||
|
||||
This is a subclass of
|
||||
:class:`deform:deform.widget.TextAreaWidget`. It uses these
|
||||
Deform templates:
|
||||
|
||||
* ``textarea``
|
||||
* ``readonly/email_recips``
|
||||
|
||||
See also the :class:`~wuttaweb.forms.schema.EmailRecipients`
|
||||
schema type, which uses this widget.
|
||||
"""
|
||||
readonly_template = 'readonly/email_recips'
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
readonly = kw.get('readonly', self.readonly)
|
||||
if readonly:
|
||||
kw['recips'] = parse_list(cstruct or '')
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
def deserialize(self, field, pstruct):
|
||||
""" """
|
||||
if pstruct is colander.null:
|
||||
return colander.null
|
||||
|
||||
values = [value for value in parse_list(pstruct)
|
||||
if value]
|
||||
return ', '.join(values)
|
||||
|
||||
|
||||
class BatchIdWidget(Widget):
|
||||
"""
|
||||
Widget for use with the
|
||||
|
|
|
@ -174,6 +174,12 @@ class MenuHandler(GenericHandler):
|
|||
'perm': 'permissions.list',
|
||||
},
|
||||
{'type': 'sep'},
|
||||
{
|
||||
'title': "Email Settings",
|
||||
'route': 'email_settings',
|
||||
'perm': 'email_settings.list',
|
||||
},
|
||||
{'type': 'sep'},
|
||||
{
|
||||
'title': "App Info",
|
||||
'route': 'appinfo',
|
||||
|
|
5
src/wuttaweb/templates/deform/readonly/email_recips.pt
Normal file
5
src/wuttaweb/templates/deform/readonly/email_recips.pt
Normal file
|
@ -0,0 +1,5 @@
|
|||
<ul>
|
||||
<tal:loop tal:repeat="recip recips">
|
||||
<li>${recip}</li>
|
||||
</tal:loop>
|
||||
</ul>
|
39
src/wuttaweb/templates/email/settings/view.mako
Normal file
39
src/wuttaweb/templates/email/settings/view.mako
Normal file
|
@ -0,0 +1,39 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/view.mako" />
|
||||
|
||||
<%def name="tool_panels()">
|
||||
${parent.tool_panels()}
|
||||
${self.tool_panel_preview()}
|
||||
</%def>
|
||||
|
||||
<%def name="tool_panel_preview()">
|
||||
<wutta-tool-panel heading="Email Preview">
|
||||
|
||||
<b-button type="is-primary"
|
||||
% if has_html_template:
|
||||
tag="a" target="_blank"
|
||||
href="${master.get_action_url('preview', setting)}?mode=html"
|
||||
% else:
|
||||
disabled
|
||||
title="HTML template not found"
|
||||
% endif
|
||||
icon-pack="fas"
|
||||
icon-left="external-link-alt">
|
||||
Preview HTML
|
||||
</b-button>
|
||||
|
||||
<b-button type="is-primary"
|
||||
% if has_txt_template:
|
||||
tag="a" target="_blank"
|
||||
href="${master.get_action_url('preview', setting)}?mode=txt"
|
||||
% else:
|
||||
disabled
|
||||
title="TXT template not found"
|
||||
% endif
|
||||
icon-pack="fas"
|
||||
icon-left="external-link-alt">
|
||||
Preview TXT
|
||||
</b-button>
|
||||
|
||||
</wutta-tool-panel>
|
||||
</%def>
|
298
src/wuttaweb/views/email.py
Normal file
298
src/wuttaweb/views/email.py
Normal file
|
@ -0,0 +1,298 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# wuttaweb -- Web App for Wutta Framework
|
||||
# Copyright © 2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
# Wutta Framework 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.
|
||||
#
|
||||
# Wutta Framework 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
|
||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Views for email settings
|
||||
"""
|
||||
|
||||
import colander
|
||||
|
||||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.forms.schema import EmailRecipients
|
||||
|
||||
|
||||
class EmailSettingView(MasterView):
|
||||
"""
|
||||
Master view for :term:`email settings <email setting>`.
|
||||
"""
|
||||
model_name = 'email_setting'
|
||||
model_key = 'key'
|
||||
model_title = "Email Setting"
|
||||
url_prefix = '/email/settings'
|
||||
filterable = False
|
||||
sortable = True
|
||||
sort_on_backend = False
|
||||
paginated = False
|
||||
creatable = False
|
||||
deletable = False
|
||||
|
||||
labels = {
|
||||
'key': "Email Key",
|
||||
'replyto': "Reply-To",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
'key',
|
||||
'subject',
|
||||
'to',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
# TODO: why does this not work?
|
||||
sort_defaults = 'key'
|
||||
|
||||
form_fields = [
|
||||
'key',
|
||||
'description',
|
||||
'subject',
|
||||
'sender',
|
||||
'replyto',
|
||||
'to',
|
||||
'cc',
|
||||
'bcc',
|
||||
'notes',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
def __init__(self, request, context=None):
|
||||
super().__init__(request, context=context)
|
||||
self.email_handler = self.app.get_email_handler()
|
||||
|
||||
def get_grid_data(self, columns=None, session=None):
|
||||
"""
|
||||
This view calls
|
||||
:meth:`~wuttjamaican:wuttjamaican.email.EmailHandler.get_email_settings()`
|
||||
on the :attr:`email_handler` to obtain its grid data.
|
||||
"""
|
||||
data = []
|
||||
for setting in self.email_handler.get_email_settings().values():
|
||||
data.append(self.normalize_setting(setting))
|
||||
return data
|
||||
|
||||
def normalize_setting(self, setting):
|
||||
""" """
|
||||
key = setting.__name__
|
||||
return {
|
||||
'key': key,
|
||||
'description': setting.__doc__,
|
||||
'subject': self.email_handler.get_auto_subject(key, rendered=False, setting=setting),
|
||||
'sender': self.email_handler.get_auto_sender(key),
|
||||
'replyto': self.email_handler.get_auto_replyto(key) or colander.null,
|
||||
'to': self.email_handler.get_auto_to(key),
|
||||
'cc': self.email_handler.get_auto_cc(key),
|
||||
'bcc': self.email_handler.get_auto_bcc(key),
|
||||
'notes': self.email_handler.get_notes(key) or colander.null,
|
||||
'enabled': self.email_handler.is_enabled(key),
|
||||
}
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
|
||||
# key
|
||||
g.set_searchable('key')
|
||||
g.set_link('key')
|
||||
|
||||
# subject
|
||||
g.set_searchable('subject')
|
||||
g.set_link('subject')
|
||||
|
||||
# to
|
||||
g.set_renderer('to', self.render_to_short)
|
||||
|
||||
def render_to_short(self, setting, field, value):
|
||||
""" """
|
||||
recips = value
|
||||
if not recips:
|
||||
return
|
||||
|
||||
if len(recips) < 3:
|
||||
return ', '.join(recips)
|
||||
|
||||
recips = ', '.join(recips[:2])
|
||||
return f"{recips}, ..."
|
||||
|
||||
def get_instance(self):
|
||||
""" """
|
||||
key = self.request.matchdict['key']
|
||||
setting = self.email_handler.get_email_setting(key, instance=False)
|
||||
if setting:
|
||||
return self.normalize_setting(setting)
|
||||
|
||||
raise self.notfound()
|
||||
|
||||
def get_instance_title(self, setting):
|
||||
""" """
|
||||
return setting['subject']
|
||||
|
||||
def configure_form(self, f):
|
||||
""" """
|
||||
super().configure_form(f)
|
||||
|
||||
# description
|
||||
f.set_readonly('description')
|
||||
|
||||
# replyto
|
||||
f.set_required('replyto', False)
|
||||
|
||||
# to
|
||||
f.set_node('to', EmailRecipients())
|
||||
|
||||
# cc
|
||||
f.set_node('cc', EmailRecipients())
|
||||
|
||||
# bcc
|
||||
f.set_node('bcc', EmailRecipients())
|
||||
|
||||
# notes
|
||||
f.set_widget('notes', 'notes')
|
||||
f.set_required('notes', False)
|
||||
|
||||
# enabled
|
||||
f.set_node('enabled', colander.Boolean())
|
||||
|
||||
def persist(self, setting):
|
||||
""" """
|
||||
session = self.Session()
|
||||
key = self.request.matchdict['key']
|
||||
|
||||
def save(name, value):
|
||||
self.app.save_setting(session, f'{self.config.appname}.email.{key}.{name}', value)
|
||||
|
||||
def delete(name):
|
||||
self.app.delete_setting(session, f'{self.config.appname}.email.{key}.{name}')
|
||||
|
||||
# subject
|
||||
if setting['subject']:
|
||||
save('subject', setting['subject'])
|
||||
else:
|
||||
delete('subject')
|
||||
|
||||
# sender
|
||||
if setting['sender']:
|
||||
save('sender', setting['sender'])
|
||||
else:
|
||||
delete('sender')
|
||||
|
||||
# replyto
|
||||
if setting['replyto']:
|
||||
save('replyto', setting['replyto'])
|
||||
else:
|
||||
delete('replyto')
|
||||
|
||||
# to
|
||||
if setting['to']:
|
||||
save('to', setting['to'])
|
||||
else:
|
||||
delete('to')
|
||||
|
||||
# cc
|
||||
if setting['cc']:
|
||||
save('cc', setting['cc'])
|
||||
else:
|
||||
delete('cc')
|
||||
|
||||
# bcc
|
||||
if setting['bcc']:
|
||||
save('bcc', setting['bcc'])
|
||||
else:
|
||||
delete('bcc')
|
||||
|
||||
# notes
|
||||
if setting['notes']:
|
||||
save('notes', setting['notes'])
|
||||
else:
|
||||
delete('notes')
|
||||
|
||||
# enabled
|
||||
save('enabled', 'true' if setting['enabled'] else 'false')
|
||||
|
||||
def render_to_response(self, template, context):
|
||||
""" """
|
||||
if self.viewing:
|
||||
setting = context['instance']
|
||||
context['setting'] = setting
|
||||
context['has_html_template'] = self.email_handler.get_auto_body_template(
|
||||
setting['key'], 'html')
|
||||
context['has_txt_template'] = self.email_handler.get_auto_body_template(
|
||||
setting['key'], 'txt')
|
||||
|
||||
return super().render_to_response(template, context)
|
||||
|
||||
def preview(self):
|
||||
"""
|
||||
View for showing a rendered preview of a given email template.
|
||||
|
||||
This will render the email template according to the "mode"
|
||||
requested - i.e. HTML or TXT.
|
||||
"""
|
||||
key = self.request.matchdict['key']
|
||||
setting = self.email_handler.get_email_setting(key)
|
||||
context = setting.sample_data()
|
||||
mode = self.request.params.get('mode', 'html')
|
||||
|
||||
if mode == 'txt':
|
||||
body = self.email_handler.get_auto_txt_body(key, context)
|
||||
self.request.response.content_type = 'text/plain'
|
||||
|
||||
else: # html
|
||||
body = self.email_handler.get_auto_html_body(key, context)
|
||||
|
||||
self.request.response.text = body
|
||||
return self.request.response
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
""" """
|
||||
cls._email_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _email_defaults(cls, config):
|
||||
""" """
|
||||
route_prefix = cls.get_route_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
model_title_plural = cls.get_model_title_plural()
|
||||
instance_url_prefix = cls.get_instance_url_prefix()
|
||||
|
||||
# fix permission group
|
||||
config.add_wutta_permission_group(permission_prefix,
|
||||
model_title_plural,
|
||||
overwrite=False)
|
||||
|
||||
# preview
|
||||
config.add_route(f'{route_prefix}.preview',
|
||||
f'{instance_url_prefix}/preview')
|
||||
config.add_view(cls, attr='preview',
|
||||
route_name=f'{route_prefix}.preview',
|
||||
permission=f'{permission_prefix}.view')
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
EmailSettingView = kwargs.get('EmailSettingView', base['EmailSettingView'])
|
||||
EmailSettingView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
|
@ -31,6 +31,7 @@ That will in turn include the following modules:
|
|||
|
||||
* :mod:`wuttaweb.views.common`
|
||||
* :mod:`wuttaweb.views.auth`
|
||||
* :mod:`wuttaweb.views.email`
|
||||
* :mod:`wuttaweb.views.settings`
|
||||
* :mod:`wuttaweb.views.progress`
|
||||
* :mod:`wuttaweb.views.people`
|
||||
|
@ -45,6 +46,7 @@ def defaults(config, **kwargs):
|
|||
|
||||
config.include(mod('wuttaweb.views.common'))
|
||||
config.include(mod('wuttaweb.views.auth'))
|
||||
config.include(mod('wuttaweb.views.email'))
|
||||
config.include(mod('wuttaweb.views.settings'))
|
||||
config.include(mod('wuttaweb.views.progress'))
|
||||
config.include(mod('wuttaweb.views.people'))
|
||||
|
|
|
@ -392,3 +392,47 @@ class TestFileDownload(DataTestCase):
|
|||
widget = typ.widget_maker()
|
||||
self.assertIsInstance(widget, widgets.FileDownloadWidget)
|
||||
self.assertEqual(widget.url, '/foo')
|
||||
|
||||
|
||||
class TestEmailRecipients(TestCase):
|
||||
|
||||
def test_serialize(self):
|
||||
typ = mod.EmailRecipients()
|
||||
node = colander.SchemaNode(typ)
|
||||
|
||||
recips = [
|
||||
'alice@example.com',
|
||||
'bob@example.com',
|
||||
]
|
||||
recips_str = ', '.join(recips)
|
||||
|
||||
# values
|
||||
result = typ.serialize(node, recips_str)
|
||||
self.assertEqual(result, '\n'.join(recips))
|
||||
|
||||
# null
|
||||
result = typ.serialize(node, colander.null)
|
||||
self.assertIs(result, colander.null)
|
||||
|
||||
def test_deserialize(self):
|
||||
typ = mod.EmailRecipients()
|
||||
node = colander.SchemaNode(typ)
|
||||
|
||||
recips = [
|
||||
'alice@example.com',
|
||||
'bob@example.com',
|
||||
]
|
||||
recips_str = ', '.join(recips)
|
||||
|
||||
# values
|
||||
result = typ.deserialize(node, recips_str)
|
||||
self.assertEqual(result, recips_str)
|
||||
|
||||
# null
|
||||
result = typ.deserialize(node, colander.null)
|
||||
self.assertIs(result, colander.null)
|
||||
|
||||
def test_widget_maker(self):
|
||||
typ = mod.EmailRecipients()
|
||||
widget = typ.widget_maker()
|
||||
self.assertIsInstance(widget, widgets.EmailRecipientsWidget)
|
||||
|
|
|
@ -10,7 +10,7 @@ from pyramid import testing
|
|||
from wuttaweb import grids
|
||||
from wuttaweb.forms import widgets as mod
|
||||
from wuttaweb.forms.schema import (FileDownload, PersonRef, RoleRefs, UserRefs, Permissions,
|
||||
WuttaDateTime)
|
||||
WuttaDateTime, EmailRecipients)
|
||||
from tests.util import WebTestCase
|
||||
|
||||
|
||||
|
@ -304,6 +304,55 @@ class TestPermissionsWidget(WebTestCase):
|
|||
self.assertIn("Polish the widgets", html)
|
||||
|
||||
|
||||
class TestEmailRecipientsWidget(WebTestCase):
|
||||
|
||||
def make_field(self, node, **kwargs):
|
||||
# TODO: not sure why default renderer is in use even though
|
||||
# pyramid_deform was included in setup? but this works..
|
||||
kwargs.setdefault('renderer', deform.Form.default_renderer)
|
||||
return deform.Field(node, **kwargs)
|
||||
|
||||
def test_serialize(self):
|
||||
node = colander.SchemaNode(EmailRecipients())
|
||||
field = self.make_field(node)
|
||||
widget = mod.EmailRecipientsWidget()
|
||||
|
||||
recips = [
|
||||
'alice@example.com',
|
||||
'bob@example.com',
|
||||
]
|
||||
recips_str = ', '.join(recips)
|
||||
|
||||
# readonly
|
||||
result = widget.serialize(field, recips_str, readonly=True)
|
||||
self.assertIn('<ul>', result)
|
||||
self.assertIn('<li>alice@example.com</li>', result)
|
||||
|
||||
# editable
|
||||
result = widget.serialize(field, recips_str)
|
||||
self.assertIn('<b-input', result)
|
||||
self.assertIn('type="textarea"', result)
|
||||
|
||||
def test_deserialize(self):
|
||||
node = colander.SchemaNode(EmailRecipients())
|
||||
field = self.make_field(node)
|
||||
widget = mod.EmailRecipientsWidget()
|
||||
|
||||
recips = [
|
||||
'alice@example.com',
|
||||
'bob@example.com',
|
||||
]
|
||||
recips_str = ', '.join(recips)
|
||||
|
||||
# values
|
||||
result = widget.deserialize(field, recips_str)
|
||||
self.assertEqual(result, recips_str)
|
||||
|
||||
# null
|
||||
result = widget.deserialize(field, colander.null)
|
||||
self.assertIs(result, colander.null)
|
||||
|
||||
|
||||
class TestBatchIdWidget(WebTestCase):
|
||||
|
||||
def make_field(self, node, **kwargs):
|
||||
|
|
23
tests/test_emails.py
Normal file
23
tests/test_emails.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from wuttjamaican.testing import DataTestCase
|
||||
from wuttjamaican.email import EmailSetting
|
||||
|
||||
from wuttaweb import emails as mod
|
||||
|
||||
|
||||
class TestAllSettings(DataTestCase):
|
||||
|
||||
def check_setting(self, setting):
|
||||
self.assertIsNotNone(setting.default_subject)
|
||||
setting = setting(self.config)
|
||||
context = setting.sample_data()
|
||||
self.assertIsInstance(context, dict)
|
||||
|
||||
def test_all(self):
|
||||
for name in dir(mod):
|
||||
obj = getattr(mod, name)
|
||||
if (isinstance(obj, type)
|
||||
and obj is not EmailSetting
|
||||
and issubclass(obj, EmailSetting)):
|
||||
self.check_setting(obj)
|
211
tests/views/test_email.py
Normal file
211
tests/views/test_email.py
Normal file
|
@ -0,0 +1,211 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from wuttjamaican.email import EmailSetting
|
||||
|
||||
import colander
|
||||
from pyramid.httpexceptions import HTTPNotFound
|
||||
from pyramid.response import Response
|
||||
|
||||
from wuttaweb.views import email as mod
|
||||
from tests.util import WebTestCase
|
||||
|
||||
|
||||
class TestEmailSettingViews(WebTestCase):
|
||||
|
||||
def make_view(self):
|
||||
return mod.EmailSettingView(self.request)
|
||||
|
||||
def test_includeme(self):
|
||||
self.pyramid_config.include('wuttaweb.views.email')
|
||||
|
||||
def test_get_grid_data(self):
|
||||
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
|
||||
view = self.make_view()
|
||||
data = view.get_grid_data()
|
||||
self.assertIsInstance(data, list)
|
||||
self.assertTrue(data) # 1+ items
|
||||
setting = data[0]
|
||||
self.assertIn('key', setting)
|
||||
self.assertIn('subject', setting)
|
||||
self.assertIn('sender', setting)
|
||||
self.assertIn('to', setting)
|
||||
self.assertIn('cc', setting)
|
||||
self.assertIn('notes', setting)
|
||||
|
||||
def test_configure_grid(self):
|
||||
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
|
||||
view = self.make_view()
|
||||
grid = view.make_model_grid()
|
||||
self.assertIn('key', grid.searchable_columns)
|
||||
self.assertIn('subject', grid.searchable_columns)
|
||||
|
||||
def test_render_to_short(self):
|
||||
view = self.make_view()
|
||||
setting = EmailSetting(self.config)
|
||||
|
||||
# more than 2 recips
|
||||
result = view.render_to_short(setting, 'to', [
|
||||
'alice@example.com',
|
||||
'bob@example.com',
|
||||
'charlie@example.com',
|
||||
'diana@example.com',
|
||||
])
|
||||
self.assertEqual(result, 'alice@example.com, bob@example.com, ...')
|
||||
|
||||
# just 2 recips
|
||||
result = view.render_to_short(setting, 'to', [
|
||||
'alice@example.com',
|
||||
'bob@example.com',
|
||||
])
|
||||
self.assertEqual(result, 'alice@example.com, bob@example.com')
|
||||
|
||||
# just 1 recip
|
||||
result = view.render_to_short(setting, 'to', ['alice@example.com'])
|
||||
self.assertEqual(result, 'alice@example.com')
|
||||
|
||||
# no recips
|
||||
result = view.render_to_short(setting, 'to', [])
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_instance(self):
|
||||
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
|
||||
view = self.make_view()
|
||||
|
||||
# normal
|
||||
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
|
||||
setting = view.get_instance()
|
||||
self.assertIsInstance(setting, dict)
|
||||
self.assertIn('key', setting)
|
||||
self.assertIn('sender', setting)
|
||||
self.assertIn('subject', setting)
|
||||
self.assertIn('to', setting)
|
||||
self.assertIn('cc', setting)
|
||||
self.assertIn('notes', setting)
|
||||
self.assertIn('enabled', setting)
|
||||
|
||||
# not found
|
||||
with patch.object(self.request, 'matchdict', new={'key': 'this-should_notEXIST'}):
|
||||
self.assertRaises(HTTPNotFound, view.get_instance)
|
||||
|
||||
def test_get_instance_title(self):
|
||||
view = self.make_view()
|
||||
result = view.get_instance_title({'subject': 'whatever'})
|
||||
self.assertEqual(result, 'whatever')
|
||||
|
||||
def test_configure_form(self):
|
||||
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
|
||||
view = self.make_view()
|
||||
|
||||
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
|
||||
setting = view.get_instance()
|
||||
form = view.make_model_form(setting)
|
||||
self.assertIn('description', form.readonly_fields)
|
||||
self.assertFalse(form.required_fields['replyto'])
|
||||
|
||||
def test_persist(self):
|
||||
model = self.app.model
|
||||
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
|
||||
view = self.make_view()
|
||||
|
||||
# start w/ no settings in db
|
||||
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
||||
|
||||
# "edit" settings for feedback email
|
||||
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
|
||||
setting = view.get_instance()
|
||||
setting['subject'] = 'Testing Feedback'
|
||||
setting['sender'] = 'feedback@example.com'
|
||||
setting['replyto'] = 'feedback4@example.com'
|
||||
setting['to'] = 'feedback@example.com'
|
||||
setting['cc'] = 'feedback2@example.com'
|
||||
setting['bcc'] = 'feedback3@example.com'
|
||||
setting['notes'] = "did this work?"
|
||||
setting['enabled'] = True
|
||||
|
||||
# persist email settings
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
view.persist(setting)
|
||||
self.session.commit()
|
||||
|
||||
# check settings in db
|
||||
self.assertEqual(self.session.query(model.Setting).count(), 8)
|
||||
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.subject'),
|
||||
"Testing Feedback")
|
||||
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.sender'),
|
||||
'feedback@example.com')
|
||||
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.replyto'),
|
||||
'feedback4@example.com')
|
||||
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.to'),
|
||||
'feedback@example.com')
|
||||
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.cc'),
|
||||
'feedback2@example.com')
|
||||
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.bcc'),
|
||||
'feedback3@example.com')
|
||||
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.notes'),
|
||||
"did this work?")
|
||||
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.enabled'),
|
||||
'true')
|
||||
|
||||
# "edit" settings for feedback email
|
||||
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
|
||||
setting = view.get_instance()
|
||||
setting['subject'] = None
|
||||
setting['sender'] = None
|
||||
setting['replyto'] = None
|
||||
setting['to'] = None
|
||||
setting['cc'] = None
|
||||
setting['bcc'] = None
|
||||
setting['notes'] = None
|
||||
setting['enabled'] = False
|
||||
|
||||
# persist email settings
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
view.persist(setting)
|
||||
self.session.commit()
|
||||
|
||||
# check settings in db
|
||||
self.assertEqual(self.session.query(model.Setting).count(), 1)
|
||||
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.subject'))
|
||||
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.sender'))
|
||||
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.replyto'))
|
||||
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.to'))
|
||||
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.cc'))
|
||||
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.bcc'))
|
||||
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.notes'))
|
||||
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.enabled'),
|
||||
'false')
|
||||
|
||||
def test_render_to_response(self):
|
||||
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
|
||||
self.pyramid_config.add_route('home', '/')
|
||||
self.pyramid_config.add_route('login', '/auth/login')
|
||||
self.pyramid_config.add_route('email_settings', '/email/settings')
|
||||
self.pyramid_config.add_route('email_settings.preview', '/email/settings/{key}/preview')
|
||||
view = self.make_view()
|
||||
|
||||
# nb. this gives coverage, but tests nothing..
|
||||
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
|
||||
setting = view.get_instance()
|
||||
with patch.object(view, 'viewing', new=True):
|
||||
context = {'instance': setting}
|
||||
response = view.render_to_response('view', context)
|
||||
self.assertIsInstance(response, Response)
|
||||
|
||||
def test_preview(self):
|
||||
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
|
||||
view = self.make_view()
|
||||
|
||||
# nb. this gives coverage, but tests nothing..
|
||||
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
|
||||
|
||||
# html
|
||||
with patch.object(self.request, 'params', new={'mode': 'html'}):
|
||||
response = view.preview()
|
||||
self.assertEqual(response.content_type, 'text/html')
|
||||
|
||||
# txt
|
||||
with patch.object(self.request, 'params', new={'mode': 'txt'}):
|
||||
response = view.preview()
|
||||
self.assertEqual(response.content_type, 'text/plain')
|
10
tests/views/test_essential.py
Normal file
10
tests/views/test_essential.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from wuttaweb.views import essential as mod
|
||||
from tests.util import WebTestCase
|
||||
|
||||
|
||||
class TestEssentialViews(WebTestCase):
|
||||
|
||||
def test_includeme(self):
|
||||
self.pyramid_config.include('wuttaweb.views.essential')
|
Loading…
Reference in a new issue