Skip to content

Commit

Permalink
Convert update notes in plaintext in email and __str__
Browse files Browse the repository at this point in the history
Signed-off-by: Mattia Verga <mattia.verga@tiscali.it>
  • Loading branch information
mattiaverga committed Nov 1, 2023
1 parent 18e3f0a commit f37f8c5
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 42 deletions.
7 changes: 3 additions & 4 deletions bodhi-server/bodhi/server/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
"""A collection of utilities for sending e-mail to Bodhi users."""
from textwrap import wrap
import os
import smtplib
import typing

from bodhi.server import log
from bodhi.server.config import config
from bodhi.server.util import get_rpm_header, get_absolute_path
from bodhi.server.util import get_rpm_header, get_absolute_path, markdown_to_text, wrap_text

if typing.TYPE_CHECKING: # pragma: no cover
from bodhi.server.models import Update # noqa: 401
Expand Down Expand Up @@ -307,8 +306,8 @@ def get_template(update: 'Update', use_template: str = 'fedora_errata_template')
info['product'] = update.release.long_name
info['notes'] = ""
if update.notes and len(update.notes):
info['notes'] = "Update Information:\n\n%s\n" % \
'\n'.join(wrap(update.notes, width=80))
plaintext = markdown_to_text(update.notes)
info['notes'] = f"Update Information:\n\n{wrap_text(plaintext)}\n"
info['notes'] += line

# Add this update's referenced Bugzillas
Expand Down
79 changes: 41 additions & 38 deletions bodhi-server/bodhi/server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
pagure_api_get,
tokenize,
build_names_by_type,
markdown_to_text,
wrap_text,
)


Expand Down Expand Up @@ -2971,15 +2973,10 @@ def get_bugstring(self, show_titles=False):
"""
val = ''
if show_titles:
i = 0
bugstr = []
for bug in self.bugs:
bugstr = '%s%s - %s\n' % (
i and ' ' * 11 + ': ' or '', bug.bug_id, bug.title)
val += '\n'.join(wrap(
bugstr, width=67,
subsequent_indent=' ' * 11 + ': ')) + '\n'
i += 1
val = val[:-1]
bugstr.append(f"{bug.bug_id} - {bug.title}")
val = '\n'.join(bugstr)
else:
val = ' '.join([str(bug.bug_id) for bug in self.bugs])
return val
Expand Down Expand Up @@ -3435,45 +3432,51 @@ def __str__(self):
Returns:
str: A string representation of the update.
"""
val = "%s\n%s\n%s\n" % ('=' * 80, '\n'.join(wrap(
self.alias, width=80, initial_indent=' ' * 5,
subsequent_indent=' ' * 5)), '=' * 80)
val += """ Release: %s
Status: %s
Type: %s
Severity: %s
Karma: %d""" % (self.release.long_name, self.status.description,
self.type.description, self.severity, self.karma)
nl = '\n'
val = f"""{'=' * 80}
{nl.join(wrap(self.alias, width=79, initial_indent=' ' * 5, subsequent_indent=' ' * 5))}
{'=' * 80}
{'Release:' : >12} {self.release.long_name}
{'Status:' : >12} {self.status.description}
{'Type:' : >12} {self.type.description}
{'Severity:' : >12} {self.severity}
{'Karma:' : >12} {self.karma}"""
if self.critpath:
val += "\n Critpath: %s" % self.critpath
val += f"{nl}{'Critpath:' : >12} {self.critpath}"
if self.request is not None:
val += "\n Request: %s" % self.request.description
val += f"{nl}{'Request:' : >12} {self.request.description}"
if self.bugs:
bugs = self.get_bugstring(show_titles=True)
val += "\n Bugs: %s" % bugs
bugs = wrap_text(
self.get_bugstring(show_titles=True), width=79,
initial_indent=f"{'Bugs:' : >12} ",
subsequent_indent=f"{' ' * 13}")
val += f"{nl}{bugs}"
if self.notes:
notes = wrap(
self.notes, width=67, subsequent_indent=' ' * 11 + ': ')
val += "\n Notes: %s" % '\n'.join(notes)
notes = wrap_text(
markdown_to_text(self.notes).strip(), width=79,
initial_indent=f"{'Notes:' : >12} ",
subsequent_indent=f"{' ' * 13}")
val += f"{nl}{notes}"
username = None
if self.user:
username = self.user.name
val += """
Submitter: %s
Submitted: %s\n""" % (username, self.date_submitted)
val += f"""
{'Submitter:' : >12} {username}
{'Submitted:' : >12} {self.date_submitted}
"""
if self.comments_since_karma_reset:
val += " Comments: "
comments = []
for comment in self.comments_since_karma_reset:
comments.append("%s%s - %s (karma %s)" % (' ' * 13,
comment.user.name, comment.timestamp,
comment.karma))
comments_list = []
for comment in reversed(self.comments_since_karma_reset):
comments_list.append(f"{comment.user.name} - {comment.timestamp} "
f"(karma {comment.karma})")
if comment.text:
text = wrap(comment.text, initial_indent=' ' * 13,
subsequent_indent=' ' * 13, width=67)
comments.append('\n'.join(text))
val += '\n'.join(comments).lstrip() + '\n'
val += "\n %s\n" % self.abs_url()
comments_list.append(comment.text)
comments = wrap_text(
'\n'.join(comments_list), width=79,
initial_indent=f"{'Comments:' : >12} ",
subsequent_indent=f"{' ' * 13}")
val += f"{comments}{nl}"
val += f"{nl} {self.abs_url()}"
return val

