diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml index 6295bb48..dfbc1c17 100644 --- a/docs/_data/navigation.yml +++ b/docs/_data/navigation.yml @@ -23,6 +23,8 @@ url: "/repos-activity" - title: "Total" url: "/repos-total" +- title: "Organizations" + url: "/org-owners" - title: "Housekeeping" url: "/housekeeping-git-traffic" subnavigation: diff --git a/docs/assets/js/charts.js b/docs/assets/js/charts.js index c21b8327..d6c02779 100644 --- a/docs/assets/js/charts.js +++ b/docs/assets/js/charts.js @@ -362,20 +362,30 @@ function createTable(table) .enter() .append("td") .each(function(d, i) { - var cell = d3.select(this); - switch (data.columns[i].toLowerCase()) { - case "user": - case "organization": - case "repository": - case "resource": - case "fork": - cell = cell - .append("a") - .attr("target", "_blank") - .attr("href", gheUrl() + "/" + d); - break; + const cell = d3.select(this); + const entries = d.split(/[\s,]+/); + const column = data.columns[i].toLowerCase(); + + for (let j = 0; j < entries.length; j++) { + if (j > 0) + cell.append().text(", ") + const entry = entries[j]; + switch (column) { + case "fork": + case "organization": + case "owner(s)": + case "repository": + case "resource": + case "user": + cell.append("a") + .attr("target", "_blank") + .attr("href", gheUrl() + "/" + entry) + .text(entry); + break; + default: + cell.append().text(entry); + } } - cell.text(function(d) { return d; }) }); }); } diff --git a/docs/demo-data/org-owners.tsv b/docs/demo-data/org-owners.tsv new file mode 100644 index 00000000..f3b480a8 --- /dev/null +++ b/docs/demo-data/org-owners.tsv @@ -0,0 +1,10 @@ +organization owner(s) +Alpha user39, user72, user81, user20 +Bravo user50, user39 +Charlie user80 +Delta user78, user59, user86 +Echo user70 +Foxtrot user24, user87 +Golf user36, user90, user27, user40, user67 +Hotel user67, user90 +India user65 diff --git a/docs/org-owners.html b/docs/org-owners.html new file mode 100644 index 00000000..1fd3537b --- /dev/null +++ b/docs/org-owners.html @@ -0,0 +1,21 @@ +--- +layout: default +title: Organization Owners +permalink: /org-owners +--- +

+Organization Owners +

+ +
+
+
+
+
+
+
+
+

Owners for each GitHub organization.

