prompt: 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()
  
  ```
  
golden_diff: 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': {
  
verification_info: {"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"}]}