From 63d74636c9c18b3243c8ea688ab5a769aa8abb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 10:26:18 +0200 Subject: [PATCH 01/19] MessageFormatter: store the template instead of the environment --- master/buildbot/reporters/message.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index 270a6850380..883f55f8d70 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -23,11 +23,14 @@ def __init__(self, template_name=None, template_dir=None, template_type=None): template_dir = os.path.join(os.path.dirname(__file__), "templates") loader = jinja2.FileSystemLoader(template_dir) - self.env = jinja2.Environment( + env = jinja2.Environment( loader=loader, undefined=jinja2.StrictUndefined) if template_name is not None: self.template_name = template_name + + self.template = env.get_template(self.template_name) + if template_type is not None: self.template_type = template_type @@ -114,7 +117,6 @@ def __call__(self, mode, buildername, buildset, build, master, previous_results, ss_list = buildset['sourcestamps'] results = build['results'] - tpl = self.env.get_template(self.template_name) cxt = dict(results=build['results'], mode=mode, buildername=buildername, @@ -133,5 +135,5 @@ def __call__(self, mode, buildername, buildset, build, master, previous_results, summary=self.messageSummary(build, results), sourcestamps=self.messageSourceStamps(ss_list) ) - contents = tpl.render(cxt) + contents = self.template.render(cxt) return {'body': contents, 'type': self.template_type} From eb6ee847193f981e3987e5327cb824b404a3b3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 10:35:17 +0200 Subject: [PATCH 02/19] b/r/message.py: Add license header --- master/buildbot/reporters/message.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index 883f55f8d70..8cd1e1ac7eb 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -1,7 +1,23 @@ +# This file is part of Buildbot. Buildbot 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, version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright Buildbot Team Members + import os import jinja2 + from buildbot.process.results import CANCELLED from buildbot.process.results import EXCEPTION from buildbot.process.results import FAILURE From e821c63c1c8a8e97768209cac2ec61faca9efb63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 10:54:30 +0200 Subject: [PATCH 03/19] MessageFormatter: Allow to give the template inline --- master/buildbot/reporters/message.py | 26 ++++++++++++------- .../test/unit/test_reporters_message.py | 7 +++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index 8cd1e1ac7eb..67ae0fe3bd7 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -17,7 +17,7 @@ import jinja2 - +from buildbot import config from buildbot.process.results import CANCELLED from buildbot.process.results import EXCEPTION from buildbot.process.results import FAILURE @@ -33,19 +33,25 @@ class MessageFormatter(object): wantProperties = True wantSteps = False - def __init__(self, template_name=None, template_dir=None, template_type=None): + def __init__(self, template_name=None, template_dir=None, template=None, template_type=None): + + if (template is not None) and ((template_name is not None) or (template_dir is not None)): + config.error("Only one of template or template path can be given") - if template_dir is None: - template_dir = os.path.join(os.path.dirname(__file__), "templates") + if template is None: + if template_dir is None: + template_dir = os.path.join(os.path.dirname(__file__), "templates") - loader = jinja2.FileSystemLoader(template_dir) - env = jinja2.Environment( - loader=loader, undefined=jinja2.StrictUndefined) + loader = jinja2.FileSystemLoader(template_dir) + env = jinja2.Environment( + loader=loader, undefined=jinja2.StrictUndefined) - if template_name is not None: - self.template_name = template_name + if template_name is not None: + self.template_name = template_name - self.template = env.get_template(self.template_name) + self.template = env.get_template(self.template_name) + else: + self.template = jinja2.Template(template) if template_type is not None: self.template_type = template_type diff --git a/master/buildbot/test/unit/test_reporters_message.py b/master/buildbot/test/unit/test_reporters_message.py index ac29d3bcc1b..10455177b8c 100644 --- a/master/buildbot/test/unit/test_reporters_message.py +++ b/master/buildbot/test/unit/test_reporters_message.py @@ -88,6 +88,13 @@ def test_message_success(self): Sincerely, -The Buildbot''')) + @defer.inlineCallbacks + def test_inline_template(self): + self.message = message.MessageFormatter(template="URL: {{ build_url }} -- {{ summary }}") + res = yield self.doOneTest(SUCCESS, SUCCESS) + self.assertEqual(res['type'], "plain") + self.assertEqual(res['body'], "URL: http://localhost:8080/#builders/80/builds/1 -- Build succeeded!") + @defer.inlineCallbacks def test_message_failure(self): res = yield self.doOneTest(SUCCESS, FAILURE) From 7e91117f7973b6687f39efc101d5c89efdebf9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 10:55:53 +0200 Subject: [PATCH 04/19] Update the release note --- master/docs/relnotes/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/master/docs/relnotes/index.rst b/master/docs/relnotes/index.rst index 41e634c22e7..fd837ae8740 100644 --- a/master/docs/relnotes/index.rst +++ b/master/docs/relnotes/index.rst @@ -66,6 +66,7 @@ Features Now, builds started with a :class:`Triggereable` scheduler will be cancelled, while other builds will be retried. The master will make sure that all latent workers are stopped. +* The ``MessageFormatter`` class also allows inline-templates with the ``template`` parameter. .. _Hyper: https://hyper.sh From 4b085b8a9e7ba83e96a4c6bceba9d5d598dc62db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 11:23:48 +0200 Subject: [PATCH 05/19] Put the MessageFormatter class in the plugins --- master/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/master/setup.py b/master/setup.py index cd7847215f5..a948ccd077c 100755 --- a/master/setup.py +++ b/master/setup.py @@ -299,13 +299,13 @@ def define_plugin_entries(groups): ]), ('buildbot.reporters', [ ('buildbot.reporters.mail', ['MailNotifier']), + ('buildbot.reporters.message', ['MessageFormatter']), ('buildbot.reporters.gerrit', ['GerritStatusPush']), ('buildbot.reporters.http', ['HttpStatusPush']), ('buildbot.reporters.github', ['GitHubStatusPush']), ('buildbot.reporters.stash', ['StashStatusPush']), ('buildbot.reporters.bitbucket', ['BitbucketStatusPush']), ('buildbot.reporters.irc', ['IRC']), - ]), ('buildbot.util', [ # Connection seems to be a way too generic name, though From 1dbfd8fac8c0cd5091c94b35400e39bed7326845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 11:30:35 +0200 Subject: [PATCH 06/19] Start fixing the doc about messageFormatter --- master/docs/manual/cfg-reporters.rst | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/master/docs/manual/cfg-reporters.rst b/master/docs/manual/cfg-reporters.rst index 5e9d2550a60..7c65f039687 100644 --- a/master/docs/manual/cfg-reporters.rst +++ b/master/docs/manual/cfg-reporters.rst @@ -109,26 +109,16 @@ If you want to require Transport Layer Security (TLS), then you can also set ``u If you see ``twisted.mail.smtp.TLSRequiredError`` exceptions in the log while using TLS, this can be due *either* to the server not supporting TLS or to a missing `PyOpenSSL`_ package on the BuildMaster system. In some cases it is desirable to have different information then what is provided in a standard MailNotifier message. -For this purpose MailNotifier provides the argument ``messageFormatter`` (a function) which allows for the creation of messages with unique content. +For this purpose MailNotifier provides the argument ``messageFormatter`` (an instance of ``MessageFormatter``) which allows for the creation of messages with unique content. For example, if only short emails are desired (e.g., for delivery to phones):: - from buildbot.plugins import reporters, util - def messageFormatter(mode, name, build, results, master_status): - result = util.Results[results] - - text = list() - text.append("STATUS: %s" % result.title()) - return { - 'body' : "\n".join(text), - 'type' : 'plain' - } - + from buildbot.plugins import reporters mn = reporters.MailNotifier(fromaddr="buildbot@example.org", sendToInterestedUsers=False, mode=('problem',), extraRecipients=['listaddr@example.org'], - messageFormatter=messageFormatter) + messageFormatter=reporters.MessageFormatter(template="STATUS: {{ summary }}")) Another example of a function delivering a customized html email containing the last 80 log lines of logs of the last build step is given below:: From 2d1a8c2b965ed229741ea0adee961dc6b1e72f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 11:53:45 +0200 Subject: [PATCH 07/19] Replace the broken HTML formatting example with a minimal one --- master/docs/manual/cfg-reporters.rst | 123 ++++----------------------- 1 file changed, 18 insertions(+), 105 deletions(-) diff --git a/master/docs/manual/cfg-reporters.rst b/master/docs/manual/cfg-reporters.rst index 7c65f039687..54a0e53c57a 100644 --- a/master/docs/manual/cfg-reporters.rst +++ b/master/docs/manual/cfg-reporters.rst @@ -120,116 +120,29 @@ For example, if only short emails are desired (e.g., for delivery to phones):: extraRecipients=['listaddr@example.org'], messageFormatter=reporters.MessageFormatter(template="STATUS: {{ summary }}")) -Another example of a function delivering a customized html email containing the last 80 log lines of logs of the last build step is given below:: - - from future.utils import text_type - from buildbot.plugins import util, reporters - - import cgi, datetime - - # FIXME: this code is barely readable, we should provide a better example with use of jinja templates - # - def html_message_formatter(mode, name, build, results, master_status): - """Provide a customized message to Buildbot's MailNotifier. - - The last 80 lines of the log are provided as well as the changes - relevant to the build. Message content is formatted as html. - """ - result = util.Results[results] - - limit_lines = 80 - text = list() - text.append(u'

Build status: %s

' % result.upper()) - text.append(u'') - text.append(u"" % build.getWorkername()) - if master_status.getURLForThing(build): - text.append(u'' - % (master_status.getURLForThing(build), - master_status.getURLForThing(build)) - ) - text.append(u'' % build.getReason()) - source = u"" - for ss in build.getSourceStamps(): - if ss.codebase: - source += u'%s: ' % ss.codebase - if ss.branch: - source += u"[branch %s] " % ss.branch - if ss.revision: - source += ss.revision - else: - source += u"HEAD" - if ss.patch: - source += u" (plus patch)" - if ss.patch_info: # add patch comment - source += u" (%s)" % ss.patch_info[1] - text.append(u"" % source) - text.append(u"" % ",".join(build.getResponsibleUsers())) - text.append(u'
Worker for this Build:%s
Complete logs for all build steps:%s
Build Reason:%s
Build Source Stamp:%s
Blamelist:%s
') - if ss.changes: - text.append(u'

Recent Changes:

') - for c in ss.changes: - cd = c.asDict() - when = datetime.datetime.fromtimestamp(cd['when'] ).ctime() - text.append(u'') - text.append(u'' % cd['repository'] ) - text.append(u'' % cd['project'] ) - text.append(u'' % when) - text.append(u'' % cd['who'] ) - text.append(u'' % cd['comments'] ) - text.append(u'
Repository:%s
Project:%s
Time:%s
Changed by:%s
Comments:%s
') - files = cd['files'] - if files: - text.append(u'') - for file in files: - text.append(u'' % file['name'] ) - text.append(u'
Files
%s:
') - text.append(u'
') - # get all the steps in build in reversed order - rev_steps = reversed(build.getSteps()) - # find the last step that finished - for step in rev_steps: - if step.isFinished(): - break - # get logs for the last finished step - if step.isFinished(): - logs = step.getLogs() - # No step finished, loop just exhausted itself; so as a special case we fetch all logs - else: - logs = build.getLogs() - # logs within a step are in reverse order. Search back until we find stdio - for log in reversed(logs): - if log.getName() == 'stdio': - break - name = "%s.%s" % (log.getStep().getName(), log.getName()) - status, dummy = log.getStep().getResults() - # XXX logs no longer have getText methods!! - content = log.getText().splitlines() # Note: can be VERY LARGE - url = u'%s/steps/%s/logs/%s' % (master_status.getURLForThing(build), - log.getStep().getName(), - log.getName()) - - text.append(u'Detailed log of last build step: %s' - % (url, url)) - text.append(u'
') - text.append(u'

Last %d lines of "%s"

' % (limit_lines, name)) - unilist = list() - for line in content[len(content)-limit_lines:]: - unilist.append(cgi.escape(text_type(line,'utf-8'))) - text.append(u'
')
-            text.extend(unilist)
-            text.append(u'
') - text.append(u'

') - text.append(u'-The Buildbot') - return { - 'body': u"\n".join(text), - 'type': 'html' - } +Another example of a function delivering a customized html email is given below:: + + from buildbot.plugins import reporters + + class DetailledMessageFormatter(reporters.MessageFormatter): + wantSteps = True + wantLogs = True + + # XXX: This is work in progress, use with care. + template=u'''\ +

Build status: {{ summary }}

+

Worker used: {{ workername }}

+ {% for step in build['steps'] %} +

{{ step['name'] }}: {{ step['result'] }}

+ {% endfor %} + -- The Buildbot

+ ''' mn = reporters.MailNotifier(fromaddr="buildbot@example.org", sendToInterestedUsers=False, mode=('failing',), extraRecipients=['listaddr@example.org'], - messageFormatter=html_message_formatter) + messageFormatter=DetailledMessageFormatter(template=template, template_type='html')) .. _PyOpenSSL: http://pyopenssl.sourceforge.net/ From 4a24a9a97ce3858761ce1b145e3e4d4bb7d76637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 12:12:32 +0200 Subject: [PATCH 08/19] Allow messageFormatters to request the logs --- master/buildbot/reporters/mail.py | 6 ++++-- master/buildbot/reporters/message.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/master/buildbot/reporters/mail.py b/master/buildbot/reporters/mail.py index 5d8eb7ee0a3..028ebda990b 100644 --- a/master/buildbot/reporters/mail.py +++ b/master/buildbot/reporters/mail.py @@ -242,7 +242,8 @@ def buildsetComplete(self, key, msg): self.master, bsid, wantProperties=self.messageFormatter.wantProperties, wantSteps=self.messageFormatter.wantSteps, - wantPreviousBuild=self.wantPreviousBuild()) + wantPreviousBuild=self.wantPreviousBuild(), + wantLogs=self.messageFormatter.wantLogs) builds = res['builds'] buildset = res['buildset'] @@ -262,7 +263,8 @@ def buildComplete(self, key, build): self.master, buildset, [build], wantProperties=self.messageFormatter.wantProperties, wantSteps=self.messageFormatter.wantSteps, - wantPreviousBuild=self.wantPreviousBuild()) + wantPreviousBuild=self.wantPreviousBuild(), + wantLogs=self.messageFormatter.wantLogs) # only include builds for which isMailNeeded returns true if self.isMailNeeded(build): self.buildMessage( diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index 67ae0fe3bd7..2cefa0f0ef6 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -32,6 +32,7 @@ class MessageFormatter(object): template_type = 'plain' wantProperties = True wantSteps = False + wantLogs = False def __init__(self, template_name=None, template_dir=None, template=None, template_type=None): From e7f1c055a2d53a3e8c5e85226c564d4f730029ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 12:33:08 +0200 Subject: [PATCH 09/19] Allow the MessageFormatter to customize the subject --- master/buildbot/reporters/message.py | 51 ++++++++++++------- .../test/unit/test_reporters_message.py | 7 +++ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index 2cefa0f0ef6..128d1e4b98b 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -34,28 +34,40 @@ class MessageFormatter(object): wantSteps = False wantLogs = False - def __init__(self, template_name=None, template_dir=None, template=None, template_type=None): + def __init__(self, template_name=None, template_dir=None, template=None, + subject_name=None, subject=None, template_type=None): if (template is not None) and ((template_name is not None) or (template_dir is not None)): config.error("Only one of template or template path can be given") - if template is None: - if template_dir is None: - template_dir = os.path.join(os.path.dirname(__file__), "templates") + self.body_template = self.getTemplate(template_name, template_dir, template) + self.subject_template = None + if subject_name or subject: + self.subject_template = self.getTemplate(subject_name, template_dir, subject) - loader = jinja2.FileSystemLoader(template_dir) - env = jinja2.Environment( - loader=loader, undefined=jinja2.StrictUndefined) + if template_type is not None: + self.template_type = template_type - if template_name is not None: - self.template_name = template_name - self.template = env.get_template(self.template_name) - else: - self.template = jinja2.Template(template) + def getTemplate(self, filename, dirname, content): + if content and filename: + config.error("Only one of template or template path can be given") + + if content: + return jinja2.Template(content) + + if dirname is None: + dirname = os.path.join(os.path.dirname(__file__), "templates") + + loader = jinja2.FileSystemLoader(dirname) + env = jinja2.Environment( + loader=loader, undefined=jinja2.StrictUndefined) + + if filename is None: + filename = self.template_name + + return env.get_template(filename) - if template_type is not None: - self.template_type = template_type def getDetectedStatus(self, mode, results, previous_results): @@ -135,8 +147,8 @@ def messageSummary(self, build, results): return text def __call__(self, mode, buildername, buildset, build, master, previous_results, blamelist): - """Generate a buildbot mail message and return a tuple of message text - and type.""" + """Generate a buildbot mail message and return a dictionnary + containing the message body, type and subject.""" ss_list = buildset['sourcestamps'] results = build['results'] @@ -158,5 +170,8 @@ def __call__(self, mode, buildername, buildset, build, master, previous_results, summary=self.messageSummary(build, results), sourcestamps=self.messageSourceStamps(ss_list) ) - contents = self.template.render(cxt) - return {'body': contents, 'type': self.template_type} + body = self.body_template.render(cxt) + email = {'body': body, 'type': self.template_type} + if self.subject_template is not None: + email['subject'] = self.subject_template.render(cxt) + return email diff --git a/master/buildbot/test/unit/test_reporters_message.py b/master/buildbot/test/unit/test_reporters_message.py index 10455177b8c..e8b638e7bea 100644 --- a/master/buildbot/test/unit/test_reporters_message.py +++ b/master/buildbot/test/unit/test_reporters_message.py @@ -87,6 +87,7 @@ def test_message_success(self): Sincerely, -The Buildbot''')) + self.assertTrue('subject' not in res) @defer.inlineCallbacks def test_inline_template(self): @@ -95,6 +96,12 @@ def test_inline_template(self): self.assertEqual(res['type'], "plain") self.assertEqual(res['body'], "URL: http://localhost:8080/#builders/80/builds/1 -- Build succeeded!") + @defer.inlineCallbacks + def test_inline_subject(self): + self.message = message.MessageFormatter(subject="subject") + res = yield self.doOneTest(SUCCESS, SUCCESS) + self.assertEqual(res['subject'], "subject") + @defer.inlineCallbacks def test_message_failure(self): res = yield self.doOneTest(SUCCESS, FAILURE) From e0ab5be98e5e777832a2c61e861cca6ebbc0adcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 12:34:36 +0200 Subject: [PATCH 10/19] Update the release note --- master/docs/relnotes/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/master/docs/relnotes/index.rst b/master/docs/relnotes/index.rst index fd837ae8740..2aac36ed7dc 100644 --- a/master/docs/relnotes/index.rst +++ b/master/docs/relnotes/index.rst @@ -68,6 +68,8 @@ Features * The ``MessageFormatter`` class also allows inline-templates with the ``template`` parameter. +* The ``MessageFormatter`` class allows custom mail's subjects with the ``subject`` and ``subject_name`` parameters. + .. _Hyper: https://hyper.sh Fixes From 6c691f5418267619bfc59cc71598dab450bac06f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Fri, 21 Oct 2016 12:57:51 +0200 Subject: [PATCH 11/19] MessageFormatter: Unify the status_text. --- master/buildbot/reporters/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index 128d1e4b98b..5e79e66a247 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -78,7 +78,7 @@ def getDetectedStatus(self, mode, results, previous_results): else: text = "failed build" elif results == WARNINGS: - text = "The Buildbot has detected a problem in the build" + text = "problem in the build" elif results == SUCCESS: if "change" in mode and previous_results is not None and previous_results != results: text = "restored build" From dd114c3119d8473a79337c55b485fe93f00a3f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Tue, 25 Oct 2016 10:11:06 +0200 Subject: [PATCH 12/19] MessageFormatter: allow to extend the context given to the template --- master/buildbot/reporters/message.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index 5e79e66a247..f7385ff63b4 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -35,7 +35,7 @@ class MessageFormatter(object): wantLogs = False def __init__(self, template_name=None, template_dir=None, template=None, - subject_name=None, subject=None, template_type=None): + subject_name=None, subject=None, template_type=None, ctx=None): if (template is not None) and ((template_name is not None) or (template_dir is not None)): config.error("Only one of template or template path can be given") @@ -48,6 +48,11 @@ def __init__(self, template_name=None, template_dir=None, template=None, if template_type is not None: self.template_type = template_type + if ctx is None: + ctx = {} + + self.ctx = ctx + def getTemplate(self, filename, dirname, content): if content and filename: @@ -152,7 +157,7 @@ def __call__(self, mode, buildername, buildset, build, master, previous_results, ss_list = buildset['sourcestamps'] results = build['results'] - cxt = dict(results=build['results'], + ctx = dict(results=build['results'], mode=mode, buildername=buildername, workername=build['properties'].get( @@ -170,8 +175,9 @@ def __call__(self, mode, buildername, buildset, build, master, previous_results, summary=self.messageSummary(build, results), sourcestamps=self.messageSourceStamps(ss_list) ) - body = self.body_template.render(cxt) + ctx.update(self.ctx) + body = self.body_template.render(ctx) email = {'body': body, 'type': self.template_type} if self.subject_template is not None: - email['subject'] = self.subject_template.render(cxt) + email['subject'] = self.subject_template.render(ctx) return email From f124ce8aed3a938ea8b15a90592de1709ad1acaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Tue, 25 Oct 2016 10:18:31 +0200 Subject: [PATCH 13/19] Improve slightly the doc about the MessageFormatter. --- master/docs/manual/cfg-reporters.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/master/docs/manual/cfg-reporters.rst b/master/docs/manual/cfg-reporters.rst index 54a0e53c57a..cfa8f33925e 100644 --- a/master/docs/manual/cfg-reporters.rst +++ b/master/docs/manual/cfg-reporters.rst @@ -124,10 +124,6 @@ Another example of a function delivering a customized html email is given below: from buildbot.plugins import reporters - class DetailledMessageFormatter(reporters.MessageFormatter): - wantSteps = True - wantLogs = True - # XXX: This is work in progress, use with care. template=u'''\

Build status: {{ summary }}

@@ -142,7 +138,9 @@ Another example of a function delivering a customized html email is given below: sendToInterestedUsers=False, mode=('failing',), extraRecipients=['listaddr@example.org'], - messageFormatter=DetailledMessageFormatter(template=template, template_type='html')) + messageFormatter=reporters.MessageFormatter( + template=template, template_type='html', + wantProperties=True, wantSteps=True, wantLogs=True)) .. _PyOpenSSL: http://pyopenssl.sourceforge.net/ From d6e04fbd7b7838a02278e41cd67044fbf62346f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Tue, 25 Oct 2016 10:27:25 +0200 Subject: [PATCH 14/19] Update the release note --- master/docs/relnotes/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/master/docs/relnotes/index.rst b/master/docs/relnotes/index.rst index 2aac36ed7dc..115aa14a157 100644 --- a/master/docs/relnotes/index.rst +++ b/master/docs/relnotes/index.rst @@ -70,6 +70,8 @@ Features * The ``MessageFormatter`` class allows custom mail's subjects with the ``subject`` and ``subject_name`` parameters. +* The ``MessageFormatter`` class allows extending the context given to the Templates via the ``ctx`` parameter. + .. _Hyper: https://hyper.sh Fixes From 09c73faf296e51bad6eb95eaff77880e16489cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Tue, 25 Oct 2016 11:36:00 +0200 Subject: [PATCH 15/19] Make pyflakes happy --- master/buildbot/reporters/message.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index f7385ff63b4..ff99ce08917 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -53,7 +53,6 @@ def __init__(self, template_name=None, template_dir=None, template=None, self.ctx = ctx - def getTemplate(self, filename, dirname, content): if content and filename: config.error("Only one of template or template path can be given") @@ -73,7 +72,6 @@ def getTemplate(self, filename, dirname, content): return env.get_template(filename) - def getDetectedStatus(self, mode, results, previous_results): if results == FAILURE: From 11a781bdd842e298224df6a2043fdb7f6fc7d7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Tue, 25 Oct 2016 12:09:55 +0200 Subject: [PATCH 16/19] MessageFormatter: Update the constructor --- master/buildbot/reporters/message.py | 30 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index ff99ce08917..9710d5a80d5 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -28,22 +28,23 @@ class MessageFormatter(object): - template_name = 'default_mail.txt' + template_filename = 'default_mail.txt' template_type = 'plain' - wantProperties = True - wantSteps = False - wantLogs = False - def __init__(self, template_name=None, template_dir=None, template=None, - subject_name=None, subject=None, template_type=None, ctx=None): + def __init__(self, template_dir=None, + template_filename=None, template=None, template_name=None, + subject_filename=None, subject=None, + template_type=None, ctx=None, + wantProperties=True, wantSteps=False, wantLogs=False): - if (template is not None) and ((template_name is not None) or (template_dir is not None)): - config.error("Only one of template or template path can be given") + if template_name is not None: + config.warnDeprecated('0.9.1', "template_name is deprecated, use template_filename") + template_filename = template_name - self.body_template = self.getTemplate(template_name, template_dir, template) + self.body_template = self.getTemplate(template_filename, template_dir, template) self.subject_template = None - if subject_name or subject: - self.subject_template = self.getTemplate(subject_name, template_dir, subject) + if subject_filename or subject: + self.subject_template = self.getTemplate(subject_filename, template_dir, subject) if template_type is not None: self.template_type = template_type @@ -52,9 +53,12 @@ def __init__(self, template_name=None, template_dir=None, template=None, ctx = {} self.ctx = ctx + self.wantProperties = wantProperties + self.wantSteps = wantSteps + self.wantLogs = wantLogs def getTemplate(self, filename, dirname, content): - if content and filename: + if content and (filename or dirname): config.error("Only one of template or template path can be given") if content: @@ -150,7 +154,7 @@ def messageSummary(self, build, results): return text def __call__(self, mode, buildername, buildset, build, master, previous_results, blamelist): - """Generate a buildbot mail message and return a dictionnary + """Generate a buildbot mail message and return a dictionary containing the message body, type and subject.""" ss_list = buildset['sourcestamps'] results = build['results'] From ae23f0b5e549903a0a4d14e169b90f6223d6cc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Tue, 25 Oct 2016 12:53:24 +0200 Subject: [PATCH 17/19] Update the documentation for the MessageFormatter --- master/docs/manual/cfg-reporters.rst | 93 ++++++++++++++++++---------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/master/docs/manual/cfg-reporters.rst b/master/docs/manual/cfg-reporters.rst index cfa8f33925e..e9465d7b466 100644 --- a/master/docs/manual/cfg-reporters.rst +++ b/master/docs/manual/cfg-reporters.rst @@ -124,14 +124,13 @@ Another example of a function delivering a customized html email is given below: from buildbot.plugins import reporters - # XXX: This is work in progress, use with care. template=u'''\