+
+
+
diff --git a/updater/reports/Report.py b/updater/reports/Report.py index b1e759d3..16b0870b 100644 --- a/updater/reports/Report.py +++ b/updater/reports/Report.py @@ -100,6 +100,15 @@ def executeScript(self, script, stdin = None): return stdout + def executeGHEConsole(self, rubyCode): + escapedRubyCode = rubyCode.replace('\\t', '\\\\t') \ + .replace('\\n', '\\\\n') \ + .replace('"', '\\"') + return self.executeScript( + ["bash -s", "--"], + "github-env bin/runner -e production \"'" + escapedRubyCode + "'\"" + ) + # Executes a database query, given as a string def executeQuery(self, query): return self.executeScript(self.configuration["databaseCommand"], stdin = query) @@ -179,27 +188,22 @@ def update(self): print("Finished update of " + self.name() + ".tsv (runtime: " + str(timeElapsed) + " s)", file = sys.stderr) sys.stderr.flush() - def andExcludedEntities(self, org, repo): + def andExcludedEntities(self, column, delimiter = "AND"): query = "" if ("excludedEntities" in self.configuration and len(self.configuration["excludedEntities"]) > 0): - if org: - query += ' AND ' + org + '.login not LIKE "' + \ - self.configuration["excludedEntities"] + '" ' - if repo: - query += ' AND ' + repo + '.name not LIKE "' + \ - self.configuration["excludedEntities"] +'" ' + query += " " + delimiter + " " + column + ' NOT LIKE "' + self.configuration["excludedEntities"] + '" ' return query - def andExcludedUsers(self, users, delimiter = "AND"): + def andExcludedUsers(self, column, delimiter = "AND"): query = "" for excludedUser in self.configuration["excludedUsers"]: - query += " " + delimiter + " " + users + '.login NOT LIKE "' + excludedUser + '" ' + query += " " + delimiter + " " + column + ' NOT LIKE "' + excludedUser + '" ' delimiter = "AND" return query - def whereExcludedUsers(self, users): - return self.andExcludedUsers(users, "WHERE") + def whereExcludedUsers(self, column): + return self.andExcludedUsers(column, "WHERE") def andExcludeMemberlessOrganizations(self, orgs): query = "" diff --git a/updater/reports/ReportContributorsByOrg.py b/updater/reports/ReportContributorsByOrg.py index d2060e0f..2f40f344 100644 --- a/updater/reports/ReportContributorsByOrg.py +++ b/updater/reports/ReportContributorsByOrg.py @@ -27,7 +27,7 @@ def query(self, timeRange): JOIN users ON users.id = repositories.owner_id WHERE pull_requests.created_at IS NOT NULL AND CAST(pull_requests.created_at AS date) BETWEEN "''' + str(timeRange[0]) + '''" AND "''' + str(timeRange[1]) + '''" - ''' + self.andExcludedUsers("users") + ''' + ''' + self.andExcludedUsers("users.login") + ''' GROUP by users.id ORDER by diff --git a/updater/reports/ReportContributorsByRepo.py b/updater/reports/ReportContributorsByRepo.py index b36f36d3..a19e66ce 100644 --- a/updater/reports/ReportContributorsByRepo.py +++ b/updater/reports/ReportContributorsByRepo.py @@ -27,7 +27,7 @@ def query(self, timeRange): JOIN users on users.id = repositories.owner_id WHERE pull_requests.created_at IS NOT NULL AND CAST(pull_requests.created_at AS date) BETWEEN "''' + str(timeRange[0]) + '''" AND "''' + str(timeRange[1]) + '''" - ''' + self.andExcludedUsers("users") + ''' + ''' + self.andExcludedUsers("users.login") + ''' GROUP BY repositories.id ORDER BY diff --git a/updater/reports/ReportForksToOrgs.py b/updater/reports/ReportForksToOrgs.py index 27667d22..06c5aab0 100644 --- a/updater/reports/ReportForksToOrgs.py +++ b/updater/reports/ReportForksToOrgs.py @@ -34,8 +34,10 @@ def query(self): parentOrgs.type = "organization" AND parentRepos.owner_id = parentOrgs.id AND parentRepos.id = repos.parent_id - ''' + self.andExcludedEntities("orgs", "repos") \ - + self.andExcludedEntities("parentOrgs", "parentRepos") + ''' + ''' + self.andExcludedEntities("orgs.login") \ + + self.andExcludedEntities("repos.name") \ + + self.andExcludedEntities("parentOrgs.login") \ + + self.andExcludedEntities("parentRepos.name") + ''' ORDER BY repos.created_at DESC ''' diff --git a/updater/reports/ReportOrgCollaboration.py b/updater/reports/ReportOrgCollaboration.py index bdac2a8c..2200f968 100644 --- a/updater/reports/ReportOrgCollaboration.py +++ b/updater/reports/ReportOrgCollaboration.py @@ -52,7 +52,8 @@ def pushCountQuery(self, timeRange): AND orgs.id = repositories.owner_id AND repositories.id = pushes.repository_id AND cast(pushes.created_at AS DATE) BETWEEN "''' + str(timeRange[0]) + '''" and "''' + str(timeRange[1]) + '''" - ''' + self.andExcludedEntities("orgs", "repositories") \ + ''' + self.andExcludedEntities("orgs.login") \ + + self.andExcludedEntities("repositories.name") \ + self.andExcludeMemberlessOrganizations("orgs") + ''' GROUP BY org_id, pusher_id ''' @@ -70,7 +71,7 @@ def homeOrgQuery(self, timeRange): OR (a.push_count = b.push_count AND a.pusher_id != b.pusher_id) ) WHERE b.push_count is NULL AND a.pusher_id = users.id ''' \ - + self.andExcludedUsers("users") + + self.andExcludedUsers("users.login") return query # Calculates a table that contains all users that have contributed to a @@ -90,7 +91,8 @@ def contributorsToOrgQuery(self, timeRange): AND orgs.id = repositories.owner_id AND repositories.id = pushes.repository_id AND cast(pushes.created_at AS DATE) BETWEEN "''' + str(timeRange[0]) + '''" and "''' + str(timeRange[1]) + '''" - ''' + self.andExcludedEntities("orgs", "repositories") + ''' + ''' + self.andExcludedEntities("orgs.login") \ + + self.andExcludedEntities("repositories.name") + ''' UNION @@ -105,7 +107,8 @@ def contributorsToOrgQuery(self, timeRange): AND repositories.id = pull_requests.repository_id AND pull_requests.merged_at IS NOT NULL AND cast(pull_requests.created_at AS DATE) BETWEEN "''' + str(timeRange[0]) + '''" and "''' + str(timeRange[1]) + '''" - ''' + self.andExcludedEntities("orgs", "repositories") + ''' + ''' + self.andExcludedEntities("orgs.login") \ + + self.andExcludedEntities("repositories.name") + ''' ) contributors GROUP BY org_id, contributor_id ''' diff --git a/updater/reports/ReportOrgOwners.py b/updater/reports/ReportOrgOwners.py new file mode 100644 index 00000000..01536c57 --- /dev/null +++ b/updater/reports/ReportOrgOwners.py @@ -0,0 +1,27 @@ +from .Report import * + +# Lists all orgs and their owners without site admins or suspended users +class ReportOrgOwners(Report): + def name(self): + return "org-owners" + + # The data is overwritten every day, so skip reading the old data + def readData(self): + pass + + def updateData(self): + self.header, self.data = self.parseData( + self.executeGHEConsole(''' + puts "organization\towner(s)\n" + User.where(:type => "Organization") + .where("''' + self.andExcludedEntities("login", "").replace('"', '\\\\"') + '''") + .order("login") + .each do |org| + owners = org.admins.where(:disabled => false, :gh_role => nil) + .where("''' + self.andExcludedUsers("login", "").replace('"', '\\\\"') + '''") + .order("login") + .join(",") + puts "#{org.login}\t#{owners}\n" + end + ''') + ) diff --git a/updater/reports/ReportPRByOrg.py b/updater/reports/ReportPRByOrg.py index 712fc7ad..3505d2a7 100644 --- a/updater/reports/ReportPRByOrg.py +++ b/updater/reports/ReportPRByOrg.py @@ -32,7 +32,7 @@ def query(self, timeRange): pull_requests JOIN repositories ON repositories.id = pull_requests.repository_id JOIN users ON users.id = repositories.owner_id - ''' + self.whereExcludedUsers("users") + ''' + ''' + self.whereExcludedUsers("users.login") + ''' GROUP BY users.id ORDER BY diff --git a/updater/reports/ReportPRByRepo.py b/updater/reports/ReportPRByRepo.py index 661495ff..009fbd35 100644 --- a/updater/reports/ReportPRByRepo.py +++ b/updater/reports/ReportPRByRepo.py @@ -32,7 +32,7 @@ def query(self, timeRange): pull_requests join repositories ON repositories.id = pull_requests.repository_id join users on users.id = repositories.owner_id - ''' + self.whereExcludedUsers("users") + ''' + ''' + self.whereExcludedUsers("users.login") + ''' GROUP BY repositories.id ORDER BY diff --git a/updater/reports/ReportPRHistory.py b/updater/reports/ReportPRHistory.py index f112e14a..095b21f1 100644 --- a/updater/reports/ReportPRHistory.py +++ b/updater/reports/ReportPRHistory.py @@ -27,7 +27,7 @@ def subquery(self, type, timeRange): WHERE CAST(pull_requests.''' + type + ''' AS date) BETWEEN "''' + str(timeRange[0]) + '''" AND "''' + str(timeRange[1]) + '''" AND pull_requests.user_id = users.id - ''' + self.andExcludedUsers("users") + ''' + ''' + self.andExcludedUsers("users.login") + ''' GROUP BY date_format(pull_requests.''' + type + ''', "%Y-%m-%d") ORDER BY diff --git a/updater/reports/ReportRepoActivity.py b/updater/reports/ReportRepoActivity.py index c13eafaa..ba9d95bb 100644 --- a/updater/reports/ReportRepoActivity.py +++ b/updater/reports/ReportRepoActivity.py @@ -31,7 +31,8 @@ def subquery(self, userType, timeRange): WHERE CAST(pushes.created_at AS DATE) BETWEEN "''' + str(timeRange[0]) + '''" AND "''' + str(timeRange[1]) + '''"''' - query += self.andExcludedEntities("users", "repositories") + query += self.andExcludedEntities("users.login") \ + + self.andExcludedEntities("repositories.name") if userType != None: query += ''' AND diff --git a/updater/reports/ReportRepositoryHistory.py b/updater/reports/ReportRepositoryHistory.py index 1e2bc7b6..4659a987 100644 --- a/updater/reports/ReportRepositoryHistory.py +++ b/updater/reports/ReportRepositoryHistory.py @@ -23,7 +23,8 @@ def subquery(self, userType): repositories JOIN users ON repositories.owner_id = users.id WHERE - TRUE ''' + self.andExcludedEntities("users", "repositories") + TRUE ''' + self.andExcludedEntities("users.login") \ + + self.andExcludedEntities("repositories.name") if userType != None: query += ''' AND diff --git a/updater/reports/ReportUsers.py b/updater/reports/ReportUsers.py index 60a1a22a..1917c1b6 100644 --- a/updater/reports/ReportUsers.py +++ b/updater/reports/ReportUsers.py @@ -24,7 +24,7 @@ def usersPushingSubquery(self, timeRange): JOIN users ON users.id = pushes.pusher_id WHERE CAST(pushes.created_at AS DATE) BETWEEN "''' + str(timeRange[0]) + '''" AND "''' + str(timeRange[1]) + '''" - ''' + self.andExcludedUsers("users") + ''' + self.andExcludedUsers("users.login") return query # Collects the number of users who currently use up a seat diff --git a/updater/update-stats.py b/updater/update-stats.py index bb0a466c..61e6c4fc 100755 --- a/updater/update-stats.py +++ b/updater/update-stats.py @@ -14,6 +14,7 @@ from reports.ReportGitRequests import * from reports.ReportGitVersions import * from reports.ReportOrgCollaboration import * +from reports.ReportOrgOwners import * from reports.ReportPRByOrg import * from reports.ReportPRByRepo import * from reports.ReportPRHistory import * @@ -76,6 +77,7 @@ def main(): ReportGitRequests(configuration, dataDirectory, metaStats).update() ReportGitVersions(configuration, dataDirectory, metaStats).update() ReportOrgCollaboration(configuration, dataDirectory, metaStats).update() + ReportOrgOwners(configuration, dataDirectory, metaStats).update() ReportPRByOrg(configuration, dataDirectory, metaStats).update() ReportPRByRepo(configuration, dataDirectory, metaStats).update() ReportPRHistory(configuration, dataDirectory, metaStats).update()