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()

```

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

{"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"}]}