diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml index dc524359..c097981a 100644 --- a/docs/_data/navigation.yml +++ b/docs/_data/navigation.yml @@ -5,6 +5,8 @@ url: "/" - title: "Contributors" url: "/users-contributors" + - title: "Issues & Comments" + url: "/users-communication" - title: "Activity" url: "/users-activity" - title: "Git Versions" diff --git a/docs/users-communication.html b/docs/users-communication.html new file mode 100644 index 00000000..f30deb0d --- /dev/null +++ b/docs/users-communication.html @@ -0,0 +1,163 @@ +--- +layout: default +title: Issues & Comments +permalink: /users-communication +--- + +
+

Issues

+ +
+

+ Issues shows how many GitHub issues were created, updated, or closed. issues updated excludes creating or closing an issue. +

+
+
+ +
+

Comments

+ +
+

+ Comments shows how many comments ware created for issues, pull requests, and commits. pull request comments includes comments on the PR as well as the ones for reviews. +

+
+
diff --git a/updater/reports/ReportComments.py b/updater/reports/ReportComments.py new file mode 100644 index 00000000..47c17c4f --- /dev/null +++ b/updater/reports/ReportComments.py @@ -0,0 +1,47 @@ +from .ReportDailySQL import * + +# Lists how many issues got created, closed, were commented on, and number of comments per day +class ReportComments(ReportDailySQL): + def name(self): + return "comments" + + def updateDailyData(self): + self.header, newData = self.parseData(self.executeQuery(self.query(self.timeRangeToUpdate()))) + self.data.extend(newData) + self.truncateData(self.timeRangeTotal()) + self.sortDataByDate() + + # Collects the issues closed in time range + def subquery(self, timeRange, table, extra_from=None, extra_where=None): + return ''' + SELECT + DATE_FORMAT(''' + table + '''.created_at, "%Y-%m-%d") AS date, + COUNT(*) AS count + FROM + ''' + table + ''' + JOIN users ON users.id = ''' + table + '''.user_id ''' + (("\n\t\t\t\t\t" + extra_from) if extra_from else '') + ''' + WHERE + CAST(''' + table + '''.created_at AS DATE) BETWEEN + "''' + str(timeRange[0]) + '''" AND "''' + str(timeRange[1]) + '''" + ''' + self.andExcludedUsers("users.login") + (("\n\t\t\t\t\t" + extra_where) if extra_where else '') + ''' + GROUP BY + date_format(''' + table + '''.created_at, "%Y-%m-%d")''' + + # Collects the number of issues created, closed, commented, and number of comments + def query(self, timeRange): + # `alldays` is used as a basis for LEFT JOIN, to prevent issues when querying multiple days, e.g. for initial run of the new report + return ''' + SELECT + alldays.date AS date, + IFNULL(i_issues.count, 0) AS "issue comments", + IFNULL(pr_issues.count, 0) AS "pull request issue comments", + IFNULL(reviews.count, 0) AS "pull request review comments", + IFNULL(pr_issues.count, 0) + IFNULL(reviews.count, 0) AS "pull request comments", + IFNULL(commits.count, 0) AS "commit comments" + FROM + (''' + self.allDaysToUpdateUnion(timeRange) + ''') AS alldays + LEFT JOIN (''' + self.subquery(timeRange, "issue_comments", "JOIN issues ON issues.id = issue_comments.issue_id", "AND issues.pull_request_id IS NULL") + ''') AS i_issues ON alldays.date = i_issues.date + LEFT JOIN (''' + self.subquery(timeRange, "issue_comments", "JOIN issues ON issues.id = issue_comments.issue_id", "AND issues.pull_request_id IS NOT NULL") + ''') AS pr_issues ON alldays.date = pr_issues.date + LEFT JOIN (''' + self.subquery(timeRange, "commit_comments") + ''') AS commits ON alldays.date = commits.date + LEFT JOIN (''' + self.subquery(timeRange, "pull_request_review_comments") + ''') AS reviews ON alldays.date = reviews.date + ORDER BY date DESC''' diff --git a/updater/reports/ReportDailySQL.py b/updater/reports/ReportDailySQL.py new file mode 100644 index 00000000..2bdd30df --- /dev/null +++ b/updater/reports/ReportDailySQL.py @@ -0,0 +1,17 @@ +from .ReportDaily import * + +# A daily report that also updates days in the past, given by timeRangeToUpdate +class ReportDailySQL(ReportDaily): + + def allDaysToUpdateUnion(self, timeRange): + # This method creates a table column of every single date in timeRange. + # It enables querying all results for timeRange in one single SQL query. + # Furthermore the result will include 0 for days without results, which + # makes the graphs look nicer especially on newly provisioned systems. + younger = max(timeRange) + older = min(timeRange) + snipped = "SELECT '" + str(younger) + "' AS date" + while (younger > older): + younger -= datetime.timedelta(1) + snipped += " UNION SELECT '" + str(younger) + "'" + return snipped diff --git a/updater/reports/ReportIssues.py b/updater/reports/ReportIssues.py new file mode 100644 index 00000000..582f0766 --- /dev/null +++ b/updater/reports/ReportIssues.py @@ -0,0 +1,45 @@ +from .ReportDailySQL import * + +# Lists how many issues got created, closed, and were updated per day +class ReportIssues(ReportDailySQL): + def name(self): + return "issues" + + def updateDailyData(self): + self.header, newData = self.parseData(self.executeQuery(self.query(self.timeRangeToUpdate()))) + self.data.extend(newData) + self.truncateData(self.timeRangeTotal()) + self.sortDataByDate() + + # Collects the issues closed in time range + def subquery(self, timeRange, type, extra=None): + return ''' + SELECT + DATE_FORMAT(issues.''' + type + ''', "%Y-%m-%d") AS date, + COUNT(*) AS count + FROM + issues + JOIN users ON users.id = issues.user_id + WHERE + issues.pull_request_id IS NULL AND + CAST(issues.''' + type + ''' AS DATE) BETWEEN + "''' + str(timeRange[0]) + '''" AND "''' + str(timeRange[1]) + '''" + ''' + self.andExcludedUsers("users.login") + (("\n\t\t\t\t\t" + extra) if extra else '') + ''' + GROUP BY + date_format(issues.''' + type + ''', "%Y-%m-%d")''' + + # Collects the number of issues created, closed, commented, and number of comments + def query(self, timeRange): + # `alldays` is used as a basis for LEFT JOIN, to prevent issues when querying multiple days, e.g. for initial run of the new report + return ''' + SELECT + alldays.date AS date, + IFNULL(created.count, 0) AS "issues created", + IFNULL(updated.count, 0) AS "issues updated", + IFNULL(closed.count, 0) AS "issues closed" + FROM + (''' + self.allDaysToUpdateUnion(timeRange) + ''') AS alldays + LEFT JOIN (''' + self.subquery(timeRange, "created_at") + ''') AS created ON alldays.date = created.date + LEFT JOIN (''' + self.subquery(timeRange, "updated_at", "AND issues.created_at != issues.updated_at AND issues.closed_at != issues.updated_at") + ''') AS updated ON alldays.date = updated.date + LEFT JOIN (''' + self.subquery(timeRange, "closed_at") + ''') AS closed ON alldays.date = closed.date + ORDER BY date DESC''' diff --git a/updater/update-stats.py b/updater/update-stats.py index f1f6166e..cb5c9825 100755 --- a/updater/update-stats.py +++ b/updater/update-stats.py @@ -36,6 +36,8 @@ from reports.ReportTeamsTotal import * from reports.ReportTokenlessAuth import * from reports.ReportUsers import * +from reports.ReportIssues import * +from reports.ReportComments import * def writeMeta(dataDirectory): outputFilePath = os.path.join(dataDirectory, "meta.tsv") @@ -104,6 +106,8 @@ def main(): ReportTeamsTotal(configuration, dataDirectory, metaStats).update() ReportTokenlessAuth(configuration, dataDirectory, metaStats).update() ReportUsers(configuration, dataDirectory, metaStats).update() + ReportIssues(configuration, dataDirectory, metaStats).update() + ReportComments(configuration, dataDirectory, metaStats).update() # Write meta infos writeMeta(dataDirectory)