Compare commits
No commits in common. "5de9c15bbdc85d992af5ceba2aa73f5fca0ce70b" and "37cf3c4400b962d31027751920a61531e11c4193" have entirely different histories.
5de9c15bbd
...
37cf3c4400
13 changed files with 50 additions and 625 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -5,17 +5,6 @@ All notable changes to WuttJamaican will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## v0.27.0 (2025-12-20)
|
|
||||||
|
|
||||||
### Feat
|
|
||||||
|
|
||||||
- add simple Diff class, to render common table
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- add `fallback_key` support for email settings
|
|
||||||
- include thousands separator for `app.render_quantity()`
|
|
||||||
|
|
||||||
## v0.26.0 (2025-12-17)
|
## v0.26.0 (2025-12-17)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
``wuttjamaican.diffs``
|
|
||||||
======================
|
|
||||||
|
|
||||||
.. automodule:: wuttjamaican.diffs
|
|
||||||
:members:
|
|
||||||
|
|
@ -203,11 +203,10 @@ Glossary
|
||||||
setting` definitions.
|
setting` definitions.
|
||||||
|
|
||||||
email setting
|
email setting
|
||||||
This refers to the :term:`config settings <config setting>` for a
|
This refers to the settings for a particular :term:`email type`,
|
||||||
particular :term:`email type`, i.e. its sender and recipients,
|
i.e. its sender and recipients, subject etc. So each email type
|
||||||
subject etc. So each email type has a "collection" of settings,
|
has a "collection" of settings, and that collection is referred
|
||||||
and that collection is referred to simply as an "email setting"
|
to simply as an "email setting" in the singular.
|
||||||
in the singular.
|
|
||||||
|
|
||||||
email template
|
email template
|
||||||
Usually this refers to the HTML or TXT template file, used to
|
Usually this refers to the HTML or TXT template file, used to
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ Contents
|
||||||
api/wuttjamaican.db.model.upgrades
|
api/wuttjamaican.db.model.upgrades
|
||||||
api/wuttjamaican.db.sess
|
api/wuttjamaican.db.sess
|
||||||
api/wuttjamaican.db.util
|
api/wuttjamaican.db.util
|
||||||
api/wuttjamaican.diffs
|
|
||||||
api/wuttjamaican.email
|
api/wuttjamaican.email
|
||||||
api/wuttjamaican.enum
|
api/wuttjamaican.enum
|
||||||
api/wuttjamaican.exc
|
api/wuttjamaican.exc
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ Basics
|
||||||
|
|
||||||
To send an email you (usually) need 3 things:
|
To send an email you (usually) need 3 things:
|
||||||
|
|
||||||
* key - unique key identifying the :term:`email type`
|
* key - unique key identifying the type of email
|
||||||
* template - :term:`email template` file to render message body
|
* template - template file to render message body
|
||||||
* context - context dict for template file rendering
|
* context - context dict for template file rendering
|
||||||
|
|
||||||
And actually the template just needs to exist somewhere it can be
|
And actually the template just needs to exist somewhere it can be
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttJamaican"
|
name = "WuttJamaican"
|
||||||
version = "0.27.0"
|
version = "0.26.0"
|
||||||
description = "Base package for Wutta Framework"
|
description = "Base package for Wutta Framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||||
|
|
@ -37,7 +37,6 @@ dependencies = [
|
||||||
"python-configuration",
|
"python-configuration",
|
||||||
"typer",
|
"typer",
|
||||||
"uuid7",
|
"uuid7",
|
||||||
"WebHelpers2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -962,8 +962,8 @@ class AppHandler: # pylint: disable=too-many-public-methods
|
||||||
value = int(value)
|
value = int(value)
|
||||||
if empty_zero and value == 0:
|
if empty_zero and value == 0:
|
||||||
return ""
|
return ""
|
||||||
return f"{value:,}"
|
return str(value)
|
||||||
return f"{value:,}".rstrip("0")
|
return str(value).rstrip("0")
|
||||||
|
|
||||||
def render_time_ago(self, value):
|
def render_time_ago(self, value):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttJamaican -- Base package for Wutta Framework
|
|
||||||
# Copyright © 2023-2025 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/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Tools for displaying simple data diffs
|
|
||||||
"""
|
|
||||||
|
|
||||||
from mako.template import Template
|
|
||||||
from webhelpers2.html import HTML
|
|
||||||
|
|
||||||
|
|
||||||
class Diff: # pylint: disable=too-many-instance-attributes
|
|
||||||
"""
|
|
||||||
Represent / display a basic "diff" between two data records.
|
|
||||||
|
|
||||||
You must provide both the "old" and "new" data records, when
|
|
||||||
constructing an instance of this class. Then call
|
|
||||||
:meth:`render_html()` to display the diff table.
|
|
||||||
|
|
||||||
:param config: The app :term:`config object`.
|
|
||||||
|
|
||||||
:param old_data: Dict of "old" data record.
|
|
||||||
|
|
||||||
:param new_data: Dict of "new" data record.
|
|
||||||
|
|
||||||
:param fields: Optional list of field names. If not specified,
|
|
||||||
will be derived from the data records.
|
|
||||||
|
|
||||||
:param nature: What sort of diff is being represented; must be one
|
|
||||||
of: ``("create", "update", "delete")``
|
|
||||||
|
|
||||||
:param old_color: Background color to display for "old/deleted"
|
|
||||||
field data, when applicable.
|
|
||||||
|
|
||||||
:param new_color: Background color to display for "new/created"
|
|
||||||
field data, when applicable.
|
|
||||||
|
|
||||||
:param cell_padding: Optional override for cell padding style.
|
|
||||||
"""
|
|
||||||
|
|
||||||
cell_padding = "0.25rem"
|
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
||||||
self,
|
|
||||||
config,
|
|
||||||
old_data: dict,
|
|
||||||
new_data: dict,
|
|
||||||
fields: list = None,
|
|
||||||
nature="update",
|
|
||||||
old_color="#ffebe9",
|
|
||||||
new_color="#dafbe1",
|
|
||||||
cell_padding=None,
|
|
||||||
):
|
|
||||||
self.config = config
|
|
||||||
self.app = self.config.get_app()
|
|
||||||
self.old_data = old_data
|
|
||||||
self.new_data = new_data
|
|
||||||
self.columns = ["field name", "old value", "new value"]
|
|
||||||
self.fields = fields or self.make_fields()
|
|
||||||
self.nature = nature
|
|
||||||
self.old_color = old_color
|
|
||||||
self.new_color = new_color
|
|
||||||
if cell_padding:
|
|
||||||
self.cell_padding = cell_padding
|
|
||||||
|
|
||||||
def make_fields(self): # pylint: disable=missing-function-docstring
|
|
||||||
return sorted(set(self.old_data) | set(self.new_data), key=lambda x: x.lower())
|
|
||||||
|
|
||||||
def render_html(self, template=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Render the diff as HTML table.
|
|
||||||
|
|
||||||
:param template: Name of template to render, if you need to
|
|
||||||
override the default.
|
|
||||||
|
|
||||||
:param \\**kwargs: Remaining kwargs are passed as context to
|
|
||||||
the template renderer.
|
|
||||||
|
|
||||||
:returns: HTML literal string
|
|
||||||
"""
|
|
||||||
context = kwargs
|
|
||||||
context["diff"] = self
|
|
||||||
|
|
||||||
if not isinstance(template, Template):
|
|
||||||
path = self.app.resource_path(
|
|
||||||
template or "wuttjamaican:templates/diff.mako"
|
|
||||||
)
|
|
||||||
template = Template(filename=path)
|
|
||||||
|
|
||||||
return HTML.literal(template.render(**context))
|
|
||||||
|
|
||||||
def render_field_row(self, field): # pylint: disable=missing-function-docstring
|
|
||||||
is_diff = self.values_differ(field)
|
|
||||||
|
|
||||||
kw = {}
|
|
||||||
if self.cell_padding:
|
|
||||||
kw["style"] = f"padding: {self.cell_padding}"
|
|
||||||
td_field = HTML.tag("td", class_="field", c=field, **kw)
|
|
||||||
|
|
||||||
td_old_value = HTML.tag(
|
|
||||||
"td",
|
|
||||||
c=self.render_old_value(field),
|
|
||||||
**self.get_old_value_attrs(is_diff),
|
|
||||||
)
|
|
||||||
|
|
||||||
td_new_value = HTML.tag(
|
|
||||||
"td",
|
|
||||||
c=self.render_new_value(field),
|
|
||||||
**self.get_new_value_attrs(is_diff),
|
|
||||||
)
|
|
||||||
|
|
||||||
return HTML.tag("tr", c=[td_field, td_old_value, td_new_value])
|
|
||||||
|
|
||||||
def render_cell_value(self, value): # pylint: disable=missing-function-docstring
|
|
||||||
return HTML.tag("span", c=[value], style="font-family: monospace;")
|
|
||||||
|
|
||||||
def render_old_value(self, field): # pylint: disable=missing-function-docstring
|
|
||||||
value = "" if self.nature == "create" else repr(self.old_value(field))
|
|
||||||
return self.render_cell_value(value)
|
|
||||||
|
|
||||||
def render_new_value(self, field): # pylint: disable=missing-function-docstring
|
|
||||||
value = "" if self.nature == "delete" else repr(self.new_value(field))
|
|
||||||
return self.render_cell_value(value)
|
|
||||||
|
|
||||||
def get_cell_attrs( # pylint: disable=missing-function-docstring
|
|
||||||
self, style=None, **attrs
|
|
||||||
):
|
|
||||||
style = dict(style or {})
|
|
||||||
|
|
||||||
if self.cell_padding and "padding" not in style:
|
|
||||||
style["padding"] = self.cell_padding
|
|
||||||
|
|
||||||
if style:
|
|
||||||
attrs["style"] = "; ".join([f"{k}: {v}" for k, v in style.items()])
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def get_old_value_attrs( # pylint: disable=missing-function-docstring
|
|
||||||
self, is_diff
|
|
||||||
):
|
|
||||||
style = {}
|
|
||||||
if self.nature == "update" and is_diff:
|
|
||||||
style["background-color"] = self.old_color
|
|
||||||
elif self.nature == "delete":
|
|
||||||
style["background-color"] = self.old_color
|
|
||||||
|
|
||||||
return self.get_cell_attrs(style)
|
|
||||||
|
|
||||||
def get_new_value_attrs( # pylint: disable=missing-function-docstring
|
|
||||||
self, is_diff
|
|
||||||
):
|
|
||||||
style = {}
|
|
||||||
if self.nature == "create":
|
|
||||||
style["background-color"] = self.new_color
|
|
||||||
elif self.nature == "update" and is_diff:
|
|
||||||
style["background-color"] = self.new_color
|
|
||||||
|
|
||||||
return self.get_cell_attrs(style)
|
|
||||||
|
|
||||||
def old_value(self, field): # pylint: disable=missing-function-docstring
|
|
||||||
return self.old_data.get(field)
|
|
||||||
|
|
||||||
def new_value(self, field): # pylint: disable=missing-function-docstring
|
|
||||||
return self.new_data.get(field)
|
|
||||||
|
|
||||||
def values_differ(self, field): # pylint: disable=missing-function-docstring
|
|
||||||
return self.new_value(field) != self.old_value(field)
|
|
||||||
|
|
@ -23,10 +23,8 @@
|
||||||
"""
|
"""
|
||||||
Email Handler
|
Email Handler
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-lines
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
@ -62,7 +60,6 @@ class EmailSetting: # pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
default_subject = "Something happened!"
|
default_subject = "Something happened!"
|
||||||
|
|
||||||
# nb. this is not used for sending; only preview
|
|
||||||
def sample_data(self):
|
def sample_data(self):
|
||||||
return {
|
return {
|
||||||
'foo': 1234,
|
'foo': 1234,
|
||||||
|
|
@ -87,80 +84,23 @@ class EmailSetting: # pylint: disable=too-few-public-methods
|
||||||
* allows for hard-coded sample context which can be used to render
|
* allows for hard-coded sample context which can be used to render
|
||||||
templates for preview
|
templates for preview
|
||||||
|
|
||||||
.. attribute:: key
|
|
||||||
|
|
||||||
Unique identifier for this :term:`email type`.
|
|
||||||
|
|
||||||
This is the :term:`email key` used for config/template lookup,
|
|
||||||
e.g. when sending an email.
|
|
||||||
|
|
||||||
This is automatically set based on the *class name* so there is
|
|
||||||
no need (or point) to set it. But the attribute is here for
|
|
||||||
read access, for convenience / code readability::
|
|
||||||
|
|
||||||
class poser_alert_foo(EmailSetting):
|
|
||||||
default_subject = "Something happened!"
|
|
||||||
|
|
||||||
handler = app.get_email_handler()
|
|
||||||
setting = handler.get_email_setting("poser_alert_foo")
|
|
||||||
assert setting.key == "poser_alert_foo"
|
|
||||||
|
|
||||||
See also :attr:`fallback_key`.
|
|
||||||
|
|
||||||
.. attribute:: default_subject
|
.. attribute:: default_subject
|
||||||
|
|
||||||
Default subject for sending emails of this type.
|
Default :attr:`Message.subject` for the email, if none is
|
||||||
|
configured.
|
||||||
Usually, if config does not override, this will become
|
|
||||||
:attr:`Message.subject`.
|
|
||||||
|
|
||||||
This is technically a Mako template string, so it will be
|
This is technically a Mako template string, so it will be
|
||||||
rendered with the email context. But in most cases that
|
rendered with the email context. But in most cases that
|
||||||
feature can be ignored, and this will be a simple string.
|
feature can be ignored, and this will be a simple string.
|
||||||
|
|
||||||
Calling code should not access this directly, but instead use
|
|
||||||
:meth:`get_default_subject()` .
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
default_subject = None
|
default_subject = None
|
||||||
|
|
||||||
fallback_key = None
|
|
||||||
"""
|
|
||||||
Optional fallback key to use for config/template lookup, if
|
|
||||||
nothing is found for :attr:`key`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.app = config.get_app()
|
self.app = config.get_app()
|
||||||
self.key = self.__class__.__name__
|
self.key = self.__class__.__name__
|
||||||
|
|
||||||
def get_description(self):
|
|
||||||
"""
|
|
||||||
This must return the full description for the :term:`email
|
|
||||||
type`. It is not used for the sending of email; only for
|
|
||||||
settings administration.
|
|
||||||
|
|
||||||
Default logic will use the class docstring.
|
|
||||||
|
|
||||||
:returns: String description for the email type
|
|
||||||
"""
|
|
||||||
return self.__class__.__doc__.strip()
|
|
||||||
|
|
||||||
def get_default_subject(self):
|
|
||||||
"""
|
|
||||||
This must return the default subject, for sending emails of
|
|
||||||
this type.
|
|
||||||
|
|
||||||
If config does not override, this will become
|
|
||||||
:attr:`Message.subject`.
|
|
||||||
|
|
||||||
Default logic here returns :attr:`default_subject` as-is.
|
|
||||||
|
|
||||||
:returns: Default subject as string
|
|
||||||
"""
|
|
||||||
return self.default_subject
|
|
||||||
|
|
||||||
def sample_data(self):
|
def sample_data(self):
|
||||||
"""
|
"""
|
||||||
Should return a dict with sample context needed to render the
|
Should return a dict with sample context needed to render the
|
||||||
|
|
@ -385,18 +325,13 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
"""
|
"""
|
||||||
if "email_settings" not in self.classes:
|
if "email_settings" not in self.classes:
|
||||||
self.classes["email_settings"] = {}
|
self.classes["email_settings"] = {}
|
||||||
|
|
||||||
# nb. we only want lower_case_names - all UpperCaseNames
|
|
||||||
# are assumed to be base classes
|
|
||||||
pattern = re.compile(r"^[a-z]")
|
|
||||||
|
|
||||||
for module in self.get_email_modules():
|
for module in self.get_email_modules():
|
||||||
for name in dir(module):
|
for name in dir(module):
|
||||||
obj = getattr(module, name)
|
obj = getattr(module, name)
|
||||||
if (
|
if (
|
||||||
isinstance(obj, type)
|
isinstance(obj, type)
|
||||||
|
and obj is not EmailSetting
|
||||||
and issubclass(obj, EmailSetting)
|
and issubclass(obj, EmailSetting)
|
||||||
and pattern.match(obj.__name__)
|
|
||||||
):
|
):
|
||||||
self.classes["email_settings"][obj.__name__] = obj
|
self.classes["email_settings"][obj.__name__] = obj
|
||||||
|
|
||||||
|
|
@ -433,9 +368,7 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
"""
|
"""
|
||||||
return Message(**kwargs)
|
return Message(**kwargs)
|
||||||
|
|
||||||
def make_auto_message(
|
def make_auto_message(self, key, context=None, default_subject=None, **kwargs):
|
||||||
self, key, context=None, default_subject=None, fallback_key=None, **kwargs
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Make a new email message using config to determine its
|
Make a new email message using config to determine its
|
||||||
properties, and auto-generating body from a template.
|
properties, and auto-generating body from a template.
|
||||||
|
|
@ -446,8 +379,7 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
:param key: Unique key for this particular "type" of message.
|
:param key: Unique key for this particular "type" of message.
|
||||||
This key is used as a prefix for all config settings and
|
This key is used as a prefix for all config settings and
|
||||||
template names pertinent to the message. See also the
|
template names pertinent to the message.
|
||||||
``fallback_key`` param, below.
|
|
||||||
|
|
||||||
:param context: Context dict used to render template(s) for
|
:param context: Context dict used to render template(s) for
|
||||||
the message.
|
the message.
|
||||||
|
|
@ -455,10 +387,6 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
:param default_subject: Optional :attr:`~Message.subject`
|
:param default_subject: Optional :attr:`~Message.subject`
|
||||||
template/string to use, if config does not specify one.
|
template/string to use, if config does not specify one.
|
||||||
|
|
||||||
:param fallback_key: Optional fallback :term:`email key` to
|
|
||||||
use for config/template lookup, if nothing is found for
|
|
||||||
``key``.
|
|
||||||
|
|
||||||
:param \\**kwargs: Any remaining kwargs are passed as-is to
|
:param \\**kwargs: Any remaining kwargs are passed as-is to
|
||||||
:meth:`make_message()`. More on this below.
|
:meth:`make_message()`. More on this below.
|
||||||
|
|
||||||
|
|
@ -483,7 +411,7 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
kwargs["sender"] = self.get_auto_sender(key)
|
kwargs["sender"] = self.get_auto_sender(key)
|
||||||
if "subject" not in kwargs:
|
if "subject" not in kwargs:
|
||||||
kwargs["subject"] = self.get_auto_subject(
|
kwargs["subject"] = self.get_auto_subject(
|
||||||
key, context, default=default_subject, fallback_key=fallback_key
|
key, context, default=default_subject
|
||||||
)
|
)
|
||||||
if "to" not in kwargs:
|
if "to" not in kwargs:
|
||||||
kwargs["to"] = self.get_auto_to(key)
|
kwargs["to"] = self.get_auto_to(key)
|
||||||
|
|
@ -492,47 +420,11 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
if "bcc" not in kwargs:
|
if "bcc" not in kwargs:
|
||||||
kwargs["bcc"] = self.get_auto_bcc(key)
|
kwargs["bcc"] = self.get_auto_bcc(key)
|
||||||
if "txt_body" not in kwargs:
|
if "txt_body" not in kwargs:
|
||||||
kwargs["txt_body"] = self.get_auto_txt_body(
|
kwargs["txt_body"] = self.get_auto_txt_body(key, context)
|
||||||
key, context, fallback_key=fallback_key
|
|
||||||
)
|
|
||||||
if "html_body" not in kwargs:
|
if "html_body" not in kwargs:
|
||||||
kwargs["html_body"] = self.get_auto_html_body(
|
kwargs["html_body"] = self.get_auto_html_body(key, context)
|
||||||
key, context, fallback_key=fallback_key
|
|
||||||
)
|
|
||||||
return self.make_message(**kwargs)
|
return self.make_message(**kwargs)
|
||||||
|
|
||||||
def get_email_context(self, key, context=None): # pylint: disable=unused-argument
|
|
||||||
"""
|
|
||||||
This must return the "full" context for rendering the email
|
|
||||||
subject and/or body templates.
|
|
||||||
|
|
||||||
Normally the input ``context`` is coming from the
|
|
||||||
:meth:`send_email()` param of the same name.
|
|
||||||
|
|
||||||
By default, this method modifies the input context to add the
|
|
||||||
following:
|
|
||||||
|
|
||||||
* ``config`` - reference to the :term:`config object`
|
|
||||||
* ``app`` - reference to the :term:`app handler`
|
|
||||||
|
|
||||||
Subclass may further modify as needed.
|
|
||||||
|
|
||||||
:param key: The :term:`email key` for which to get context.
|
|
||||||
|
|
||||||
:param context: Input context dict.
|
|
||||||
|
|
||||||
:returns: Final context dict
|
|
||||||
"""
|
|
||||||
if context is None:
|
|
||||||
context = {}
|
|
||||||
context.update(
|
|
||||||
{
|
|
||||||
"config": self.config,
|
|
||||||
"app": self.app,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_auto_sender(self, key):
|
def get_auto_sender(self, key):
|
||||||
"""
|
"""
|
||||||
Returns automatic
|
Returns automatic
|
||||||
|
|
@ -563,13 +455,7 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
return self.config.get(f"{self.config.appname}.email.default.replyto")
|
return self.config.get(f"{self.config.appname}.email.default.replyto")
|
||||||
|
|
||||||
def get_auto_subject( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
def get_auto_subject( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||||
self,
|
self, key, context=None, rendered=True, setting=None, default=None
|
||||||
key,
|
|
||||||
context=None,
|
|
||||||
rendered=True,
|
|
||||||
setting=None,
|
|
||||||
default=None,
|
|
||||||
fallback_key=None,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Returns automatic :attr:`~wuttjamaican.email.Message.subject`
|
Returns automatic :attr:`~wuttjamaican.email.Message.subject`
|
||||||
|
|
@ -578,8 +464,7 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
This calls :meth:`get_auto_subject_template()` and then
|
This calls :meth:`get_auto_subject_template()` and then
|
||||||
(usually) renders the result using the given context.
|
(usually) renders the result using the given context.
|
||||||
|
|
||||||
:param key: Key for the :term:`email type`. See also the
|
:param key: Key for the :term:`email type`.
|
||||||
``fallback_key`` param, below.
|
|
||||||
|
|
||||||
:param context: Dict of context for rendering the subject
|
:param context: Dict of context for rendering the subject
|
||||||
template, if applicable.
|
template, if applicable.
|
||||||
|
|
@ -594,23 +479,16 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
:param default: Default subject to use if none is configured.
|
:param default: Default subject to use if none is configured.
|
||||||
|
|
||||||
:param fallback_key: Optional fallback :term:`email key` to
|
|
||||||
use for config lookup, if nothing is found for ``key``.
|
|
||||||
|
|
||||||
:returns: Final subject text, either "raw" or rendered.
|
:returns: Final subject text, either "raw" or rendered.
|
||||||
"""
|
"""
|
||||||
template = self.get_auto_subject_template(
|
template = self.get_auto_subject_template(key, setting=setting, default=default)
|
||||||
key, setting=setting, default=default, fallback_key=fallback_key
|
|
||||||
)
|
|
||||||
if not rendered:
|
if not rendered:
|
||||||
return template
|
return template
|
||||||
|
|
||||||
context = self.get_email_context(key, context)
|
context = context or {}
|
||||||
return Template(template).render(**context)
|
return Template(template).render(**context)
|
||||||
|
|
||||||
def get_auto_subject_template(
|
def get_auto_subject_template(self, key, setting=None, default=None):
|
||||||
self, key, setting=None, default=None, fallback_key=None
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Returns the template string to use for automatic subject line
|
Returns the template string to use for automatic subject line
|
||||||
of a message, as determined by config.
|
of a message, as determined by config.
|
||||||
|
|
@ -631,32 +509,22 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
:param default: Default subject to use if none is configured.
|
:param default: Default subject to use if none is configured.
|
||||||
|
|
||||||
:param fallback_key: Optional fallback :term:`email key` to
|
|
||||||
use for config lookup, if nothing is found for ``key``.
|
|
||||||
|
|
||||||
:returns: Final subject template, as raw text.
|
:returns: Final subject template, as raw text.
|
||||||
"""
|
"""
|
||||||
# prefer configured subject specific to key
|
# prefer configured subject specific to key
|
||||||
if template := self.config.get(f"{self.config.appname}.email.{key}.subject"):
|
template = self.config.get(f"{self.config.appname}.email.{key}.subject")
|
||||||
|
if template:
|
||||||
return template
|
return template
|
||||||
|
|
||||||
# or use caller-specified default, if applicable
|
# or use caller-specified default, if applicable
|
||||||
if default:
|
if default:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
# or use fallback key, if provided
|
|
||||||
if fallback_key:
|
|
||||||
if template := self.config.get(
|
|
||||||
f"{self.config.appname}.email.{fallback_key}.subject"
|
|
||||||
):
|
|
||||||
return template
|
|
||||||
|
|
||||||
# or subject from email setting, if defined
|
# or subject from email setting, if defined
|
||||||
if not setting:
|
if not setting:
|
||||||
setting = self.get_email_setting(key)
|
setting = self.get_email_setting(key)
|
||||||
if setting:
|
if setting and setting.default_subject:
|
||||||
if subject := setting.get_default_subject():
|
return setting.default_subject
|
||||||
return subject
|
|
||||||
|
|
||||||
# fall back to global default
|
# fall back to global default
|
||||||
return self.config.get(
|
return self.config.get(
|
||||||
|
|
@ -701,34 +569,32 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
f"{self.config.appname}.email.default.{typ}", default=[]
|
f"{self.config.appname}.email.default.{typ}", default=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_auto_txt_body(self, key, context=None, fallback_key=None):
|
def get_auto_txt_body(self, key, context=None):
|
||||||
"""
|
"""
|
||||||
Returns automatic :attr:`~wuttjamaican.email.Message.txt_body`
|
Returns automatic :attr:`~wuttjamaican.email.Message.txt_body`
|
||||||
content for a message, as determined by config. This renders
|
content for a message, as determined by config. This renders
|
||||||
a template with the given context.
|
a template with the given context.
|
||||||
"""
|
"""
|
||||||
template = self.get_auto_body_template(key, "txt", fallback_key=fallback_key)
|
template = self.get_auto_body_template(key, "txt")
|
||||||
if template:
|
if template:
|
||||||
context = self.get_email_context(key, context)
|
context = context or {}
|
||||||
return template.render(**context)
|
return template.render(**context)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_auto_html_body(self, key, context=None, fallback_key=None):
|
def get_auto_html_body(self, key, context=None):
|
||||||
"""
|
"""
|
||||||
Returns automatic
|
Returns automatic
|
||||||
:attr:`~wuttjamaican.email.Message.html_body` content for a
|
:attr:`~wuttjamaican.email.Message.html_body` content for a
|
||||||
message, as determined by config. This renders a template
|
message, as determined by config. This renders a template
|
||||||
with the given context.
|
with the given context.
|
||||||
"""
|
"""
|
||||||
template = self.get_auto_body_template(key, "html", fallback_key=fallback_key)
|
template = self.get_auto_body_template(key, "html")
|
||||||
if template:
|
if template:
|
||||||
context = self.get_email_context(key, context)
|
context = context or {}
|
||||||
return template.render(**context)
|
return template.render(**context)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_auto_body_template( # pylint: disable=empty-docstring
|
def get_auto_body_template(self, key, mode): # pylint: disable=empty-docstring
|
||||||
self, key, mode, fallback_key=None
|
|
||||||
):
|
|
||||||
""" """
|
""" """
|
||||||
mode = mode.lower()
|
mode = mode.lower()
|
||||||
if mode == "txt":
|
if mode == "txt":
|
||||||
|
|
@ -739,19 +605,9 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
raise ValueError("requested mode not supported")
|
raise ValueError("requested mode not supported")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
# prefer specific template for key
|
|
||||||
return templates.get_template(f"{key}.{mode}.mako")
|
return templates.get_template(f"{key}.{mode}.mako")
|
||||||
|
|
||||||
except TopLevelLookupException:
|
except TopLevelLookupException:
|
||||||
|
pass
|
||||||
# but can use fallback if applicable
|
|
||||||
if fallback_key:
|
|
||||||
try:
|
|
||||||
return templates.get_template(f"{fallback_key}.{mode}.mako")
|
|
||||||
except TopLevelLookupException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_notes(self, key):
|
def get_notes(self, key):
|
||||||
|
|
@ -900,14 +756,7 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_email( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
def send_email( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||||
self,
|
self, key=None, context=None, message=None, sender=None, recips=None, **kwargs
|
||||||
key=None,
|
|
||||||
context=None,
|
|
||||||
message=None,
|
|
||||||
sender=None,
|
|
||||||
recips=None,
|
|
||||||
fallback_key=None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Send an email message.
|
Send an email message.
|
||||||
|
|
@ -924,7 +773,6 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
:param key: When auto-creating a message, this is the
|
:param key: When auto-creating a message, this is the
|
||||||
:term:`email key` identifying the type of email to send.
|
:term:`email key` identifying the type of email to send.
|
||||||
Used to lookup config settings and template files.
|
Used to lookup config settings and template files.
|
||||||
See also the ``fallback_key`` param, below.
|
|
||||||
|
|
||||||
:param context: Context dict for rendering automatic email
|
:param context: Context dict for rendering automatic email
|
||||||
template(s).
|
template(s).
|
||||||
|
|
@ -964,10 +812,6 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
context = {'data': [1, 2, 3]}
|
context = {'data': [1, 2, 3]}
|
||||||
app.send_email('foo', context, to='me@example.com', cc='bobby@example.com')
|
app.send_email('foo', context, to='me@example.com', cc='bobby@example.com')
|
||||||
|
|
||||||
:param fallback_key: Optional fallback :term:`email key` to
|
|
||||||
use for config/template lookup, if nothing is found for
|
|
||||||
``key``.
|
|
||||||
|
|
||||||
:param \\**kwargs: Any remaining kwargs are passed along to
|
:param \\**kwargs: Any remaining kwargs are passed along to
|
||||||
:meth:`make_auto_message()`. So, not used if you provide
|
:meth:`make_auto_message()`. So, not used if you provide
|
||||||
the ``message``.
|
the ``message``.
|
||||||
|
|
@ -983,9 +827,7 @@ class EmailHandler(GenericHandler): # pylint: disable=too-many-public-methods
|
||||||
# auto-create message from key + context
|
# auto-create message from key + context
|
||||||
if sender:
|
if sender:
|
||||||
kwargs["sender"] = sender
|
kwargs["sender"] = sender
|
||||||
message = self.make_auto_message(
|
message = self.make_auto_message(key, context or {}, **kwargs)
|
||||||
key, context or {}, fallback_key=fallback_key, **kwargs
|
|
||||||
)
|
|
||||||
if not (message.txt_body or message.html_body):
|
if not (message.txt_body or message.html_body):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"message (type: {key}) has no body - "
|
f"message (type: {key}) has no body - "
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
## -*- coding: utf-8; -*-
|
|
||||||
<table border="1" style="border-collapse: collapse;">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
% for column in diff.columns:
|
|
||||||
<th style="padding: 0.25rem;">${column}</th>
|
|
||||||
% endfor
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
% for field in diff.fields:
|
|
||||||
${diff.render_field_row(field)}
|
|
||||||
% endfor
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
@ -672,14 +672,6 @@ app_title = WuttaTest
|
||||||
self.assertEqual(self.app.render_quantity(0), "0")
|
self.assertEqual(self.app.render_quantity(0), "0")
|
||||||
self.assertEqual(self.app.render_quantity(0, empty_zero=True), "")
|
self.assertEqual(self.app.render_quantity(0, empty_zero=True), "")
|
||||||
|
|
||||||
# has thousands separator
|
|
||||||
value = 1234
|
|
||||||
self.assertEqual(self.app.render_quantity(value), "1,234")
|
|
||||||
value = decimal.Decimal("1234.567")
|
|
||||||
self.assertEqual(self.app.render_quantity(value), "1,234.567")
|
|
||||||
value = decimal.Decimal("1234.567000")
|
|
||||||
self.assertEqual(self.app.render_quantity(value), "1,234.567")
|
|
||||||
|
|
||||||
def test_render_time_ago(self):
|
def test_render_time_ago(self):
|
||||||
with patch.object(mod, "humanize") as humanize:
|
with patch.object(mod, "humanize") as humanize:
|
||||||
humanize.naturaltime.return_value = "now"
|
humanize.naturaltime.return_value = "now"
|
||||||
|
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
from wuttjamaican import diffs as mod
|
|
||||||
from wuttjamaican.testing import ConfigTestCase
|
|
||||||
|
|
||||||
|
|
||||||
class TestDiff(ConfigTestCase):
|
|
||||||
|
|
||||||
def make_diff(self, *args, **kwargs):
|
|
||||||
return mod.Diff(self.config, *args, **kwargs)
|
|
||||||
|
|
||||||
def test_constructor(self):
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "baz"}
|
|
||||||
diff = self.make_diff(old_data, new_data, fields=["foo"])
|
|
||||||
self.assertEqual(diff.fields, ["foo"])
|
|
||||||
self.assertEqual(diff.cell_padding, "0.25rem")
|
|
||||||
diff = self.make_diff(old_data, new_data, cell_padding="0.5rem")
|
|
||||||
self.assertEqual(diff.cell_padding, "0.5rem")
|
|
||||||
|
|
||||||
def test_make_fields(self):
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "bar", "baz": "zer"}
|
|
||||||
# nb. this calls make_fields()
|
|
||||||
diff = self.make_diff(old_data, new_data)
|
|
||||||
# TODO: should the fields be cumulative? or just use new_data?
|
|
||||||
self.assertEqual(diff.fields, ["baz", "foo"])
|
|
||||||
|
|
||||||
def test_values(self):
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "baz"}
|
|
||||||
diff = self.make_diff(old_data, new_data)
|
|
||||||
self.assertEqual(diff.old_value("foo"), "bar")
|
|
||||||
self.assertEqual(diff.new_value("foo"), "baz")
|
|
||||||
|
|
||||||
def test_values_differ(self):
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "baz"}
|
|
||||||
diff = self.make_diff(old_data, new_data)
|
|
||||||
self.assertTrue(diff.values_differ("foo"))
|
|
||||||
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "bar"}
|
|
||||||
diff = self.make_diff(old_data, new_data)
|
|
||||||
self.assertFalse(diff.values_differ("foo"))
|
|
||||||
|
|
||||||
def test_render_values(self):
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "baz"}
|
|
||||||
diff = self.make_diff(old_data, new_data)
|
|
||||||
self.assertEqual(
|
|
||||||
diff.render_old_value("foo"),
|
|
||||||
'<span style="font-family: monospace;">'bar'</span>',
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
diff.render_new_value("foo"),
|
|
||||||
'<span style="font-family: monospace;">'baz'</span>',
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_old_value_attrs(self):
|
|
||||||
|
|
||||||
# no change
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "bar"}
|
|
||||||
diff = self.make_diff(old_data, new_data, nature="update")
|
|
||||||
self.assertEqual(diff.get_old_value_attrs(False), {"style": "padding: 0.25rem"})
|
|
||||||
|
|
||||||
# update
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "baz"}
|
|
||||||
diff = self.make_diff(old_data, new_data, nature="update")
|
|
||||||
self.assertEqual(
|
|
||||||
diff.get_old_value_attrs(True),
|
|
||||||
{"style": f"background-color: {diff.old_color}; padding: 0.25rem"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# delete
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {}
|
|
||||||
diff = self.make_diff(old_data, new_data, nature="delete")
|
|
||||||
self.assertEqual(
|
|
||||||
diff.get_old_value_attrs(True),
|
|
||||||
{"style": f"background-color: {diff.old_color}; padding: 0.25rem"},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_new_value_attrs(self):
|
|
||||||
|
|
||||||
# no change
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "bar"}
|
|
||||||
diff = self.make_diff(old_data, new_data, nature="update")
|
|
||||||
self.assertEqual(diff.get_new_value_attrs(False), {"style": "padding: 0.25rem"})
|
|
||||||
|
|
||||||
# update
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "baz"}
|
|
||||||
diff = self.make_diff(old_data, new_data, nature="update")
|
|
||||||
self.assertEqual(
|
|
||||||
diff.get_new_value_attrs(True),
|
|
||||||
{"style": f"background-color: {diff.new_color}; padding: 0.25rem"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# create
|
|
||||||
old_data = {}
|
|
||||||
new_data = {"foo": "bar"}
|
|
||||||
diff = self.make_diff(old_data, new_data, nature="create")
|
|
||||||
self.assertEqual(
|
|
||||||
diff.get_new_value_attrs(True),
|
|
||||||
{"style": f"background-color: {diff.new_color}; padding: 0.25rem"},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_render_field_row(self):
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "baz"}
|
|
||||||
diff = self.make_diff(old_data, new_data)
|
|
||||||
row = diff.render_field_row("foo")
|
|
||||||
self.assertIn("<tr>", row)
|
|
||||||
self.assertIn("'bar'", row)
|
|
||||||
self.assertIn(
|
|
||||||
f'style="background-color: {diff.old_color}; padding: 0.25rem"', row
|
|
||||||
)
|
|
||||||
self.assertIn("'baz'", row)
|
|
||||||
self.assertIn(
|
|
||||||
f'style="background-color: {diff.new_color}; padding: 0.25rem"', row
|
|
||||||
)
|
|
||||||
self.assertIn("</tr>", row)
|
|
||||||
|
|
||||||
def test_render_html(self):
|
|
||||||
old_data = {"foo": "bar"}
|
|
||||||
new_data = {"foo": "baz"}
|
|
||||||
diff = self.make_diff(old_data, new_data)
|
|
||||||
html = diff.render_html()
|
|
||||||
self.assertIn("<table", html)
|
|
||||||
self.assertIn("<tr>", html)
|
|
||||||
self.assertIn("'bar'", html)
|
|
||||||
self.assertIn(
|
|
||||||
f'style="background-color: {diff.old_color}; padding: 0.25rem"', html
|
|
||||||
)
|
|
||||||
self.assertIn("'baz'", html)
|
|
||||||
self.assertIn(
|
|
||||||
f'style="background-color: {diff.new_color}; padding: 0.25rem"', html
|
|
||||||
)
|
|
||||||
self.assertIn("</tr>", html)
|
|
||||||
self.assertIn("</table>", html)
|
|
||||||
|
|
@ -5,7 +5,6 @@ from unittest import TestCase
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from mako.template import Template
|
|
||||||
|
|
||||||
from wuttjamaican import email as mod
|
from wuttjamaican import email as mod
|
||||||
from wuttjamaican.util import resource_path
|
from wuttjamaican.util import resource_path
|
||||||
|
|
@ -21,16 +20,6 @@ class TestEmailSetting(ConfigTestCase):
|
||||||
self.assertIs(setting.app, self.app)
|
self.assertIs(setting.app, self.app)
|
||||||
self.assertEqual(setting.key, "EmailSetting")
|
self.assertEqual(setting.key, "EmailSetting")
|
||||||
|
|
||||||
def test_get_description(self):
|
|
||||||
|
|
||||||
class MockSetting(mod.EmailSetting):
|
|
||||||
"""
|
|
||||||
this should be a good test
|
|
||||||
"""
|
|
||||||
|
|
||||||
setting = MockSetting(self.config)
|
|
||||||
self.assertEqual(setting.get_description(), "this should be a good test")
|
|
||||||
|
|
||||||
def test_sample_data(self):
|
def test_sample_data(self):
|
||||||
setting = mod.EmailSetting(self.config)
|
setting = mod.EmailSetting(self.config)
|
||||||
self.assertEqual(setting.sample_data(), {})
|
self.assertEqual(setting.sample_data(), {})
|
||||||
|
|
@ -310,9 +299,7 @@ class TestEmailHandler(ConfigTestCase):
|
||||||
msg = handler.make_auto_message("foo", subject=None)
|
msg = handler.make_auto_message("foo", subject=None)
|
||||||
get_auto_subject.assert_not_called()
|
get_auto_subject.assert_not_called()
|
||||||
msg = handler.make_auto_message("foo")
|
msg = handler.make_auto_message("foo")
|
||||||
get_auto_subject.assert_called_once_with(
|
get_auto_subject.assert_called_once_with("foo", {}, default=None)
|
||||||
"foo", {}, default=None, fallback_key=None
|
|
||||||
)
|
|
||||||
|
|
||||||
# to
|
# to
|
||||||
with patch.object(handler, "get_auto_to") as get_auto_to:
|
with patch.object(handler, "get_auto_to") as get_auto_to:
|
||||||
|
|
@ -343,18 +330,14 @@ class TestEmailHandler(ConfigTestCase):
|
||||||
msg = handler.make_auto_message("foo", txt_body=None)
|
msg = handler.make_auto_message("foo", txt_body=None)
|
||||||
get_auto_txt_body.assert_not_called()
|
get_auto_txt_body.assert_not_called()
|
||||||
msg = handler.make_auto_message("foo")
|
msg = handler.make_auto_message("foo")
|
||||||
get_auto_txt_body.assert_called_once_with(
|
get_auto_txt_body.assert_called_once_with("foo", {})
|
||||||
"foo", {"config": self.config, "app": self.app}, fallback_key=None
|
|
||||||
)
|
|
||||||
|
|
||||||
# html_body
|
# html_body
|
||||||
with patch.object(handler, "get_auto_html_body") as get_auto_html_body:
|
with patch.object(handler, "get_auto_html_body") as get_auto_html_body:
|
||||||
msg = handler.make_auto_message("foo", html_body=None)
|
msg = handler.make_auto_message("foo", html_body=None)
|
||||||
get_auto_html_body.assert_not_called()
|
get_auto_html_body.assert_not_called()
|
||||||
msg = handler.make_auto_message("foo")
|
msg = handler.make_auto_message("foo")
|
||||||
get_auto_html_body.assert_called_once_with(
|
get_auto_html_body.assert_called_once_with("foo", {})
|
||||||
"foo", {"config": self.config, "app": self.app}, fallback_key=None
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_auto_sender(self):
|
def test_get_auto_sender(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
@ -401,11 +384,6 @@ class TestEmailHandler(ConfigTestCase):
|
||||||
template = handler.get_auto_subject_template("foo")
|
template = handler.get_auto_subject_template("foo")
|
||||||
self.assertEqual(template, "Foo Message")
|
self.assertEqual(template, "Foo Message")
|
||||||
|
|
||||||
# can configure via fallback_key
|
|
||||||
self.config.setdefault("wutta.email.bar.subject", "Bar Message")
|
|
||||||
template = handler.get_auto_subject_template("baz", fallback_key="bar")
|
|
||||||
self.assertEqual(template, "Bar Message")
|
|
||||||
|
|
||||||
# EmailSetting can provide default subject
|
# EmailSetting can provide default subject
|
||||||
providers = {
|
providers = {
|
||||||
"wuttatest": MagicMock(email_modules=["tests.test_email"]),
|
"wuttatest": MagicMock(email_modules=["tests.test_email"]),
|
||||||
|
|
@ -468,48 +446,26 @@ class TestEmailHandler(ConfigTestCase):
|
||||||
self.assertEqual(recips, ["bob@example.com"])
|
self.assertEqual(recips, ["bob@example.com"])
|
||||||
|
|
||||||
def test_get_auto_body_template(self):
|
def test_get_auto_body_template(self):
|
||||||
|
from mako.template import Template
|
||||||
|
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
||||||
# error if invalid mode (must be 'html' or 'txt')
|
# error if bad request
|
||||||
self.assertRaises(ValueError, handler.get_auto_body_template, "foo", "BAD_MODE")
|
self.assertRaises(ValueError, handler.get_auto_body_template, "foo", "BADTYPE")
|
||||||
|
|
||||||
# no template by default
|
# empty by default
|
||||||
self.assertIsNone(handler.get_auto_body_template("foo", "html"))
|
template = handler.get_auto_body_template("foo", "txt")
|
||||||
self.assertIsNone(handler.get_auto_body_template("foo", "txt"))
|
self.assertIsNone(template)
|
||||||
|
|
||||||
# mock template lookup
|
# but returns a template if it exists
|
||||||
providers = {
|
providers = {
|
||||||
"wuttatest": MagicMock(email_templates=["tests:email-templates"]),
|
"wuttatest": MagicMock(email_templates=["tests:email-templates"]),
|
||||||
}
|
}
|
||||||
with patch.object(self.app, "providers", new=providers):
|
with patch.object(self.app, "providers", new=providers):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
template = handler.get_auto_body_template("test_foo", "txt")
|
||||||
# template exists (txt)
|
self.assertIsInstance(template, Template)
|
||||||
template = handler.get_auto_body_template("test_foo", "txt")
|
self.assertEqual(template.uri, "test_foo.txt.mako")
|
||||||
self.assertIsInstance(template, Template)
|
|
||||||
self.assertEqual(template.uri, "test_foo.txt.mako")
|
|
||||||
|
|
||||||
# template exists (html)
|
|
||||||
template = handler.get_auto_body_template("test_foo", "html")
|
|
||||||
self.assertIsInstance(template, Template)
|
|
||||||
self.assertEqual(template.uri, "test_foo.html.mako")
|
|
||||||
|
|
||||||
# no such template
|
|
||||||
template = handler.get_auto_body_template("no_such_template", "html")
|
|
||||||
self.assertIsNone(template)
|
|
||||||
|
|
||||||
# but can use fallback
|
|
||||||
template = handler.get_auto_body_template(
|
|
||||||
"no_such_template", "html", fallback_key="test_foo"
|
|
||||||
)
|
|
||||||
self.assertIsInstance(template, Template)
|
|
||||||
self.assertEqual(template.uri, "test_foo.html.mako")
|
|
||||||
|
|
||||||
# what if fallback is also not found
|
|
||||||
template = handler.get_auto_body_template(
|
|
||||||
"no_such_template", "html", fallback_key="this_neither"
|
|
||||||
)
|
|
||||||
self.assertIsNone(template)
|
|
||||||
|
|
||||||
def test_get_auto_txt_body(self):
|
def test_get_auto_txt_body(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue