From 59d645a75e83dbbc04833aeecfa8c31e163303f0 Mon Sep 17 00:00:00 2001
From: Jared Hancock <gravydish@gmail.com>
Date: Sat, 28 Apr 2018 21:22:16 -0500
Subject: [PATCH] queue: Improve queries necessary for rendering

This removes a significant number of queries used to render the drop-down
menu for the queues.

Each queue displayed on the page previously required a database query to
determine its children. This patch changes the strategy to fetch all the
queues and organize them as a tree. The tree can then be walked as the menu
is rendered and does not require any further queries.

On my test system, it reduces the number of queries for the ticket listing
page from 56 to 46.
---
 include/class.queue.php                       | 38 +++++++++++++++++++
 .../staff/templates/queue-navigation.tmpl.php | 18 ++++++++-
 .../templates/queue-subnavigation.tmpl.php    | 23 ++++++-----
 scp/tickets.php                               | 17 +++------
 4 files changed, 74 insertions(+), 22 deletions(-)

diff --git a/include/class.queue.php b/include/class.queue.php
index cea1404ed..dfa384642 100644
--- a/include/class.queue.php
+++ b/include/class.queue.php
@@ -1265,6 +1265,44 @@ class CustomQueue extends VerySimpleModel {
             && $this->sorts->saveAll();
     }
 
