You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kibble.apache.org by hu...@apache.org on 2018/03/02 15:37:25 UTC

[kibble] branch master updated (bf9fbb3 -> 3a747d9)

This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git.


    from bf9fbb3  Note that we now require ES 6.x for new installations
     new fefdee8  fix creator column type
     new 810f3d2  Initial stab at forum statistics
     new 67e6f86  regen OpenAPI yaml
     new e38233d  add widget yaml for forums
     new 02a7fec  make a forum explorer widget
     new a173801  have forum explorer reload pages on filter changes
     new e27377d  URLs may sometimes have double slashes in them
     new 842cd4e  add link to forum stats
     new 3a747d9  regen JS

The 9 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 api/pages/{issue => forum}/actors.py               |  35 ++-
 api/pages/{issue/openers.py => forum/creators.py}  |  16 +-
 api/pages/{issue => forum}/issues.py               |  38 +--
 .../{issue/closers.py => forum/responders.py}      |  18 +-
 api/pages/{issue => forum}/top-count.py            |  12 +-
 api/pages/{issue => forum}/top.py                  |  15 +-
 api/pages/{issue => forum}/trends.py               |  72 +++---
 api/yaml/openapi.yaml                              | 281 +++++++++++++++++++++
 api/yaml/widgets.yaml                              |  81 +++++-
 setup/mappings.json                                |   2 +-
 ui/dashboard.html                                  |   2 +-
 ui/js/coffee/explorer.coffee                       |  57 +++++
 ui/js/coffee/pageloader.coffee                     |   1 +
 ui/js/coffee/widget.coffee                         |   1 +
 ui/js/coffee/widget_top5.coffee                    |   2 +
 ui/js/kibble.v1.js                                 |  98 ++++++-
 16 files changed, 619 insertions(+), 112 deletions(-)
 copy api/pages/{issue => forum}/actors.py (88%)
 copy api/pages/{issue/openers.py => forum/creators.py} (93%)
 copy api/pages/{issue => forum}/issues.py (87%)
 copy api/pages/{issue/closers.py => forum/responders.py} (92%)
 copy api/pages/{issue => forum}/top-count.py (93%)
 copy api/pages/{issue => forum}/top.py (91%)
 copy api/pages/{issue => forum}/trends.py (84%)

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.

[kibble] 09/09: regen JS

Posted by hu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git

commit 3a747d9ac3fdaa56618efe65c70faadc07b79a32
Author: Daniel Gruno <hu...@apache.org>
AuthorDate: Fri Mar 2 16:36:57 2018 +0100

    regen JS
---
 ui/js/kibble.v1.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 97 insertions(+), 1 deletion(-)

diff --git a/ui/js/kibble.v1.js b/ui/js/kibble.v1.js
index ee90131..35fec4c 100644
--- a/ui/js/kibble.v1.js
+++ b/ui/js/kibble.v1.js
@@ -1,5 +1,5 @@
 // Generated by CoffeeScript 1.10.0
-var API, APIVERSION, Chart, HTML, Row, Widget, aSourceTypes, accountCallback, addSourceType, addSources, addorguser, addsources, affiliate, affiliation, affiliationWizard, altemail, app, badModal, bio, chartOnclick, chartToSvg, chartWrapperButtons, charts_donutchart, charts_gaugechart, charts_linechart, charts_linechart_stacked, charts_linked, charts_radarchart, ciexplorer, cog, comShow, comstat, copyCSS, currentSources, dataTable, datepicker, datepickers, defaultOrgChanged, deletesource [...]
+var API, APIVERSION, Chart, HTML, Row, Widget, aSourceTypes, accountCallback, addSourceType, addSources, addorguser, addsources, affiliate, affiliation, affiliationWizard, altemail, app, badModal, bio, chartOnclick, chartToSvg, chartWrapperButtons, charts_donutchart, charts_gaugechart, charts_linechart, charts_linechart_stacked, charts_linked, charts_radarchart, ciexplorer, cog, comShow, comstat, copyCSS, currentSources, dataTable, datepicker, datepickers, defaultOrgChanged, deletesource [...]
   indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
 
 signup = function(form) {
@@ -2092,6 +2092,94 @@ issueexplorer = function(json, state) {
   });
 };
 