def update_bugs(self, bug_ids, session):
Expand Down
45 changes: 45 additions & 0 deletions bodhi-server/bodhi/server/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from contextlib import contextmanager
from datetime import datetime, timedelta
from importlib import import_module
from textwrap import TextWrapper
from urllib.parse import urlencode
import bz2
import errno
Expand All @@ -38,6 +39,7 @@
import types
import typing

from bs4 import BeautifulSoup
from pyramid.i18n import TranslationStringFactory
import arrow
import bleach
Expand Down Expand Up @@ -1362,3 +1364,46 @@ def eol_releases(days: int = 30) -> list:
eol_releases.append((release.long_name, release.eol))

return eol_releases


def markdown_to_text(markdown_string: str) -> str:
"""
Convert a markdown string to plaintext.
Credit about this method goes to Github gist at
https://gist.github.com/lorey/eb15a7f3338f959a78cc3661fbc255fe
Args:
markdown_string: a markdown formatted text.
Returns:
Text with markdown tags stripped out.
"""
html = markdown.markdown(markdown_string, extensions=['fenced_code'])

# extract text
soup = BeautifulSoup(html, "html.parser")
text = ''.join(soup.findAll(string=True))

return text


def wrap_text(text: str, width: int = 80, initial_indent: str = '',
subsequent_indent: str = '', **kwargs) -> str:
"""
Wrap text to the specified line length preserving existing newlines.
Args:
text: the text that needs to be wrapped.
width: the maximum line length.
Returns:
Text wrapped at the desired length.
"""
wrapper = TextWrapper(width=width, subsequent_indent=subsequent_indent, **kwargs)

paragraphs = []
for i, paragraph in enumerate(text.splitlines()):
paragraphs.extend(wrapper.wrap(f"{not i and initial_indent or ''}"
f"{i and subsequent_indent or ''}"
f"{paragraph}"))

return '\n'.join(paragraphs)
1 change: 1 addition & 0 deletions bodhi-server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ alembic = ">=1.5.5"
arrow = ">=0.17.0"
authlib = ">=0.15.4"
backoff = ">=1.10.0"
beautifulsoup4 = "^4.12.0"
bleach = ">=3.2.3"
bodhi-messages = "^7.0"
celery = ">=5.2.1"
Expand Down
19 changes: 19 additions & 0 deletions bodhi-server/tests/test_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ def test_testing_update(self):
# The advisory flag should be included in the dnf instructions.
assert 'dnf --enablerepo=updates-testing upgrade --advisory {}'.format(u.alias) in t

def test_no_markdown(self):
"""Update notes should be sent in plaintext."""
u = models.Update.query.first()
u.notes = """Some **fancy** update description:
- first element
- second element
Let's also have some code:
```<test@example.com>```
"""

t = mail.get_template(u)

# Assemble the template for easier asserting.
t = '\n'.join([line for line in t[0]])
assert 'Some fancy update description:' in t
assert '```<test@example.com>```' not in t

def test_read_template(self):
"""Ensure that email template is read correctly."""
tpl_name = "maillist_template"
Expand Down
26 changes: 26 additions & 0 deletions bodhi-server/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5001,6 +5001,32 @@ def test_obsolete_older_updates_with_notes_too_long(self, comment, obsolete):
new_update.obsolete_older_updates(self.db)
assert new_update.notes == 'b' * 101

def test_update__str__(self):
"""
Test the __str__ representation of the Update.
"""
update = self.obj
update.notes = 'a' * 100
expected = f"""\
================================================================================
{update.alias}
================================================================================
Release: Fedora 11
Status: pending
Type: security
Severity: medium
Karma: 0
Request: testing
Bugs: 1 - None
2 - None
Notes: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Submitter: lmacken
Submitted: {update.date_submitted}
{update.abs_url()}"""
assert update.__str__() == expected


class TestUser(ModelTest):
klass = model.User
Expand Down
1 change: 1 addition & 0 deletions news/5049.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update notes are now converted to plaintext when printed in email or messages

0 comments on commit f37f8c5

Please sign in to comment.