prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Custom templates still not working
  Hi, this is the original issue with broken custom templates #4980
  
  But it doesn't work even after the fix (2.4.1).
  The web part of Buildbot is far to complicated for me. But I was able to find lines like this in scripts.js?_1568233606304
  ```
      , function(e, t) {
          e.exports = window.T["undefined/properties.html"] || '<table class="table table-hover...
      }
  ```
  And I presume there is something wrong if there is "**undefined**/properties.html".
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `master/buildbot/www/config.py`
  Content:
  ```
  1 # This file is part of Buildbot.  Buildbot is free software: you can
  2 # redistribute it and/or modify it under the terms of the GNU General Public
  3 # License as published by the Free Software Foundation, version 2.
  4 #
  5 # This program is distributed in the hope that it will be useful, but WITHOUT
  6 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  7 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
  8 # details.
  9 #
  10 # You should have received a copy of the GNU General Public License along with
  11 # this program; if not, write to the Free Software Foundation, Inc., 51
  12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  13 #
  14 # Copyright Buildbot Team Members
  15 
  16 
  17 import json
  18 import os
  19 import posixpath
  20 
  21 import jinja2
  22 
  23 from twisted.internet import defer
  24 from twisted.python import log
  25 from twisted.web.error import Error
  26 
  27 from buildbot.interfaces import IConfigured
  28 from buildbot.util import unicode2bytes
  29 from buildbot.www import resource
  30 
  31 
  32 class IndexResource(resource.Resource):
  33     # enable reconfigResource calls
  34     needsReconfig = True
  35 
  36     def __init__(self, master, staticdir):
  37         super().__init__(master)
  38         loader = jinja2.FileSystemLoader(staticdir)
  39         self.jinja = jinja2.Environment(
  40             loader=loader, undefined=jinja2.StrictUndefined)
  41 
  42     def reconfigResource(self, new_config):
  43         self.config = new_config.www
  44 
  45         versions = self.getEnvironmentVersions()
  46         vs = self.config.get('versions')
  47         if isinstance(vs, list):
  48             versions += vs
  49         self.config['versions'] = versions
  50 
  51         self.custom_templates = {}
  52         template_dir = self.config.pop('custom_templates_dir', None)
  53         if template_dir is not None:
  54             template_dir = os.path.join(self.master.basedir, template_dir)
  55             self.custom_templates = self.parseCustomTemplateDir(template_dir)
  56 
  57     def render_GET(self, request):
  58         return self.asyncRenderHelper(request, self.renderIndex)
  59 
  60     def parseCustomTemplateDir(self, template_dir):
  61         res = {}
  62         allowed_ext = [".html"]
  63         try:
  64             import pyjade
  65             allowed_ext.append(".jade")
  66         except ImportError:  # pragma: no cover
  67             log.msg("pyjade not installed. Ignoring .jade files from %s" %
  68                     (template_dir,))
  69             pyjade = None
  70         for root, dirs, files in os.walk(template_dir):
  71             if root == template_dir:
  72                 template_name = posixpath.join("views", "%s.html")
  73             else:
  74                 # template_name is a url, so we really want '/'
  75                 # root is a os.path, though
  76                 template_name = posixpath.join(
  77                     os.path.basename(root), "views", "%s.html")
  78             for f in files:
  79                 fn = os.path.join(root, f)
  80                 basename, ext = os.path.splitext(f)
  81                 if ext not in allowed_ext:
  82                     continue
  83                 if ext == ".html":
  84                     with open(fn) as f:
  85                         html = f.read().strip()
  86                 elif ext == ".jade":
  87                     with open(fn) as f:
  88                         jade = f.read()
  89                         parser = pyjade.parser.Parser(jade)
  90                         block = parser.parse()
  91                         compiler = pyjade.ext.html.Compiler(
  92                             block, pretty=False)
  93                         html = compiler.compile()
  94                 res[template_name % (basename,)] = json.dumps(html)
  95 
  96         return res
  97 
  98     @staticmethod
  99     def getEnvironmentVersions():
  100         import sys
  101         import twisted
  102         from buildbot import version as bbversion
  103 
  104         pyversion = '.'.join(map(str, sys.version_info[:3]))
  105 
  106         tx_version_info = (twisted.version.major,
  107                            twisted.version.minor,
  108                            twisted.version.micro)
  109         txversion = '.'.join(map(str, tx_version_info))
  110 
  111         return [
  112             ('Python', pyversion),
  113             ('Buildbot', bbversion),
  114             ('Twisted', txversion),
  115         ]
  116 
  117     @defer.inlineCallbacks
  118     def renderIndex(self, request):
  119         config = {}
  120         request.setHeader(b"content-type", b'text/html')
  121         request.setHeader(b"Cache-Control", b"public;max-age=0")
  122 
  123         try:
  124             yield self.config['auth'].maybeAutoLogin(request)
  125         except Error as e:
  126             config["on_load_warning"] = e.message
  127 
  128         user_info = self.master.www.getUserInfos(request)
  129         config.update({"user": user_info})
  130 
  131         config.update(self.config)
  132         config['buildbotURL'] = self.master.config.buildbotURL
  133         config['title'] = self.master.config.title
  134         config['titleURL'] = self.master.config.titleURL
  135         config['multiMaster'] = self.master.config.multiMaster
  136 
  137         # delete things that may contain secrets
  138         if 'change_hook_dialects' in config:
  139             del config['change_hook_dialects']
  140 
  141         def toJson(obj):
  142             try:
  143                 obj = IConfigured(obj).getConfigDict()
  144             except TypeError:
  145                 # this happens for old style classes (not deriving objects)
  146                 pass
  147             if isinstance(obj, dict):
  148                 return obj
  149             # don't leak object memory address
  150             obj = obj.__class__.__module__ + "." + obj.__class__.__name__
  151             return repr(obj) + " not yet IConfigured"
  152 
  153         tpl = self.jinja.get_template('index.html')
  154         # we use Jinja in order to render some server side dynamic stuff
  155         # For example, custom_templates javascript is generated by the
  156         # layout.jade jinja template
  157         tpl = tpl.render(configjson=json.dumps(config, default=toJson),
  158                          custom_templates=self.custom_templates,
  159                          config=self.config)
  160         return unicode2bytes(tpl, encoding='ascii')
  161
  ```
  --- 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/master/buildbot/www/config.py b/master/buildbot/www/config.py
  --- a/master/buildbot/www/config.py
  +++ b/master/buildbot/www/config.py
  @@ -91,7 +91,7 @@
                           compiler = pyjade.ext.html.Compiler(
                               block, pretty=False)
                           html = compiler.compile()
  -                res[template_name % (basename,)] = json.dumps(html)
  +                res[template_name % (basename,)] = html
   
           return res
  
