Skip to content

Commit 3874f56

Browse files
authored
Allow to customize email publication template in setup (senaite#2063)
* Implemented SENAITE Registry * Allow to set publication email body text in Setup * Implemented SENAITE RichTextField * Use new RichTextField for registry * Use body text for email from setup * send HTML Emails * Test fixture * Added test for HTML email * Added upgrade step * Fix bypassed default factory * Description changed * Added SENAITE Setup Object * Proxy to SENAITE setup * Moved to SENAITE Setup * WYSIWYG Support * Handle default value * Added test for API * Added test for setup * Changelog updated
1 parent 353ef78 commit 3874f56

33 files changed

+769
-56
lines changed

CHANGES.rst

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55
2.3.0 (unreleased)
66
------------------
77

8+
- #2063 Allow to customize email publication template in setup
89
- #2062 Fix listing not updated after instrument assignment in Worksheet's view
910
- #2061 Fire notifications when UID references are created/destroyed
1011
- #2058 Filter 'Interpretation templates' in sample view by template and type

src/bika/lims/api/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ def get_bika_setup():
120120
return get_setup()
121121

122122

123+
def get_senaite_setup():
124+
"""Fetch the new DX `setup` folder.
125+
"""
126+
portal = get_portal()
127+
return portal.get("setup")
128+
129+
123130
def create(container, portal_type, *args, **kwargs):
124131
"""Creates an object in Bika LIMS
125132

src/bika/lims/api/mail.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,12 @@ def to_email_body_text(body, **kw):
106106
:returns: MIMEText
107107
"""
108108
body_template = Template(safe_unicode(body)).safe_substitute(**kw)
109-
return MIMEText(body_template, _subtype="plain", _charset="utf8")
109+
subtype = "plain"
110+
# Allow to send HTML messages
111+
# https://docs.python.org/2/library/email-examples.html#id5
112+
if kw.get("html", False):
113+
subtype = "html"
114+
return MIMEText(body_template, _subtype=subtype, _charset="utf8")
110115

111116

112117
def to_email_attachment(filedata, filename="", **kw):

src/bika/lims/browser/publish/emailview.py

+17-7
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ class EmailView(BrowserView):
5656
implements(IPublishTraverse)
5757

5858
template = ViewPageTemplateFile("templates/email.pt")
59-
email_template = ViewPageTemplateFile("templates/email_template.pt")
6059

6160
def __init__(self, context, request):
6261
super(EmailView, self).__init__(context, request)
@@ -300,7 +299,14 @@ def email_body(self):
300299
body = self.request.get("body", None)
301300
if body is not None:
302301
return body
303-
return self.context.translate(_(self.email_template(self)))
302+
setup = api.get_setup()
303+
body = setup.getEmailBodySamplePublication()
304+
template_context = {
305+
"client_name": self.client_name,
306+
}
307+
rendered_body = self.render_email_template(
308+
body, template_context=template_context)
309+
return rendered_body
304310

305311
@property
306312
def email_attachments(self):
@@ -468,7 +474,7 @@ def publish(self, sample):
468474
except WorkflowException as e:
469475
logger.error(e)
470476

471-
def render_email_template(self, template):
477+
def render_email_template(self, template, template_context=None):
472478
"""Return the rendered email template
473479
474480
This method interpolates the $recipients variable with the selected
@@ -478,10 +484,13 @@ def render_email_template(self, template):
478484
:returns: Rendered email template
479485
"""
480486

487+
# allow to add translation for initial template
488+
template = self.context.translate(template)
481489
recipients = self.email_recipients_and_responsibles
482-
template_context = {
483-
"recipients": "\n".join(recipients)
484-
}
490+
if template_context is None:
491+
template_context = {
492+
"recipients": "\n".join(recipients),
493+
}
485494

486495
email_template = Template(safe_unicode(template)).safe_substitute(
487496
**template_context)
@@ -510,7 +519,8 @@ def send_email(self, recipients, subject, body, attachments=None):
510519
to_address,
511520
subject,
512521
email_body,
513-
attachments=attachments)
522+
attachments=attachments,
523+
html=True)
514524
sent = mailapi.send_email(mime_msg)
515525
if not sent:
516526
msg = _("Could not send email to {0} ({1})").format(pair[0],

src/bika/lims/browser/publish/templates/email.pt

+2-2
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@
114114

115115
<div class="form-group">
116116
<label i18n:translate="" for="body">Text</label>
117-
<textarea name="body" class="form-control" rows="12"
118-
tal:content="view/email_body">
117+
<textarea name="body" class="richTextWidget form-control" rows="12"
118+
tal:content="structure view/email_body">
119119
</textarea>
120120
<p i18n:translate="" class="help-block discreet">
121121
The variable <code i18n:name="recipients">$recipients</code> will

src/bika/lims/browser/publish/templates/email_template.pt

-28
This file was deleted.

src/bika/lims/content/bikasetup.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@
3030
from bika.lims.browser.worksheet.tools import getWorksheetLayouts
3131
from bika.lims.config import CURRENCIES
3232
from bika.lims.config import DECIMAL_MARKS
33+
from bika.lims.config import DEFAULT_WORKSHEET_LAYOUT
3334
from bika.lims.config import MULTI_VERIFICATION_TYPE
3435
from bika.lims.config import PROJECTNAME
3536
from bika.lims.config import SCINOTATION_OPTIONS
3637
from bika.lims.config import WEEKDAYS
37-
from bika.lims.config import DEFAULT_WORKSHEET_LAYOUT
3838
from bika.lims.content.bikaschema import BikaFolderSchema
3939
from bika.lims.interfaces import IBikaSetup
4040
from bika.lims.numbergenerator import INumberGenerator
@@ -53,12 +53,12 @@
5353
from Products.Archetypes.atapi import StringField
5454
from Products.Archetypes.atapi import TextAreaWidget
5555
from Products.Archetypes.atapi import registerType
56-
from Products.Archetypes.Field import BooleanField
5756
from Products.Archetypes.Field import TextField
5857
from Products.Archetypes.utils import DisplayList
5958
from Products.Archetypes.utils import IntDisplayList
6059
from Products.Archetypes.Widget import RichWidget
6160
from Products.CMFCore.utils import getToolByName
61+
from senaite.core import registry as senaite_registry
6262
from senaite.core.api import geo
6363
from senaite.core.browser.fields.records import RecordsField
6464
from senaite.core.interfaces import IHideActionsMenu
@@ -575,6 +575,25 @@ def getCounterTypes(self, instance=None):
575575
"in the sample types setup"),
576576
)
577577
),
578+
# NOTE: This is a Proxy Field which delegates to the SENAITE Registry!
579+
TextField(
580+
"EmailBodySamplePublication",
581+
default_content_type="text/html",
582+
default_output_type="text/x-html-safe",
583+
schemata="Notifications",
584+
# Needed to fetch the default value from the registry
585+
edit_accessor="getEmailBodySamplePublication",
586+
widget=RichWidget(
587+
label=_("Email body for Sample publication notifications"),
588+
description=_(
589+
"The default text that is used for the publication email. "
590+
" sending publication reports."),
591+
default_mime_type="text/x-html",
592+
output_mime_type="text/x-html",
593+
allow_file_upload=False,
594+
rows=15,
595+
),
596+
),
578597
BooleanField(
579598
'NotifyOnSampleRejection',
580599
schemata="Notifications",
@@ -966,5 +985,17 @@ def getIDServerValuesHTML(self):
966985
results.append('%s: %s' % (keys[i], values[i]))
967986
return "\n".join(results)
968987

988+
def getEmailBodySamplePublication(self):
989+
"""Get the value from the senaite setup
990+
"""
991+
setup = api.get_senaite_setup()
992+
return setup.getEmailBodySamplePublication()
993+
994+
def setEmailBodySamplePublication(self, value):
995+
"""Set the value in the senaite setup
996+
"""
997+
setup = api.get_senaite_setup()
998+
setup.setEmailBodySamplePublication(value)
999+
9691000

9701001
registerType(BikaSetup, PROJECTNAME)

src/senaite/core/browser/configure.zcml

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<include package=".modals"/>
3232
<include package=".portlets"/>
3333
<include package=".samples"/>
34+
<include package=".setup"/>
3435
<include package=".viewlets"/>
3536
<include package=".widgets"/>
3637

src/senaite/core/browser/setup/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<configure
2+
xmlns="http://namespaces.zope.org/zope"
3+
xmlns:browser="http://namespaces.zope.org/browser">
4+
5+
</configure>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<tal:template
2+
tal:define="laboratory python:context.laboratory"
3+
i18n:domain="senaite.core">
4+
5+
<p i18n:translate="">
6+
Thank you for your analysis request.
7+
</p>
8+
9+
<p i18n:translate="">
10+
Please find attached the analysis result(s) for
11+
<tal:client i18n:name="client_name" tal:content="options/client_name|default">$client_name</tal:client>
12+
</p>
13+
14+
<p i18n:translate="">
15+
This report was sent to the following contacts:
16+
</p>
17+
18+
$recipients
19+
20+
<p i18n:translate="">
21+
With best regards
22+
</p>
23+
<tal:laboratory tal:replace="python:laboratory.getName() or 'SENAITE LIMS'"/>
24+
25+
<p i18n:translate="">
26+
*** This is an automatically generated email, please do not reply to this message. ***
27+
</p>
28+
29+
</tal:template>

src/senaite/core/configure.zcml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<include package=".migration" />
2121
<include package=".patches" />
2222
<include package=".permissions" />
23+
<include package=".registry" />
2324
<include package=".schema" />
2425
<include package=".upgrade" />
2526
<include package=".z3cform" />

src/senaite/core/content/base.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from AccessControl import ClassSecurityInfo
4+
from bika.lims import api
5+
from plone.dexterity.content import Container as BaseContainer
6+
from plone.dexterity.content import Item as BaseItem
7+
from senaite.core.interfaces import IContainer
8+
from senaite.core.interfaces import IItem
9+
from zope.interface import implementer
10+
11+
12+
@implementer(IContainer)
13+
class Container(BaseContainer):
14+
"""Base class for SENAITE folderish contents
15+
"""
16+
security = ClassSecurityInfo()
17+
18+
@security.private
19+
def accessor(self, fieldname):
20+
"""Return the field accessor for the fieldname
21+
"""
22+
schema = api.get_schema(self)
23+
if fieldname not in schema:
24+
return None
25+
return schema[fieldname].get
26+
27+
@security.private
28+
def mutator(self, fieldname):
29+
"""Return the field mutator for the fieldname
30+
"""
31+
schema = api.get_schema(self)
32+
if fieldname not in schema:
33+
return None
34+
return schema[fieldname].set
35+
36+
37+
@implementer(IItem)
38+
class Item(BaseItem):
39+
"""Base class for SENAITE contentish contents
40+
"""
41+
security = ClassSecurityInfo()
42+
43+
@security.private
44+
def accessor(self, fieldname):
45+
"""Return the field accessor for the fieldname
46+
"""
47+
schema = api.get_schema(self)
48+
if fieldname not in schema:
49+
return None
50+
return schema[fieldname].get
51+
52+
@security.private
53+
def mutator(self, fieldname):
54+
"""Return the field mutator for the fieldname
55+
"""
56+
schema = api.get_schema(self)
57+
if fieldname not in schema:
58+
return None
59+
return schema[fieldname].set

0 commit comments

Comments
 (0)