← Back to team overview

canonical-ubuntu-qa team mailing list archive

[Merge] ~hyask/autopkgtest-cloud:skia/ease_browse_dev into autopkgtest-cloud:master

 

Skia has proposed merging ~hyask/autopkgtest-cloud:skia/ease_browse_dev into autopkgtest-cloud:master.

Requested reviews:
  Canonical's Ubuntu QA (canonical-ubuntu-qa)

For more details, see:
https://code.launchpad.net/~hyask/autopkgtest-cloud/+git/autopkgtest-cloud/+merge/461027

Allow easier local development.
-- 
Your team Canonical's Ubuntu QA is requested to review the proposed merge of ~hyask/autopkgtest-cloud:skia/ease_browse_dev into autopkgtest-cloud:master.
diff --git a/charms/focal/autopkgtest-web/charmcraft.yaml b/charms/focal/autopkgtest-web/charmcraft.yaml
index 93c0816..546437f 100644
--- a/charms/focal/autopkgtest-web/charmcraft.yaml
+++ b/charms/focal/autopkgtest-web/charmcraft.yaml
@@ -6,6 +6,7 @@ parts:
     build-snaps: [charm]
     build-packages:
       - libjs-jquery
+      - libjs-bootstrap
       - python3-dev
 bases:
   - build-on:
diff --git a/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py b/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py
index e1be1a2..6920fc1 100644
--- a/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py
+++ b/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py
@@ -338,20 +338,6 @@ def clear_github_status_credentials():
         pass
 
 
-@when_not("autopkgtest-web.bootstrap-symlinked")
-def symlink_bootstrap():
-    try:
-        os.symlink(
-            os.path.join(
-                os.path.sep, "usr", "share", "javascript", "bootstrap"
-            ),
-            os.path.join(charm_dir(), "webcontrol", "static", "bootstrap"),
-        )
-        set_flag("autopkgtest-web.bootstrap-symlinked")
-    except FileExistsError:
-        pass
-
-
 @when_not("autopkgtest-web.runtime-dir-created")
 def make_runtime_tmpfiles():
     with open("/etc/tmpfiles.d/autopkgtest-web-runtime.conf", "w") as r:
diff --git a/charms/focal/autopkgtest-web/webcontrol/README.md b/charms/focal/autopkgtest-web/webcontrol/README.md
new file mode 100644
index 0000000..e489c2a
--- /dev/null
+++ b/charms/focal/autopkgtest-web/webcontrol/README.md
@@ -0,0 +1,12 @@
+# autopkgtest-cloud web frontend
+
+## Developing browse.cgi locally
+
+Install the dependencies:
+`sudo apt install python3-flask python3-distro-info libjs-jquery libjs-bootstrap`
+
+Then simply run `./browse-test-py`, it will launch the flask application locally
+with some mocked data.
+As the import of `browse.cgi` is done trough `importlib`, changes in that file
+will not be reloaded automatically, so you'll still need to restart the app
+manually.
diff --git a/charms/focal/autopkgtest-web/webcontrol/browse-test.py b/charms/focal/autopkgtest-web/webcontrol/browse-test.py
new file mode 100755
index 0000000..a5c5b4e
--- /dev/null
+++ b/charms/focal/autopkgtest-web/webcontrol/browse-test.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+"""Run browse app in local debug mode for testing."""
+
+import importlib
+from pathlib import Path
+
+from helpers import tests, utils
+
+# import browse.cgi
+browse_path = str(Path(__file__).parent / "browse.cgi")
+loader = importlib.machinery.SourceFileLoader("browse", browse_path)
+spec = importlib.util.spec_from_loader("browse", loader)
+browse = importlib.util.module_from_spec(spec)
+loader.exec_module(browse)
+
+
+if __name__ == "__main__":
+    browse.db_con = utils.init_db(":memory:", check_same_thread=False)
+    with browse.db_con:
+        tests.populate_dummy_db(browse.db_con)
+    browse.swift_container_url = "swift-%s"
+    browse.AMQP_QUEUE_CACHE = Path("/dev/shm/queue.json")
+    tests.populate_dummy_amqp_cache(browse.AMQP_QUEUE_CACHE)
+    browse.RUNNING_CACHE = Path("/dev/shm/running.json")
+    tests.populate_dummy_running_cache(browse.RUNNING_CACHE)
+
+    browse.app.run(host="0.0.0.0", debug=True)
diff --git a/charms/focal/autopkgtest-web/webcontrol/browse.cgi b/charms/focal/autopkgtest-web/webcontrol/browse.cgi
index f39b30f..f6be794 100755
--- a/charms/focal/autopkgtest-web/webcontrol/browse.cgi
+++ b/charms/focal/autopkgtest-web/webcontrol/browse.cgi
@@ -10,9 +10,9 @@ import sqlite3
 from collections import OrderedDict
 from wsgiref.handlers import CGIHandler
 
-import distro_info
 import flask
 from helpers.admin import select_abnormally_long_jobs
+from helpers.utils import get_all_releases, get_supported_releases
 from werkzeug.middleware.proxy_fix import ProxyFix
 
 app = flask.Flask("browse")
@@ -20,13 +20,11 @@ app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
 db_con = None
 swift_container_url = None
 
-UDI = distro_info.UbuntuDistroInfo()
-ALL_UBUNTU_RELEASES = UDI.all
-SUPPORTED_UBUNTU_RELEASES = sorted(
-    set(UDI.supported() + UDI.supported_esm()), key=ALL_UBUNTU_RELEASES.index
-)
-
+ALL_UBUNTU_RELEASES = get_all_releases()
+SUPPORTED_UBUNTU_RELEASES = get_supported_releases()
 INDEXED_PACKAGES_FP = ""
+AMQP_QUEUE_CACHE = "/var/lib/cache-amqp/queued.json"
+RUNNING_CACHE = "/run/amqp-status-collector/running.json"
 
 
 def init_config():
@@ -60,6 +58,16 @@ def get_test_id(release, arch, src):
         return None
 
 
+def get_running_jobs():
+    try:
+        with open(RUNNING_CACHE) as f:
+            # package -> runhash -> release -> arch -> (params, duration, logtail)
+            running_info = json.load(f)
+    except FileNotFoundError:
+        running_info = {}
+    return running_info
+
+
 def render(template, code=200, **kwargs):
     # sort the values passed in, so that releases are in the right order
     try:
@@ -81,7 +89,7 @@ def render(template, code=200, **kwargs):
         flask.render_template(
             template,
             base_url=flask.url_for("index_root"),
-            static_url=flask.url_for("static", filename="/"),
+            static_url=flask.url_for("static", filename=""),
             **kwargs
         ),
         code,
@@ -147,7 +155,7 @@ def get_queue_info():
     Return (releases, arches, context -> release -> arch -> (queue_size, [requests])).
     """
 
-    with open("/var/lib/cache-amqp/queued.json", "r") as json_file:
+    with open(AMQP_QUEUE_CACHE, "r") as json_file:
         queue_info_j = json.load(json_file)
 
         arches = queue_info_j["arches"]
@@ -268,6 +276,10 @@ def package_overview(package, _=None):
         arches.add(row[3])
         results.setdefault(row[2], {})[row[3]] = human_exitcode(row[1])
 
+    running_info = dict(
+        (k, v) for (k, v) in get_running_jobs().items() if k == package
+    )
+
     return render(
         "browse-package.html",
         package=package,
@@ -280,6 +292,7 @@ def package_overview(package, _=None):
         arches=sorted(arches),
         results=results,
         title_suffix="- %s" % package,
+        running=running_info,
     )
 
 
@@ -371,12 +384,7 @@ def running():
                     a
                 ] = queue_length
 
-    try:
-        with open("/run/amqp-status-collector/running.json") as f:
-            # package -> runhash -> release -> arch -> (params, duration, logtail)
-            running_info = json.load(f)
-    except FileNotFoundError:
-        running_info = {}
+    running_info = get_running_jobs()
 
     return render(
         "browse-running.html",
@@ -391,13 +399,7 @@ def running():
 
 @app.route("/admin")
 def admin():
-    try:
-        with open("/run/amqp-status-collector/running.json") as f:
-            # package -> runhash -> release -> arch -> (params, duration, logtail)
-            running_info = json.load(f)
-    except FileNotFoundError as exc:
-        running_info = {}
-        raise FileNotFoundError("running.json doesn't exist!") from exc
+    running_info = get_running_jobs()
     pruned_running_info = select_abnormally_long_jobs(
         running_info, get_test_id=get_test_id, db_con=db_con
     )
@@ -439,7 +441,7 @@ def queues_json():
 
 @app.route("/queued.json")
 def return_queued_exactly():
-    with open("/var/lib/cache-amqp/queued.json") as json_file:
+    with open(AMQP_QUEUE_CACHE) as json_file:
         queue_info = json.load(json_file)
     return queue_info
 
diff --git a/charms/focal/autopkgtest-web/webcontrol/helpers/tests.py b/charms/focal/autopkgtest-web/webcontrol/helpers/tests.py
new file mode 100644
index 0000000..52017c2
--- /dev/null
+++ b/charms/focal/autopkgtest-web/webcontrol/helpers/tests.py
@@ -0,0 +1,148 @@
+import json
+from datetime import datetime
+from uuid import uuid4
+
+from .utils import get_supported_releases
+
+
+def populate_dummy_db(db_con):
+    supported_releases = get_supported_releases()
+
+    c = db_con.cursor()
+    tests = [
+        (1, supported_releases[0], "amd64", "hello"),
+        (2, supported_releases[1], "amd64", "hello"),
+        (3, supported_releases[0], "ppc64el", "hello"),
+        (4, supported_releases[1], "ppc64el", "hello"),
+        (5, supported_releases[2], "amd64", "hello"),
+    ]
+    c.executemany("INSERT INTO test values(?, ?, ?, ?)", tests)
+    results = [
+        # fmt: off
+        # test_id | run_id | version | trigger | duration | exit_code | requester | env | uuid
+        (1, datetime.now(), "1.2.3", "hello/1.2.3", 42, 0, "hyask", "", str(uuid4())),
+        (1, datetime.now(), "1.2.3", "hello/1.2.3", 42, 0, "hyask", "all-proposed=1", str(uuid4())),
+        (2, datetime.now(), "1.2.3", "hello/1.2.3", 42, 0, "", "", str(uuid4())),
+        (3, datetime.now(), "1.2.3", "hello/1.2.3", 42, 20, "", "", str(uuid4())),
+        # fmt: on
+    ]
+    c.executemany(
+        "INSERT INTO result values(?, ?, ?, ?, ?, ?, ?, ?, ?)", results
+    )
+    db_con.commit()
+
+
+def populate_dummy_amqp_cache(path):
+    supported_releases = get_supported_releases()
+    with open(path, "w") as f:
+        # pylint: disable=line-too-long
+        json.dump(
+            {
+                "arches": ["amd64", "ppc64el"],
+                "queues": {
+                    "ubuntu": {
+                        supported_releases[0]: {
+                            "amd64": {
+                                "size": 1,
+                                "requests": [
+                                    'hello\n{"triggers": ["hello/1.2.3ubuntu1"], "submit-time": "2024-02-22 01:55:03"}',
+                                ],
+                            }
+                        }
+                    },
+                    "huge": {
+                        supported_releases[1]: {
+                            "amd64": {
+                                "size": 1,
+                                "requests": [
+                                    'hello\n{"triggers": ["migration-reference/0"], "submit-time": "2024-02-22 01:55:03"}',
+                                ],
+                            }
+                        }
+                    },
+                    "ppa": {
+                        supported_releases[2]: {
+                            "amd64": {
+                                "size": 2,
+                                "requests": [
+                                    'hello\n{"triggers": ["hello/1.2.4~ppa1"], "submit-time": "2024-02-22 01:55:03"}',
+                                    'hello2\n{"triggers": ["hello2/2.0.0~ppa1"], "submit-time": "2024-02-22 01:55:03"}',
+                                ],
+                            }
+                        }
+                    },
+                    "upstream": {
+                        supported_releases[3]: {
+                            "amd64": {
+                                "size": 1,
+                                "requests": [
+                                    'hello\n{"triggers": ["hello/1.2.4~ppa1"], "submit-time": "2024-02-22 01:55:03"}',
+                                ],
+                            }
+                        }
+                    },
+                },
+            },
+            f,
+        )
+
+
+def populate_dummy_running_cache(path):
+    supported_releases = get_supported_releases()
+    with open(path, "w") as f:
+        json.dump(
+            {
+                "hello": {
+                    "hash1": {
+                        supported_releases[0]: {
+                            "amd64": [
+                                {
+                                    "requester": "hyask",
+                                    "submit-time": "2024-02-21 11:00:51",
+                                    "triggers": [
+                                        "hello/1.2.3",
+                                    ],
+                                    "uuid": "84669a9c-ac08-46a3-a5fd-6247d0d2021c",
+                                },
+                                3504,
+                                """
+3071s hello/test_XYZ.hello .                                        [ 54%]
+3153s hello/test_XYZ.hello ......                                   [ 64%]
+3271s hello/test_XYZ.hello ..........                               [ 74%]
+3292s hello/test_XYZ.hello ..................                       [ 84%]
+3493s hello/test_XYZ.hello ............................             [ 94%]
+3494s hello/test_XYZ.hello ....................................     [ 98%]
+""",
+                            ]
+                        }
+                    }
+                },
+                "hello2": {
+                    "hash1": {
+                        supported_releases[4]: {
+                            "amd64": [
+                                {
+                                    "all-proposed": "1",
+                                    "requester": "hyask",
+                                    "submit-time": "2024-02-21 11:01:21",
+                                    "triggers": [
+                                        "hello2/1.2.3-0ubuntu1",
+                                    ],
+                                    "uuid": "42369a9c-ac08-46a3-a5fd-6247d0d2021c",
+                                },
+                                3504,
+                                """
+3071s hello2/test_XYZ.hello    [ 54%]
+3153s hello2/test_XYZ.hello    [ 64%]
+3271s hello2/test_XYZ.hello    [ 74%]
+3292s hello2/test_XYZ.hello    [ 84%]
+3493s hello2/test_XYZ.hello    [ 94%]
+3494s hello2/test_XYZ.hello    [ 98%]
+""",
+                            ]
+                        }
+                    }
+                },
+            },
+            f,
+        )
diff --git a/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py b/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py
index 58a9514..12d93b5 100644
--- a/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py
+++ b/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py
@@ -8,6 +8,22 @@ import random
 import sqlite3
 import time
 
+import distro_info
+
+
+def get_all_releases():
+    udi = distro_info.UbuntuDistroInfo()
+    return udi.all
+
+
+def get_supported_releases():
+    udi = distro_info.UbuntuDistroInfo()
+    all_ubuntu_releases = get_all_releases()
+    return sorted(
+        set(udi.supported() + udi.supported_esm()),
+        key=all_ubuntu_releases.index,
+    )
+
 
 def setup_key(app, path):
     """Create or load app.secret_key for cookie encryption."""
@@ -22,10 +38,10 @@ def setup_key(app, path):
         app.secret_key = key
 
 
-def init_db(path):
+def init_db(path, **kwargs):
     """Create DB if it does not exist, and connect to it"""
 
-    db = sqlite3.connect(path)
+    db = sqlite3.connect(path, **kwargs)
     c = db.cursor()
     try:
         c.execute("PRAGMA journal_mode = WAL")
diff --git a/charms/focal/autopkgtest-web/webcontrol/static/bootstrap b/charms/focal/autopkgtest-web/webcontrol/static/bootstrap
new file mode 120000
index 0000000..fe0f86b
--- /dev/null
+++ b/charms/focal/autopkgtest-web/webcontrol/static/bootstrap
@@ -0,0 +1 @@
+/usr/share/javascript/bootstrap
\ No newline at end of file
diff --git a/charms/focal/autopkgtest-web/webcontrol/templates/browse-admin.html b/charms/focal/autopkgtest-web/webcontrol/templates/browse-admin.html
index 5bc2459..266f584 100644
--- a/charms/focal/autopkgtest-web/webcontrol/templates/browse-admin.html
+++ b/charms/focal/autopkgtest-web/webcontrol/templates/browse-admin.html
@@ -1,29 +1,14 @@
 {% extends "browse-layout.html" %}
+{% import "macros.html" as macros %}
+
 {% block content %}
   <h1 class="page-header">Admin</h1>
   <p>Click on the package name to jump to the tests of the package for all arches/releases.</p>
   <p>This page is simply a bunch of heuristics filtering all running jobs to try to get the problematic ones. Feel free to come help improve the heuristics <a href="https://code.launchpad.net/~ubuntu-release/autopkgtest-cloud/+git/autopkgtest-cloud/+ref/master";>here.</a></p>
 
   <!-- Running tests -->
-  {% for p in running|sort %}
-  <h2 id="pkg-{{p}}"><a href="/packages/{{p}}">{{p}}</a></h2>
-    {% for runhash, relinfo in running[p].items() %}
-      {% for release, archinfo in relinfo.items() %}
-        {% for arch, (params, duration, logtail) in archinfo.items() %}
-          <table class="table-condensed">
-            <tr><th>Release:</th><td>{{release}}</td></tr>
-            <tr><th>Architecture:</th><td>{{arch}}</td></tr>
-            {% for param, v in params.items() %}
-            <tr><th>{{param|capitalize}}:</th><td>{{v}}</td></tr>
-            {% endfor %}
-            <tr><th>Running for:</th><td>{{duration//3600 }}h {{duration % 3600//60}}m {{duration % 60}}s</td></tr>
-         </table>
-        <pre>
-{{logtail}}
-        </pre>
-        {% endfor %}
-      {% endfor %}
-    {% endfor %}
+  {% for p, info in running.items()|sort %}
+    {{ macros.display_running_job(p, info) }}
   {% endfor %}
 
 {% endblock %}
diff --git a/charms/focal/autopkgtest-web/webcontrol/templates/browse-layout.html b/charms/focal/autopkgtest-web/webcontrol/templates/browse-layout.html
index 0f90de3..c91457d 100644
--- a/charms/focal/autopkgtest-web/webcontrol/templates/browse-layout.html
+++ b/charms/focal/autopkgtest-web/webcontrol/templates/browse-layout.html
@@ -6,9 +6,9 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Ubuntu Autopkgtest Results {{title_suffix}}</title>
     <!-- <link rel="icon" type="image/png" href="/debian.png"/> -->
-    <link rel="stylesheet" type="text/css" href="{{static_url}}/bootstrap/css/bootstrap.css"/>
-    <link rel="stylesheet" type="text/css" href="{{static_url}}/bootstrap/css/bootstrap-theme.css"/>
-    <link rel="stylesheet" type="text/css" href="{{static_url}}/style.css"/>
+    <link rel="stylesheet" type="text/css" href="{{static_url}}bootstrap/css/bootstrap.css"/>
+    <link rel="stylesheet" type="text/css" href="{{static_url}}bootstrap/css/bootstrap-theme.css"/>
+    <link rel="stylesheet" type="text/css" href="{{static_url}}style.css"/>
   </head>
   <body>
     <div id='wrap'>
@@ -44,7 +44,7 @@
 {% block content %}{% endblock %}
 </div>
 
-    <script type="text/javascript" src="{{static_url}}/jquery/jquery.min.js"></script>
-    <script type="text/javascript" src="{{static_url}}/bootstrap/js/bootstrap.min.js"></script>
+    <script type="text/javascript" src="{{static_url}}jquery/jquery.min.js"></script>
+    <script type="text/javascript" src="{{static_url}}bootstrap/js/bootstrap.min.js"></script>
   </body>
 </html>
diff --git a/charms/focal/autopkgtest-web/webcontrol/templates/browse-package.html b/charms/focal/autopkgtest-web/webcontrol/templates/browse-package.html
index 3ac81c3..fb12afa 100644
--- a/charms/focal/autopkgtest-web/webcontrol/templates/browse-package.html
+++ b/charms/focal/autopkgtest-web/webcontrol/templates/browse-package.html
@@ -1,4 +1,6 @@
 {% extends "browse-layout.html" %}
+{% import "macros.html" as macros %}
+
 {% block content %}
   <h2>{{package}}</h2>
 
@@ -17,4 +19,8 @@
       </tr>
     {% endfor %}
   </table>
+
+  {% for p, info in running.items()|sort %}
+    {{ macros.display_running_job(p, info) }}
+  {% endfor %}
 {% endblock %}
diff --git a/charms/focal/autopkgtest-web/webcontrol/templates/browse-running.html b/charms/focal/autopkgtest-web/webcontrol/templates/browse-running.html
index 3711b97..53ae2f9 100644
--- a/charms/focal/autopkgtest-web/webcontrol/templates/browse-running.html
+++ b/charms/focal/autopkgtest-web/webcontrol/templates/browse-running.html
@@ -1,4 +1,6 @@
 {% extends "browse-layout.html" %}
+{% import "macros.html" as macros %}
+
 {% block content %}
   <h1 class="page-header">Currently running tests</h1>
   <p>Click on the package name to jump to the currently running tests of that package.</p>
@@ -36,31 +38,8 @@
   {% endfor %}
 
   <!-- Running tests -->
-  {% for p in running|sort %}
-  <h2 id="pkg-{{p}}"><a href="/packages/{{p}}">{{p}}</a></h2>
-    {% for runhash, relinfo in running[p].items() %}
-      {% for release, archinfo in relinfo.items() %}
-        {% for arch, (params, duration, logtail) in archinfo.items() %}
-          <table class="table-condensed">
-            <tr><th>Release:</th><td>{{release}}</td></tr>
-            <tr><th>Architecture:</th><td>{{arch}}</td></tr>
-            {% for param, v in params.items() %}
-              {% if param == "requester" %}
-            <tr><th>{{param|capitalize}}:</th><td><a href="https://launchpad.net/~{{v}}";>{{v}}</a></td></tr>
-              {% elif param == "uuid" %}
-            <tr><th>{{param|capitalize}}:</th><td>{{v}}</td></tr>
-              {% else %}
-            <tr><th>{{param|capitalize}}:</th><td>{{v}}</td></tr>
-              {% endif %}
-            {% endfor %}
-            <tr><th>Running for:</th><td>{{duration//3600 }}h {{duration % 3600//60}}m {{duration % 60}}s</td></tr>
-         </table>
-        <pre>
-{{logtail}}
-        </pre>
-        {% endfor %}
-      {% endfor %}
-    {% endfor %}
+  {% for p, info in running.items()|sort %}
+    {{ macros.display_running_job(p, info) }}
   {% endfor %}
 
   <!-- queue contents -->
diff --git a/charms/focal/autopkgtest-web/webcontrol/templates/macros.html b/charms/focal/autopkgtest-web/webcontrol/templates/macros.html
new file mode 100644
index 0000000..7d43d20
--- /dev/null
+++ b/charms/focal/autopkgtest-web/webcontrol/templates/macros.html
@@ -0,0 +1,26 @@
+{% macro display_running_job(package, info) -%}
+<h2 id="pkg-{{ package }}"><a href="/packages/{{ package }}">{{ package }}</a></h2>
+  {% for runhash, relinfo in info.items() %}
+    {% for release, archinfo in relinfo.items() %}
+      {% for arch, (params, duration, logtail) in archinfo.items() %}
+        <table class="table-condensed">
+          <tr><th>Release:</th><td>{{ release }}</td></tr>
+          <tr><th>Architecture:</th><td>{{ arch }}</td></tr>
+          {% for param, v in params.items() %}
+            {% if param == "requester" %}
+            <tr><th>{{ param|capitalize }}:</th><td><a href="https://launchpad.net/~{{ v }}">{{ v }}</a></td></tr>
+              {% elif param == "uuid" %}
+            <tr><th>{{ param|upper }}:</th><td>{{ v }}</td></tr>
+              {% else %}
+            <tr><th>{{ param|capitalize }}:</th><td>{{ v }}</td></tr>
+              {% endif %}
+          {% endfor %}
+          <tr><th>Running for:</th><td>{{ duration//3600 }}h {{ duration % 3600//60 }}m {{ duration % 60 }}s ({{ duration }}s)</td></tr>
+       </table>
+      <pre>
+{{ logtail }}
+      </pre>
+      {% endfor %}
+    {% endfor %}
+  {% endfor %}
+{%- endmacro %}

Follow ups