verification_info: {"golden_diff": "diff --git a/master/buildbot/www/config.py b/master/buildbot/www/config.py\n--- a/master/buildbot/www/config.py\n+++ b/master/buildbot/www/config.py\n@@ -91,7 +91,7 @@\n                         compiler = pyjade.ext.html.Compiler(\n                             block, pretty=False)\n                         html = compiler.compile()\n-                res[template_name % (basename,)] = json.dumps(html)\n+                res[template_name % (basename,)] = html\n \n         return res\n", "issue": "Custom templates still not working\nHi, this is the original issue with broken custom templates #4980\r\n\r\nBut it doesn't work even after the fix (2.4.1).\r\nThe web part of Buildbot is far to complicated for me. But I was able to find lines like this in scripts.js?_1568233606304\r\n```\r\n    , function(e, t) {\r\n        e.exports = window.T[\"undefined/properties.html\"] || '<table class=\"table table-hover...\r\n    }\r\n```\r\nAnd I presume there is something wrong if there is \"**undefined**/properties.html\".\n", "before_files": [{"content": "# This file is part of Buildbot.  Buildbot is free software: you can\n# redistribute it and/or modify it under the terms of the GNU General Public\n# License as published by the Free Software Foundation, version 2.\n#\n# This program is distributed in the hope that it will be useful, but WITHOUT\n# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more\n# details.\n#\n# You should have received a copy of the GNU General Public License along with\n# this program; if not, write to the Free Software Foundation, Inc., 51\n# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n#\n# Copyright Buildbot Team Members\n\n\nimport json\nimport os\nimport posixpath\n\nimport jinja2\n\nfrom twisted.internet import defer\nfrom twisted.python import log\nfrom twisted.web.error import Error\n\nfrom buildbot.interfaces import IConfigured\nfrom buildbot.util import unicode2bytes\nfrom buildbot.www import resource\n\n\nclass IndexResource(resource.Resource):\n    # enable reconfigResource calls\n    needsReconfig = True\n\n    def __init__(self, master, staticdir):\n        super().__init__(master)\n        loader = jinja2.FileSystemLoader(staticdir)\n        self.jinja = jinja2.Environment(\n            loader=loader, undefined=jinja2.StrictUndefined)\n\n    def reconfigResource(self, new_config):\n        self.config = new_config.www\n\n        versions = self.getEnvironmentVersions()\n        vs = self.config.get('versions')\n        if isinstance(vs, list):\n            versions += vs\n        self.config['versions'] = versions\n\n        self.custom_templates = {}\n        template_dir = self.config.pop('custom_templates_dir', None)\n        if template_dir is not None:\n            template_dir = os.path.join(self.master.basedir, template_dir)\n            self.custom_templates = self.parseCustomTemplateDir(template_dir)\n\n    def render_GET(self, request):\n        return self.asyncRenderHelper(request, self.renderIndex)\n\n    def parseCustomTemplateDir(self, template_dir):\n        res = {}\n        allowed_ext = [\".html\"]\n        try:\n            import pyjade\n            allowed_ext.append(\".jade\")\n        except ImportError:  # pragma: no cover\n            log.msg(\"pyjade not installed. Ignoring .jade files from %s\" %\n                    (template_dir,))\n            pyjade = None\n        for root, dirs, files in os.walk(template_dir):\n            if root == template_dir:\n                template_name = posixpath.join(\"views\", \"%s.html\")\n            else:\n                # template_name is a url, so we really want '/'\n                # root is a os.path, though\n                template_name = posixpath.join(\n                    os.path.basename(root), \"views\", \"%s.html\")\n            for f in files:\n                fn = os.path.join(root, f)\n                basename, ext = os.path.splitext(f)\n                if ext not in allowed_ext:\n                    continue\n                if ext == \".html\":\n                    with open(fn) as f:\n                        html = f.read().strip()\n                elif ext == \".jade\":\n                    with open(fn) as f:\n                        jade = f.read()\n                        parser = pyjade.parser.Parser(jade)\n                        block = parser.parse()\n                        compiler = pyjade.ext.html.Compiler(\n                            block, pretty=False)\n                        html = compiler.compile()\n                res[template_name % (basename,)] = json.dumps(html)\n\n        return res\n\n    @staticmethod\n    def getEnvironmentVersions():\n        import sys\n        import twisted\n        from buildbot import version as bbversion\n\n        pyversion = '.'.join(map(str, sys.version_info[:3]))\n\n        tx_version_info = (twisted.version.major,\n                           twisted.version.minor,\n                           twisted.version.micro)\n        txversion = '.'.join(map(str, tx_version_info))\n\n        return [\n            ('Python', pyversion),\n            ('Buildbot', bbversion),\n            ('Twisted', txversion),\n        ]\n\n    @defer.inlineCallbacks\n    def renderIndex(self, request):\n        config = {}\n        request.setHeader(b\"content-type\", b'text/html')\n        request.setHeader(b\"Cache-Control\", b\"public;max-age=0\")\n\n        try:\n            yield self.config['auth'].maybeAutoLogin(request)\n        except Error as e:\n            config[\"on_load_warning\"] = e.message\n\n        user_info = self.master.www.getUserInfos(request)\n        config.update({\"user\": user_info})\n\n        config.update(self.config)\n        config['buildbotURL'] = self.master.config.buildbotURL\n        config['title'] = self.master.config.title\n        config['titleURL'] = self.master.config.titleURL\n        config['multiMaster'] = self.master.config.multiMaster\n\n        # delete things that may contain secrets\n        if 'change_hook_dialects' in config:\n            del config['change_hook_dialects']\n\n        def toJson(obj):\n            try:\n                obj = IConfigured(obj).getConfigDict()\n            except TypeError:\n                # this happens for old style classes (not deriving objects)\n                pass\n            if isinstance(obj, dict):\n                return obj\n            # don't leak object memory address\n            obj = obj.__class__.__module__ + \".\" + obj.__class__.__name__\n            return repr(obj) + \" not yet IConfigured\"\n\n        tpl = self.jinja.get_template('index.html')\n        # we use Jinja in order to render some server side dynamic stuff\n        # For example, custom_templates javascript is generated by the\n        # layout.jade jinja template\n        tpl = tpl.render(configjson=json.dumps(config, default=toJson),\n                         custom_templates=self.custom_templates,\n                         config=self.config)\n        return unicode2bytes(tpl, encoding='ascii')\n", "path": "master/buildbot/www/config.py"}], "after_files": [{"content": "# This file is part of Buildbot.  Buildbot is free software: you can\n# redistribute it and/or modify it under the terms of the GNU General Public\n# License as published by the Free Software Foundation, version 2.\n#\n# This program is distributed in the hope that it will be useful, but WITHOUT\n# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more\n# details.\n#\n# You should have received a copy of the GNU General Public License along with\n# this program; if not, write to the Free Software Foundation, Inc., 51\n# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n#\n# Copyright Buildbot Team Members\n\n\nimport json\nimport os\nimport posixpath\n\nimport jinja2\n\nfrom twisted.internet import defer\nfrom twisted.python import log\nfrom twisted.web.error import Error\n\nfrom buildbot.interfaces import IConfigured\nfrom buildbot.util import unicode2bytes\nfrom buildbot.www import resource\n\n\nclass IndexResource(resource.Resource):\n    # enable reconfigResource calls\n    needsReconfig = True\n\n    def __init__(self, master, staticdir):\n        super().__init__(master)\n        loader = jinja2.FileSystemLoader(staticdir)\n        self.jinja = jinja2.Environment(\n            loader=loader, undefined=jinja2.StrictUndefined)\n\n    def reconfigResource(self, new_config):\n        self.config = new_config.www\n\n        versions = self.getEnvironmentVersions()\n        vs = self.config.get('versions')\n        if isinstance(vs, list):\n            versions += vs\n        self.config['versions'] = versions\n\n        self.custom_templates = {}\n        template_dir = self.config.pop('custom_templates_dir', None)\n        if template_dir is not None:\n            template_dir = os.path.join(self.master.basedir, template_dir)\n            self.custom_templates = self.parseCustomTemplateDir(template_dir)\n\n    def render_GET(self, request):\n        return self.asyncRenderHelper(request, self.renderIndex)\n\n    def parseCustomTemplateDir(self, template_dir):\n        res = {}\n        allowed_ext = [\".html\"]\n        try:\n            import pyjade\n            allowed_ext.append(\".jade\")\n        except ImportError:  # pragma: no cover\n            log.msg(\"pyjade not installed. Ignoring .jade files from %s\" %\n                    (template_dir,))\n            pyjade = None\n        for root, dirs, files in os.walk(template_dir):\n            if root == template_dir:\n                template_name = posixpath.join(\"views\", \"%s.html\")\n            else:\n                # template_name is a url, so we really want '/'\n                # root is a os.path, though\n                template_name = posixpath.join(\n                    os.path.basename(root), \"views\", \"%s.html\")\n            for f in files:\n                fn = os.path.join(root, f)\n                basename, ext = os.path.splitext(f)\n                if ext not in allowed_ext:\n                    continue\n                if ext == \".html\":\n                    with open(fn) as f:\n                        html = f.read().strip()\n                elif ext == \".jade\":\n                    with open(fn) as f:\n                        jade = f.read()\n                        parser = pyjade.parser.Parser(jade)\n                        block = parser.parse()\n                        compiler = pyjade.ext.html.Compiler(\n                            block, pretty=False)\n                        html = compiler.compile()\n                res[template_name % (basename,)] = html\n\n        return res\n\n    @staticmethod\n    def getEnvironmentVersions():\n        import sys\n        import twisted\n        from buildbot import version as bbversion\n\n        pyversion = '.'.join(map(str, sys.version_info[:3]))\n\n        tx_version_info = (twisted.version.major,\n                           twisted.version.minor,\n                           twisted.version.micro)\n        txversion = '.'.join(map(str, tx_version_info))\n\n        return [\n            ('Python', pyversion),\n            ('Buildbot', bbversion),\n            ('Twisted', txversion),\n        ]\n\n    @defer.inlineCallbacks\n    def renderIndex(self, request):\n        config = {}\n        request.setHeader(b\"content-type\", b'text/html')\n        request.setHeader(b\"Cache-Control\", b\"public;max-age=0\")\n\n        try:\n            yield self.config['auth'].maybeAutoLogin(request)\n        except Error as e:\n            config[\"on_load_warning\"] = e.message\n\n        user_info = self.master.www.getUserInfos(request)\n        config.update({\"user\": user_info})\n\n        config.update(self.config)\n        config['buildbotURL'] = self.master.config.buildbotURL\n        config['title'] = self.master.config.title\n        config['titleURL'] = self.master.config.titleURL\n        config['multiMaster'] = self.master.config.multiMaster\n\n        # delete things that may contain secrets\n        if 'change_hook_dialects' in config:\n            del config['change_hook_dialects']\n\n        def toJson(obj):\n            try:\n                obj = IConfigured(obj).getConfigDict()\n            except TypeError:\n                # this happens for old style classes (not deriving objects)\n                pass\n            if isinstance(obj, dict):\n                return obj\n            # don't leak object memory address\n            obj = obj.__class__.__module__ + \".\" + obj.__class__.__name__\n            return repr(obj) + \" not yet IConfigured\"\n\n        tpl = self.jinja.get_template('index.html')\n        # we use Jinja in order to render some server side dynamic stuff\n        # For example, custom_templates javascript is generated by the\n        # layout.jade jinja template\n        tpl = tpl.render(configjson=json.dumps(config, default=toJson),\n                         custom_templates=self.custom_templates,\n                         config=self.config)\n        return unicode2bytes(tpl, encoding='ascii')\n", "path": "master/buildbot/www/config.py"}]}