We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
RSS feed gives no information about packages build
Since commit 60dc56c that RSS feed doesn't give information about the packages affected in a build.
Example from [Fedora30 Stable - RSS](https://bodhi.fedoraproject.org/rss/updates/?releases=F30&status=stable) :
```xml
<item>
  <title>FEDORA-2019-59d394e0fd</title>
  <link>https://bodhi.fedoraproject.org/updates/createrepo_c-0.14.1-1.fc30</link>
  <description>- Update to 0.14.1
- Add --pkgorigins mode for Koji
- Correct pkg count in headers if there were invalid pkgs (RhBug:1596211)
- Prevent exiting with 0 if errors occur while finalizing repodata.
  </description>
  <pubDate>Fri, 24 May 2019 12:20:49 +0000</pubDate>
</item>
```

Also the link is invalid as in #3248

Looking to the [Fedora30 Stable - Web UI](https://bodhi.fedoraproject.org/updates/?releases=F30&status=stable) it seems that it should be:
```
  <title>createrepo_c-0.14.1-1.fc30</title>
  <link>https://bodhi.fedoraproject.org/updates/FEDORA-2019-59d394e0fd</link>
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bodhi/server/renderers.py`
Content:
```
1 # Copyright © 2014-2019 Red Hat, Inc.
2 #
3 # This file is part of Bodhi.
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 """Define special view renderers, such as RSS."""
19 import logging
20 import operator
21 
22 from pytz import utc
23 from feedgen.feed import FeedGenerator
24 from pyramid.exceptions import HTTPBadRequest
25 
26 
27 log = logging.getLogger(__name__)
28 
29 
30 def rss(info):
31     """
32     Return a RSS renderer.
33 
34     Args:
35         info (pyramid.renderers.RendererHelper): Unused.
36     Returns:
37         function: A function that can be used to render a RSS view.
38     """
39     def render(data, system):
40         """
41         Render the given data as an RSS view.
42 
43         If the request's content type is set to the default, this function will change it to
44         application/rss+xml.
45 
46         Args:
47             data (dict): A dictionary describing the information to be rendered. The information can
48                 be different types of objects, such as updates, users, comments, or overrides.
49             system (pyramid.events.BeforeRender): Used to get the current request.
50         Returns:
51             str: An RSS document representing the given data.
52         """
53         request = system.get('request')
54         if request is not None:
55             response = request.response
56             ct = response.content_type
57             if ct == response.default_content_type:
58                 response.content_type = 'application/rss+xml'
59 
60         if 'updates' in data:
61             key = 'updates'
62             feed_title = 'Released updates'
63         elif 'users' in data:
64             key = 'users'
65             feed_title = 'Bodhi users'
66         elif 'comments' in data:
67             key = 'comments'
68             feed_title = 'User comments'
69         elif 'overrides' in data:
70             key = 'overrides'
71             feed_title = 'Update overrides'
72         else:
73             # This is a request we don't know how to render. Let's return BadRequest and log.
74             log.debug('Unable to render RSS feed for data: %s', data)
75             # See if we have a request so we can set a code without raising an Exception
76             if request is not None:
77                 response.status = HTTPBadRequest.code
78                 return 'Invalid RSS feed request'
79             else:
80                 raise HTTPBadRequest('Invalid RSS feed request')
81 
82         feed_description_list = []
83         for k in request.GET.keys():
84             feed_description_list.append('%s(%s)' % (k, request.GET[k]))
85         if feed_description_list:
86             feed_description = 'Filtered on: ' + ', '.join(feed_description_list)
87         else:
88             feed_description = "All %s" % (key)
89 
90         feed = FeedGenerator()
91         feed.title(feed_title)
92         feed.link(href=request.url, rel='self')
93         feed.description(feed_description)
94         feed.language('en')
95 
96         def linker(route, param, key):
97             def link_dict(obj):
98                 return dict(href=request.route_url(route, **{param: obj[key]}))
99             return link_dict
100 
101         getters = {
102             'updates': {
103                 'title': operator.itemgetter('alias'),
104                 'link': linker('update', 'id', 'alias'),
105                 'description': operator.itemgetter('notes'),
106                 'pubDate': lambda obj: utc.localize(obj['date_submitted']),
107             },
108             'users': {
109                 'title': operator.itemgetter('name'),
110                 'link': linker('user', 'name', 'name'),
111                 'description': operator.itemgetter('name'),
112             },
113             'comments': {
114                 'title': operator.itemgetter('rss_title'),
115                 'link': linker('comment', 'id', 'id'),
116                 'description': operator.itemgetter('text'),
117                 'pubDate': lambda obj: utc.localize(obj['timestamp']),
118             },
119             'overrides': {
120                 'title': operator.itemgetter('nvr'),
121                 'link': linker('override', 'nvr', 'nvr'),
122                 'description': operator.itemgetter('notes'),
123                 'pubDate': lambda obj: utc.localize(obj['submission_date']),
124             },
125         }
126 
127         for value in data[key]:
128             feed_item = feed.add_item()
129             for name, getter in getters[key].items():
130                 # Because we have to use methods to fill feed entry attributes,
131                 # it's done by getting methods by name and calling them
132                 # on the same line.
133                 getattr(feed_item, name)(getter(value))
134 
135         return feed.rss_str()
136 
137     return render
138
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bodhi/server/renderers.py b/bodhi/server/renderers.py
--- a/bodhi/server/renderers.py
+++ b/bodhi/server/renderers.py
@@ -23,6 +23,8 @@
 from feedgen.feed import FeedGenerator
 from pyramid.exceptions import HTTPBadRequest
 
+from bodhi.server.util import markup
+
 
 log = logging.getLogger(__name__)
 
@@ -98,11 +100,44 @@
                 return dict(href=request.route_url(route, **{param: obj[key]}))
             return link_dict
 
+        def describe_update(alias, notes, builds):
+            """
+            Wrap calls to operator.itemgetter to retrieve notes and builds list.
+
+            Methods are used to fill feed entry values, so we must use a wrapper
+            to get an HTML formatted description from the `notes` and the `builds`
+            properties of the update.
+
+            For example:
+            getter = describe_update(operator.itemgetter('notes'),operator.itemgetter('builds'))
+            description_value = getter(update_data)
+
+            Args:
+                alias (operator.itemgetter): A callable object which returns update alias
+                    as string.
+                notes (operator.itemgetter): A callable object which returns update notes
+                    as string.
+                builds (operator.itemgetter): A callable object which returns a list of builds
+                    associated to the update.
+            Returns:
+                function: A function which accepts a dict representing an update as parameter.
+            """
+            def describe(*args, **kwargs):
+                text = f'# {alias(*args, **kwargs)}\n'
+                text += f'## Packages in this update:\n'
+                for p in builds(*args, **kwargs):
+                    text += f'* {p.nvr}\n'
+                text += f'## Update description:\n{notes(*args, **kwargs)}'
+                return markup(None, text)
+            return describe
+
         getters = {
             'updates': {
-                'title': operator.itemgetter('alias'),
+                'title': operator.itemgetter('title'),
                 'link': linker('update', 'id', 'alias'),
-                'description': operator.itemgetter('notes'),
+                'description': describe_update(operator.itemgetter('alias'),
+                                               operator.itemgetter('notes'),
+                                               operator.itemgetter('builds')),
                 'pubDate': lambda obj: utc.localize(obj['date_submitted']),
             },
             'users': {

{"golden_diff": "diff --git a/bodhi/server/renderers.py b/bodhi/server/renderers.py\n--- a/bodhi/server/renderers.py\n+++ b/bodhi/server/renderers.py\n@@ -23,6 +23,8 @@\n from feedgen.feed import FeedGenerator\n from pyramid.exceptions import HTTPBadRequest\n \n+from bodhi.server.util import markup\n+\n \n log = logging.getLogger(__name__)\n \n@@ -98,11 +100,44 @@\n                 return dict(href=request.route_url(route, **{param: obj[key]}))\n             return link_dict\n \n+        def describe_update(alias, notes, builds):\n+            \"\"\"\n+            Wrap calls to operator.itemgetter to retrieve notes and builds list.\n+\n+            Methods are used to fill feed entry values, so we must use a wrapper\n+            to get an HTML formatted description from the `notes` and the `builds`\n+            properties of the update.\n+\n+            For example:\n+            getter = describe_update(operator.itemgetter('notes'),operator.itemgetter('builds'))\n+            description_value = getter(update_data)\n+\n+            Args:\n+                alias (operator.itemgetter): A callable object which returns update alias\n+                    as string.\n+                notes (operator.itemgetter): A callable object which returns update notes\n+                    as string.\n+                builds (operator.itemgetter): A callable object which returns a list of builds\n+                    associated to the update.\n+            Returns:\n+                function: A function which accepts a dict representing an update as parameter.\n+            \"\"\"\n+            def describe(*args, **kwargs):\n+                text = f'# {alias(*args, **kwargs)}\\n'\n+                text += f'## Packages in this update:\\n'\n+                for p in builds(*args, **kwargs):\n+                    text += f'* {p.nvr}\\n'\n+                text += f'## Update description:\\n{notes(*args, **kwargs)}'\n+                return markup(None, text)\n+            return describe\n+\n         getters = {\n             'updates': {\n-                'title': operator.itemgetter('alias'),\n+                'title': operator.itemgetter('title'),\n                 'link': linker('update', 'id', 'alias'),\n-                'description': operator.itemgetter('notes'),\n+                'description': describe_update(operator.itemgetter('alias'),\n+                                               operator.itemgetter('notes'),\n+                                               operator.itemgetter('builds')),\n                 'pubDate': lambda obj: utc.localize(obj['date_submitted']),\n             },\n             'users': {\n", "issue": "RSS feed gives no information about packages build\nSince commit 60dc56c that RSS feed doesn't give information about the packages affected in a build.\r\nExample from [Fedora30 Stable - RSS](https://bodhi.fedoraproject.org/rss/updates/?releases=F30&status=stable) :\r\n```xml\r\n<item>\r\n  <title>FEDORA-2019-59d394e0fd</title>\r\n  <link>https://bodhi.fedoraproject.org/updates/createrepo_c-0.14.1-1.fc30</link>\r\n  <description>- Update to 0.14.1\r\n- Add --pkgorigins mode for Koji\r\n- Correct pkg count in headers if there were invalid pkgs (RhBug:1596211)\r\n- Prevent exiting with 0 if errors occur while finalizing repodata.\r\n  </description>\r\n  <pubDate>Fri, 24 May 2019 12:20:49 +0000</pubDate>\r\n</item>\r\n```\r\n\r\nAlso the link is invalid as in #3248\r\n\r\nLooking to the [Fedora30 Stable - Web UI](https://bodhi.fedoraproject.org/updates/?releases=F30&status=stable) it seems that it should be:\r\n```\r\n  <title>createrepo_c-0.14.1-1.fc30</title>\r\n  <link>https://bodhi.fedoraproject.org/updates/FEDORA-2019-59d394e0fd</link>\r\n```\n", "before_files": [{"content": "# Copyright \u00a9 2014-2019 Red Hat, Inc.\n#\n# This file is part of Bodhi.\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n\"\"\"Define special view renderers, such as RSS.\"\"\"\nimport logging\nimport operator\n\nfrom pytz import utc\nfrom feedgen.feed import FeedGenerator\nfrom pyramid.exceptions import HTTPBadRequest\n\n\nlog = logging.getLogger(__name__)\n\n\ndef rss(info):\n    \"\"\"\n    Return a RSS renderer.\n\n    Args:\n        info (pyramid.renderers.RendererHelper): Unused.\n    Returns:\n        function: A function that can be used to render a RSS view.\n    \"\"\"\n    def render(data, system):\n        \"\"\"\n        Render the given data as an RSS view.\n\n        If the request's content type is set to the default, this function will change it to\n        application/rss+xml.\n\n        Args:\n            data (dict): A dictionary describing the information to be rendered. The information can\n                be different types of objects, such as updates, users, comments, or overrides.\n            system (pyramid.events.BeforeRender): Used to get the current request.\n        Returns:\n            str: An RSS document representing the given data.\n        \"\"\"\n        request = system.get('request')\n        if request is not None:\n            response = request.response\n            ct = response.content_type\n            if ct == response.default_content_type:\n                response.content_type = 'application/rss+xml'\n\n        if 'updates' in data:\n            key = 'updates'\n            feed_title = 'Released updates'\n        elif 'users' in data:\n            key = 'users'\n            feed_title = 'Bodhi users'\n        elif 'comments' in data:\n            key = 'comments'\n            feed_title = 'User comments'\n        elif 'overrides' in data:\n            key = 'overrides'\n            feed_title = 'Update overrides'\n        else:\n            # This is a request we don't know how to render. Let's return BadRequest and log.\n            log.debug('Unable to render RSS feed for data: %s', data)\n            # See if we have a request so we can set a code without raising an Exception\n            if request is not None:\n                response.status = HTTPBadRequest.code\n                return 'Invalid RSS feed request'\n            else:\n                raise HTTPBadRequest('Invalid RSS feed request')\n\n        feed_description_list = []\n        for k in request.GET.keys():\n            feed_description_list.append('%s(%s)' % (k, request.GET[k]))\n        if feed_description_list:\n            feed_description = 'Filtered on: ' + ', '.join(feed_description_list)\n        else:\n            feed_description = \"All %s\" % (key)\n\n        feed = FeedGenerator()\n        feed.title(feed_title)\n        feed.link(href=request.url, rel='self')\n        feed.description(feed_description)\n        feed.language('en')\n\n        def linker(route, param, key):\n            def link_dict(obj):\n                return dict(href=request.route_url(route, **{param: obj[key]}))\n            return link_dict\n\n        getters = {\n            'updates': {\n                'title': operator.itemgetter('alias'),\n                'link': linker('update', 'id', 'alias'),\n                'description': operator.itemgetter('notes'),\n                'pubDate': lambda obj: utc.localize(obj['date_submitted']),\n            },\n            'users': {\n                'title': operator.itemgetter('name'),\n                'link': linker('user', 'name', 'name'),\n                'description': operator.itemgetter('name'),\n            },\n            'comments': {\n                'title': operator.itemgetter('rss_title'),\n                'link': linker('comment', 'id', 'id'),\n                'description': operator.itemgetter('text'),\n                'pubDate': lambda obj: utc.localize(obj['timestamp']),\n            },\n            'overrides': {\n                'title': operator.itemgetter('nvr'),\n                'link': linker('override', 'nvr', 'nvr'),\n                'description': operator.itemgetter('notes'),\n                'pubDate': lambda obj: utc.localize(obj['submission_date']),\n            },\n        }\n\n        for value in data[key]:\n            feed_item = feed.add_item()\n            for name, getter in getters[key].items():\n                # Because we have to use methods to fill feed entry attributes,\n                # it's done by getting methods by name and calling them\n                # on the same line.\n                getattr(feed_item, name)(getter(value))\n\n        return feed.rss_str()\n\n    return render\n", "path": "bodhi/server/renderers.py"}], "after_files": [{"content": "# Copyright \u00a9 2014-2019 Red Hat, Inc.\n#\n# This file is part of Bodhi.\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n\"\"\"Define special view renderers, such as RSS.\"\"\"\nimport logging\nimport operator\n\nfrom pytz import utc\nfrom feedgen.feed import FeedGenerator\nfrom pyramid.exceptions import HTTPBadRequest\n\nfrom bodhi.server.util import markup\n\n\nlog = logging.getLogger(__name__)\n\n\ndef rss(info):\n    \"\"\"\n    Return a RSS renderer.\n\n    Args:\n        info (pyramid.renderers.RendererHelper): Unused.\n    Returns:\n        function: A function that can be used to render a RSS view.\n    \"\"\"\n    def render(data, system):\n        \"\"\"\n        Render the given data as an RSS view.\n\n        If the request's content type is set to the default, this function will change it to\n        application/rss+xml.\n\n        Args:\n            data (dict): A dictionary describing the information to be rendered. The information can\n                be different types of objects, such as updates, users, comments, or overrides.\n            system (pyramid.events.BeforeRender): Used to get the current request.\n        Returns:\n            str: An RSS document representing the given data.\n        \"\"\"\n        request = system.get('request')\n        if request is not None:\n            response = request.response\n            ct = response.content_type\n            if ct == response.default_content_type:\n                response.content_type = 'application/rss+xml'\n\n        if 'updates' in data:\n            key = 'updates'\n            feed_title = 'Released updates'\n        elif 'users' in data:\n            key = 'users'\n            feed_title = 'Bodhi users'\n        elif 'comments' in data:\n            key = 'comments'\n            feed_title = 'User comments'\n        elif 'overrides' in data:\n            key = 'overrides'\n            feed_title = 'Update overrides'\n        else:\n            # This is a request we don't know how to render. Let's return BadRequest and log.\n            log.debug('Unable to render RSS feed for data: %s', data)\n            # See if we have a request so we can set a code without raising an Exception\n            if request is not None:\n                response.status = HTTPBadRequest.code\n                return 'Invalid RSS feed request'\n            else:\n                raise HTTPBadRequest('Invalid RSS feed request')\n\n        feed_description_list = []\n        for k in request.GET.keys():\n            feed_description_list.append('%s(%s)' % (k, request.GET[k]))\n        if feed_description_list:\n            feed_description = 'Filtered on: ' + ', '.join(feed_description_list)\n        else:\n            feed_description = \"All %s\" % (key)\n\n        feed = FeedGenerator()\n        feed.title(feed_title)\n        feed.link(href=request.url, rel='self')\n        feed.description(feed_description)\n        feed.language('en')\n\n        def linker(route, param, key):\n            def link_dict(obj):\n                return dict(href=request.route_url(route, **{param: obj[key]}))\n            return link_dict\n\n        def describe_update(alias, notes, builds):\n            \"\"\"\n            Wrap calls to operator.itemgetter to retrieve notes and builds list.\n\n            Methods are used to fill feed entry values, so we must use a wrapper\n            to get an HTML formatted description from the `notes` and the `builds`\n            properties of the update.\n\n            For example:\n            getter = describe_update(operator.itemgetter('notes'),operator.itemgetter('builds'))\n            description_value = getter(update_data)\n\n            Args:\n                alias (operator.itemgetter): A callable object which returns update alias\n                    as string.\n                notes (operator.itemgetter): A callable object which returns update notes\n                    as string.\n                builds (operator.itemgetter): A callable object which returns a list of builds\n                    associated to the update.\n            Returns:\n                function: A function which accepts a dict representing an update as parameter.\n            \"\"\"\n            def describe(*args, **kwargs):\n                text = f'# {alias(*args, **kwargs)}\\n'\n                text += f'## Packages in this update:\\n'\n                for p in builds(*args, **kwargs):\n                    text += f'* {p.nvr}\\n'\n                text += f'## Update description:\\n{notes(*args, **kwargs)}'\n                return markup(None, text)\n            return describe\n\n        getters = {\n            'updates': {\n                'title': operator.itemgetter('title'),\n                'link': linker('update', 'id', 'alias'),\n                'description': describe_update(operator.itemgetter('alias'),\n                                               operator.itemgetter('notes'),\n                                               operator.itemgetter('builds')),\n                'pubDate': lambda obj: utc.localize(obj['date_submitted']),\n            },\n            'users': {\n                'title': operator.itemgetter('name'),\n                'link': linker('user', 'name', 'name'),\n                'description': operator.itemgetter('name'),\n            },\n            'comments': {\n                'title': operator.itemgetter('rss_title'),\n                'link': linker('comment', 'id', 'id'),\n                'description': operator.itemgetter('text'),\n                'pubDate': lambda obj: utc.localize(obj['timestamp']),\n            },\n            'overrides': {\n                'title': operator.itemgetter('nvr'),\n                'link': linker('override', 'nvr', 'nvr'),\n                'description': operator.itemgetter('notes'),\n                'pubDate': lambda obj: utc.localize(obj['submission_date']),\n            },\n        }\n\n        for value in data[key]:\n            feed_item = feed.add_item()\n            for name, getter in getters[key].items():\n                # Because we have to use methods to fill feed entry attributes,\n                # it's done by getting methods by name and calling them\n                # on the same line.\n                getattr(feed_item, name)(getter(value))\n\n        return feed.rss_str()\n\n    return render\n", "path": "bodhi/server/renderers.py"}]}