+    /**
+     * Fetch a tree-organized listing of the queues. Each queue is listed in
+     * the tree exactly once, and every visible queue is represented. The
+     * returned structure is an array where the items are two-item arrays
+     * where the first item is a CustomQueue object an the second is a list
+     * of the children using the same pattern (two-item arrays of a CustomQueue
+     * and its children). Visually:
+     *
+     * [ [ $queue, [ [ $child, [] ], [ $child, [] ] ], [ $queue, ... ] ]
+     *
+     * Parameters:
+     * $staff - <Staff> staff object which should be used to determine
+     *      visible queues.
+     * $pid - <int> parent_id of root queue. Default is zero (top-level)
+     */
+    static function getHierarchicalQueues(Staff $staff, $pid=0) {
+        $all = static::objects()
+            ->filter(Q::any(array(
+                'flags__hasbit' => self::FLAG_PUBLIC,
+                'flags__hasbit' => static::FLAG_QUEUE,
+                'staff_id' => $staff->getId(),
+            )))
+            ->exclude(['flags__hasbit' => self::FLAG_DISABLED])
+            ->asArray();
+
+        // Find all the queues with a given parent
+        $for_parent = function($pid) use ($all, &$for_parent) {
+            $results = [];
+            foreach (new \ArrayIterator($all) as $q) {
+                if ($q->parent_id == $pid)
+                    $results[] = [ $q, $for_parent($q->getId()) ];
+            }
+            return $results;
+        };
+
+        return $for_parent($pid);
+    }
+
     static function getOrmPath($name, $query=null) {
         // Special case for custom data `__answers!id__value`. Only add the
         // join and constraint on the query the first pass, when the query
diff --git a/include/staff/templates/queue-navigation.tmpl.php b/include/staff/templates/queue-navigation.tmpl.php
index b903f29fd..3e40918fd 100644
--- a/include/staff/templates/queue-navigation.tmpl.php
+++ b/include/staff/templates/queue-navigation.tmpl.php
@@ -4,6 +4,7 @@
 // $q - <CustomQueue> object for this navigation entry
 // $selected - <bool> true if this queue is currently active
 // $child_selected - <bool> true if the selected queue is a descendent
+$childs = $children;
 $this_queue = $q;
 $selected = (!isset($_REQUEST['a'])  && $_REQUEST['queue'] == $this_queue->getId());
 ?>
@@ -28,8 +29,21 @@ $selected = (!isset($_REQUEST['a'])  && $_REQUEST['queue'] == $this_queue->getId
       </li>
 
       <!-- Start Dropdown and child queues -->
-      <?php foreach ($this_queue->getPublicChildren() as $q) {
-          include 'queue-subnavigation.tmpl.php';
+      <?php foreach ($childs as $_) {
+          list($q, $children) = $_;
+          if (!$q->isPrivate())
+              include 'queue-subnavigation.tmpl.php';
+      }
+      $first_child = true;
+      foreach ($childs as $_) {
+        list($q, $children) = $_;
+        if (!$q->isPrivate())
+            continue;
+        if ($first_child) {
+            $first_child = false;
+            echo '<li class="personalQ"></li>';
+        }
+        include 'queue-subnavigation.tmpl.php';
       } ?>
       <!-- Personal Queues -->
       <?php
diff --git a/include/staff/templates/queue-subnavigation.tmpl.php b/include/staff/templates/queue-subnavigation.tmpl.php
index b0cbeeb75..fa33b7b72 100644
--- a/include/staff/templates/queue-subnavigation.tmpl.php
+++ b/include/staff/templates/queue-subnavigation.tmpl.php
@@ -1,10 +1,9 @@
 <?php
 // Calling conventions
 // $q - <CustomQueue> object for this navigation entry
+// $children - <Array<CustomQueue>> all direct children of this queue
 $queue = $q;
-$children = !$queue instanceof SavedSearch ? $queue->getPublicChildren() : array();
-$subq_searches = !$queue instanceof SavedSearch ? $queue->getMyChildren() : array();
-$hasChildren = count($children) + count($subq_searches) > 0;
+$hasChildren = count($children) > 0;
 $selected = $_REQUEST['queue'] == $q->getId();
 global $thisstaff;
 ?>
@@ -27,24 +26,30 @@ global $thisstaff;
     </a>
 
     <?php
-    $closure_include = function($q) use ($thisstaff, $ost, $cfg) {
+    $closure_include = function($q, $children) {
         global $thisstaff, $ost, $cfg;
         include __FILE__;
     };
     if ($hasChildren) { ?>
     <ul class="subMenuQ">
     <?php
-    foreach ($children as $q)
-        $closure_include($q);
+    foreach ($children as $_) {
+        list($q, $childs) = $_;
+        if (!$q->isPrivate())
+          $closure_include($q, $childs);
+    }
 
     // Include personal sub-queues
     $first_child = true;
-    foreach ($subq_searches as $q) {
-      if ($first_child) {
+    foreach ($children as $_) {
+      list($q, $childs) = $_;
+      if ($q->isPrivate()) {
+        if ($first_child) {
           $first_child = false;
           echo '<li class="personalQ"></li>';
+        }
+        $closure_include($q, $childs);
       }
-      $closure_include($q);
     } ?>
     </ul>
 <?php
diff --git a/scp/tickets.php b/scp/tickets.php
index 450403f99..82e9b0703 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -445,19 +445,14 @@ $nav->setTabActive('tickets');
 $nav->addSubNavInfo('jb-overflowmenu', 'customQ_nav');
 
 // Fetch ticket queues organized by root and sub-queues
-$queues = SavedQueue::queues()
-    ->filter(Q::any(array(
-        'flags__hasbit' => CustomQueue::FLAG_PUBLIC,
-        'staff_id' => $thisstaff->getId(),
-    )))
-    ->exclude(['flags__hasbit' => CustomQueue::FLAG_DISABLED])
-    ->getIterator();
+$queues = CustomQueue::getHierarchicalQueues($thisstaff);
 
 // Start with all the top-level (container) queues
-foreach ($queues->findAll(array('parent_id' => 0))
-as $q) {
-    $nav->addSubMenu(function() use ($q, $queue) {
-        $selected = false;
+foreach ($queues as $_) {
+    list($q, $children) = $_;
+    if ($q->isPrivate())
+        continue;
+    $nav->addSubMenu(function() use ($q, $queue, $children) {
         // A queue is selected if it is the one being displayed. It is
         // "child" selected if its ID is in the path of the one selected
         $child_selected = $queue
-- 
GitLab