+forumexplorer = function(json, state) {
+  var ID, ezURL, h, item, len, len1, list, m, n, opt, org, q, ref, ref1, ref2, ref3, slen, u;
+  org = json.organisation;
+  if (json.tag) {
+    org.name += " (Filter: " + json.tag + ")";
+  }
+  h = document.createElement('h4');
+  h.appendChild(document.createTextNode("Exploring " + org.name + ":"));
+  state.widget.inject(h, true);
+  list = document.createElement('select');
+  state.widget.inject(list);
+  opt = document.createElement('option');
+  opt.value = "";
+  slen = 0;
+  ref = json.sources;
+  for (q = 0, len = ref.length; q < len; q++) {
+    item = ref[q];
+    if ((ref1 = item.type) === 'forum' || ref1 === 'discourse' || ref1 === 'askbot') {
+      slen++;
+    }
+  }
+  opt.text = "All " + slen + " forum(s)";
+  list.appendChild(opt);
+  json.sources.sort(function(a, b) {
+    if (a.sourceURL === b.sourceURL) {
+      return 0;
+    } else {
+      if (a.sourceURL > b.sourceURL) {
+        return 1;
+      } else {
+        return -1;
+      }
+    }
+  });
+  ref2 = json.sources;
+  for (u = 0, len1 = ref2.length; u < len1; u++) {
+    item = ref2[u];
+    if ((ref3 = item.type) === 'forum' || ref3 === 'discourse' || ref3 === 'askbot') {
+      opt = document.createElement('option');
+      opt.value = item.sourceID;
+      ezURL = null;
+      n = item.sourceURL.match(/^([a-z]+:\/\/.+?)\/([-.A-Z0-9]+)$/i);
+      m = item.sourceURL.match(/^([a-z]+:\/\/.+?)\s(.+)$/i);
+      if (n && n.length === 3) {
+        ezURL = n[2] + " - (" + n[1] + ")";
+      } else if (m && m.length === 3) {
+        ezURL = m[2] + " - (" + m[1] + ")";
+      }
+      opt.text = ezURL ? ezURL : item.sourceURL;
+      if (globArgs.source && globArgs.source === item.sourceID) {
+        opt.selected = 'selected';
+      }
+      list.appendChild(opt);
+    }
+  }
+  ID = Math.floor(Math.random() * 987654321).toString(16);
+  list.setAttribute('id', ID);
+  return $("#" + ID).chosen().change(function() {
+    var source;
+    source = this.value;
+    if (source === "") {
+      source = null;
+    }
+    globArgs.source = source;
+    updateWidgets('donut', null, {
+      source: source
+    });
+    updateWidgets('gauge', null, {
+      source: source
+    });
+    updateWidgets('line', null, {
+      source: source
+    });
+    updateWidgets('contacts', null, {
+      source: source
+    });
+    updateWidgets('top5', null, {
+      source: source
+    });
+    updateWidgets('factors', null, {
+      source: source
+    });
+    return updateWidgets('trends', null, {
+      source: source
+    });
+  });
+};
+
 imexplorer = function(json, state) {
   var ID, ezURL, h, item, len, len1, list, m, n, opt, org, q, ref, ref1, ref2, ref3, slen, u;
   org = json.organisation;
@@ -2364,6 +2452,7 @@ subFilter = function() {
   updateWidgets('sourcepicker', null, x);
   updateWidgets('repopicker', null, x);
   updateWidgets('issuepicker', null, x);
+  updateWidgets('forumpicker', null, x);
   updateWidgets('mailpicker', null, x);
   updateWidgets('logpicker', null, x);
   updateWidgets('donut', null, x);
@@ -3690,6 +3779,9 @@ setupPage = function(json, state) {
           case 'issuepicker':
             results1.push(widget.load(issueexplorer));
             break;
+          case 'forumpicker':
+            results1.push(widget.load(forumexplorer));
+            break;
           case 'viewpicker':
             results1.push(widget.load(viewexplorer));
             break;
@@ -5146,6 +5238,9 @@ updateWidgets = function(type, target, eargs) {
         case 'issuepicker':
           results.push(widget.load(issueexplorer));
           break;
+        case 'forumpicker':
+          results.push(widget.load(forumexplorer));
+          break;
         case 'viewpicker':
           results.push(widget.load(viewexplorer));
           break;
@@ -7063,6 +7158,7 @@ make5 = function(obj, json, pos) {
         }
         item.name += ": " + item.title;
       }
+      item.url = item.url.replace(/([^:])(\/\/+)/g, '$1/');
       title = new HTML('a', {
         title: item.tooltip,
         "class": "title",

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.

[kibble] 07/09: URLs may sometimes have double slashes in them

Posted by hu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git

commit e27377d6fc8f1f4c9b7605316844c53f170f9ff5
Author: Daniel Gruno <hu...@apache.org>
AuthorDate: Fri Mar 2 16:36:42 2018 +0100

    URLs may sometimes have double slashes in them
---
 ui/js/coffee/widget_top5.coffee | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/ui/js/coffee/widget_top5.coffee b/ui/js/coffee/widget_top5.coffee
index 7cd2727..7cddea0 100644
--- a/ui/js/coffee/widget_top5.coffee
+++ b/ui/js/coffee/widget_top5.coffee
@@ -39,6 +39,8 @@ make5 = (obj, json, pos) ->
                 if item.title.length > 40
                     item.title = item.title.toString().substring(0,40) + "..."
                 item.name += ": " + item.title
+            # Sometimes, scanners add a spurious extra slash. nix it.
+            item.url = item.url.replace(/([^:])(\/\/+)/g, '$1/')
             title = new HTML('a', { title: item.tooltip, class: "title", href:item.url}, txt(item.name))
             rightInner.inject(title)
         else

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.

[kibble] 08/09: add link to forum stats

Posted by hu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git

commit 842cd4ea39068df6b420898bb2833bf495bd8bb6
Author: Daniel Gruno <hu...@apache.org>
AuthorDate: Fri Mar 2 16:36:51 2018 +0100

    add link to forum stats
---
 ui/dashboard.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ui/dashboard.html b/ui/dashboard.html
index 74bb880..f93cedb 100644
--- a/ui/dashboard.html
+++ b/ui/dashboard.html
@@ -61,7 +61,7 @@
         <a href="dashboard.html?page=issues"><li>Issue Trackers</li></a>
         <a href="dashboard.html?page=mail"><li>Mailing Lists</li></a>
         <a href="dashboard.html?page=ci"><li>Continuous Integration</li></a>
-        <a href="dashboard.html?page=repos"><li>Forums</li></a>
+        <a href="dashboard.html?page=forums"><li>Forums</li></a>
       </ul>
       </div>
     </nav>

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.

[kibble] 01/09: fix creator column type

Posted by hu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git

commit fefdee8b1253daaafcc13bf01e1d7eea8ce1b571
Author: Daniel Gruno <hu...@apache.org>
AuthorDate: Fri Mar 2 16:35:04 2018 +0100

    fix creator column type
---
 setup/mappings.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup/mappings.json b/setup/mappings.json
index 95fdb1c..c7d56df 100644
--- a/setup/mappings.json
+++ b/setup/mappings.json
@@ -57,7 +57,7 @@
             "type": "double"
           },
           "creator": {
-            "type": "double"
+            "type": "keyword"
           },
           "createdDate": {
             "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis",

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.

[kibble] 02/09: Initial stab at forum statistics

Posted by hu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git

commit 810f3d2484f40af146c4bd4043de1ea886bef589
Author: Daniel Gruno <hu...@apache.org>
AuthorDate: Fri Mar 2 16:35:42 2018 +0100

    Initial stab at forum statistics
---
 api/pages/forum/actors.py     | 244 +++++++++++++++++++++++++++++
 api/pages/forum/creators.py   | 181 ++++++++++++++++++++++
 api/pages/forum/issues.py     | 258 +++++++++++++++++++++++++++++++
 api/pages/forum/responders.py | 182 ++++++++++++++++++++++
 api/pages/forum/top-count.py  | 170 ++++++++++++++++++++
 api/pages/forum/top.py        | 159 +++++++++++++++++++
 api/pages/forum/trends.py     | 351 ++++++++++++++++++++++++++++++++++++++++++
 7 files changed, 1545 insertions(+)

diff --git a/api/pages/forum/actors.py b/api/pages/forum/actors.py
new file mode 100644
index 0000000..345f59a
--- /dev/null
+++ b/api/pages/forum/actors.py
@@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+########################################################################
+# OPENAPI-URI: /api/forum/actors
+########################################################################
+# get:
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Timeseries'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows timeseries of no. of people opening/closing issues over time
+# post:
+#   requestBody:
+#     content:
+#       application/json:
+#         schema:
+#           $ref: '#/components/schemas/defaultWidgetArgs'
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Timeseries'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows timeseries of no. of people opening topics or replying to them.
+# 
+########################################################################
+
+
+
+
+
+"""
+This is the forum actors stats page for Kibble
+"""
+
+import json
+import time
+import hashlib
+
+def run(API, environ, indata, session):
+    
+    # We need to be logged in for this!
+    if not session.user:
+        raise API.exception(403, "You must be logged in to use this API endpoint! %s")
+    
+    now = time.time()
+    
+    # First, fetch the view if we have such a thing enabled
+    viewList = []
+    if indata.get('view'):
+        viewList = session.getView(indata.get('view'))
+    if indata.get('subfilter'):
+        viewList = session.subFilter(indata.get('subfilter'), view = viewList) 
+    
+    
+    dateTo = indata.get('to', int(time.time()))
+    dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span
+        
+    interval = indata.get('interval', 'month')
+    
+    
+    ####################################################################
+    ####################################################################
+    dOrg = session.user['defaultOrganisation'] or "apache"
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateFrom,
+                                        'to': dateTo
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+    # Source-specific or view-specific??
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+    if indata.get('email'):
+        query['query']['bool']['should'] = [{'term': {'issueCreator': indata.get('email')}}]
+    
+    # Get timeseries for this period
+    query['aggs'] = {
+            'per_interval': {
+                'date_histogram': {
+                    'field': 'createdDate',
+                    'interval': interval
+                },
+                'aggs': {
+                    'by_user': {
+                        'cardinality': {
+                            'field': 'creator'
+                        }
+                    }
+                }
+            }
+        }
+    
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_post",
+            size = 0,
+            body = query
+        )
+    
+    timeseries = {}
+
+    for bucket in res['aggregations']['per_interval']['buckets']:
+        ts = int(bucket['key'] / 1000)
+        ccount = bucket['by_user']['value']
+        timeseries[ts] = {
+            'date': ts,
+            'topic responders': ccount,
+            'topic creators': 0
+        }
+        
+    
+    ####################################################################
+    ####################################################################
+    dOrg = session.user['defaultOrganisation'] or "apache"
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateFrom,
+                                        'to': dateTo
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+    # Source-specific or view-specific??
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+    if indata.get('email'):
+        query['query']['bool']['should'] = [{'term': {'creator': indata.get('email')}}]
+    
+    # Get timeseries for this period
+    query['aggs'] = {
+            'per_interval': {
+                'date_histogram': {
+                    'field': 'createdDate',
+                    'interval': interval
+                },
+                'aggs': {
+                    'by_user': {
+                        'cardinality': {
+                            'field': 'creator'
+                        }
+                    }
+                }
+            }
+        }
+    
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_topic",
+            size = 0,
+            body = query
+        )
+
+    for bucket in res['aggregations']['per_interval']['buckets']:
+        ts = int(bucket['key'] / 1000)
+        ccount = bucket['by_user']['value']
+        if ts in timeseries:
+            timeseries[ts]['topic creators'] = ccount
+        else:
+            timeseries[ts] = {
+                'date': ts,
+                'topic creators': 0,
+                'topic responders': ccount
+            }
+    
+    ts = []
+    for x, el in timeseries.items():
+        ts.append(el)
+        
+    JSON_OUT = {
+        'timeseries': ts,
+        'okay': True,
+        'responseTime': time.time() - now,
+        'widgetType': {
+            'chartType': 'bar'
+        }
+    }
+    yield json.dumps(JSON_OUT)
diff --git a/api/pages/forum/creators.py b/api/pages/forum/creators.py
new file mode 100644
index 0000000..dc6a6c6
--- /dev/null
+++ b/api/pages/forum/creators.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+########################################################################
+# OPENAPI-URI: /api/forum/creators
+########################################################################
+# get:
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/CommitterList'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows the top N of issue openers
+# post:
+#   requestBody:
+#     content:
+#       application/json:
+#         schema:
+#           $ref: '#/components/schemas/defaultWidgetArgs'
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/CommitterList'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows the top N of forum topic creators
+# 
+########################################################################
+
+
+
+
+
+"""
+This is the TopN issue openers list renderer for Kibble
+"""
+
+import json
+import time
+import hashlib
+
+def run(API, environ, indata, session):
+    
+    # We need to be logged in for this!
+    if not session.user:
+        raise API.exception(403, "You must be logged in to use this API endpoint! %s")
+    
+    now = time.time()
+    
+    # First, fetch the view if we have such a thing enabled
+    viewList = []
+    if indata.get('view'):
+        viewList = session.getView(indata.get('view'))
+    if indata.get('subfilter'):
+        viewList = session.subFilter(indata.get('subfilter'), view = viewList) 
+    
+    
+    dateTo = indata.get('to', int(time.time()))
+    dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span
+    
+    interval = indata.get('interval', 'month')
+    xtitle = None
+    
+    ####################################################################
+    ####################################################################
+    dOrg = session.user['defaultOrganisation'] or "apache"
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateFrom,
+                                        'to': dateTo
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+    # Source-specific or view-specific??
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+    if indata.get('email'):
+        query['query']['bool']['must'].append({'term': {'creator': indata.get('email')}})
+        xtitle = "People opening issues solved by %s" % indata.get('email')
+    
+    # Get top 25 committers this period
+    query['aggs'] = {
+            'committers': {
+                'terms': {
+                    'field': 'creator',
+                    'size': 25
+                },
+                'aggs': {
+                
+            }
+        }        
+    }
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_topic",
+            size = 0,
+            body = query
+        )
+
+    people = {}
+    for bucket in res['aggregations']['committers']['buckets']:
+        email = bucket['key']
+        count = bucket['doc_count']
+        sha = email
+        if session.DB.ES.exists(index=session.DB.dbname,doc_type="person",id = sha):
+            pres = session.DB.ES.get(
+                index=session.DB.dbname,
+                doc_type="person",
+                id = email
+                )
+            person = pres['_source']
+            person['name'] = person.get('name', 'unknown')
+            people[email] = person
+            people[email]['gravatar'] = hashlib.md5(person.get('email', 'unknown').encode('utf-8')).hexdigest()
+            people[email]['count'] = count
+        
+    topN = []
+    for email, person in people.items():
+        topN.append(person)
+    topN = sorted(topN, key = lambda x: x['count'], reverse = True)
+    JSON_OUT = {
+        'topN': {
+            'denoter': 'topics created',
+            'items': topN,
+        },
+        'okay': True,
+        'responseTime': time.time() - now,
+        'widgetType': {
+            'chartType': 'bar',
+            'title': xtitle
+        }
+    }
+    yield json.dumps(JSON_OUT)
diff --git a/api/pages/forum/issues.py b/api/pages/forum/issues.py
new file mode 100644
index 0000000..a485bfa
--- /dev/null
+++ b/api/pages/forum/issues.py
@@ -0,0 +1,258 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+########################################################################
+# OPENAPI-URI: /api/forum/issues
+########################################################################
+# get:
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Timeseries'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows timeseries of issues opened/closed over time
+# post:
+#   requestBody:
+#     content:
+#       application/json:
+#         schema:
+#           $ref: '#/components/schemas/defaultWidgetArgs'
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Timeseries'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows timeseries of forum topics opened/responded-to over time
+# 
+########################################################################
+
+
+
+
+
+"""
+This is the forum timeseries renderer for Kibble
+"""
+
+import json
+import time
+import hashlib
+
+# This creates an empty timeseries object with
+# all categories initialized as 0 opened, 0 closed.
+def makeTS(dist):
+    ts = {}
+    for k in dist:
+        ts[k + ' topics'] = 0
+        ts[k + ' replies'] = 0
+    return ts
+
+def run(API, environ, indata, session):
+    
+    # We need to be logged in for this!
+    if not session.user:
+        raise API.exception(403, "You must be logged in to use this API endpoint! %s")
+    
+    now = time.time()
+    
+    # First, fetch the view if we have such a thing enabled
+    viewList = []
+    if indata.get('view'):
+        viewList = session.getView(indata.get('view'))
+    if indata.get('subfilter'):
+        viewList = session.subFilter(indata.get('subfilter'), view = viewList) 
+    
+    
+    dateTo = indata.get('to', int(time.time()))
+    dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span
+    
+    interval = indata.get('interval', 'month')
+    
+    # By default, we lump generic forums and question/answer (like SO, askbot) together as one
+    distinct = {
+        'forum': ['discourse', 'stackoverflow', 'askbot']
+    }
+    
+    # If requested, we split them into two
+    if indata.get('distinguish', False):
+        distinct = {
+            'forum':        ['discourse'],
+            'question bank': ['stackoverflow', 'askbot']
+        }
+    
+    timeseries = {}
+    
+    # For each category and the issue types that go along with that,
+    # grab opened and closed over time.
+    for iType, iValues in distinct.items():
+        ####################################################################
+        # ISSUES OPENED                                                    #
+        ####################################################################
+        dOrg = session.user['defaultOrganisation'] or "apache"
+        query = {
+                    'query': {
+                        'bool': {
+                            'must': [
+                                {'range':
+                                    {
+                                        'created': {
+                                            'from': dateFrom,
+                                            'to': dateTo
+                                        }
+                                    }
+                                },
+                                {
+                                    'term': {
+                                        'organisation': dOrg
+                                    }
+                                },
+                                {
+                                    'terms': {
+                                        'type': iValues
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+        # Source-specific or view-specific??
+        if indata.get('source'):
+            query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+        elif viewList:
+            query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+        if indata.get('email'):
+            query['query']['bool']['must'].append({'term': {'creator': indata.get('email')}})
+        
+        # Get number of opened ones, this period
+        query['aggs'] = {
+                'commits': {
+                    'date_histogram': {
+                        'field': 'createdDate',
+                        'interval': interval
+                    }                
+                }
+            }
+        res = session.DB.ES.search(
+                index=session.DB.dbname,
+                doc_type="forum_topic",
+                size = 0,
+                body = query
+            )
+        
+        for bucket in res['aggregations']['commits']['buckets']:
+            ts = int(bucket['key'] / 1000)
+            count = bucket['doc_count']
+            timeseries[ts] = timeseries.get(ts, makeTS(distinct))
+            timeseries[ts][iType + ' topics'] = timeseries[ts].get(iType + ' topics', 0) + count
+            
+        
+        ####################################################################
+        # ISSUES CLOSED                                                    #
+        ####################################################################
+        dOrg = session.user['defaultOrganisation'] or "apache"
+        query = {
+                    'query': {
+                        'bool': {
+                            'must': [
+                                {'range':
+                                    {
+                                        'created': {
+                                            'from': dateFrom,
+                                            'to': dateTo
+                                        }
+                                    }
+                                },
+                                {
+                                    'term': {
+                                        'organisation': dOrg
+                                    }
+                                },
+                                {
+                                    'terms': {
+                                        'type': iValues
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+        if viewList:
+            query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+        if indata.get('source'):
+            query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+        if indata.get('email'):
+            query['query']['bool']['must'].append({'term': {'creator': indata.get('email')}})
+        
+        # Get number of closed ones, this period
+        query['aggs'] = {
+                'commits': {
+                    'date_histogram': {
+                        'field': 'createdDate',
+                        'interval': interval
+                    }                
+                }
+            }
+        res = session.DB.ES.search(
+                index=session.DB.dbname,
+                doc_type="forum_post",
+                size = 0,
+                body = query
+            )
+        
+        for bucket in res['aggregations']['commits']['buckets']:
+            ts = int(bucket['key'] / 1000)
+            count = bucket['doc_count']
+            timeseries[ts] = timeseries.get(ts, makeTS(distinct))
+            timeseries[ts][iType + ' replies'] = timeseries[ts].get(iType + ' replies', 0) + count
+        
+    ts = []
+    for k, v in timeseries.items():
+        v['date'] = k
+        ts.append(v)
+        
+    
+    JSON_OUT = {
+        'widgetType': {
+            'chartType': 'line',  # Recommendation for the UI
+            'nofill': True
+        },
+        'timeseries': ts,
+        'interval': interval,
+        'okay': True,
+        'distinguishable': True,
+        'responseTime': time.time() - now
+    }
+    yield json.dumps(JSON_OUT)
diff --git a/api/pages/forum/responders.py b/api/pages/forum/responders.py
new file mode 100644
index 0000000..6c12ca2
--- /dev/null
+++ b/api/pages/forum/responders.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+########################################################################
+# OPENAPI-URI: /api/forum/responders
+########################################################################
+# get:
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/CommitterList'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows the top N of issue closers
+# post:
+#   requestBody:
+#     content:
+#       application/json:
+#         schema:
+#           $ref: '#/components/schemas/defaultWidgetArgs'
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/CommitterList'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows the top N of issue closers
+# 
+########################################################################
+
+
+
+
+
+"""
+This is the TopN forum posters list renderer for Kibble
+"""
+
+import json
+import time
+import hashlib
+
+def run(API, environ, indata, session):
+    
+    # We need to be logged in for this!
+    if not session.user:
+        raise API.exception(403, "You must be logged in to use this API endpoint! %s")
+    
+    now = time.time()
+    
+    # First, fetch the view if we have such a thing enabled
+    viewList = []
+    if indata.get('view'):
+        viewList = session.getView(indata.get('view'))
+    if indata.get('subfilter'):
+        viewList = session.subFilter(indata.get('subfilter'), view = viewList) 
+    
+    
+    dateTo = indata.get('to', int(time.time()))
+    dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span
+    
+    interval = indata.get('interval', 'month')
+    xtitle = None
+    
+    
+    ####################################################################
+    ####################################################################
+    dOrg = session.user['defaultOrganisation'] or "apache"
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateFrom,
+                                        'to': dateTo
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+    # Source-specific or view-specific??
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+    if indata.get('email'):
+        query['query']['bool']['must'].append({'term': {'creator': indata.get('email')}})
+        xTitle = "People closing %s's issues" % indata.get('email')
+    
+    # Get top 25 committers this period
+    query['aggs'] = {
+            'committers': {
+                'terms': {
+                    'field': 'creator',
+                    'size': 25
+                },
+                'aggs': {
+                
+            }
+        }        
+    }
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_post",
+            size = 0,
+            body = query
+        )
+
+    people = {}
+    for bucket in res['aggregations']['committers']['buckets']:
+        email = bucket['key']
+        count = bucket['doc_count']
+        sha = email
+        if session.DB.ES.exists(index=session.DB.dbname,doc_type="person",id = sha):
+            pres = session.DB.ES.get(
+                index=session.DB.dbname,
+                doc_type="person",
+                id = email
+                )
+            person = pres['_source']
+            person['name'] = person.get('name', 'unknown')
+            people[email] = person
+            people[email]['gravatar'] = hashlib.md5(person.get('email', 'unknown').encode('utf-8')).hexdigest()
+            people[email]['count'] = count
+        
+    topN = []
+    for email, person in people.items():
+        topN.append(person)
+    topN = sorted(topN, key = lambda x: x['count'], reverse = True)
+    JSON_OUT = {
+        'topN': {
+            'denoter': 'replies posted',
+            'items': topN,
+        },
+        'okay': True,
+        'responseTime': time.time() - now,
+        'widgetType': {
+            'chartType': 'bar',
+            'title': xtitle
+        }
+    }
+    yield json.dumps(JSON_OUT)
diff --git a/api/pages/forum/top-count.py b/api/pages/forum/top-count.py
new file mode 100644
index 0000000..58d345c
--- /dev/null
+++ b/api/pages/forum/top-count.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+########################################################################
+# OPENAPI-URI: /api/forum/top-count
+########################################################################
+# get:
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Timeseries'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows top 25 issue trackers by issues
+# post:
+#   requestBody:
+#     content:
+#       application/json:
+#         schema:
+#           $ref: '#/components/schemas/defaultWidgetArgs'
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Timeseries'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows top 25 forums by interactions
+# 
+########################################################################
+
+
+
+
+
+"""
+This is the TopN repos by commits list renderer for Kibble
+"""
+
+import json
+import time
+import re
+
+def run(API, environ, indata, session):
+    
+    # We need to be logged in for this!
+    if not session.user:
+        raise API.exception(403, "You must be logged in to use this API endpoint! %s")
+    
+    now = time.time()
+    
+    # First, fetch the view if we have such a thing enabled
+    viewList = []
+    if indata.get('view'):
+        viewList = session.getView(indata.get('view'))
+    if indata.get('subfilter'):
+        viewList = session.subFilter(indata.get('subfilter'), view = viewList) 
+    
+    
+    dateTo = indata.get('to', int(time.time()))
+    dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span
+    
+    ####################################################################
+    ####################################################################
+    dOrg = session.user['defaultOrganisation'] or "apache"
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateFrom,
+                                        'to': dateTo
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+    # Source-specific or view-specific??
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+    if indata.get('email'):
+        query['query']['bool']['should'] = [
+            {'term': {'creator': indata.get('email')}}
+        ]
+    
+    
+    # Get top 25 committers this period
+    query['aggs'] = {
+            'by_repo': {
+                'terms': {
+                    'field': 'sourceID',
+                    'size': 5000
+                }                
+            }
+        }
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_post",
+            size = 0,
+            body = query
+        )
+    
+    toprepos = []
+    for bucket in res['aggregations']['by_repo']['buckets']:
+        ID = bucket['key']
+        if session.DB.ES.exists(index=session.DB.dbname, doc_type="source", id = ID):
+            it = session.DB.ES.get(index=session.DB.dbname, doc_type="source", id = ID)['_source']
+            repo = re.sub(r".+/([^/]+)$", r"\1", it['sourceURL'])
+            count = bucket['doc_count']
+            toprepos.append([repo, count])
+        
+    toprepos = sorted(toprepos, key = lambda x: x[1], reverse = True)
+    top = toprepos[0:24]
+    if len(toprepos) > 25:
+        count = 0
+        for repo in toprepos[25:]:
+            count += repo[1]
+        top.append(["Other forums", count])
+    
+    tophash = {}
+    for v in top:
+        tophash[v[0]] = v[1]
+        
+    JSON_OUT = {
+        'counts': tophash,
+        'okay': True,
+        'responseTime': time.time() - now,
+    }
+    yield json.dumps(JSON_OUT)
diff --git a/api/pages/forum/top.py b/api/pages/forum/top.py
new file mode 100644
index 0000000..c29c28f
--- /dev/null
+++ b/api/pages/forum/top.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+########################################################################
+# OPENAPI-URI: /api/forum/top
+########################################################################
+# get:
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/TopList'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows the top N issues by interactions
+# post:
+#   requestBody:
+#     content:
+#       application/json:
+#         schema:
+#           $ref: '#/components/schemas/defaultWidgetArgs'
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/TopList'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows the top N topics by interactions
+# 
+########################################################################
+
+
+
+
+
+"""
+This is the issue actors stats page for Kibble
+"""
+
+import json
+import time
+import hashlib
+
+def run(API, environ, indata, session):
+    
+    # We need to be logged in for this!
+    if not session.user:
+        raise API.exception(403, "You must be logged in to use this API endpoint! %s")
+    
+    now = time.time()
+    
+    # First, fetch the view if we have such a thing enabled
+    viewList = []
+    if indata.get('view'):
+        viewList = session.getView(indata.get('view'))
+    if indata.get('subfilter'):
+        viewList = session.subFilter(indata.get('subfilter'), view = viewList) 
+    
+    
+    dateTo = indata.get('to', int(time.time()))
+    dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span
+    
+    interval = indata.get('interval', 'month')
+    
+    
+    ####################################################################
+    ####################################################################
+    dOrg = session.user['defaultOrganisation'] or "apache"
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateFrom,
+                                        'to': dateTo
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                },
+                'sort': {
+                    'posts': 'desc'
+                }
+            }
+    # Source-specific or view-specific??
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+    if indata.get('email'):
+        query['query']['bool']['should'] = [{'term': {'creator': indata.get('email')}}]
+    
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_topic",
+            size = 25,
+            body = query
+        )
+    top = []
+    for bucket in res['hits']['hits']:
+        doc = bucket['_source']
+        doc['source'] = doc.get('url', '#')
+        doc['name'] = doc.get('type', 'unknown')
+        doc['subject'] = doc.get('title')
+        doc['count'] = doc.get('posts', 0)
+        top.append(doc)
+    
+        
+    JSON_OUT = {
+        'topN': {
+            'denoter': 'interactions',
+            'icon': 'bug',
+            'items': top
+        },
+        'okay': True,
+        'responseTime': time.time() - now,
+        'widgetType': {
+            'chartType': 'line'
+        }
+    }
+    yield json.dumps(JSON_OUT)
diff --git a/api/pages/forum/trends.py b/api/pages/forum/trends.py
new file mode 100644
index 0000000..542029a
--- /dev/null
+++ b/api/pages/forum/trends.py
@@ -0,0 +1,351 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+########################################################################
+# OPENAPI-URI: /api/forum/trends
+########################################################################
+# get:
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Trend'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows trend data for a set of issue trackers over a given period of time
+# post:
+#   requestBody:
+#     content:
+#       application/json:
+#         schema:
+#           $ref: '#/components/schemas/defaultWidgetArgs'
+#   responses:
+#     '200':
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Trend'
+#       description: 200 Response
+#     default:
+#       content:
+#         application/json:
+#           schema:
+#             $ref: '#/components/schemas/Error'
+#       description: unexpected error
+#   security:
+#   - cookieAuth: []
+#   summary: Shows trend data for a set of forums over a given period of time
+# 
+########################################################################
+
+
+
+
+
+"""
+This is the forum trends renderer for Kibble
+"""
+
+import json
+import time
+
+def run(API, environ, indata, session):
+    
+    # We need to be logged in for this!
+    if not session.user:
+        raise API.exception(403, "You must be logged in to use this API endpoint! %s")
+    
+    now = time.time()
+    
+    # First, fetch the view if we have such a thing enabled
+    viewList = []
+    if indata.get('view'):
+        viewList = session.getView(indata.get('view'))
+    if indata.get('subfilter'):
+        viewList = session.subFilter(indata.get('subfilter'), view = viewList) 
+    
+    
+    dateTo = indata.get('to', int(time.time()))
+    dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span
+    if dateFrom < 0:
+        dateFrom = 0
+    dateYonder = dateFrom - (dateTo - dateFrom)
+    
+    
+    dOrg = session.user['defaultOrganisation'] or "apache"
+    
+    ####################################################################
+    # We start by doing all the queries for THIS period.               #
+    # Then we reset the query, and change date to yonder-->from        #
+    # and rerun the same queries.                                      #
+    ####################################################################
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateFrom,
+                                        'to': dateTo
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+    # Source-specific or view-specific??
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+    
+    # Get number of issues created, this period
+    res = session.DB.ES.count(
+            index=session.DB.dbname,
+            doc_type="forum_topic",
+            body = query
+        )
+    no_issues_created = res['count']
+    
+    
+    # Get number of open/close, this period
+    query['aggs'] = {
+            'opener': {
+                'cardinality': {
+                    'field': 'creator'
+                }
+            }
+        }
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_topic",
+            size = 0,
+            body = query
+        )
+    no_creators = res['aggregations']['opener']['value']
+    
+    
+    # REPLIERS
+    
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateFrom,
+                                        'to': dateTo
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+    # Source-specific or view-specific??
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+
+    
+    # Get number of issues created, this period
+    res = session.DB.ES.count(
+            index=session.DB.dbname,
+            doc_type="forum_post",
+            body = query
+        )
+    no_issues_closed = res['count']
+    
+    
+    # Get number of open/close, this period
+    query['aggs'] = {
+            'closer': {
+                'cardinality': {
+                    'field': 'creator'
+                }
+            }
+        }
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_post",
+            size = 0,
+            body = query
+        )
+    no_closers = res['aggregations']['closer']['value']
+    
+    
+    ####################################################################
+    # Change to PRIOR SPAN                                             #
+    ####################################################################
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateYonder,
+                                        'to': dateFrom-1
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+    
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+    
+    
+    # Get number of issues, this period
+    res = session.DB.ES.count(
+            index=session.DB.dbname,
+            doc_type="forum_topic",
+            body = query
+        )
+    no_issues_created_before = res['count']
+    
+    # Get number of committers, this period
+    query['aggs'] = {
+            'opener': {
+                'cardinality': {
+                    'field': 'creator'
+                }
+            }
+        }
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_topic",
+            size = 0,
+            body = query
+        )
+    no_creators_before = res['aggregations']['opener']['value']
+    
+    
+    
+    # REPLIERS
+    
+    query = {
+                'query': {
+                    'bool': {
+                        'must': [
+                            {'range':
+                                {
+                                    'created': {
+                                        'from': dateYonder,
+                                        'to': dateFrom-1
+                                    }
+                                }
+                            },
+                            {
+                                'term': {
+                                    'organisation': dOrg
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+    if indata.get('source'):
+        query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}})
+    elif viewList:
+        query['query']['bool']['must'].append({'terms': {'sourceID': viewList}})
+    
+    # Get number of issues created, this period
+    res = session.DB.ES.count(
+            index=session.DB.dbname,
+            doc_type="forum_post",
+            body = query
+        )
+    no_issues_closed_before = res['count']
+    
+    
+    # Get number of open/close, this period
+    query['aggs'] = {
+            'closer': {
+                'cardinality': {
+                    'field': "creator"
+                }
+            }
+        }
+    res = session.DB.ES.search(
+            index=session.DB.dbname,
+            doc_type="forum_post",
+            size = 0,
+            body = query
+        )
+    no_closers_before = res['aggregations']['closer']['value']
+    print(res)
+    
+    trends = {
+        "created": {
+            'before': no_issues_created_before,
+            'after': no_issues_created,
+            'title': "Topics started this period"
+        },
+        "authors": {
+            'before': no_creators_before,
+            'after': no_creators,
+            'title': "People starting topics this period"
+        },
+        "closed": {
+            'before': no_issues_closed_before,
+            'after': no_issues_closed,
+            'title': "Replies this period"
+        },
+        "closers": {
+            'before': no_closers_before,
+            'after': no_closers,
+            'title': "People replying this period"
+        }
+    }
+    
+    JSON_OUT = {
+        'trends': trends,
+        'okay': True,
+        'responseTime': time.time() - now
+    }
+    yield json.dumps(JSON_OUT)

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.

[kibble] 06/09: have forum explorer reload pages on filter changes

Posted by hu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git

commit a173801ef167b12ec2bcce552fb60a2d8417f1bb
Author: Daniel Gruno <hu...@apache.org>
AuthorDate: Fri Mar 2 16:36:28 2018 +0100

    have forum explorer reload pages on filter changes
---
 ui/js/coffee/pageloader.coffee | 1 +
 ui/js/coffee/widget.coffee     | 1 +
 2 files changed, 2 insertions(+)

diff --git a/ui/js/coffee/pageloader.coffee b/ui/js/coffee/pageloader.coffee
index d34022f..b42b065 100644
--- a/ui/js/coffee/pageloader.coffee
+++ b/ui/js/coffee/pageloader.coffee
@@ -80,6 +80,7 @@ setupPage = (json, state) ->
                 when 'repopicker' then widget.load(explorer)
                 when 'mailpicker' then widget.load(mailexplorer)
                 when 'issuepicker' then widget.load(issueexplorer)
+                when 'forumpicker' then widget.load(forumexplorer)
                 when 'viewpicker' then widget.load(viewexplorer)
                 when 'logpicker' then widget.load(logexplorer)
                 when 'impicker' then widget.load(imexplorer)
diff --git a/ui/js/coffee/widget.coffee b/ui/js/coffee/widget.coffee
index 517171b..0abd34b 100644
--- a/ui/js/coffee/widget.coffee
+++ b/ui/js/coffee/widget.coffee
@@ -124,6 +124,7 @@ updateWidgets = (type, target, eargs) ->
                 when 'repopicker' then widget.load(explorer)
                 when 'sourcepicker' then widget.load(sourceexplorer)
                 when 'issuepicker' then widget.load(issueexplorer)
+                when 'forumpicker' then widget.load(forumexplorer)
                 when 'viewpicker' then widget.load(viewexplorer)
                 when 'mailpicker' then widget.load(mailexplorer)
                 when 'cipicker' then widget.load(ciexplorer)

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.

[kibble] 03/09: regen OpenAPI yaml

Posted by hu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git

commit 67e6f866935d02480b19acf65a5dedc1970caacc
Author: Daniel Gruno <hu...@apache.org>
AuthorDate: Fri Mar 2 16:35:50 2018 +0100

    regen OpenAPI yaml
---
 api/yaml/openapi.yaml | 281 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 281 insertions(+)

diff --git a/api/yaml/openapi.yaml b/api/yaml/openapi.yaml
index d06f1ca..03f0e4c 100644
--- a/api/yaml/openapi.yaml
+++ b/api/yaml/openapi.yaml
@@ -1385,6 +1385,287 @@ paths:
       security:
       - cookieAuth: []
       summary: Shows trend data for a set of repos over a given period of time
+  /api/forum/actors:
+    get:
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Timeseries'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows timeseries of no. of people opening/closing issues over time
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/defaultWidgetArgs'
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Timeseries'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows timeseries of no. of people opening topics or replying to them.
+  /api/forum/creators:
+    get:
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CommitterList'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows the top N of issue openers
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/defaultWidgetArgs'
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CommitterList'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows the top N of forum topic creators
+  /api/forum/issues:
+    get:
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Timeseries'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows timeseries of issues opened/closed over time
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/defaultWidgetArgs'
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Timeseries'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows timeseries of forum topics opened/responded-to over time
+  /api/forum/responders:
+    get:
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CommitterList'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows the top N of issue closers
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/defaultWidgetArgs'
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CommitterList'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows the top N of issue closers
+  /api/forum/top:
+    get:
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/TopList'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows the top N issues by interactions
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/defaultWidgetArgs'
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/TopList'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows the top N topics by interactions
+  /api/forum/top-count:
+    get:
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Timeseries'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows top 25 issue trackers by issues
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/defaultWidgetArgs'
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Timeseries'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows top 25 forums by interactions
+  /api/forum/trends:
+    get:
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Trend'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows trend data for a set of issue trackers over a given period of
+        time
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/defaultWidgetArgs'
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Trend'
+          description: 200 Response
+        default:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: unexpected error
+      security:
+      - cookieAuth: []
+      summary: Shows trend data for a set of forums over a given period of time
   /api/issue/actors:
     get:
       responses:

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.

[kibble] 05/09: make a forum explorer widget

Posted by hu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git

commit 02a7fec4d596b945dbeb18fbc736bac99277b476
Author: Daniel Gruno <hu...@apache.org>
AuthorDate: Fri Mar 2 16:36:12 2018 +0100

    make a forum explorer widget
---
 ui/js/coffee/explorer.coffee | 57 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 57 insertions(+)

diff --git a/ui/js/coffee/explorer.coffee b/ui/js/coffee/explorer.coffee
index e06e7fa..e96ec2c 100644
--- a/ui/js/coffee/explorer.coffee
+++ b/ui/js/coffee/explorer.coffee
@@ -326,6 +326,62 @@ issueexplorer = (json, state) ->
         
 
 
+forumexplorer = (json, state) ->
+        
+        org = json.organisation
+        if json.tag
+            org.name += " (Filter: " + json.tag + ")"
+        h = document.createElement('h4')
+        h.appendChild(document.createTextNode("Exploring " + org.name + ":"))
+        state.widget.inject(h, true)
+        list = document.createElement('select')
+        state.widget.inject(list)
+        opt = document.createElement('option')
+        opt.value = ""
+        slen = 0
+        for item in json.sources
+            if item.type in ['forum', 'discourse', 'askbot']
+                slen++
+        opt.text = "All " + slen + " forum(s)"
+        list.appendChild(opt)
+        json.sources.sort((a,b) ->
+            return if (a.sourceURL == b.sourceURL) then 0 else (if a.sourceURL > b.sourceURL then 1 else -1)
+            )
+        for item in json.sources
+            if item.type in ['forum', 'discourse', 'askbot']
+                opt = document.createElement('option')
+                opt.value = item.sourceID
+                ezURL = null
+                n = item.sourceURL.match(/^([a-z]+:\/\/.+?)\/([-.A-Z0-9]+)$/i)                
+                m = item.sourceURL.match(/^([a-z]+:\/\/.+?)\s(.+)$/i)
+                if n and n.length == 3
+                    ezURL = "#{n[2]} - (#{n[1]})"
+                else if m and m.length == 3
+                    ezURL = "#{m[2]} - (#{m[1]})"
+                opt.text = if ezURL then ezURL else item.sourceURL
+                if globArgs.source and globArgs.source == item.sourceID
+                    opt.selected = 'selected'
+                list.appendChild(opt)
+        
+        ID = Math.floor(Math.random() * 987654321).toString(16)
+        list.setAttribute('id', ID)
+        $("#"+ID).chosen().change(() ->
+                source = this.value
+                
+                if source == ""
+                        source = null
+                globArgs.source = source
+                updateWidgets('donut', null, { source: source })
+                updateWidgets('gauge', null, { source: source })
+                updateWidgets('line', null, { source: source })
+                updateWidgets('contacts', null, { source: source })
+                updateWidgets('top5', null, { source: source })
+                updateWidgets('factors', null, { source: source })
+                updateWidgets('trends', null, { source: source })
+                
+        )
+        
+
 
 imexplorer = (json, state) ->
         
@@ -505,6 +561,7 @@ subFilter = () ->
         updateWidgets('sourcepicker', null, x)
         updateWidgets('repopicker', null, x)
         updateWidgets('issuepicker', null, x)
+        updateWidgets('forumpicker', null, x)
         updateWidgets('mailpicker', null, x)
         updateWidgets('logpicker', null, x)
         updateWidgets('donut', null, x)

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.

[kibble] 04/09: add widget yaml for forums

Posted by hu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

humbedooh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kibble.git

commit e38233d5f4846b9b0786774a47270e48281c2056
Author: Daniel Gruno <hu...@apache.org>
AuthorDate: Fri Mar 2 16:36:01 2018 +0100

    add widget yaml for forums
---
 api/yaml/widgets.yaml | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 80 insertions(+), 1 deletion(-)

diff --git a/api/yaml/widgets.yaml b/api/yaml/widgets.yaml
index edda4f2..542ac06 100644
--- a/api/yaml/widgets.yaml
+++ b/api/yaml/widgets.yaml
@@ -761,4 +761,83 @@ widgets:
                         source: "ci/top-buildcount"
                         name:   "Busiest jobs by build count"
                         blocks: 6
-                    
\ No newline at end of file
+                    
+### FORUMS
+    forums:
+        title: "Forums"
+        rows:
+            -
+                name: "Date picker row"
+                children:
+                    -
+                        type:   "datepicker"
+                        blocks: 4
+                        name:   "Date picker"
+                    -
+                        type:   "viewpicker"
+                        blocks: 4
+                        source: "views"
+                        name:   "Quick filter"
+                    -
+                        type:   forumpicker
+                        blocks: 4
+                        source: "sources"
+                        eargs:
+                            types: ['forum', 'discourse', 'askbot']
+                        name:   "Forum picker"
+            -
+                name: "Trends"
+                children:
+                    -
+                        type:   "trends"
+                        source: "forum/trends"
+                        name:   "Forum trends"
+                        blocks: 12
+            -
+                name: "Forum timeline"
+                children:
+                    -
+                        type:       "line"
+                        datatype:   "issue"
+                        blocks:     4
+                        source:     "forum/issues"
+                        name:       "Topics created/updated"
+                        target:     ""
+                    -
+                        type:       "line"
+                        datatype:   "issue"
+                        blocks:     4
+                        source:     "forum/actors"
+                        name:       "Creators/Posters of topics"
+                        target:     ""
+                    -
+                        type:       "top5"
+                        datatype:   "issue"
+                        blocks:     4
+                        source:     "forum/top"
+                        name:       "Most active topics"
+                        target:     ""
+            -
+                name: "Topics and handlers"
+                children:
+                    -
+                        type:       "top5"
+                        datatype:   "issue"
+                        blocks:     4
+                        source:     "forum/creators"
+                        name:       "Top 5 topic starters"
+                        target:     ""
+                    -
+                        type:       "top5"
+                        datatype:   "issue"
+                        blocks:     4
+                        source:     "forum/responders"
+                        name:       "Top 5 posters"
+                        target:     ""
+                    -
+                        type:       "donut"
+                        datatype:   "issue"
+                        blocks:     4
+                        source:     "forum/top-count"
+                        name:       "Busiest forums by posts"
+                        target:     ""

-- 
To stop receiving notification emails like this one, please contact
humbedooh@apache.org.