Build status: {{ summary }}

Worker used: {{ workername }}

{% for step in build['steps'] %}

{{ step['name'] }}: {{ step['result'] }}

{% endfor %} - -- The Buildbot

+

-- The Buildbot

''' mn = reporters.MailNotifier(fromaddr="buildbot@example.org", @@ -140,7 +139,7 @@ Another example of a function delivering a customized html email is given below: extraRecipients=['listaddr@example.org'], messageFormatter=reporters.MessageFormatter( template=template, template_type='html', - wantProperties=True, wantSteps=True, wantLogs=True)) + wantProperties=True, wantSteps=True)) .. _PyOpenSSL: http://pyopenssl.sourceforge.net/ @@ -279,13 +278,9 @@ MailNotifier arguments Regardless of the setting of ``lookup``, ``MailNotifier`` will also send mail to addresses in the ``extraRecipients`` list. ``messageFormatter`` - This is a optional function that can be used to generate a custom mail message. - A :func:`messageFormatter` function takes the mail mode (``mode``), builder name (``name``), the build Data API results (``build``), the result code (``results``), and a reference to the BuildMaster object (``master``), which can then be used to create additional Data API calls. - It returns a dictionary. - The ``body`` key gives a string that is the complete text of the message. - The ``type`` key is the message type ('plain' or 'html'). - The 'html' type should be used when generating an HTML message. - The ``subject`` key is optional, but gives the subject for the email. + This is an optional instance of the ``reporters.MessageFormatter`` class that can be used to generate a custom mail message. + This class uses the Jinja2_ templating language to generate the body and optionally the subject of the mails. + Templates can either be given inline (as string), or read from the filesystem. ``extraHeaders`` (dictionary). @@ -293,46 +288,82 @@ MailNotifier arguments Both the keys and the values may be a `Interpolate` instance. -As a help to those writing :func:`messageFormatter` functions, the following table describes how to get some useful pieces of information from the various data objects: +MessageFormatter arguments +++++++++++++++++++++++++++ -Name of the builder that generated this event - ``name`` +The easiest way to use the ``messageFormatter`` parameter is to create a new instance of the ``reporters.MessageFormatter`` class. +The constructor to that class takes the following arguments: -Title of the BuildMaster - ``master.config.title`` +``template_dir`` + This is the directory that is used to look for the various templates. -MailNotifier mode - ``mode`` (a combination of ``change``, ``failing``, ``passing``, ``problem``, ``warnings``, ``exception``, ``all``) +``template_filename`` + This is the name of the file in the ``template_dir`` directory that will be used to generate the body of the mail. + It defaults to 'default_mail.txt'. + +``template`` + If this parameter is set, this parameter indicates the content of the template used to generate the body of the mail as string. + +``template_type`` + This indicates the type of the generated template. + Use either 'plain' (the default) or 'html'. + +``subject_filename`` + This is the name of the file in the ``template_dir`` directory that contains the content of the subject of the mail. + +``subject`` + Alternatively, this is the content of the subject of the mail as string. + +``ctx`` + This is an extension of the standard context that will be given to the templates. + Use this to add content to the templates that is otherwise not available. + +``wantProperties`` + This parameter (defaults to True) will extend the content of the given ``build`` object with the Properties from the build. -Builder result as a string +``wantSteps`` + This parameter (defaults to False) will extend the content of the given ``build`` object with information about the steps of the build. + Use it only when necessary as this increases the overhead in term of CPU and memory on the master. - :: +``wantLogs`` + This parameter (defaults to False) will extend the content of the steps of the given ``build`` object with the full Logs of each steps from the build. + This requires ``wantSteps`` to be True. + Use it only when mandatory as this increases the overhead in term of CPU and memory on the master greatly. - from buildbot.plugins import util - result_str = util.Results[results] - # one of 'success', 'warnings', 'failure', 'skipped', or 'exception' + +As a help to those writing Jinja2 templates the following table describes how to get some useful pieces of information from the various data objects: + +Name of the builder that generated this event + ``{{ buildername }}`` + +Title of the BuildMaster + ``{{ projects }}`` + +MailNotifier mode + ``{{ mode }}`` (a combination of ``change``, ``failing``, ``passing``, ``problem``, ``warnings``, ``exception``, ``all``) URL to build page - ``reporters.utils.getURLForBuild(master, build['buildid'])`` + ``{{ build_url }}`` URL to buildbot main page - ``master.config.buildbotURL`` + ``{{ buildbot_url }}`` Build text - ``build['state_string']`` + ``{{ build['state_string'] }}`` Mapping of property names to (values, source) - ``build['properties']`` + ``{{ build['properties'] }}`` -Worker name - ``build['properties']['workername']`` +For instance the build reason (from a forced build) + ``{{ build['properties']['reason'][0] }}`` -Build reason (from a forced build) - ``build['properties']['reason']`` +Worker name + ``{{ workername }}`` List of responsible users - ``reporters.utils.getResponsibleUsersForBuild(master, build['buildid'])`` + ``{{ blamelist | join(', ') }}`` +.. _Jinja2: http://jinja.pocoo.org/docs/dev/templates/ .. bb:reporter:: IRC From ddb3bd0c955e9de78be9f80f9e3ce283f2446c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Tue, 25 Oct 2016 13:32:01 +0200 Subject: [PATCH 18/19] Fix typo in attribute name --- master/buildbot/reporters/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index 9710d5a80d5..3b2197d7cf3 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -72,7 +72,7 @@ def getTemplate(self, filename, dirname, content): loader=loader, undefined=jinja2.StrictUndefined) if filename is None: - filename = self.template_name + filename = self.template_filename return env.get_template(filename) From a6c2010ab10ed9743a2a6008d5d6604b03ab7698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Allard?= Date: Tue, 25 Oct 2016 14:09:58 +0200 Subject: [PATCH 19/19] Slight documentation fixes --- master/docs/manual/cfg-reporters.rst | 2 +- master/docs/relnotes/index.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/master/docs/manual/cfg-reporters.rst b/master/docs/manual/cfg-reporters.rst index e9465d7b466..070a0110700 100644 --- a/master/docs/manual/cfg-reporters.rst +++ b/master/docs/manual/cfg-reporters.rst @@ -299,7 +299,7 @@ The constructor to that class takes the following arguments: ``template_filename`` This is the name of the file in the ``template_dir`` directory that will be used to generate the body of the mail. - It defaults to 'default_mail.txt'. + It defaults to ``default_mail.txt``. ``template`` If this parameter is set, this parameter indicates the content of the template used to generate the body of the mail as string. diff --git a/master/docs/relnotes/index.rst b/master/docs/relnotes/index.rst index 115aa14a157..aebb8e9309a 100644 --- a/master/docs/relnotes/index.rst +++ b/master/docs/relnotes/index.rst @@ -132,6 +132,8 @@ Deprecations, Removals, and Non-Compatible Changes * The ``user`` and ``password`` parameters of the ``HttpStatusPush`` reporter have been deprecated in favor of the ``auth`` parameter. +* The ``template_name`` parameter of the ``MessageFormatter`` class has been deprecated in favor of ``template_filename``. + Buildslave ----------