Commit 8aebdaf7 authored by Arkadiusz Hiler's avatar Arkadiusz Hiler

Add support for Markdown results

Results send to Patchwork can now be marked as markdown (on a Test model
level). Such results are then rendered into HTML and displayed on the
web views.

Emails that are sent to the mailing list / recipients for such results
are going to be multipart/alternative containing both the text form (raw
Markdown) and the rendered HTML.

Fixes: #6Signed-off-by: default avatarArkadiusz Hiler <arkadiusz.hiler@intel.com>
parent 189b72f2
Pipeline #173921 passed with stage
in 1 minute and 31 seconds
......@@ -5,3 +5,5 @@ jsonfield
django-filter==2.2
celery>=4.3,<5.0
redis
Markdown>=3.2,<3.3
lxml
# Generated by Django 2.2.14 on 2020-07-07 22:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('patchwork', '0034_seriesrevision_skip_requester'),
]
operations = [
migrations.AddField(
model_name='test',
name='is_markdown',
field=models.BooleanField(default=False),
),
]
......@@ -824,6 +824,7 @@ class Test(models.Model):
help_text='Comma separated list of emails')
mail_condition = models.SmallIntegerField(choices=CONDITION_CHOICES,
default=CONDITION_ALWAYS)
is_markdown = models.BooleanField(default=False)
class Meta:
unique_together = [('project', 'name')]
......
{% load results %}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Project List - Patchwork</title>
<style id="css-table-select" type="text/css">
td { padding: 2pt; }
</style>
</head>
<body>
<b>Patch Details</b>
<table>
<tr><td><b>Series:</b></td><td>{{ human_name }}</td></tr>
<tr><td><b>URL:</b></td><td><a href="{{ url }}">{{ url }}</a></td></tr>
<tr><td><b>State:</b></td><td>{{ state }}</td></tr>
{% if result.url %}
<tr><td><b>Details:</b></td><td><a href="{{ result.url }}">{{ result.url }}</a></td></tr>
{% endif %}
</table>
{% if result.summary %}
{{ result.summary|render_markdown }}
{% endif %}
</body>
</html>
{% autoescape off %}== {{ object_type }} Details ==
Series: {{ human_name }}
URL : {{ url }}
State : {{ state }}
{% if result.summary %}== Summary ==
{{ result.summary }}
{% if result.url %}== Logs ==
For more details see: {{ result.url }}{% endif %}{% endif %}{% endautoescape %}
{% load results %}
<div class="panel panel-default">
{% if test_result.summary %}
<div class="panel-heading small-panel-heading cursor-pointer" role="tab" data-toggle="collapse" href="#collapse-{{ revision.version }}-{{ forloop.counter }}" aria-expanded="false" aria-controls="collapse-{{ revision.version}}-{{ forloop.counter }}" id="heading-{{ forloop.counter }}">
......@@ -18,8 +19,13 @@
</div>
{% if test_result.summary %}
<div id="collapse-{{ revision.version }}-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-{{ forloop.counter}}">
<pre class="panel-body test-result">
{{ test_result.summary }}</pre>
{% if test_result.test.is_markdown %}
<div class="panel-body test-result">
{{ test_result.summary|render_markdown }}
</div>
{% else %}
<pre class="panel-body test-result">{{ test_result.summary }}</pre>
{% endif %}
</div>
{% endif %}
</div>
......
# Patchwork - automated patch tracking system
# Copyright (C) 2020 Arkadiusz Hiler <arkadiusz.hiler@intel.com>
#
# This file is part of the Patchwork package.
#
# Patchwork 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 2 of the License, or
# (at your option) any later version.
#
# Patchwork 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 Patchwork; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from markdown import markdown
from django import template
from django.utils.safestring import mark_safe
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter
@stringfilter
def render_markdown(text):
return mark_safe(markdown(text, extensions=['nl2br']))
......@@ -27,6 +27,7 @@ import re
import os
import dateutil.parser as dateparse
from lxml import html
from django.core import mail
from django.test.utils import override_settings
......@@ -837,7 +838,7 @@ class TestResultTest(APITestBase):
self._cleanup_tests()
def _configure_test(self, url, test_name, recipient, condition,
to_list=None, cc_list=None):
to_list=None, cc_list=None, is_markdown=False):
"""Create test_name and configure it"""
self._post_result(url, test_name, 'pending')
tests = Test.objects.all()
......@@ -847,6 +848,7 @@ class TestResultTest(APITestBase):
test.mail_condition = condition
test.mail_to_list = to_list
test.mail_cc_list = cc_list
test.is_markdown = is_markdown
test.save()
def testMailHeaders(self):
......@@ -1046,6 +1048,61 @@ class TestResultTest(APITestBase):
u"✓ super test: success for " + test[1])
mail.outbox = []
def testSummary(self):
for url in self.test_urls:
self.assertEqual(len(mail.outbox), 0)
self._configure_test(url, 'super test', Test.RECIPIENT_SUBMITTER,
Test.CONDITION_ALWAYS)
self._post_result(url, 'super test', 'success',
summary="I'm\na\nsummary\n",
url="https://example.com")
self.assertEqual(len(mail.outbox), 1)
m = mail.outbox[0]
self.assertTrue('https://example.com' in m.body)
self.assertTrue("I'm\na\nsummary\n" in m.body)
mail.outbox = []
def testMarkdown(self):
for url in self.test_urls:
self.assertEqual(len(mail.outbox), 0)
self._configure_test(url, 'super test', Test.RECIPIENT_SUBMITTER,
Test.CONDITION_ALWAYS,
is_markdown=True)
self._post_result(url, 'super test', 'success',
summary="[foo](https://example.com)")
self.assertEqual(len(mail.outbox), 1)
m = mail.outbox[0]
self.assertTrue('[foo](https://example.com)' in m.body)
self.assertEqual(len(m.alternatives), 1)
alt = m.alternatives[0]
self.assertEqual(alt[1], 'text/html')
tree = html.fromstring(alt[0])
self.assertTrue(tree.xpath("//a[@href='https://example.com']"))
mail.outbox = []
def testNoMarkdown(self):
for url in self.test_urls:
self.assertEqual(len(mail.outbox), 0)
self._configure_test(url, 'super test', Test.RECIPIENT_SUBMITTER,
Test.CONDITION_ALWAYS,
is_markdown=False)
self._post_result(url, 'super test', 'success',
summary="[foo](https://example.com)")
self.assertEqual(len(mail.outbox), 1)
m = mail.outbox[0]
self.assertTrue('[foo](https://example.com)' in m.body)
self.assertEqual(len(m.alternatives), 0)
mail.outbox = []
def _test_state(self, serializer, series):
return serializer.get_test_state(Series.objects.get(pk=series.pk))
......
# Patchwork - automated patch tracking system
# Copyright (C) 2020 Arkadiusz Hiler <arkadiusz.hiler@intel.com>
#
# This file is part of the Patchwork package.
#
# Patchwork 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 2 of the License, or
# (at your option) any later version.
#
# Patchwork 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 Patchwork; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from lxml import html
from patchwork.models import (Test, TestResult, TestState,
SeriesRevision, Series)
from patchwork.tests.utils import create_user
from .test_series import Series0010
class RenderingModes(Series0010):
fixtures = ['default_states', 'default_events']
SERIES_RESULTS_URL = '/series/{}/'
def setUp(self):
super(Series0010, self).setUp()
self.series = Series.objects.get()
self.series_revision = SeriesRevision.objects.get()
self.test = Test(project=self.project, name="some.test")
self.test.save()
self.test_result = TestResult(test=self.test,
revision=self.series_revision,
user=create_user(),
state=TestState.STATE_SUCCESS)
self.test_result.save()
def test_markdown(self):
self.test_result.summary = "[foo](http://example.com)"
self.test_result.save()
self.test.is_markdown = True
self.test.save()
resp = self.client.get('/series/%s/' % self.series.pk)
self.assertEquals(resp.status_code, 200)
content = resp.content.decode()
tree = html.fromstring(content)
self.assertTrue(tree.xpath("//a[@href='http://example.com']"))
self.assertFalse(self.test_result.summary in content)
def test_no_markdown(self):
self.test_result.summary = "[foo](http://example.com)"
self.test_result.save()
self.test.is_markdown = False
self.test.save()
resp = self.client.get('/series/%s/' % self.series.pk)
self.assertEquals(resp.status_code, 200)
content = resp.content.decode()
tree = html.fromstring(content)
self.assertFalse(tree.xpath("//a[@href='http://example.com']"))
self.assertTrue(self.test_result.summary in content)
......@@ -24,6 +24,7 @@ from django.conf import settings
from django.core import mail
from django.db.models import Q
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
from django.template.loader import render_to_string
from patchwork.tasks import send_reviewer_notification
from patchwork.models import (Project, Series, SeriesRevision, Patch, EventLog,
State, Test, TestResult, TestState, Person,
......@@ -387,26 +388,23 @@ class ResultMixin(object):
subject = tick + u" %s: %s for %s" % (result.test.name,
result.get_state_display(),
obj.human_name())
body = ''
body += '== %s Details ==\n\n' % self._object_type(obj)
body += 'Series: ' + obj.human_name() + '\n'
body += 'URL : ' + \
request.build_absolute_uri(check_obj.get_absolute_url()) + '\n'
body += 'State : ' + result.get_state_display() + '\n'
body += '\n'
if result.summary:
body += "== Summary ==\n\n"
body += result.summary
if body.endswith('\n'):
body += "\n"
else:
body += "\n\n"
if result.url:
body += "== Logs ==\n\n"
body += "For more details see: " + result.url
body += "\n"
return (subject, body)
url = request.build_absolute_uri(check_obj.get_absolute_url())
context = {
'result': result,
'object_type': self._object_type(obj),
'human_name': obj.human_name(),
'url': url,
'state': result.get_state_display()
}
body = render_to_string('emails/result.body.txt', context)
if result.test.is_markdown:
html = render_to_string('emails/result.body.html', context)
else:
html = None
return (subject, body, html)
def _get_msgid(self, obj):
try:
......@@ -471,8 +469,8 @@ class ResultMixin(object):
to = []
if to:
subject, body = self._prepare_mail(request, instance,
obj, check_obj)
subject, body, html = self._prepare_mail(request, instance,
obj, check_obj)
msgid = self._get_msgid(obj)
headers = {
'X-Patchwork-Hint': 'ignore',
......@@ -480,9 +478,13 @@ class ResultMixin(object):
'In-Reply-To': msgid,
'Reply-To': check_obj.project.listemail,
}
email = mail.EmailMessage(subject, body,
settings.DEFAULT_FROM_EMAIL,
to=to, cc=cc, headers=headers)
email = mail.EmailMultiAlternatives(subject, body,
settings.DEFAULT_FROM_EMAIL,
to=to, cc=cc, headers=headers)
if html:
email.attach_alternative(html, "text/html")
email.send()
return Response(result.data, status=status.HTTP_201_CREATED)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment