From 75dd0ad497174bf4b2a0ddda5bbc1102fe346901 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 07 2025 16:25:52 +0000
Subject: [PATCH 1/13] repo requests web ui


* new page for request details
* new page for querying requests
* add links in taginfo and taskinfo pages
* show more data in repoinfo page

---

diff --git a/tests/test_www/test_repoinfo.py b/tests/test_www/test_repoinfo.py
index 81aafed..1994b66 100644
--- a/tests/test_www/test_repoinfo.py
+++ b/tests/test_www/test_repoinfo.py
@@ -33,7 +33,7 @@ class TestRepoInfo(unittest.TestCase):
 
         webidx.repoinfo(self.environ, self.repo_id)
         self.server.repoInfo.assert_called_once_with(int(self.repo_id), strict=False)
-        self.server.listBuildroots.assert_called_once_with(repoID=int(self.repo_id))
+        self.server.listBuildroots.assert_called_once_with(repoID=int(self.repo_id), queryOpts={'countOnly': True})
 
     def test_repoinfo_not_dist(self):
         """Test repoinfo function - not dist repo"""
@@ -46,4 +46,7 @@ class TestRepoInfo(unittest.TestCase):
 
         webidx.repoinfo(self.environ, self.repo_id)
         self.server.repoInfo.assert_called_once_with(int(self.repo_id), strict=False)
-        self.server.listBuildroots.assert_called_once_with(repoID=int(self.repo_id))
+        self.server.listBuildroots.assert_called_once_with(repoID=int(self.repo_id), queryOpts={'countOnly': True})
+
+
+# the end
diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py
index c237d4f..4a86592 100644
--- a/www/kojiweb/index.py
+++ b/www/kojiweb/index.py
@@ -797,6 +797,10 @@ def taskinfo(environ, taskID):
     else:
         values['perms'] = []
 
+    values['requests'] = []
+    if task['method'] == 'newRepo':
+        values['requests'] = server.repo.queryQueue([['task_id', '=', task['id']]], ['id'])
+
     values['koji'] = koji
     values['S'] = SafeValue
 
@@ -1051,8 +1055,9 @@ def taginfo(environ, tagID, all='0', packageOrder='package_name', packageStart=N
     values['srcTargets'] = srcTargets
     values['destTargets'] = destTargets
     values['all'] = all
-    values['repo'] = server.getRepo(tag['id'], state=koji.REPO_READY)
+    values['repo'] = server.repo.get(tag['id'])
     values['external_repos'] = server.getExternalRepoList(tag['id'])
+    values['request_count'] = server.repo.queryQueue([['tag_id', '=', tag['id']]], opts={'countOnly': True})
 
     child = None
     if childID is not None:
@@ -2696,13 +2701,72 @@ def repoinfo(environ, repoID):
         else:
             values['repo_json'] = os.path.join(
                 pathinfo.repo(repo_info['id'], repo_info['tag_name']), 'repo.json')
-    num_buildroots = len(server.listBuildroots(repoID=repoID)) or 0
-    values['numBuildroots'] = num_buildroots
+        num_buildroots = server.listBuildroots(repoID=repoID, queryOpts={'countOnly': True})
+        values['numBuildroots'] = num_buildroots
+        values['requests'] = server.repo.queryQueue([['repo_id', '=', repoID]], ['id'])
+
     values['state_name'] = kojiweb.util.repoState(repo_info['state'])
     values['create_time'] = kojiweb.util.formatTimeLong(repo_info['create_ts'])
     return _genHTML(environ, 'repoinfo.html.j2')
 
 
+def reporequest(environ, reqID):
+    values = _initValues(environ, 'Repo Request', 'tags')
+    server = _getServer(environ)
+
+    req_id = int(reqID)
+    values['req_id'] = req_id
+    rows = server.repo.queryQueue([['id', '=', req_id]], '**')
+    if not rows:
+        raise koji.GenericError("No such repo request: %s" % req_id)
+    req = rows[0]
+    if req['at_event']:
+        values['at_event'] = server.getEvent(req['at_event'])
+    elif req['min_event']:
+        values['min_event'] = server.getEvent(req['min_event'])
+    else:
+        # invalid, but technically not blocked in db
+        values['min_event'] = None
+    values['req'] = req
+    return _genHTML(environ, 'reporequest.chtml')
+
+
+def reporequests(environ, active="true", tag=None, start=None, order=None):
+    values = _initValues(environ, 'Repo Requests', 'tags')
+    server = _getServer(environ)
+
+    clauses = []
+    desc_parts = []
+    if active.lower() == 'all':
+        desc_parts.append('Recent repo requests')
+    elif active.lower() in ('false', 'no', '0'):
+        desc_parts.append('Inactive repo requests')
+        clauses.append(["active", False])
+        active = 'false'
+    else:
+        desc_parts.append('Active repo requests')
+        clauses.append(["active", True])
+        active = 'true'
+    if tag:
+        taginfo = server.getTag(_convert_if_int(tag), strict=True, event='auto')
+        clauses.append(["tag_id", taginfo['id']])
+        desc_parts.append('for tag %(name)s' % taginfo)
+        tag = taginfo['name']
+    else:
+        tag = None
+    if order is None:
+        order = '-id'
+    values['desc'] = ' '.join(desc_parts)
+    values['order'] = order
+    values['active'] = active
+    values['tag'] = tag
+    kojiweb.util.paginateMethod(server, values, 'repo.queryQueue',
+                                args=(clauses, '**'),
+                                start=start, dataName='reqs', prefix='req', order=order,
+                                optsarg='opts')
+    return _genHTML(environ, 'reporequests.chtml')
+
+
 def activesession(environ, start=None, order=None):
     values = _initValues(environ, 'Active sessions', 'activesession')
     server = _getServer(environ)
diff --git a/www/kojiweb/reporequest.chtml b/www/kojiweb/reporequest.chtml
new file mode 100644
index 0000000..e67fe65
--- /dev/null
+++ b/www/kojiweb/reporequest.chtml
@@ -0,0 +1,41 @@
+#import json
+#import koji
+#from kojiweb import util
+
+#include "includes/header.chtml"
+
+<h4>Information for repo request $req_id</h4>
+
+#if not $req
+Repo request $req_id not found.
+#else
+<table>
+  <tr><th>ID</th><td>$req.id</td><th></tr>
+  <tr><th>Active</th><td>$req.active</td><th></tr>
+  <tr><th>Priority</th><td>$req.priority</td><th></tr>
+  <tr><th>Tag</th><td><a href="taginfo?tagID=$req.tag_id">$req.tag_name</a></td></tr>
+  #if $req.at_event
+  <tr><th>At specific event</th><td>$at_event.id ($util.formatTimeLong($at_event.ts))</td><th></tr>
+  #elif $req.min_event
+  <tr><th>Minimum event</th><td>$min_event.id ($util.formatTimeLong($min_event.ts))</td><th></tr>
+  #else
+  <tr><th>Invalid event</th><td>Unable to determine event for request</td><th></tr>
+  #end if
+  #if $req.opts
+  <th>Options</th><td class="usertext">$json.dumps($req.opts, indent=4)</td>
+  #end if
+  #if $req.repo_id
+  <tr><th>Fulfilled by repo</th><td><a href="repoinfo?repoID=$req.repo_id">$req.repo_id</a</td></tr>
+  #end if
+  #if $req.task_id
+  <tr><th>Task ID</th><td><a href="taskinfo?taskID=$req.task_id">$req.task_id</a> ($util.taskState($req.task_state))</td></tr>
+  <tr><th>Tries</th><td>$req.tries</td></tr>
+  #end if
+  <tr><th>Owner</th><td><a href="userinfo?userID=$req.owner">$req.owner_name</a></td></tr>
+  <tr><th>Created</th><td>$util.formatTimeLong($req.create_ts)</td></tr>
+  <tr><th>Updated</th><td>$util.formatTimeLong($req.update_ts)</td></tr>
+</table>
+#end if
+
+
+#include "includes/footer.chtml"
diff --git a/www/kojiweb/reporequests.chtml b/www/kojiweb/reporequests.chtml
new file mode 100644
index 0000000..7f85281
--- /dev/null
+++ b/www/kojiweb/reporequests.chtml
@@ -0,0 +1,129 @@
+#encoding UTF-8
+
+#from kojiweb import util
+#from kojiweb.util import passthrough as P
+
+#include "includes/header.chtml"
+
+
+#set $Pvars = ('active', 'tag', 'order')
+
+  <h4>$desc</h4>
+  <table class="data-list">
+      <td colspan="6">
+        <form action="">
+        <table class="nested">
+          <tr><td>
+              <strong>Active</strong>:
+          </td><td>
+              <select name="active" class="filterlist" onchange="javascript: window.location = 'reporequests?active=' + this.value + '$P($self, 'tag', 'order')';">
+                <option value="true" #if $active == 'true' then 'selected' else ''#>true</option>
+                <option value="false" #if $active == 'false' then 'selected' else ''#>false</option>
+                <option value="all" #if $active == 'all' then 'selected' else ''#>all</option>
+              </select>
+          </td>
+          <td>
+              <strong>Tag</strong>:
+          </td><td>
+              <input type="text" name="tag" value="$tag"/>
+          </td>
+          </tr>
+        </table>
+        </form>
+      </td>
+    </tr>
+    <tr>
+      <td class="paginate" colspan="6">
+        #if $len($reqPages) > 1
+        <form class="pageJump" action="">
+          Page:
+          <select onchange="javascript: window.location = 'reporequests?start=' + this.value * $reqRange + '$P($self, *Pvars)';">
+            #for $pageNum in $reqPages
+            <option value="$pageNum"#if $pageNum == $reqCurrentPage then ' selected' else ''#>#echo $pageNum + 1#</option>
+            #end for
+          </select>
+        </form>
+        #end if
+        #if $reqStart > 0
+        <a href="reporequests?start=#echo $reqStart - $reqRange #$P($self, *Pvars)">&lt;&lt;&lt;</a>
+        #end if
+        #if $totalReqs != 0
+        <strong>Requests #echo $reqStart + 1 # through #echo $reqStart + $reqCount # of $totalReqs</strong>
+        #end if
+        #if $reqStart + $reqCount < $totalReqs
+        <a href="reporequests?start=#echo $reqStart + $reqRange#$P($self, *Pvars)">&gt;&gt;&gt;</a>
+        #end if
+      </td>
+    </tr>
+    <tr class="list-header">
+      <th><a href="reporequests?$P($self, *Pvars, toggleOrder='id', prefix='')">ID</a> $util.sortImage($self, 'id')</th>
+      <th><a href="reporequests?$P($self, *Pvars, toggleOrder='priority', prefix='')">Priority</a> $util.sortImage($self, 'priority')</th>
+      <th><a href="reporequests?$P($self, *Pvars, toggleOrder='tag_name', prefix='')">Tag</a> $util.sortImage($self, 'tag_name')</th>
+      <th>Task</th>
+      <th>Repo</th>
+      <th>Status</th>
+    </tr>
+    #if $len($reqs) > 0
+      #for $req in $reqs
+        <tr class="$util.rowToggle($self)">
+          <td><a href="reporequest?reqID=$req.id">$req.id</a></td>
+          <td>$req.priority</td>
+          <td>
+            <a href="taginfo?tagID=$req.tag_id">$req.tag_name</a>
+            #if not $tag
+            <a href="reporequests?tag=$req.tag_id$P($self, 'active', 'order')" title="Filter by tag"><img src="$util.themePath('images/funnel.svg')"></a>
+            #end if
+          </td>
+          #if $req.task_id
+          <td><a href="taskinfo?taskID=$req.task_id">$req.task_id</a></td>
+          #else
+          <td>...</td>
+          #end if
+          #if $req.repo_id
+          <td><a href="repoinfo?repoID=$req.repo_id">$req.repo_id</a></td>
+          #else
+          <td>...</td>
+          #end if
+          <td>
+          ## simulate a more helpful status
+          #if $req.active
+          $util.imageTag('waiting')
+          #elif $req.repo_id
+          $util.imageTag('yes')
+          #else
+          $util.imageTag('no')
+          #end if
+          </td>
+        </tr>
+      #end for
+    #else
+      <tr class="row-odd">
+        <td colspan="2">No repo requests</td>
+      </tr>
+    #end if
+    <tr>
+      <td class="paginate" colspan="2">
+        #if $len($reqPages) > 1
+        <form class="pageJump" action="">
+          Page:
+          <select onchange="javascript: window.location = 'reporequests?start=' + this.value * $reqRange + '$P($self, *Pvars)';">
+            #for $pageNum in $reqPages
+            <option value="$pageNum"#if $pageNum == $reqCurrentPage then ' selected' else ''#>#echo $pageNum + 1#</option>
+            #end for
+          </select>
+        </form>
+        #end if
+        #if $reqStart > 0
+        <a href="reporequests?start=#echo $reqStart - $reqRange #$P($self, *Pvars)">&lt;&lt;&lt;</a>
+        #end if
+        #if $totalReqs != 0
+        <strong>Reqs #echo $reqStart + 1 # through #echo $reqStart + $reqCount # of $totalReqs</strong>
+        #end if
+        #if $reqStart + $reqCount < $totalReqs
+        <a href="reporequests?start=#echo $reqStart + $reqRange#$P($self, *Pvars)">&gt;&gt;&gt;</a>
+        #end if
+      </td>
+    </tr>
+  </table>
+
+#include "includes/footer.chtml"
diff --git a/www/kojiweb/templates/repoinfo.html.j2 b/www/kojiweb/templates/repoinfo.html.j2
index cfcd284..0d856b6 100644
--- a/www/kojiweb/templates/repoinfo.html.j2
+++ b/www/kojiweb/templates/repoinfo.html.j2
@@ -7,16 +7,35 @@
   <tr><th>ID</th><td>{{ repo.id }}</td><th></tr>
   <tr><th>Tag</th><td><a href="taginfo?tagID={{ repo.tag_id }}">{{ repo.tag_name }}</a></td></tr>
   {% if repo.task_id %}
-  <tr><th>Task ID</th><td><a href="taskinfo?taskID={{ repo.task_id }}">{{ repo.task_id }}</a></td></tr>
+  <tr><th>Task ID</th><td><a href="taskinfo?taskID={{ repo.task_id }}">{{ repo.task_id }}</a> ({{ util.taskState(repo.task_state) }})</td></tr>
   {% endif %}
   <tr><th>State</th><td class="repo{{ state_name }}">{{ state_name }}</td></tr>
-  <tr><th>Event</th><td>{{ repo.create_event }} ({{ create_time }})</td></tr>
+  <tr><th>Created</th><td>{{ util.formatTimeLong(repo.creation_ts) }}</td></tr>
+  <tr><th>State changed</th><td>{{ util.formatTimeLong(repo.state_ts) }}</td></tr>
+  <tr><th>Created from Event</th><td>{{ repo.create_event }} ({{ util.formatTimeLong(repo.create_ts) }})</td></tr>
+  {%- if repo.begin_event %}
+  {%- if repo.end_event %}
+  <tr><th>Event range</th><td>{{ repo.begin_event }} ... {{ repo.end_event }}</td></tr>
+  {%- else %}
+  <tr><th>Event range</th><td>{{ repo.begin_event }} ... </td></tr>
+  {%- endif %}
+  {%- endif %}
   {%- if state_name != 'deleted' %}
   <tr><th>URL</th><td><a href="{{ url }}">repodata</a></td></tr>
   <tr><th>Repo json</th><td><a href="{{ repo_json }}">repo.json</a></td></tr>
   {%- endif %}
+  #if repo.custom_opts
+  <th>Custom Opts</th><td class="usertext">{{ json.dumps(repo.custom_opts, indent=4) }}</td>
+  #endif
   <tr><th>Dist repo?</th><td class="{{ repo.dist | lower }}">{{ 'yes' if repo.dist else 'no' }}</td></tr>
   <tr><th>Number of buildroots: </th><td><a href="buildroots?repoID={{ repo.id }}">{{ numBuildroots }}</a></td></tr>
+  #if requests
+  <tr><th>Fulfills requests:</th><td>
+    #for req in requests
+    <a href="reporequest?reqID={{ req.id }}">{{ req.id }}</a>
+    #endfor
+  </td/></tr>
+  #endif
 </table>
 {% else %}
 Repo {{ repo_id }} not found.
diff --git a/www/kojiweb/templates/taginfo.html.j2 b/www/kojiweb/templates/taginfo.html.j2
index 3b4bc3e..5202596 100644
--- a/www/kojiweb/templates/taginfo.html.j2
+++ b/www/kojiweb/templates/taginfo.html.j2
@@ -118,6 +118,10 @@
       </td>
     </tr>
     <tr>
+      <th>Repo&nbsp;requests</th>
+      <td><a href="reporequests?active=all&tag={{ tag.id }}">{{ request_count }}</a></td>
+    </tr>
+    <tr>
       <th>Packages</th>
       <td><a href="packages?blocked=0&tagID={{ tag.id }}">{{ numPackages }}</a></td>
     </tr>
diff --git a/www/kojiweb/templates/taskinfo.html.j2 b/www/kojiweb/templates/taskinfo.html.j2
index 453d37d..f2ff5a8 100644
--- a/www/kojiweb/templates/taskinfo.html.j2
+++ b/www/kojiweb/templates/taskinfo.html.j2
@@ -140,6 +140,14 @@ None
     </tr>
     #endfor
     #endif
+    #if requests
+    <tr><th>For request:</th><td>
+      ## we only expect one, but if we get more print them all
+      #for req in requests
+      <a href="reporequest?reqID={{ req.id }}">{{ req.id }}</a>
+      #endfor
+    </td/></tr>
+    #endif
     <tr>
       <th>Created</th><td>{{ util.formatTimeLong(task.create_ts) }}</td>
     </tr>
diff --git a/www/lib/kojiweb/util.py b/www/lib/kojiweb/util.py
index c30a689..ad840b4 100644
--- a/www/lib/kojiweb/util.py
+++ b/www/lib/kojiweb/util.py
@@ -353,17 +353,17 @@ def _sortImage(orderVal, sortKey, orderVar):
 
 @safe_return
 @pass_context
-def passthrough(context, *varnames, prefix='&', invert=False):
+def passthrough(context, *varnames, prefix='&', invert=False, toggleOrder=None):
     if invert:
         _PASSTHROUGH = context.get('_PASSTHROUGH', None)
         if _PASSTHROUGH is None:
             raise Exception('template does not define _PASSTHROUGH')
         varnames = {n for n in _PASSTHROUGH if n not in varnames}
     data = {n: context.get(n, default=None) for n in varnames}
-    return _passthrough(data, prefix)
+    return _passthrough(data, prefixi, toggleOrder)
 
 
-def _passthrough(data, prefix='&'):
+def _passthrough(data, prefix='&', toggleOrder=None):
     """
     Construct a url parameter string from template vars
 
@@ -380,6 +380,11 @@ def _passthrough(data, prefix='&'):
     result = []
     for var in sorted(data):
         value = data[var]
+        if var == 'order' and toggleOrder is not None:
+            if value == toggleOrder:
+                value = '-' + value
+            else:
+                value = toggleOrder
         if value is not None:
             if isinstance(value, str):
                 if value.isdigit():
@@ -396,7 +401,7 @@ def _passthrough(data, prefix='&'):
 
 
 @pass_context
-def passthrough_except(context, *exclude, prefix='&'):
+def passthrough_except(context, *exclude, prefix='&', toggleOrder=None):
     """
     Construct a string suitable for use as URL
     parameters.  The template calling this method must have
@@ -408,7 +413,7 @@ def passthrough_except(context, *exclude, prefix='&'):
     """
     # note that we have to pass context ourselves here
     # the decorator only works when called directly from the template
-    return passthrough(context, *exclude, prefix=prefix, invert=True)
+    return passthrough(context, *exclude, prefix=prefix, invert=True, toggleOrder=toggleOrder)
 
 
 def sortByKeyFuncNoneGreatest(key):
@@ -459,7 +464,7 @@ def paginateList(values, data, start, dataName, prefix=None, order=None, noneGre
 
 def paginateMethod(server, values, methodName, args=None, kw=None,
                    start=None, dataName=None, prefix=None, order=None, pageSize=50,
-                   first_page_count=True):
+                   first_page_count=True, optsarg='queryOpts'):
     """Paginate the results of the method with the given name when called with the given args and
     kws. The method must support the queryOpts keyword parameter, and pagination is done in the
     database.
@@ -483,10 +488,10 @@ def paginateMethod(server, values, methodName, args=None, kw=None,
     if start == 0 and not first_page_count:
         totalRows = None
     else:
-        kw['queryOpts'] = {'countOnly': True}
+        kw[optsarg] = {'countOnly': True}
         totalRows = getattr(server, methodName)(*args, **kw)
 
-    kw['queryOpts'] = {'order': order,
+    kw[optsarg] = {'order': order,
                        'offset': start,
                        'limit': pageSize}
     data = getattr(server, methodName)(*args, **kw)
diff --git a/www/static/images/funnel.svg b/www/static/images/funnel.svg
new file mode 100644
index 0000000..2e1b054
--- /dev/null
+++ b/www/static/images/funnel.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" height="16px" width="16px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
+	 viewBox="0 0 485.008 485.008" xml:space="preserve">
+<g>
+	<g>
+		<path d="M171.501,464.698v-237.9l-166.3-192.6c-8.9-10.9-7.9-33.3,15.1-33.3h443.6c21.6,0,26.6,19.8,15.1,33.3l-162.3,187.5v147.2
+			c0,6-2,11.1-7.1,15.1l-103.8,95.8C193.801,488.698,171.501,483.898,171.501,464.698z M64.701,41.298l142.2,164.3c3,4,5,8.1,5,13.1
+			v200.6l64.5-58.5v-146.1c0-5,2-9.1,5-13.1l138.1-160.3L64.701,41.298L64.701,41.298z"/>
+	</g>
+</g>
+</svg>

From 0a6eb52c0a635bc34484356974e9de7a6c75fd4f Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 07 2025 16:27:02 +0000
Subject: [PATCH 2/13] typo


---

diff --git a/www/lib/kojiweb/util.py b/www/lib/kojiweb/util.py
index ad840b4..6deacc7 100644
--- a/www/lib/kojiweb/util.py
+++ b/www/lib/kojiweb/util.py
@@ -360,7 +360,7 @@ def passthrough(context, *varnames, prefix='&', invert=False, toggleOrder=None):
             raise Exception('template does not define _PASSTHROUGH')
         varnames = {n for n in _PASSTHROUGH if n not in varnames}
     data = {n: context.get(n, default=None) for n in varnames}
-    return _passthrough(data, prefixi, toggleOrder)
+    return _passthrough(data, prefix, toggleOrder)
 
 
 def _passthrough(data, prefix='&', toggleOrder=None):

From ba6352ba812f18b35edf719c4651c3edec55aff9 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 07 2025 16:40:40 +0000
Subject: [PATCH 3/13] port new templates to jinja


---

diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py
index 4a86592..fa2c6d5 100644
--- a/www/kojiweb/index.py
+++ b/www/kojiweb/index.py
@@ -2728,7 +2728,7 @@ def reporequest(environ, reqID):
         # invalid, but technically not blocked in db
         values['min_event'] = None
     values['req'] = req
-    return _genHTML(environ, 'reporequest.chtml')
+    return _genHTML(environ, 'reporequest.html.j2')
 
 
 def reporequests(environ, active="true", tag=None, start=None, order=None):
@@ -2764,7 +2764,7 @@ def reporequests(environ, active="true", tag=None, start=None, order=None):
                                 args=(clauses, '**'),
                                 start=start, dataName='reqs', prefix='req', order=order,
                                 optsarg='opts')
-    return _genHTML(environ, 'reporequests.chtml')
+    return _genHTML(environ, 'reporequests.html.j2')
 
 
 def activesession(environ, start=None, order=None):
diff --git a/www/kojiweb/reporequest.chtml b/www/kojiweb/reporequest.chtml
deleted file mode 100644
index e67fe65..0000000
--- a/www/kojiweb/reporequest.chtml
+++ /dev/null
@@ -1,41 +0,0 @@
-#import json
-#import koji
-#from kojiweb import util
-
-#include "includes/header.chtml"
-
-<h4>Information for repo request $req_id</h4>
-
-#if not $req
-Repo request $req_id not found.
-#else
-<table>
-  <tr><th>ID</th><td>$req.id</td><th></tr>
-  <tr><th>Active</th><td>$req.active</td><th></tr>
-  <tr><th>Priority</th><td>$req.priority</td><th></tr>
-  <tr><th>Tag</th><td><a href="taginfo?tagID=$req.tag_id">$req.tag_name</a></td></tr>
-  #if $req.at_event
-  <tr><th>At specific event</th><td>$at_event.id ($util.formatTimeLong($at_event.ts))</td><th></tr>
-  #elif $req.min_event
-  <tr><th>Minimum event</th><td>$min_event.id ($util.formatTimeLong($min_event.ts))</td><th></tr>
-  #else
-  <tr><th>Invalid event</th><td>Unable to determine event for request</td><th></tr>
-  #end if
-  #if $req.opts
-  <th>Options</th><td class="usertext">$json.dumps($req.opts, indent=4)</td>
-  #end if
-  #if $req.repo_id
-  <tr><th>Fulfilled by repo</th><td><a href="repoinfo?repoID=$req.repo_id">$req.repo_id</a</td></tr>
-  #end if
-  #if $req.task_id
-  <tr><th>Task ID</th><td><a href="taskinfo?taskID=$req.task_id">$req.task_id</a> ($util.taskState($req.task_state))</td></tr>
-  <tr><th>Tries</th><td>$req.tries</td></tr>
-  #end if
-  <tr><th>Owner</th><td><a href="userinfo?userID=$req.owner">$req.owner_name</a></td></tr>
-  <tr><th>Created</th><td>$util.formatTimeLong($req.create_ts)</td></tr>
-  <tr><th>Updated</th><td>$util.formatTimeLong($req.update_ts)</td></tr>
-</table>
-#end if
-
-
-#include "includes/footer.chtml"
diff --git a/www/kojiweb/reporequests.chtml b/www/kojiweb/reporequests.chtml
deleted file mode 100644
index 7f85281..0000000
--- a/www/kojiweb/reporequests.chtml
+++ /dev/null
@@ -1,129 +0,0 @@
-#encoding UTF-8
-
-#from kojiweb import util
-#from kojiweb.util import passthrough as P
-
-#include "includes/header.chtml"
-
-
-#set $Pvars = ('active', 'tag', 'order')
-
-  <h4>$desc</h4>
-  <table class="data-list">
-      <td colspan="6">
-        <form action="">
-        <table class="nested">
-          <tr><td>
-              <strong>Active</strong>:
-          </td><td>
-              <select name="active" class="filterlist" onchange="javascript: window.location = 'reporequests?active=' + this.value + '$P($self, 'tag', 'order')';">
-                <option value="true" #if $active == 'true' then 'selected' else ''#>true</option>
-                <option value="false" #if $active == 'false' then 'selected' else ''#>false</option>
-                <option value="all" #if $active == 'all' then 'selected' else ''#>all</option>
-              </select>
-          </td>
-          <td>
-              <strong>Tag</strong>:
-          </td><td>
-              <input type="text" name="tag" value="$tag"/>
-          </td>
-          </tr>
-        </table>
-        </form>
-      </td>
-    </tr>
-    <tr>
-      <td class="paginate" colspan="6">
-        #if $len($reqPages) > 1
-        <form class="pageJump" action="">
-          Page:
-          <select onchange="javascript: window.location = 'reporequests?start=' + this.value * $reqRange + '$P($self, *Pvars)';">
-            #for $pageNum in $reqPages
-            <option value="$pageNum"#if $pageNum == $reqCurrentPage then ' selected' else ''#>#echo $pageNum + 1#</option>
-            #end for
-          </select>
-        </form>
-        #end if
-        #if $reqStart > 0
-        <a href="reporequests?start=#echo $reqStart - $reqRange #$P($self, *Pvars)">&lt;&lt;&lt;</a>
-        #end if
-        #if $totalReqs != 0
-        <strong>Requests #echo $reqStart + 1 # through #echo $reqStart + $reqCount # of $totalReqs</strong>
-        #end if
-        #if $reqStart + $reqCount < $totalReqs
-        <a href="reporequests?start=#echo $reqStart + $reqRange#$P($self, *Pvars)">&gt;&gt;&gt;</a>
-        #end if
-      </td>
-    </tr>
-    <tr class="list-header">
-      <th><a href="reporequests?$P($self, *Pvars, toggleOrder='id', prefix='')">ID</a> $util.sortImage($self, 'id')</th>
-      <th><a href="reporequests?$P($self, *Pvars, toggleOrder='priority', prefix='')">Priority</a> $util.sortImage($self, 'priority')</th>
-      <th><a href="reporequests?$P($self, *Pvars, toggleOrder='tag_name', prefix='')">Tag</a> $util.sortImage($self, 'tag_name')</th>
-      <th>Task</th>
-      <th>Repo</th>
-      <th>Status</th>
-    </tr>
-    #if $len($reqs) > 0
-      #for $req in $reqs
-        <tr class="$util.rowToggle($self)">
-          <td><a href="reporequest?reqID=$req.id">$req.id</a></td>
-          <td>$req.priority</td>
-          <td>
-            <a href="taginfo?tagID=$req.tag_id">$req.tag_name</a>
-            #if not $tag
-            <a href="reporequests?tag=$req.tag_id$P($self, 'active', 'order')" title="Filter by tag"><img src="$util.themePath('images/funnel.svg')"></a>
-            #end if
-          </td>
-          #if $req.task_id
-          <td><a href="taskinfo?taskID=$req.task_id">$req.task_id</a></td>
-          #else
-          <td>...</td>
-          #end if
-          #if $req.repo_id
-          <td><a href="repoinfo?repoID=$req.repo_id">$req.repo_id</a></td>
-          #else
-          <td>...</td>
-          #end if
-          <td>
-          ## simulate a more helpful status
-          #if $req.active
-          $util.imageTag('waiting')
-          #elif $req.repo_id
-          $util.imageTag('yes')
-          #else
-          $util.imageTag('no')
-          #end if
-          </td>
-        </tr>
-      #end for
-    #else
-      <tr class="row-odd">
-        <td colspan="2">No repo requests</td>
-      </tr>
-    #end if
-    <tr>
-      <td class="paginate" colspan="2">
-        #if $len($reqPages) > 1
-        <form class="pageJump" action="">
-          Page:
-          <select onchange="javascript: window.location = 'reporequests?start=' + this.value * $reqRange + '$P($self, *Pvars)';">
-            #for $pageNum in $reqPages
-            <option value="$pageNum"#if $pageNum == $reqCurrentPage then ' selected' else ''#>#echo $pageNum + 1#</option>
-            #end for
-          </select>
-        </form>
-        #end if
-        #if $reqStart > 0
-        <a href="reporequests?start=#echo $reqStart - $reqRange #$P($self, *Pvars)">&lt;&lt;&lt;</a>
-        #end if
-        #if $totalReqs != 0
-        <strong>Reqs #echo $reqStart + 1 # through #echo $reqStart + $reqCount # of $totalReqs</strong>
-        #end if
-        #if $reqStart + $reqCount < $totalReqs
-        <a href="reporequests?start=#echo $reqStart + $reqRange#$P($self, *Pvars)">&gt;&gt;&gt;</a>
-        #end if
-      </td>
-    </tr>
-  </table>
-
-#include "includes/footer.chtml"
diff --git a/www/kojiweb/templates/reporequest.html.j2 b/www/kojiweb/templates/reporequest.html.j2
new file mode 100644
index 0000000..dc90c08
--- /dev/null
+++ b/www/kojiweb/templates/reporequest.html.j2
@@ -0,0 +1,38 @@
+
+#include "header.html.j2"
+
+<h4>Information for repo request {{ req_id }}</h4>
+
+#if not req
+Repo request {{ req_id }} not found.
+#else
+<table>
+  <tr><th>ID</th><td>{{ req.id }}</td><th></tr>
+  <tr><th>Active</th><td>{{ req.active }}</td><th></tr>
+  <tr><th>Priority</th><td>{{ req.priority }}</td><th></tr>
+  <tr><th>Tag</th><td><a href="taginfo?tagID={{ req.tag_id }}">{{ req.tag_name }}</a></td></tr>
+  #if req.at_event
+  <tr><th>At specific event</th><td>{{ at_event.id }} ({{ util.formatTimeLong(at_event.ts) }})</td><th></tr>
+  #elif req.min_event
+  <tr><th>Minimum event</th><td>{{ min_event.id }} ({{ util.formatTimeLong(min_event.ts) }})</td><th></tr>
+  #else
+  <tr><th>Invalid event</th><td>Unable to determine event for request</td><th></tr>
+  #endif
+  #if req.opts
+  <th>Options</th><td class="usertext">{{ json.dumps(req.opts, indent=4) }}</td>
+  #endif
+  #if req.repo_id
+  <tr><th>Fulfilled by repo</th><td><a href="repoinfo?repoID={{ req.repo_id }}">{{ req.repo_id }}</a</td></tr>
+  #endif
+  #if req.task_id
+  <tr><th>Task ID</th><td><a href="taskinfo?taskID={{ req.task_id }}">{{ req.task_id }}</a> ({{ util.taskState(req.task_state) }})</td></tr>
+  <tr><th>Tries</th><td>{{ req.tries }}</td></tr>
+  #endif
+  <tr><th>Owner</th><td><a href="userinfo?userID={{ req.owner }}">{{ req.owner_name }}</a></td></tr>
+  <tr><th>Created</th><td>{{ util.formatTimeLong(req.create_ts) }}</td></tr>
+  <tr><th>Updated</th><td>{{ util.formatTimeLong(req.update_ts) }}</td></tr>
+</table>
+#endif
+
+
+#include "footer.html.j2"
diff --git a/www/kojiweb/templates/reporequests.html.j2 b/www/kojiweb/templates/reporequests.html.j2
new file mode 100644
index 0000000..a06c691
--- /dev/null
+++ b/www/kojiweb/templates/reporequests.html.j2
@@ -0,0 +1,126 @@
+#include "header.html.j2"
+
+# from "macros.html.j2" import rowToggle
+
+#set Pvars = ('active', 'tag', 'order')
+#set P = util.passthrough
+
+  <h4>{{ desc }}</h4>
+  <table class="data-list">
+      <td colspan="6">
+        <form action="">
+        <table class="nested">
+          <tr><td>
+              <strong>Active</strong>:
+          </td><td>
+              <select name="active" class="filterlist" onchange="javascript: window.location = 'reporequests?active=' + this.value + '{{ P('tag', 'order') }}';">
+                <option value="true" {{ 'selected' if active == 'true' else '' }}>true</option>
+                <option value="false" {{ 'selected' if active == 'false' else '' }}>false</option>
+                <option value="all" {{ 'selected' if active == 'all' else '' }}>all</option>
+              </select>
+          </td>
+          <td>
+              <strong>Tag</strong>:
+          </td><td>
+              <input type="text" name="tag" value="{{ tag }}"/>
+          </td>
+          </tr>
+        </table>
+        </form>
+      </td>
+    </tr>
+    <tr>
+      <td class="paginate" colspan="6">
+        #if (reqPages |length) > 1
+        <form class="pageJump" action="">
+          Page:
+          <select onchange="javascript: window.location = 'reporequests?start=' + this.value * {{ reqRange }} + '{{ P(*Pvars) }}';">
+            #for pageNum in reqPages
+            <option value="{{ pageNum }}"{{ ' selected' if pageNum == reqCurrentPage else '' }}>{{ pageNum + 1 }}</option>
+            #endfor
+          </select>
+        </form>
+        #endif
+        #if reqStart > 0
+        <a href="reporequests?start={{ reqStart - reqRange  }}{{ P(*Pvars) }}">&lt;&lt;&lt;</a>
+        #endif
+        #if totalReqs != 0
+        <strong>Requests {{ reqStart + 1  }} through {{ reqStart + reqCount  }} of {{ totalReqs }}</strong>
+        #endif
+        #if reqStart + reqCount < totalReqs
+        <a href="reporequests?start={{ reqStart + reqRange }}{{ P(*Pvars) }}">&gt;&gt;&gt;</a>
+        #endif
+      </td>
+    </tr>
+    <tr class="list-header">
+      <th><a href="reporequests?{{ P(*Pvars, toggleOrder='id', prefix='') }}">ID</a> {{ util.sortImage('id') }}</th>
+      <th><a href="reporequests?{{ P(*Pvars, toggleOrder='priority', prefix='') }}">Priority</a> {{ util.sortImage('priority') }}</th>
+      <th><a href="reporequests?{{ P(*Pvars, toggleOrder='tag_name', prefix='') }}">Tag</a> {{ util.sortImage('tag_name') }}</th>
+      <th>Task</th>
+      <th>Repo</th>
+      <th>Status</th>
+    </tr>
+    #if (reqs |length) > 0
+      #for req in reqs
+        <tr class="{{ rowToggle(loop) }}">
+          <td><a href="reporequest?reqID={{ req.id }}">{{ req.id }}</a></td>
+          <td>{{ req.priority }}</td>
+          <td>
+            <a href="taginfo?tagID={{ req.tag_id }}">{{ req.tag_name }}</a>
+            #if not tag
+            <a href="reporequests?tag={{ req.tag_id }}{{ P('active', 'order') }}" title="Filter by tag"><img src="{{ util.themePath('images/funnel.svg') }}"></a>
+            #endif
+          </td>
+          #if req.task_id
+          <td><a href="taskinfo?taskID={{ req.task_id }}">{{ req.task_id }}</a></td>
+          #else
+          <td>...</td>
+          #endif
+          #if req.repo_id
+          <td><a href="repoinfo?repoID={{ req.repo_id }}">{{ req.repo_id }}</a></td>
+          #else
+          <td>...</td>
+          #endif
+          <td>
+          ## simulate a more helpful status
+          #if req.active
+          {{ util.imageTag('waiting') }}
+          #elif req.repo_id
+          {{ util.imageTag('yes') }}
+          #else
+          {{ util.imageTag('no') }}
+          #endif
+          </td>
+        </tr>
+      #endfor
+    #else
+      <tr class="row-odd">
+        <td colspan="2">No repo requests</td>
+      </tr>
+    #endif
+    <tr>
+      <td class="paginate" colspan="2">
+        #if (reqPages |length) > 1
+        <form class="pageJump" action="">
+          Page:
+          <select onchange="javascript: window.location = 'reporequests?start=' + this.value * {{ reqRange }} + '{{ P(*Pvars) }}';">
+            #for pageNum in reqPages
+            <option value="{{ pageNum }}"{{ ' selected' if pageNum == reqCurrentPage else '' }}>{{ pageNum + 1 }}</option>
+            #endfor
+          </select>
+        </form>
+        #endif
+        #if reqStart > 0
+        <a href="reporequests?start={{ reqStart - reqRange  }}{{ P(*Pvars) }}">&lt;&lt;&lt;</a>
+        #endif
+        #if totalReqs != 0
+        <strong>Reqs {{ reqStart + 1  }} through {{ reqStart + reqCount  }} of {{ totalReqs }}</strong>
+        #endif
+        #if reqStart + reqCount < totalReqs
+        <a href="reporequests?start={{ reqStart + reqRange }}{{ P(*Pvars) }}">&gt;&gt;&gt;</a>
+        #endif
+      </td>
+    </tr>
+  </table>
+
+#include "footer.html.j2"

From 4944e3960bc1bc8d26ac913f3578d66558ae13ee Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 07 2025 18:31:55 +0000
Subject: [PATCH 4/13] update call data for unit test


---

diff --git a/tests/test_cli/fakeclient.py b/tests/test_cli/fakeclient.py
index f074dce..0190974 100644
--- a/tests/test_cli/fakeclient.py
+++ b/tests/test_cli/fakeclient.py
@@ -44,6 +44,8 @@ class FakeClientSession(BaseFakeClientSession):
         super(FakeClientSession, self).__init__(*a, **kw)
         self._calldata = {}
         self._offsets = {}
+        self._missing_rsession = None
+        # caller can set _missing_rsession to a Recording session to handle missing calls
 
     def load_calls(self, data):
         """Load call data
@@ -75,6 +77,9 @@ class FakeClientSession(BaseFakeClientSession):
         # we may have a series of calls for each key
         calls = self._calldata.get(key)
         ofs = self._offsets.get(key, 0)
+        if calls is None:
+            # we don't have it
+            return self._handle_missing(name, args, kwargs)
         call = calls[ofs]
         ofs += 1
         if ofs < len(calls):
@@ -90,6 +95,15 @@ class FakeClientSession(BaseFakeClientSession):
         else:
             return mock.MagicMock()
 
+    def _handle_missing(self, name, args, kwargs):
+        print('Missing call data for: %s %r %r' % (name, args, kwargs))
+        rsession = self._missing_rsession
+        if rsession is None:
+            return mock.MagicMock()
+
+        # otherwise use the recording session
+        return rsession._callMethod(name, args, kwargs)
+
     def _munge(self, data):
         def callback(value):
             if isinstance(value, list):
diff --git a/tests/test_www/data/pages_calls.json b/tests/test_www/data/pages_calls.json
index 5c45cc1..0a53d33 100644
--- a/tests/test_www/data/pages_calls.json
+++ b/tests/test_www/data/pages_calls.json
@@ -56142,5 +56142,155 @@
                 "task_id": null
             }
         ]
+    },
+    {
+        "args": [],
+        "kwargs": {
+            "queryOpts": {
+                "countOnly": true
+            },
+            "repoID": 2580
+        },
+        "method": "listBuildroots",
+        "result": 2
+    },
+    {
+        "args": [
+            [
+                [
+                    "repo_id",
+                    "=",
+                    2580
+                ]
+            ],
+            [
+                "id"
+            ]
+        ],
+        "kwargs": {},
+        "method": "repo.queryQueue",
+        "result": []
+    },
+    {
+        "args": [
+            798
+        ],
+        "kwargs": {},
+        "method": "repo.get",
+        "result": null
+    },
+    {
+        "args": [
+            [
+                [
+                    "tag_id",
+                    "=",
+                    798
+                ]
+            ]
+        ],
+        "kwargs": {
+            "opts": {
+                "countOnly": true
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": 0
+    },
+    {
+        "args": [],
+        "kwargs": {
+            "queryOpts": {
+                "countOnly": true
+            },
+            "repoID": 88
+        },
+        "method": "listBuildroots",
+        "result": 0
+    },
+    {
+        "args": [
+            [
+                [
+                    "repo_id",
+                    "=",
+                    88
+                ]
+            ],
+            [
+                "id"
+            ]
+        ],
+        "kwargs": {},
+        "method": "repo.queryQueue",
+        "result": []
+    },
+    {
+        "args": [
+            2
+        ],
+        "kwargs": {},
+        "method": "repo.get",
+        "result": {
+            "begin_event": 497768,
+            "begin_ts": 1732049290.238136,
+            "create_event": 497784,
+            "create_ts": 1732569672.685269,
+            "creation_time": "2024-11-25 21:21:12.682068+00:00",
+            "creation_ts": 1732569672.682068,
+            "custom_opts": {},
+            "dist": false,
+            "end_event": null,
+            "end_ts": null,
+            "id": 2599,
+            "opts": {
+                "debuginfo": false,
+                "maven": false,
+                "separate_src": false,
+                "src": false
+            },
+            "state": 1,
+            "state_time": "2024-11-25 21:21:19.711390+00:00",
+            "state_ts": 1732569679.71139,
+            "tag_id": 2,
+            "tag_name": "f24-build",
+            "task_id": 14431,
+            "task_state": 2
+        }
+    },
+    {
+        "args": [
+            [
+                [
+                    "tag_id",
+                    "=",
+                    2
+                ]
+            ]
+        ],
+        "kwargs": {
+            "opts": {
+                "countOnly": true
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": 0
+    },
+    {
+        "args": [
+            [
+                [
+                    "task_id",
+                    "=",
+                    1
+                ]
+            ],
+            [
+                "id"
+            ]
+        ],
+        "kwargs": {},
+        "method": "repo.queryQueue",
+        "result": []
     }
-]
\ No newline at end of file
+]
diff --git a/tests/test_www/test_pages.py b/tests/test_www/test_pages.py
index f4be222..9e739f9 100644
--- a/tests/test_www/test_pages.py
+++ b/tests/test_www/test_pages.py
@@ -27,7 +27,9 @@ class TestPages(unittest.TestCase):
     def setUpClass(cls):
         # recording session used across tests in recording mode
         cls.cfile = os.path.dirname(__file__) + f'/data/pages_calls.json'
+        cls.cfile2 = os.path.dirname(__file__) + f'/data/pages_calls_updates.json'
         cls.recording = False
+        cls.updating = False
         cls.rsession = RecordingClientSession('http://localhost/kojihub', {})
 
     @classmethod
@@ -35,6 +37,8 @@ class TestPages(unittest.TestCase):
         if cls.recording:
             # save recorded calls
             cls.rsession.dump(cls.cfile)
+        elif cls.updating:
+            cls.rsession.dump(cls.cfile2)
 
     def setUp(self):
         self.environ = {
@@ -77,6 +81,8 @@ class TestPages(unittest.TestCase):
         else:
             self.server = FakeClientSession('SERVER', {})
             self.server.load(self.cfile)
+            if self.updating:
+                self.server._missing_rsession = self.rsession
         return self.server
 
     def tearDown(self):

From bf8dd0a1a89c1eb12e8932c420e59bea012b66cd Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 07 2025 18:35:49 +0000
Subject: [PATCH 5/13] fix None display in query form


---

diff --git a/www/kojiweb/templates/reporequests.html.j2 b/www/kojiweb/templates/reporequests.html.j2
index a06c691..1019766 100644
--- a/www/kojiweb/templates/reporequests.html.j2
+++ b/www/kojiweb/templates/reporequests.html.j2
@@ -22,7 +22,7 @@
           <td>
               <strong>Tag</strong>:
           </td><td>
-              <input type="text" name="tag" value="{{ tag }}"/>
+              <input type="text" name="tag" value="{{ tag if tag else '' }}"/>
           </td>
           </tr>
         </table>

From 6ef45649897d057cb8f7456d004ff1eca53e846f Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 07 2025 18:50:09 +0000
Subject: [PATCH 6/13] flake8


---

diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py
index fa2c6d5..c9ae5ff 100644
--- a/www/kojiweb/index.py
+++ b/www/kojiweb/index.py
@@ -1057,7 +1057,8 @@ def taginfo(environ, tagID, all='0', packageOrder='package_name', packageStart=N
     values['all'] = all
     values['repo'] = server.repo.get(tag['id'])
     values['external_repos'] = server.getExternalRepoList(tag['id'])
-    values['request_count'] = server.repo.queryQueue([['tag_id', '=', tag['id']]], opts={'countOnly': True})
+    values['request_count'] = server.repo.queryQueue([['tag_id', '=', tag['id']]],
+                                                     opts={'countOnly': True})
 
     child = None
     if childID is not None:
diff --git a/www/lib/kojiweb/util.py b/www/lib/kojiweb/util.py
index 6deacc7..2519f9d 100644
--- a/www/lib/kojiweb/util.py
+++ b/www/lib/kojiweb/util.py
@@ -492,8 +492,8 @@ def paginateMethod(server, values, methodName, args=None, kw=None,
         totalRows = getattr(server, methodName)(*args, **kw)
 
     kw[optsarg] = {'order': order,
-                       'offset': start,
-                       'limit': pageSize}
+                   'offset': start,
+                   'limit': pageSize}
     data = getattr(server, methodName)(*args, **kw)
     count = len(data)
 

From eb795960dd90a64d01b72962693d1359de593a6c Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 08 2025 11:28:59 +0000
Subject: [PATCH 7/13] install svg file


---

diff --git a/www/static/images/Makefile b/www/static/images/Makefile
index fd7f2ca..3a7891c 100644
--- a/www/static/images/Makefile
+++ b/www/static/images/Makefile
@@ -1,5 +1,5 @@
 SERVERDIR = /images
-FILES = $(wildcard *.gif *.png *.ico)
+FILES = $(wildcard *.gif *.png *.ico *.svg)
 
 _default:
 	@echo "nothing to make.  try make install"

From 5e622be80ce2bf6037a34bad001dc6312bde5a31 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 08 2025 12:17:38 +0000
Subject: [PATCH 8/13] fix json handling in template


---

diff --git a/www/kojiweb/templates/reporequest.html.j2 b/www/kojiweb/templates/reporequest.html.j2
index dc90c08..9430c70 100644
--- a/www/kojiweb/templates/reporequest.html.j2
+++ b/www/kojiweb/templates/reporequest.html.j2
@@ -19,7 +19,7 @@ Repo request {{ req_id }} not found.
   <tr><th>Invalid event</th><td>Unable to determine event for request</td><th></tr>
   #endif
   #if req.opts
-  <th>Options</th><td class="usertext">{{ json.dumps(req.opts, indent=4) }}</td>
+  <th>Options</th><td class="usertext">{{ req.opts | tojson(indent=4) }}</td>
   #endif
   #if req.repo_id
   <tr><th>Fulfilled by repo</th><td><a href="repoinfo?repoID={{ req.repo_id }}">{{ req.repo_id }}</a</td></tr>

From 1a2e96b1c502b8bc72a305e83e27d1859d3026c5 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 08 2025 12:17:38 +0000
Subject: [PATCH 9/13] update test_pages to cover new code


---

diff --git a/tests/test_www/data/pages_calls.json b/tests/test_www/data/pages_calls.json
index 0a53d33..911e8e0 100644
--- a/tests/test_www/data/pages_calls.json
+++ b/tests/test_www/data/pages_calls.json
@@ -56292,5 +56292,931 @@
         "kwargs": {},
         "method": "repo.queryQueue",
         "result": []
+    },
+    {
+        "args": [
+            [
+                [
+                    "active",
+                    true
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {
+            "opts": {
+                "countOnly": true
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": 7
+    },
+    {
+        "args": [
+            [
+                [
+                    "active",
+                    true
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {
+            "opts": {
+                "limit": 50,
+                "offset": 0,
+                "order": "-id"
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": [
+            {
+                "active": true,
+                "at_event": 497085,
+                "create_time": "2025-05-08 11:43:40.126768+00:00",
+                "create_ts": 1746704620.126768,
+                "id": 134,
+                "min_event": null,
+                "opts": {
+                    "src": true
+                },
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:43:40.126768+00:00",
+                "update_ts": 1746704620.126768
+            },
+            {
+                "active": true,
+                "at_event": 497085,
+                "create_time": "2025-05-08 11:43:36.529538+00:00",
+                "create_ts": 1746704616.529538,
+                "id": 133,
+                "min_event": null,
+                "opts": {
+                    "debuginfo": true
+                },
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:43:36.529538+00:00",
+                "update_ts": 1746704616.529538
+            },
+            {
+                "active": true,
+                "at_event": 497086,
+                "create_time": "2025-05-08 11:42:21.704141+00:00",
+                "create_ts": 1746704541.704141,
+                "id": 132,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:21.704141+00:00",
+                "update_ts": 1746704541.704141
+            },
+            {
+                "active": true,
+                "at_event": 497087,
+                "create_time": "2025-05-08 11:42:18.526583+00:00",
+                "create_ts": 1746704538.526583,
+                "id": 131,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:18.526583+00:00",
+                "update_ts": 1746704538.526583
+            },
+            {
+                "active": true,
+                "at_event": 497088,
+                "create_time": "2025-05-08 11:42:15.500883+00:00",
+                "create_ts": 1746704535.500883,
+                "id": 130,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:15.500883+00:00",
+                "update_ts": 1746704535.500883
+            },
+            {
+                "active": true,
+                "at_event": 497089,
+                "create_time": "2025-05-08 11:42:03.370177+00:00",
+                "create_ts": 1746704523.370177,
+                "id": 129,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:03.370177+00:00",
+                "update_ts": 1746704523.370177
+            },
+            {
+                "active": true,
+                "at_event": null,
+                "create_time": "2025-05-08 11:32:15.973760+00:00",
+                "create_ts": 1746703935.97376,
+                "id": 128,
+                "min_event": 497768,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:32:15.973760+00:00",
+                "update_ts": 1746703935.97376
+            }
+        ]
+    },
+    {
+        "args": [
+            [],
+            "**"
+        ],
+        "kwargs": {
+            "opts": {
+                "countOnly": true
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": 9
+    },
+    {
+        "args": [
+            [],
+            "**"
+        ],
+        "kwargs": {
+            "opts": {
+                "limit": 50,
+                "offset": 0,
+                "order": "tag_name"
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": [
+            {
+                "active": true,
+                "at_event": null,
+                "create_time": "2025-05-08 11:32:15.973760+00:00",
+                "create_ts": 1746703935.97376,
+                "id": 128,
+                "min_event": 497768,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:32:15.973760+00:00",
+                "update_ts": 1746703935.97376
+            },
+            {
+                "active": true,
+                "at_event": 497089,
+                "create_time": "2025-05-08 11:42:03.370177+00:00",
+                "create_ts": 1746704523.370177,
+                "id": 129,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:03.370177+00:00",
+                "update_ts": 1746704523.370177
+            },
+            {
+                "active": true,
+                "at_event": 497088,
+                "create_time": "2025-05-08 11:42:15.500883+00:00",
+                "create_ts": 1746704535.500883,
+                "id": 130,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:15.500883+00:00",
+                "update_ts": 1746704535.500883
+            },
+            {
+                "active": true,
+                "at_event": 497087,
+                "create_time": "2025-05-08 11:42:18.526583+00:00",
+                "create_ts": 1746704538.526583,
+                "id": 131,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:18.526583+00:00",
+                "update_ts": 1746704538.526583
+            },
+            {
+                "active": true,
+                "at_event": 497086,
+                "create_time": "2025-05-08 11:42:21.704141+00:00",
+                "create_ts": 1746704541.704141,
+                "id": 132,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:21.704141+00:00",
+                "update_ts": 1746704541.704141
+            },
+            {
+                "active": true,
+                "at_event": 497085,
+                "create_time": "2025-05-08 11:43:36.529538+00:00",
+                "create_ts": 1746704616.529538,
+                "id": 133,
+                "min_event": null,
+                "opts": {
+                    "debuginfo": true
+                },
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:43:36.529538+00:00",
+                "update_ts": 1746704616.529538
+            },
+            {
+                "active": true,
+                "at_event": 497085,
+                "create_time": "2025-05-08 11:43:40.126768+00:00",
+                "create_ts": 1746704620.126768,
+                "id": 134,
+                "min_event": null,
+                "opts": {
+                    "src": true
+                },
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:43:40.126768+00:00",
+                "update_ts": 1746704620.126768
+            },
+            {
+                "active": false,
+                "at_event": null,
+                "create_time": "2025-01-17 22:04:43.253576+00:00",
+                "create_ts": 1737151483.253576,
+                "id": 126,
+                "min_event": 497788,
+                "opts": {
+                    "debuginfo": true
+                },
+                "owner": 4,
+                "owner_name": "builder-01",
+                "priority": 20,
+                "repo_id": 2602,
+                "tag_id": 2099,
+                "tag_name": "kpatch-kernel-fake-1.1-5-build",
+                "task_id": 14444,
+                "task_state": 2,
+                "tries": 1,
+                "update_time": "2025-01-17 22:21:48.169150+00:00",
+                "update_ts": 1737152508.16915
+            },
+            {
+                "active": false,
+                "at_event": null,
+                "create_time": "2025-01-17 22:21:15.725044+00:00",
+                "create_ts": 1737152475.725044,
+                "id": 127,
+                "min_event": 497788,
+                "opts": {},
+                "owner": 4,
+                "owner_name": "builder-01",
+                "priority": 20,
+                "repo_id": 2602,
+                "tag_id": 2099,
+                "tag_name": "kpatch-kernel-fake-1.1-5-build",
+                "task_id": 14445,
+                "task_state": 2,
+                "tries": 1,
+                "update_time": "2025-01-17 22:21:48.169150+00:00",
+                "update_ts": 1737152508.16915
+            }
+        ]
+    },
+    {
+        "args": [
+            [
+                [
+                    "active",
+                    false
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {
+            "opts": {
+                "countOnly": true
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": 2
+    },
+    {
+        "args": [
+            [
+                [
+                    "active",
+                    false
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {
+            "opts": {
+                "limit": 50,
+                "offset": 0,
+                "order": "-id"
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": [
+            {
+                "active": false,
+                "at_event": null,
+                "create_time": "2025-01-17 22:21:15.725044+00:00",
+                "create_ts": 1737152475.725044,
+                "id": 127,
+                "min_event": 497788,
+                "opts": {},
+                "owner": 4,
+                "owner_name": "builder-01",
+                "priority": 20,
+                "repo_id": 2602,
+                "tag_id": 2099,
+                "tag_name": "kpatch-kernel-fake-1.1-5-build",
+                "task_id": 14445,
+                "task_state": 2,
+                "tries": 1,
+                "update_time": "2025-01-17 22:21:48.169150+00:00",
+                "update_ts": 1737152508.16915
+            },
+            {
+                "active": false,
+                "at_event": null,
+                "create_time": "2025-01-17 22:04:43.253576+00:00",
+                "create_ts": 1737151483.253576,
+                "id": 126,
+                "min_event": 497788,
+                "opts": {
+                    "debuginfo": true
+                },
+                "owner": 4,
+                "owner_name": "builder-01",
+                "priority": 20,
+                "repo_id": 2602,
+                "tag_id": 2099,
+                "tag_name": "kpatch-kernel-fake-1.1-5-build",
+                "task_id": 14444,
+                "task_state": 2,
+                "tries": 1,
+                "update_time": "2025-01-17 22:21:48.169150+00:00",
+                "update_ts": 1737152508.16915
+            }
+        ]
+    },
+    {
+        "args": [
+            1
+        ],
+        "kwargs": {
+            "event": "auto",
+            "strict": true
+        },
+        "method": "getTag",
+        "result": {
+            "arches": "x86_64",
+            "extra": {
+                "bar": 101,
+                "foo": 1,
+                "fuzz": 3
+            },
+            "id": 1,
+            "locked": false,
+            "maven_include_all": false,
+            "maven_support": false,
+            "name": "f24",
+            "perm": null,
+            "perm_id": null
+        }
+    },
+    {
+        "args": [
+            [
+                [
+                    "tag_id",
+                    1
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {
+            "opts": {
+                "countOnly": true
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": 7
+    },
+    {
+        "args": [
+            [
+                [
+                    "tag_id",
+                    1
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {
+            "opts": {
+                "limit": 50,
+                "offset": 0,
+                "order": "-id"
+            }
+        },
+        "method": "repo.queryQueue",
+        "result": [
+            {
+                "active": true,
+                "at_event": 497085,
+                "create_time": "2025-05-08 11:43:40.126768+00:00",
+                "create_ts": 1746704620.126768,
+                "id": 134,
+                "min_event": null,
+                "opts": {
+                    "src": true
+                },
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:43:40.126768+00:00",
+                "update_ts": 1746704620.126768
+            },
+            {
+                "active": true,
+                "at_event": 497085,
+                "create_time": "2025-05-08 11:43:36.529538+00:00",
+                "create_ts": 1746704616.529538,
+                "id": 133,
+                "min_event": null,
+                "opts": {
+                    "debuginfo": true
+                },
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:43:36.529538+00:00",
+                "update_ts": 1746704616.529538
+            },
+            {
+                "active": true,
+                "at_event": 497086,
+                "create_time": "2025-05-08 11:42:21.704141+00:00",
+                "create_ts": 1746704541.704141,
+                "id": 132,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:21.704141+00:00",
+                "update_ts": 1746704541.704141
+            },
+            {
+                "active": true,
+                "at_event": 497087,
+                "create_time": "2025-05-08 11:42:18.526583+00:00",
+                "create_ts": 1746704538.526583,
+                "id": 131,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:18.526583+00:00",
+                "update_ts": 1746704538.526583
+            },
+            {
+                "active": true,
+                "at_event": 497088,
+                "create_time": "2025-05-08 11:42:15.500883+00:00",
+                "create_ts": 1746704535.500883,
+                "id": 130,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:15.500883+00:00",
+                "update_ts": 1746704535.500883
+            },
+            {
+                "active": true,
+                "at_event": 497089,
+                "create_time": "2025-05-08 11:42:03.370177+00:00",
+                "create_ts": 1746704523.370177,
+                "id": 129,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:03.370177+00:00",
+                "update_ts": 1746704523.370177
+            },
+            {
+                "active": true,
+                "at_event": null,
+                "create_time": "2025-05-08 11:32:15.973760+00:00",
+                "create_ts": 1746703935.97376,
+                "id": 128,
+                "min_event": 497768,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:32:15.973760+00:00",
+                "update_ts": 1746703935.97376
+            }
+        ]
+    },
+    {
+        "args": [
+            [
+                [
+                    "id",
+                    "=",
+                    127
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {},
+        "method": "repo.queryQueue",
+        "result": [
+            {
+                "active": false,
+                "at_event": null,
+                "create_time": "2025-01-17 22:21:15.725044+00:00",
+                "create_ts": 1737152475.725044,
+                "id": 127,
+                "min_event": 497788,
+                "opts": {},
+                "owner": 4,
+                "owner_name": "builder-01",
+                "priority": 20,
+                "repo_id": 2602,
+                "tag_id": 2099,
+                "tag_name": "kpatch-kernel-fake-1.1-5-build",
+                "task_id": 14445,
+                "task_state": 2,
+                "tries": 1,
+                "update_time": "2025-01-17 22:21:48.169150+00:00",
+                "update_ts": 1737152508.16915
+            }
+        ]
+    },
+    {
+        "args": [
+            497788
+        ],
+        "kwargs": {},
+        "method": "getEvent",
+        "result": {
+            "id": 497788,
+            "ts": 1737145925.263978
+        }
+    },
+    {
+        "args": [
+            [
+                [
+                    "id",
+                    "=",
+                    128
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {},
+        "method": "repo.queryQueue",
+        "result": [
+            {
+                "active": true,
+                "at_event": null,
+                "create_time": "2025-05-08 11:32:15.973760+00:00",
+                "create_ts": 1746703935.97376,
+                "id": 128,
+                "min_event": 497768,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:32:15.973760+00:00",
+                "update_ts": 1746703935.97376
+            }
+        ]
+    },
+    {
+        "args": [
+            497768
+        ],
+        "kwargs": {},
+        "method": "getEvent",
+        "result": {
+            "id": 497768,
+            "ts": 1732049290.238136
+        }
+    },
+    {
+        "args": [
+            [
+                [
+                    "id",
+                    "=",
+                    132
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {},
+        "method": "repo.queryQueue",
+        "result": [
+            {
+                "active": true,
+                "at_event": 497086,
+                "create_time": "2025-05-08 11:42:21.704141+00:00",
+                "create_ts": 1746704541.704141,
+                "id": 132,
+                "min_event": null,
+                "opts": {},
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:42:21.704141+00:00",
+                "update_ts": 1746704541.704141
+            }
+        ]
+    },
+    {
+        "args": [
+            497086
+        ],
+        "kwargs": {},
+        "method": "getEvent",
+        "result": {
+            "id": 497086,
+            "ts": 1706727187.174471
+        }
+    },
+    {
+        "args": [
+            [
+                [
+                    "id",
+                    "=",
+                    133
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {},
+        "method": "repo.queryQueue",
+        "result": [
+            {
+                "active": true,
+                "at_event": 497085,
+                "create_time": "2025-05-08 11:43:36.529538+00:00",
+                "create_ts": 1746704616.529538,
+                "id": 133,
+                "min_event": null,
+                "opts": {
+                    "debuginfo": true
+                },
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:43:36.529538+00:00",
+                "update_ts": 1746704616.529538
+            }
+        ]
+    },
+    {
+        "args": [
+            497085
+        ],
+        "kwargs": {},
+        "method": "getEvent",
+        "result": {
+            "id": 497085,
+            "ts": 1706727156.407864
+        }
+    },
+    {
+        "args": [
+            [
+                [
+                    "id",
+                    "=",
+                    134
+                ]
+            ],
+            "**"
+        ],
+        "kwargs": {},
+        "method": "repo.queryQueue",
+        "result": [
+            {
+                "active": true,
+                "at_event": 497085,
+                "create_time": "2025-05-08 11:43:40.126768+00:00",
+                "create_ts": 1746704620.126768,
+                "id": 134,
+                "min_event": null,
+                "opts": {
+                    "src": true
+                },
+                "owner": 1,
+                "owner_name": "mikem",
+                "priority": 20,
+                "repo_id": null,
+                "tag_id": 1,
+                "tag_name": "f24",
+                "task_id": null,
+                "task_state": null,
+                "tries": 0,
+                "update_time": "2025-05-08 11:43:40.126768+00:00",
+                "update_ts": 1746704620.126768
+            }
+        ]
+    },
+    {
+        "args": [
+            497085
+        ],
+        "kwargs": {},
+        "method": "getEvent",
+        "result": {
+            "id": 497085,
+            "ts": 1706727156.407864
+        }
     }
 ]
diff --git a/tests/test_www/test_pages.py b/tests/test_www/test_pages.py
index 9e739f9..3ff9f1d 100644
--- a/tests/test_www/test_pages.py
+++ b/tests/test_www/test_pages.py
@@ -172,6 +172,15 @@ class TestPages(unittest.TestCase):
         ['buildroots', ''],
         ['buildroots', 'start=50&order=id'],
         #['builds', 'start=50&order=id'],
+        ['reporequests', ''],
+        ['reporequests', 'active=all&order=tag_name'],
+        ['reporequests', 'active=false&order=-id'],
+        ['reporequests', 'tag=1&active=all'],
+        ['reporequest', 'reqID=127'],
+        ['reporequest', 'reqID=128'],
+        ['reporequest', 'reqID=132'],
+        ['reporequest', 'reqID=133'],
+        ['reporequest', 'reqID=134'],
     ]
 
     def prep_handler(self, method, query):

From ba29013abf0fc7dc0242063ba4832a0217ab2aab Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 08 2025 15:12:30 +0000
Subject: [PATCH 10/13] test_pages updates


This fixes issues with test_pages that I found while checking
coverage. The environ dict was recycled across each call in the loop,
but those calls can modify it, leaking data from one pass to another.
Also, the mocking for _getServer was not placing the session in the
environ, which was causing the tests to miss some cases.

---

diff --git a/tests/test_www/data/pages_calls.json b/tests/test_www/data/pages_calls.json
index 911e8e0..49268e1 100644
--- a/tests/test_www/data/pages_calls.json
+++ b/tests/test_www/data/pages_calls.json
@@ -51058,6 +51058,12 @@
         "result": true
     },
     {
+        "args": [],
+        "kwargs": {},
+        "method": "winEnabled",
+        "result": true
+    },
+    {
         "args": [
             2
         ],
diff --git a/tests/test_www/test_pages.py b/tests/test_www/test_pages.py
index 3ff9f1d..612c8ba 100644
--- a/tests/test_www/test_pages.py
+++ b/tests/test_www/test_pages.py
@@ -70,6 +70,8 @@ class TestPages(unittest.TestCase):
         self.time.return_value = 1735707600.0
 
         def __get_server(env):
+            # this is replacing the call to _getServer
+            env['koji.session'] = self.server
             return self.server
 
         self.get_server.side_effect = __get_server
@@ -186,12 +188,13 @@ class TestPages(unittest.TestCase):
     def prep_handler(self, method, query):
         """Takes method name and query string, returns handler and data"""
         # based loosely on publisher prep_handler
-        self.environ['QUERY_STRING'] = query
-        self.environ['koji.method'] = method
-        self.environ['SCRIPT_NAME'] = method
+        environ = self.environ.copy()
+        environ['QUERY_STRING'] = query
+        environ['koji.method'] = method
+        environ['SCRIPT_NAME'] = method
         handler = getattr(webidx, method)
-        fs = FieldStorageCompat(self.environ)
-        self.environ['koji.form'] = fs
+        fs = FieldStorageCompat(environ)
+        environ['koji.form'] = fs
         # even though we have curated urls, we need to filter args for some cases, e.g. search
         args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, ann = \
             inspect.getfullargspec(handler)
@@ -199,14 +202,14 @@ class TestPages(unittest.TestCase):
             data = dslice(fs.data, args, strict=False)
         else:
             data = fs.data.copy()
-        return handler, data
+        return handler, data, environ
 
     def test_web_handlers(self):
         """Test a bunch of web handlers"""
         for method, query in self.CALLS:
-            handler, data = self.prep_handler(method, query)
+            handler, data, environ = self.prep_handler(method, query)
 
-            result = handler(self.environ, **data)
+            result = handler(environ, **data)
 
             # result should be a string containing the rendered template
             self.assertIsInstance(result, str)

From 9c5c6be678d51adf4ac41516801a3cda3cf31643 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 29 2025 14:15:44 +0000
Subject: [PATCH 11/13] fix missing tr element


---

diff --git a/www/kojiweb/templates/reporequests.html.j2 b/www/kojiweb/templates/reporequests.html.j2
index 1019766..decf27f 100644
--- a/www/kojiweb/templates/reporequests.html.j2
+++ b/www/kojiweb/templates/reporequests.html.j2
@@ -7,6 +7,7 @@
 
   <h4>{{ desc }}</h4>
   <table class="data-list">
+    <tr>
       <td colspan="6">
         <form action="">
         <table class="nested">

From 8554932e3e87d8594051ee553cbcdc3987be71ef Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 29 2025 15:02:32 +0000
Subject: [PATCH 12/13] fix malformed html element


---

diff --git a/www/kojiweb/templates/reporequest.html.j2 b/www/kojiweb/templates/reporequest.html.j2
index 9430c70..6eeaebc 100644
--- a/www/kojiweb/templates/reporequest.html.j2
+++ b/www/kojiweb/templates/reporequest.html.j2
@@ -22,7 +22,7 @@ Repo request {{ req_id }} not found.
   <th>Options</th><td class="usertext">{{ req.opts | tojson(indent=4) }}</td>
   #endif
   #if req.repo_id
-  <tr><th>Fulfilled by repo</th><td><a href="repoinfo?repoID={{ req.repo_id }}">{{ req.repo_id }}</a</td></tr>
+  <tr><th>Fulfilled by repo</th><td><a href="repoinfo?repoID={{ req.repo_id }}">{{ req.repo_id }}</a></td></tr>
   #endif
   #if req.task_id
   <tr><th>Task ID</th><td><a href="taskinfo?taskID={{ req.task_id }}">{{ req.task_id }}</a> ({{ util.taskState(req.task_state) }})</td></tr>

From c5ed63e4e84fab74aa3c343bba4920586356c285 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: May 30 2025 14:36:09 +0000
Subject: [PATCH 13/13] html fixes


---

diff --git a/www/kojiweb/templates/reporequest.html.j2 b/www/kojiweb/templates/reporequest.html.j2
index 6eeaebc..19818b0 100644
--- a/www/kojiweb/templates/reporequest.html.j2
+++ b/www/kojiweb/templates/reporequest.html.j2
@@ -7,16 +7,16 @@
 Repo request {{ req_id }} not found.
 #else
 <table>
-  <tr><th>ID</th><td>{{ req.id }}</td><th></tr>
-  <tr><th>Active</th><td>{{ req.active }}</td><th></tr>
-  <tr><th>Priority</th><td>{{ req.priority }}</td><th></tr>
+  <tr><th>ID</th><td>{{ req.id }}</td></tr>
+  <tr><th>Active</th><td>{{ req.active }}</td></tr>
+  <tr><th>Priority</th><td>{{ req.priority }}</td></tr>
   <tr><th>Tag</th><td><a href="taginfo?tagID={{ req.tag_id }}">{{ req.tag_name }}</a></td></tr>
   #if req.at_event
-  <tr><th>At specific event</th><td>{{ at_event.id }} ({{ util.formatTimeLong(at_event.ts) }})</td><th></tr>
+  <tr><th>At specific event</th><td>{{ at_event.id }} ({{ util.formatTimeLong(at_event.ts) }})</td></tr>
   #elif req.min_event
-  <tr><th>Minimum event</th><td>{{ min_event.id }} ({{ util.formatTimeLong(min_event.ts) }})</td><th></tr>
+  <tr><th>Minimum event</th><td>{{ min_event.id }} ({{ util.formatTimeLong(min_event.ts) }})</td></tr>
   #else
-  <tr><th>Invalid event</th><td>Unable to determine event for request</td><th></tr>
+  <tr><th>Invalid event</th><td>Unable to determine event for request</td></tr>
   #endif
   #if req.opts
   <th>Options</th><td class="usertext">{{ req.opts | tojson(indent=4) }}</td>
diff --git a/www/kojiweb/templates/reporequests.html.j2 b/www/kojiweb/templates/reporequests.html.j2
index decf27f..c97f9f0 100644
--- a/www/kojiweb/templates/reporequests.html.j2
+++ b/www/kojiweb/templates/reporequests.html.j2
@@ -69,7 +69,7 @@
           <td>
             <a href="taginfo?tagID={{ req.tag_id }}">{{ req.tag_name }}</a>
             #if not tag
-            <a href="reporequests?tag={{ req.tag_id }}{{ P('active', 'order') }}" title="Filter by tag"><img src="{{ util.themePath('images/funnel.svg') }}"></a>
+            <a href="reporequests?tag={{ req.tag_id }}{{ P('active', 'order') }}" title="Filter by tag"><img src="{{ util.themePath('images/funnel.svg') }}" alt="^" /></a>
             #endif
           </td>
           #if req.task_id

