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 <humbed...@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 humbed...@apache.org.