├── Business
├── GitHub-Analytics Process Overview.graffle
└── GitHub-Analytics Process Overview.png
├── README.md
├── Ruby
├── GTableGenerate_BETA.rb
└── src
│ ├── gh-issue-Mongodb.rb
│ ├── gh-issue-analyze.rb
│ ├── gh-issue-app.rb
│ ├── gh-issue-download.rb
│ ├── gh-issue-orchestrator.rb
│ ├── public
│ └── chartkick.js
│ └── views
│ ├── index.erb
│ └── layout.erb
├── app
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app.rb
├── config.ru
├── lib
│ ├── sinatra
│ │ └── auth
│ │ │ ├── github.rb
│ │ │ ├── github
│ │ │ └── version.rb
│ │ │ └── views
│ │ │ ├── 401.html
│ │ │ └── securocat.png
│ └── sinatra_auth_github.rb
├── public
│ └── vendor
│ │ ├── bootstrap
│ │ ├── css
│ │ │ ├── bootstrap-theme.css
│ │ │ ├── bootstrap-theme.min.css
│ │ │ ├── bootstrap.css
│ │ │ └── bootstrap.min.css
│ │ ├── fonts
│ │ │ ├── glyphicons-halflings-regular.eot
│ │ │ ├── glyphicons-halflings-regular.svg
│ │ │ ├── glyphicons-halflings-regular.ttf
│ │ │ └── glyphicons-halflings-regular.woff
│ │ └── js
│ │ │ ├── bootstrap.js
│ │ │ └── bootstrap.min.js
│ │ ├── chartkick.js
│ │ └── jquery-1.9.1.min.js
├── sinatra_auth_github.gemspec
├── sinatra_helpers.rb
└── views
│ ├── analyze_issues_opened_closed_per_month.erb
│ ├── analyze_issues_opened_per_user.erb
│ ├── analyze_labels_for_repo.erb
│ ├── analyze_repo_issue_events.erb
│ ├── download.erb
│ ├── index.erb
│ ├── layout.erb
│ ├── repos_listing.erb
│ └── unauthenticated.erb
├── github-analytics-analyze-data
├── events_aggregation.rb
├── events_processor.rb
├── helpers.rb
├── issues_aggregation.rb
├── issues_date_aggregation.rb
├── issues_date_processor.rb
├── issues_processor.rb
├── labels_aggregation.rb
├── labels_processor.rb
├── mongo.rb
├── system_wide_aggregation.rb
├── system_wide_processor.rb
└── time_analyzer.rb
└── github-analytics-data-download
├── code_commits.rb
├── controller.rb
├── convert_dates.rb
├── github_data.rb
├── helpers.rb
└── mongo.rb
/Business/GitHub-Analytics Process Overview.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitHub-Analytics/7f464fa06da3d44da5f415a1d606b59d63b7a854/Business/GitHub-Analytics Process Overview.graffle
--------------------------------------------------------------------------------
/Business/GitHub-Analytics Process Overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitHub-Analytics/7f464fa06da3d44da5f415a1d606b59d63b7a854/Business/GitHub-Analytics Process Overview.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | GitHub-Analytics
2 | ================
3 |
4 | [](https://coderwall.com/stephenott)
5 |
6 | Dec 10, 2013: Check out https://github.com/StephenOTT/GitHub-Time-Tracking for Time Tracking with GitHub Issue Tracker, Milestones, and Code Commits
7 |
8 | Dec 1, 2013: Make sure to add the following commit/code to Sewyer gem for proper JSON response. This is needed because of the way that GitHub Octokit.rb 2.x returns its responses as `Sewyer::Reponses` --https://github.com/lostisland/sawyer/pull/15
9 |
10 | **I welcome any and all feedback!!! Please post a issue, question or pull request!!**
11 |
12 | Downloads issues their comments from GitHub repositories into a mongodb database.
13 |
14 | Analytics are then run on the issues and comments in the mongodb database.
15 |
16 | Primary use at this point is project management based analytics that are not currently available on GitHub.com
17 |
18 |
19 | **New:::** Support for downloading multiple repositories into the same database allowing analysis of multiple repositories at the same time: EXAMPLE:
20 |
21 |
22 | ## How to run the Web App:
23 |
24 | 1. Register/Create a Application at https://github.com/settings/applications/new. Set your fields to the following:
25 |
26 | 1.1. Homepage URL: `http://localhost:9292`
27 |
28 | 1.2. Authorization callback URL: `http://localhost:9292/auth/github/callback`
29 |
30 | 1.3. Application Name: `GitHub-Analytics` or whatever you want to call your application.
31 |
32 | 2. Install MongoDB (typically: `brew update`, followed by: `brew install mongodb`)
33 |
34 | 3. `cd` into the `app` folder and run the following commands in the `app` folder:
35 |
36 | 3.1. Run `mongod` in terminal
37 |
38 | 3.2. Open a second terminal window and run: `bundle install`
39 |
40 | 3.3.`GITHUB_CLIENT_ID="YOUR CLIENT ID" GITHUB_CLIENT_SECRET="YOUR CLIENT SECRET" bundle exec rackup`
41 | Get the Client ID and Client Secret from the settings of your created/registered GitHub Application in Step 1.
42 |
43 | 4. Go to `http://localhost:9292`
44 |
45 |
46 |
47 | ## Presentations:
48 |
49 | Github Analytics: Ruby Ottawa Meetup (http://bit.ly/1cVFe9s)
50 |
51 |
52 |
53 | ##Screenshots:
54 | 
55 |
56 |
57 | -----
58 |
59 |
60 | 
61 |
62 |
63 | ##Process Overview:
64 | 
65 |
66 |
67 | -
68 |
69 | ## Actionable Reporting and Analysis
70 |
71 | 1. Issues Assigned to each user with size/complexity of issue identified: See Sprint.ly for great example: https://github.com/StephenOTT/GitHub-Analytics/issues/1#issuecomment-30169941
72 |
73 | 1.1. Group issues assigned to each user across multiple repositories
74 |
75 | 2. Time Reporting.
76 |
77 | 3. using the emoji ```:alarm_clock:``` plus a DateTime format such as Feb 2, 2013 3pm or any sort of combination a email notification would be created to notify the user as a reminder. Could be used about tasks, due dates, milestones, label changes, etc.
78 |
79 | 4. Using the ```:clock1:``` or any version of the clock emoji to provide time tracking. Example would be `:clock1: 2h`. This would signify 2h of work spend on the issue done by the following user. Adding additional features like: `:clock: 2h Sept 22, 2014` or other variations would provide time details time tracking down to dates, time of day etc.
80 |
81 | 4.1. Using the Clock Emoji we can extend the details that can posted. Example you could include categorization information, comments, descriptions, followups, etc.
82 |
83 | 5. Labels Assignment across multiple Repositories.
84 |
85 | 6. History of Label Assignment per issue.
86 |
87 | 6.1. Using the history of label assignment per issue, you can use labels such as "25% Complete", "50% Complete", "75% Complete", etc to show a history of a specific issues timeline from start to completion in terms of perceived % completed. This helps with post-launch reviews, and with analysis of problematic issues.
88 |
89 | 7. History of Issues within a Milestone
90 |
91 | 7.1. History of percentage complete of a Milestone
92 |
93 | 8. Break down of GFM Task Lists per users and aggregate per repo and across multiple repos
94 |
95 | 9. History of Issue Assignment per issue.
96 |
97 |
98 |
99 |
100 | ## Types of Analysis:
101 |
102 | 1. Issues Closed (Count) per user.
103 | 2. Total comments made per user.
104 | 3. Breakdown of comments made referencing other users per user
105 | - example: Number of times a user references other users (breakdown of each user (histogram).
106 | 4. Issues per milestone.
107 | 5. Duration of issues open.
108 | 6. Avg time issues open/Time is takes to close.
109 | 7. Character count per post: avg per user, per issue, etc.
110 | 8. Issues opened and closed per min, hour, day, week, month, quarter, year.
111 | 9. Issues being watched
112 | 10. Most popular issues (most watched, most commented, etc)
113 | 11. Issues assigned to users
114 | 12. Counts of issues that are assigned to users and closed by the same user
115 | 13. Number of times issues are opened and closed repeatedly.
116 | 14. Sentiment analysis of issues and comments for that issue
117 | - Breakdown of sentiment per user and types of issues
118 | - Analysis of Issue Titles: Looking to have better descriptive titles.
119 | 15. Labels analysis
120 | 16. Assignment changes of issues: Visual of issue assignment changes per user and timeframe
121 | 17. Printable table breakdown of issues assigned to each user
122 | 18. Printable table breakdown of weekly activity metrics of specific users: HIGHLY used by old school PMs that staff often report to.
123 | 19. Milestone changes (Event Analysis) - Changes of milestone
124 | 20. Analysis of URLs being used in issues and comments (popular url mentions, number of github issues uploads etc)
125 | 21. Analysis of number of comments per issues before they are closed.
126 | 22. Analysis of Popular labels (has cloud implications if analysis becomes a service you could analyze popular labels across repos as well as their usage.
127 | 23. Comment Streaks and Issue Creation/Close streaks
128 | - Comments Streak: Number of times a user makes multiple comments one after another in a single issue.
129 | 24. Emoticon usage: PMs could say to use specific emoticons when they want to support something like "+1", and this can be tallied.
130 | 25. Task counts and usage analysis.
131 | 26. Events Analysis: Modification of Issues and Comments
132 | - Users that make the most modifications to Issues and Comments
133 | - Users that make the most modifications to their own posts vs others posts
134 | - Weekly breakdown of modifications made
135 | - Timeframe breakdown of modifications made when and by who.
136 | 27. Deleted comments: when, by who, whos posts, their own posts? etc.
137 | 28. Pintrest style breakdown of images in comments with links back to comments/issues and context for specific image.
138 | 29. Cross-Issue reference usage. Most referenced Issues. Timeframe breakdown
139 | 30. Pie charts of issues and label assignment
140 | 31. Analysis of issues with more than one label
141 | 32. Analysis of Events and Label assignment
142 | 33. Change in milestone due dates
143 | 34. Change in milestone number of issues and %completed over time (line graph (%completed and time/dates)
144 | 35. Analysis of users on which teams: duration, added, removed dates, etc.
145 | 36. Breakdown of Repo Activity at high level: starts, forks, issues opened, closed, commits, etc. Exec style printable report that provides a high level overview for review when in high level meetings.
146 | 37. Pull Requests: when, by who, refs of other issues, comments made, duration open, amount of code etc.
147 | 38. Creation of new repos
148 | 39. deletion of repos
149 | 40. Repos analysis: languages, teams, branches, tags, deletions of repos, contributors, etc. Meant to be high level for reporting.
150 | 41. bar graph is issue activity (number of posts broken down by time)
151 | 42. Add special characters to GitHub post + time value to do time tracking within issues. Github GFM text does not show all text.
152 | 43. Track Thanks yous. Tracking when a user submits a pull request or issue and people thank you for submitting. See if that person is more likely to submit another issue/pull request (because people thanked them they are more likely to submit more requests/issues in the future).
153 |
154 | 44. Use new BETA feature of MongoDB for Text Analysis/Text Search for providing Time Tracking feature. Use invisible text in issues (html comments) to provide time tracking capability
155 |
156 | 45. View issues from multiple repositories with labels and milestones to provide PMs with high level overview of priority issues
157 | 46. Most Referenced Issues
158 | 47. Most Referenced Users
159 | 48. Most Referenced Repos
160 | 49. Compare Followers of 2 or more users. See which followers they have in common.
161 | 50. Compare Stared Repos of 2 or more users: The Repos the users have Stared. See which Repos they users have in common.
162 |
163 |
164 | ##Events Analysis:
165 |
166 | 1. hourly or daily download of events
167 | 2. types of events most popular, per user, etc.
168 | 3. Label Assignment
169 | 4. Milestone assignment
170 |
171 |
172 |
173 | ## Image Samples:
174 |
175 | 
176 |
177 | --
178 |
179 | 
180 |
181 | --
182 |
183 | Printable Issue queues for PMs with spikelines shows activity:
184 | 
185 |
186 | --
187 |
188 | Issues Count Assigned to specific users + unassigned count:
189 | 
190 |
191 | --
192 |
193 | Issue Events Timeline (This is all issue events for all issues grouped together)
194 | 
195 |
196 | --
197 |
198 | Pie chart of Issue Event Types - All Issue events for all issues
199 | 
200 |
201 |
202 |
203 | ## To Do:
204 |
205 | - [x] Downloading of Repo Events into Mongodb
206 | - [x] Convert to Sinatra app
207 | - [x] Downloading of Team data
208 | - [x] Turn Github DateTime string into recognized Mongodb dateTime. Currently github datetime string is not properly recognized by Mongodb.
209 | - [x] refactor method usage of Date conversions
210 | - [x] refactor analyze methods names and structure
211 | - [x] refactor methods into multifile MVC part of sinatra conversion
212 | - [ ] Build Dashboard that is equiv of the Github Survivor app (https://github.com/99designs/githubsurvivor)
213 | - [ ] PRIORITY: Develop Temp glue code for proper timezone query and output. Because of Mongos lack of timeline support at the query level for the Aggregation framework.
214 | - [ ] Refactor code to follow worker/job model to support sidekiq for calling jobs. Current issue is mainly based in the calling of issues, issue comments, and issue events. Each of these three can easily tax out the GitHub API hourly rate limit for the specific user. This mainly occurs for large projects such as WET-BOEW/WET-BOEW.
215 | - [x] Refactor code for more ruby like Naming conventions and integration of methods and classes.
216 |
217 |
218 |
219 | ## Data structure Overview (Out of Date):
220 | ---
221 | https://github.com/StephenOTT/GitHub-Analytics/issues/1#issuecomment-29685800
222 |
223 |
224 |
225 | ## Issues API Issues:
226 |
227 | 1. Issue and Issue Comment **revisions** that are exposed through API. No event or record is created when a revision occurs.
228 | 2. No event or record is created when an issue **comment is deleted**.
229 | 3. No event or record is created when an issue **label** is applied or removed.
230 | 4. No event or record is created when a **milestone** is applied or removed.
231 | 5. No event or record is created when a issue **Assignee** is un-assigned, and the issue has not assignee.
232 | 6. Issue Events do not have a payload in the API and therefore you must do another API call for details of the event. Example: If a user is assigned a Issue Event is created but there is not details about which user was assigned.
233 | 7. No Repo details other than the API url are not part of the Issue Events API.
234 | 8. No event or record is created when a Label is created or deleted in a repo (this is the creation or deletion of a label for the entire repository).
235 | 9. No event or record is created when a repo is un-stared. A Repo Event is created when a repo is Stared, but not when it is un-stared.
236 | 10. No event or record is created when a repo is Watched, ignored, or Not-Watching
237 | 11. Issue Events do not have a field that indicate the specific issue the event comes from. Issue Events should have a "repo" object like Repository Events have that indicates the specific repo and issue number that the events come from. (This issue has been resolved manually during the data download. See the 'getIssueEventsAllIssue' Method).
238 | 12. Getting list of Repos for a Team contains a array called "permissions" that is just a T/F value list for the current authenticated user. Proper returned values/expected values would be to return users for each of the T/F values showing which users have the specific permissions.
239 |
240 | ## Github Design Issues:
241 |
242 | 1. When you delete a Label from the master list it deletes all labels assigned to issues for that deleted label. This is a problem for maintaining a "current" label list. This means that if you ever get a legacy label you must keep it in the list forever or the old labels will lose their assigned labels.
243 |
244 |
245 | ## Other Possible uses for GitHub-Analytics Code
246 |
247 | 1. With some simple modifications for uploading to GitHub rather than just downloading from GitHub, a full GitHub "Backup your Repo" solution can be provided. Backup.co is currently providing a private beta for this. This would be a easy solution to provide
248 |
249 | 2. ~~Time Tracking. Using GFM and text searching on the DB side we can gather time tracking information~~ Added as part of GitHub-Time-Tracking Web App.
250 |
251 | 3. Network Analysis of GitHub Issues to show network graph of issue mentions and use mentions. This can show dependencies within the project/issue tracker and show who is the most popular user to be mentioned for types of issues. This would produce very interesting graphics for large projects.
252 |
253 | 4. Provide CSV, Excel, JSON, etc upload capability to GitHub Issue Tracker. This would build off of number 1 above but would also provide the added benefit for groups to generate issues based on something like an Excel file. (Other projects have accomplished this already)
254 |
255 | 5. "Project Templates" - A series of issues, milestones, and labels that are created and applied in a repo for when a new project is started. These would be customizable based on the company's/organization's specific project methodology and process.
256 |
257 | 6. Repo Comparisons - Allows you to select 2 or more Repos and be provided with comparative stats (issues, users, stars, languages, labels, activity, Git Stats, Keywords, Revision Activity, similar users, similar code, Gems/Libraries/etc used, similar dependencies
258 |
259 | 7. Have a ability to grab all of the users stats about their system when creating an issue in GitHub. Stats like: Device, browser, resolution, browser version, OS, etc.
260 |
261 |
--------------------------------------------------------------------------------
/Ruby/GTableGenerate_BETA.rb:
--------------------------------------------------------------------------------
1 | require 'json'
2 |
3 | class GenerateGDataTable
4 |
5 | def initialize
6 | @columnArray = []
7 | @rowArray = []
8 | end
9 |
10 |
11 | def addColumn(columnData={})
12 | @columnArray << {:id => columnData[:id], :label => columnData[:label], :type => columnData[:type] }
13 | end
14 |
15 | def addRow(rowData = [],hashValueName = "value", hashFormatName = "format", arrayValueNum = 0, arrayFormatNum = 1 )
16 | tempArray = []
17 |
18 | rowData.each do |x|
19 | if x.is_a?(Hash) == true
20 | tempArray << {:v => x[hashValueName], :f => x[hashFormatName]}
21 |
22 | elsif x.is_a?(Array) == true
23 | tempArray << {:v => x[arrayValueNum], :f => x[arrayFormatNum]}
24 |
25 | elsif x.is_a?(String) == true or x.is_a?(Integer) == true
26 | tempArray << {:v => x }
27 | end
28 | end
29 |
30 | @rowArray << {:c => tempArray}
31 | end
32 |
33 | def completeDataTable
34 | completedHash = {}
35 | completedHash[:cols] = @columnArray
36 | completedHash[:rows] = @rowArray
37 | return completedHash.to_json
38 | end
39 | end
40 |
41 | # dog = GenerateGDataTable.new
42 | # dog.addColumn(:id => "dogName", :label => "Dog Name", :type => "string")
43 | # dog.addRow(["Frank", "Steve", "Sam", "Cattle", [2222, "$24 222"], {"value" => 2222, "format" => "$24 222"}])
44 | # puts dog.completeDataTable
45 |
46 |
--------------------------------------------------------------------------------
/Ruby/src/gh-issue-Mongodb.rb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitHub-Analytics/7f464fa06da3d44da5f415a1d606b59d63b7a854/Ruby/src/gh-issue-Mongodb.rb
--------------------------------------------------------------------------------
/Ruby/src/gh-issue-analyze.rb:
--------------------------------------------------------------------------------
1 |
2 | require 'mongo'
3 | require 'gchart'
4 | require 'date'
5 | require 'time_difference'
6 | # require 'sinatra'
7 | # require 'chartkick'
8 | require 'groupdate'
9 | require '../../../add_missing_dates_ruby/add_missing_dates_months.rb'
10 |
11 | include Mongo
12 |
13 | class AnalyzeGHData
14 |
15 | def initialize (optionsDefaultsOverrides = {})
16 |
17 | optionsDefaults = {
18 | :mongoURL => "localhost",
19 | :mongoPort => 27017,
20 | :mongoDBName => "GitHub-Analytics",
21 | :mongoIssuesColl => "Issues",
22 | :mongoRepoEventsColl => "Repo-Events",
23 | :mongoIssueEventsColl => "Issue-Events",
24 | :mongoOrgMembersColl => "Org-Members",
25 | :mongoOrgTeamsInfoColl => "Org-Teams-Info",
26 | :mongoRepoLabelsColl => "Repo-Labels",
27 | :mongoRepoMilestonesColl => "Repo-Milestones",
28 | :mongoClearRecords => false
29 | }
30 | optionsDefaults.merge!(optionsDefaultsOverrides)
31 |
32 | # MongoDB Database Connect
33 | @client = MongoClient.new(optionsDefaults[:mongoURL], optionsDefaults[:mongoPort])
34 | @db = @client[optionsDefaults[:mongoDBName]]
35 |
36 | @collIssues = @db[optionsDefaults[:mongoIssuesColl]]
37 | @collRepoEvents = @db[optionsDefaults[:mongoRepoEventsColl]]
38 | @collIssueEvents = @db[optionsDefaults[:mongoIssueEventsColl]]
39 | @collOrgMembers = @db[optionsDefaults[:mongoOrgMembersColl]]
40 | @collOrgTeamsInfo = @db[optionsDefaults[:mongoOrgTeamsInfoColl]]
41 | @collRepoLabels = @db[optionsDefaults[:mongoRepoLabelsColl]]
42 | @collRepoMilestones = @db[optionsDefaults[:mongoRepoMilestonesColl]]
43 |
44 | end
45 |
46 |
47 | def analyzeIssuesCreatedClosedCountPerMonth
48 |
49 | issuesCreatedPerMonth = @collIssues.aggregate([
50 | { "$match" => {closed_at: {"$ne" => nil}}},
51 | { "$project" => {created_month: {"$month" => "$created_at"}, created_year: {"$year" => "$created_at"}, closed_month: {"$month" => "$closed_at"}, closed_year: {"$year" => "$closed_at"}, state: 1}},
52 | { "$group" => {_id: {"created_month" => "$created_month", "created_year" => "$created_year", state: "$state", "closed_month" => "$closed_month", "closed_year" => "$closed_year"}, number: { "$sum" => 1 }}},
53 | #{ "$sort" => {"_id.created_month" => 1}}
54 | ])
55 |
56 | issuesOpenCount = @collIssues.aggregate([
57 | { "$match" => {state: {"$ne" => "closed"}}},
58 | { "$project" => {state: 1}},
59 | { "$group" => {_id: {state: "$state"}, number: { "$sum" => 1 }}},
60 | ])
61 |
62 | newHashOpened={}
63 | newHashClosed={}
64 | issuesCreatedPerMonth.each do |x|
65 | newHashOpened[Date.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["number"]
66 |
67 | if x["_id"]["closed_month"] != nil
68 | newHashClosed[Date.strptime(x["_id"].values_at('closed_month', 'closed_year').join(" "), '%m %Y')] = x["number"]
69 | end
70 | end
71 |
72 | dateConvert = DateManipulate.new()
73 |
74 | return dateConvert.sortHashPlain(newHashOpened), dateConvert.sortHashPlain(newHashClosed), issuesOpenCount
75 | end
76 |
77 | # TODO Need to rebuild this as the Events data should be used rather than Issues data
78 | def analyzeIssuesOpenClosedPerUserPerMonth
79 | issuesOpenClosedPerUser = @collIssues.aggregate([
80 | { "$project" => {created_month: {"$month" => "$created_at"}, created_year: {"$year" => "$created_at"}, state: 1, user:{login:1}}},
81 | { "$group" => {_id: {user:"$user.login", "created_month" => "$created_month", "created_year" => "$created_year", state:"$state"}, number: { "$sum" => 1 }}},
82 | { "$sort" => {"_id.user" => 1}}
83 | ])
84 | # puts issuesOpenClosedPerUser
85 |
86 | usersBase = []
87 | issuesOpenClosedPerUser.each do |y|
88 | usersBase << y["_id"]["user"]
89 | end
90 | usersBase.uniq!
91 |
92 | usersBase.each do |ub|
93 | issuesOpenClosedForUniqueUser = @collIssues.aggregate([
94 | { "$project" => {created_month: {"$month" => "$created_at"}, created_year: {"$year" => "$created_at"}, state: 1, user:{login:1}}},
95 | { "$match" => {user:{login:ub }}},
96 | { "$group" => {_id: {user:"$user.login", "created_month" => "$created_month", "created_year" => "$created_year", state:"$state"}, number: { "$sum" => 1 }}},
97 | # { "$sort" => {"_id.user" => 1}}
98 | ])
99 | # puts issuesOpenClosedForUniqueUser
100 | end
101 | end
102 |
103 | def analyzeIssuesClosedDurationOpen
104 |
105 | issuesOpenClosedPerUser = @collIssues.aggregate([
106 | { "$match" => {state: "closed" }},
107 | { "$project" => {state: 1, created_at: 1, closed_at: 1, user:{login:1}}},
108 | { "$group" => {_id: {created_at:"$created_at",closed_at:"$closed_at", state:"$state", user:"$user.login"}}},
109 | { "$sort" => {"_id.created_at" => 1}}
110 | ])
111 |
112 | issuesOpenClosedPerUser.each do |y|
113 | durationDays = TimeDifference.between(y["_id"]["created_at"], y["_id"]["closed_at"]).in_days
114 | durationWeeks = TimeDifference.between(y["_id"]["created_at"], y["_id"]["closed_at"]).in_weeks
115 | durationMonths = TimeDifference.between(y["_id"]["created_at"], y["_id"]["closed_at"]).in_months
116 | durationFull = TimeDifference.between(y["_id"]["created_at"], y["_id"]["closed_at"]).in_general
117 | y["_id"]["duration_open_full"] = durationFull
118 | y["_id"]["duration_open_days"] = durationDays
119 | y["_id"]["duration_open_weeks"] = durationWeeks
120 | y["_id"]["duration_open_months"] = durationMonths
121 | end
122 |
123 | return issuesOpenClosedPerUser
124 | end
125 |
126 | def analyzeIssuesAssignedCountPerUser (inlcudeUnassigned = true)
127 | # inlcudeUnassigned = true
128 | issuesAssignedCountPerUser = @collIssues.aggregate([
129 | # { "$project" => {assignee:{login: 1}, state: 1}},
130 | { "$group" => {_id: {assignee:{"$ifNull" => ["$assignee.login","Unassigned"]}, state:"$state"}, number: { "$sum" => 1 }}},
131 | { "$sort" => {"_id.assignee" => 1 }}
132 | ])
133 |
134 | openCountHash = {}
135 | closedCountHash = {}
136 | issuesAssignedCountPerUser.each do |x|
137 | # x["_id"]["number"] = x["number"]
138 | if inlcudeUnassigned == false and x["_id"]["assignee"] != "Unassigned"
139 | if x["_id"]["state"] == "open"
140 | openCountHash[x["_id"]["assignee"]] = x["number"]
141 | elsif x["_id"]["state"] == "closed"
142 | closedCountHash[x["_id"]["assignee"]] = x["number"]
143 | end
144 | elsif inlcudeUnassigned == true
145 | if x["_id"]["state"] == "open"
146 | openCountHash[x["_id"]["assignee"]] = x["number"]
147 |
148 | elsif x["_id"]["state"] == "closed"
149 | closedCountHash[x["_id"]["assignee"]] = x["number"]
150 | end
151 | end
152 | end
153 | return openCountHash, closedCountHash
154 | end
155 |
156 | def analyzeEventsTypes
157 | # Query Mongodb and group event Types from RepoEvents collection and produce a count
158 | eventsTypesAnalysis = @collRepoEvents.aggregate([
159 | {"$group" => { _id: "$type", count: {"$sum" => 1}}}
160 | ])
161 |
162 | newHash={}
163 | eventsTypesAnalysis.each do |x|
164 | newHash[x["_id"]] = x["count"]
165 | end
166 | return newHash
167 | end
168 |
169 | def analyzeIssueEventsTypes
170 | # Query Mongodb and group event Types from RepoEvents collection and produce a count
171 | issueEventsTypesAnalysis = @collIssueEvents.aggregate([
172 | {"$group" => { _id: "$event", count: {"$sum" => 1}}}
173 | ])
174 |
175 | newHash={}
176 | issueEventsTypesAnalysis.each do |x|
177 | newHash[x["_id"]] = x["count"]
178 | end
179 | return newHash
180 | end
181 |
182 | def analyzeEventsTypesOverTime
183 | # REPO EVENTS
184 | eventsTypesAnalysis = @collRepoEvents.aggregate([
185 | { "$project" => {created_month: {"$month" => "$created_at"}, created_year: {"$year" => "$created_at"}, type:1}},
186 | {"$group" => { _id: {type:"$type", "created_month" => "$created_month", "created_year" => "$created_year"}, count: {"$sum" => 1}}},
187 | { "$sort" => {"_id.type" => 1}}
188 | ])
189 |
190 | commitCommentEventHash = {}
191 | createEventHash = {}
192 | deleteEvent = {}
193 | downloadEvent = {}
194 | followEvent = {}
195 | forkEventHash = {}
196 | forkApplyEvent = {}
197 | gistEvent = {}
198 | gollumEvent = {}
199 | issueCommentEventHash = {}
200 | issuesEventHash = {}
201 | memberEvent = {}
202 | publicEvent = {}
203 | pullRequestEventHash = {}
204 | pullRequestReviewCommentEvent = {}
205 | pushEventHash = {}
206 | releaseEventHash = {}
207 | statusEvent = {}
208 | teamAddEvent = {}
209 | watchEventHash = {}
210 |
211 |
212 |
213 |
214 | # TODO Adjust event type for proper type of analysis for the Repo Events
215 | # TODO Convert Hash Key into a variable to decrease change maint time.
216 | eventsTypesAnalysis.each do |x|
217 | case x["_id"]["type"]
218 | when "CommitCommentEvent"
219 | commitCommentEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
220 |
221 | when "CreateEvent"
222 | createEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
223 |
224 | when "DeleteEvent"
225 |
226 | when "DownloadEvent"
227 |
228 | when "FollowEvent"
229 |
230 | when "ForkEvent"
231 | forkEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
232 |
233 | when "ForkApplyEvent"
234 |
235 | when "GistEvent"
236 |
237 | when "GollumEvent"
238 |
239 | when "IssueCommentEvent"
240 | issueCommentEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
241 |
242 | when "IssuesEvent"
243 | issuesEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
244 |
245 | when "MemberEvent"
246 |
247 | when "PublicEvent"
248 |
249 | when "PullRequestEvent"
250 | pullRequestEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
251 |
252 | when "PullRequestReviewCommentEvent"
253 |
254 | when "PushEvent"
255 | pushEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
256 |
257 | when "ReleaseEvent"
258 | releaseEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
259 |
260 | when "StatusEvent"
261 |
262 | when "TeamAddEvent"
263 |
264 | when "WatchEvent"
265 | watchEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
266 | end
267 |
268 | end
269 |
270 | # TODO convert into a Hash
271 | dateConvert = DateManipulate.new()
272 | createEventHash_DatesAdjust = dateConvert.sortHashPlain(createEventHash)
273 | forkEventHash_DatesAdjust = dateConvert.sortHashPlain(forkEventHash)
274 | releaseEventHash_DatesAdjust = dateConvert.sortHashPlain(releaseEventHash)
275 | issueCommentEventHash_DatesAdjust = dateConvert.sortHashPlain(issueCommentEventHash)
276 | watchEventHash_DatesAdjust = dateConvert.sortHashPlain(watchEventHash)
277 | issuesEventHash_DatesAdjust = dateConvert.sortHashPlain(issuesEventHash)
278 | pushEventHash_DatesAdjust = dateConvert.sortHashPlain(pushEventHash)
279 | commitCommentEventHash_DatesAdjust = dateConvert.sortHashPlain(commitCommentEventHash)
280 | pullRequestEventHash_DatesAdjust = dateConvert.sortHashPlain(pullRequestEventHash)
281 |
282 | return createEventHash_DatesAdjust, forkEventHash_DatesAdjust, releaseEventHash_DatesAdjust, issueCommentEventHash_DatesAdjust, watchEventHash_DatesAdjust, issuesEventHash_DatesAdjust, pushEventHash_DatesAdjust, commitCommentEventHash_DatesAdjust, pullRequestEventHash_DatesAdjust
283 | end
284 |
285 | def analyzeIssueEventsTypesOverTime
286 | # ISSUE EVENTS
287 | eventsTypesAnalysis = @collIssueEvents.aggregate([
288 | { "$project" => {created_month: {"$month" => "$created_at"}, created_year: {"$year" => "$created_at"}, event:1}},
289 | { "$group" => { _id: {event:"$event", "created_month" => "$created_month", "created_year" => "$created_year"}, count: {"$sum" => 1}}},
290 | { "$sort" => {"_id.event" => 1}}
291 | ])
292 |
293 | # return eventsTypesAnalysis
294 |
295 | closedEventHash = {}
296 | reopenedEventHash = {}
297 | subscribedEventHash = {}
298 | mergedEventHash = {}
299 | referencedEventHash = {}
300 | mentionedEventHash = {}
301 | assignedEventHash = {}
302 |
303 |
304 | eventsTypesAnalysis.each do |x|
305 | case x["_id"]["event"]
306 | when "closed"
307 | closedEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
308 | when "reopened"
309 | reopenedEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
310 | when "subscribed"
311 | subscribedEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
312 | when "merged"
313 | mergedEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
314 | when "referenced"
315 | referencedEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
316 | when "mentioned"
317 | mentionedEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
318 | when "assigned"
319 | assignedEventHash[DateTime.strptime(x["_id"].values_at('created_month', 'created_year').join(" "), '%m %Y')] = x["count"]
320 | else
321 | #Debug code until all the stray event types are found and accounted for in the Github API system
322 | puts "DEBUG: Stray Issues Event Type found:: Event Type: #{x["_id"]["event"]}"
323 | end
324 | end
325 |
326 | dateConvert = DateManipulate.new()
327 |
328 | closedEventHash_DatesAdjust = dateConvert.sortHashPlain(closedEventHash)
329 | reopenedEventHash_DatesAdjust = dateConvert.sortHashPlain(reopenedEventHash)
330 | subscribedEventHash_DatesAdjust = dateConvert.sortHashPlain(subscribedEventHash)
331 | mergedEventHash_DatesAdjust = dateConvert.sortHashPlain(mergedEventHash)
332 | referencedEventHash_DatesAdjust = dateConvert.sortHashPlain(referencedEventHash)
333 | mentionedEventHash_DatesAdjust = dateConvert.sortHashPlain(mentionedEventHash)
334 | assignedEventHash_DatesAdjust = dateConvert.sortHashPlain(assignedEventHash)
335 |
336 | return closedEventHash_DatesAdjust, reopenedEventHash_DatesAdjust, subscribedEventHash_DatesAdjust, mergedEventHash_DatesAdjust, referencedEventHash_DatesAdjust, mentionedEventHash_DatesAdjust, assignedEventHash_DatesAdjust
337 | end
338 |
339 | def analyzeEvents_IssueCommmentEvent
340 |
341 | issuesOpenClosedForUniqueUser = @collRepoEvents.aggregate([
342 | # { "$project" => {payload:{action:1}, _id:1}},
343 | # { "$match" => {type:"IssuesEvent"}},
344 | { "$group" => {_id: {"user" => "$actor.login","type" =>"$payload.action"}, number: { "$sum" => 1 }}},
345 | { "$sort" => {"_id.user" => 1}}
346 | ])
347 | puts issuesOpenClosedForUniqueUser
348 | end
349 |
350 | def analyzeIssuesPrintableTable
351 | issuesPrintableTable = @collIssues.aggregate([
352 | # { "$project" => {assignee:{login: 1}, state: 1, milestone:{title: 1}, number: 1, title: 1, created_at: 1, closed_at: 1, _id: 0}},
353 | { "$group" => {_id: {
354 | issueCurrentState:"$state",
355 | issueNumber:"$number",
356 | issueAssignedMilestone:"$milestone.title",
357 | issueTitle:"$title",
358 | issueCurrentAssignee:"$assignee.login",
359 | created_at:"$created_at",
360 | closed_at:"$closed_at",
361 | createdBy:"$user.login",
362 | createdByAvatar:"$user.avatar_url",
363 | commentsCount:"$comments"}}},
364 | { "$sort" => {"_id.issueCurrentState" => -1, "_id.issueNumber" => -1}}
365 | ])
366 | printableArray = []
367 | issuesPrintableTable.each do |x|
368 | #gets comments and sparkline data for supplied issue number and the CURRENT year
369 | dog = self.issueCommentsDatesBreakdownWeek(x["_id"]["issueNumber"], Time.now.strftime('%Y').to_i)
370 | x["_id"]["sparkline"] = dog
371 | printableArray << x["_id"]
372 | end
373 | return printableArray
374 | # return buildSampleTable(printableArray)
375 | end
376 |
377 | # TODO add better support for sparklines/images in the table. Currently images cannot be added because of the code.
378 | # TODO remove this method as it is not needed anymore. Double check dependencies.
379 | def buildSampleTable (data)
380 | xm = Builder::XmlMarkup.new(:indent => 2)
381 | xm.table {
382 | xm.tr { data[0].keys.each { |key| xm.th(key)}}
383 | data.each { |row| xm.tr { row.values.each { |value| xm.td(value)}}}
384 | }
385 | end
386 |
387 | def issueCommentsDatesBreakdownWeek(issueNumber, yearSpan)
388 |
389 | issueCommentsDatesSpark = @collIssues.aggregate([
390 | { "$match" => {number: issueNumber}},
391 | { "$unwind" => "$comments_Text" },
392 | # { "$project" => {created_month: {"$month" => "$comments_Text.created_at"}, created_year: {"$year" => "$comments_Text.created_at"}}},
393 | { "$project" => {created_week: {"$week" => "$comments_Text.created_at"}, created_year: {"$year" => "$comments_Text.created_at"}}},
394 | { "$match" => {created_year: yearSpan}},
395 | # TODO write a blog post about dealing match and how $eq does not work correctly
396 | # { "$match" => {created_year: {"$gt" => yearSpan-1}}},
397 | # { "$match" => {created_year: {"$lt" => yearSpan+1}}},
398 | # { "$group" => {_id:{"created_month" => "$created_month", "created_year" => "$created_year"}, number: { "$sum" => 1 }}},
399 | { "$group" => {_id:{"created_week" => "$created_week", "created_year" => "$created_year"}, number: { "$sum" => 1 }}},
400 | ])
401 |
402 | newHash = {}
403 | issueCommentsDatesSpark.each do |x|
404 | # newHash[Date.strptime(x["_id"].values_at('created_week', 'created_year').join(" "), '%U %Y')] = x["number"]
405 | newHash[x["_id"]["created_week"]] = x["number"]
406 | end
407 |
408 | # figures out missing week numbers and if the week number is missing creates it and assigns value as 0
409 | for i in 0..Time.now.strftime('%W').to_i # gets week 0 to current week number of activity
410 | # If the week does not already exist in the hash then add a new hash value with the key being the week number and the value is 0 becuase there was not previous value
411 | if newHash.key?(i) == false
412 | newHash[i] = 0
413 | end
414 | end
415 |
416 | # TODO support for sparkline images in table builder is still required
417 | dateConvert = DateManipulate.new()
418 | sortedHash = dateConvert.simpleHashSort(newHash)
419 | return self.produceSparklineChart(sortedHash)
420 | end
421 |
422 | # TODO add support for custom sizes and colours when calling spark line generator
423 | def produceSparklineChart(data)
424 | return chartURL = Gchart.sparkline(
425 | :data => data.values,
426 | :size => '80x20'
427 | )
428 | end
429 | end
--------------------------------------------------------------------------------
/Ruby/src/gh-issue-app.rb:
--------------------------------------------------------------------------------
1 | require './gh-issue-analyze.rb'
2 | # require './Test.rb'
3 | require 'sinatra'
4 | require "sinatra/reloader" if development?
5 | require 'chartkick'
6 | # require '../../../chartkick/lib/chartkick.rb'
7 |
8 | get '/' do
9 |
10 | @foo = 'erb23' # Debug code
11 |
12 | analyze = AnalyzeGHData.new()
13 | # generateData = GenerateGDataTable.new()
14 | # generateData.addColumn(:id => "col1", :label => "Dogs Generated1", :type => "number")
15 | # generateData.addColumn(:id => "col2", :label => "Dogs Generated2", :type => "number")
16 | # generateData.addRow([2004,0])
17 | # generateData.addRow([2005,22])
18 | # generateData.addRow([2006,50])
19 | # generateData.addRow([2007,100])
20 | # generateData.addRow([2008,40])
21 | # generateData.addRow([2009,70])
22 | # generateData.addRow([2010,10])
23 | # generateData.addRow([2011,90])
24 | # generateData.addRow([2012,40])
25 |
26 | # @sampleChart = generateData.completeDataTable
27 | # puts @sampleChart
28 |
29 |
30 | @eventTypesCount = analyze.analyzeEventsTypes
31 | # @eventstext = analyze.analyzeEventsTypes.to_a.to_s
32 |
33 | @issueEventTypesCount = analyze.analyzeIssueEventsTypes
34 |
35 | # TODO Convert to Hash
36 | @issuesClosedEventHash, @issuesReopenedEventHash, @issuesSubscribedEventHash, @issuesMergedEventHash, @issuesReferencedEventHash, @issuesMentionedEventHash, @issuesAssignedEventHash = analyze.analyzeIssueEventsTypesOverTime
37 |
38 | # TODO Convert to Hash
39 | @createEvent, @forkEvent, @releaseEvent, @issueCommentEvent, @watchEvent, @issuesEvent, @pushEvent, @commitCommentEvent, @pullRequestEvent = analyze.analyzeEventsTypesOverTime
40 |
41 | @issuesCreatedMonthCount, @issuesClosedMonthCount, @issuesOpenCountPrep = analyze.analyzeIssuesCreatedClosedCountPerMonth
42 | @issuesOpenClosedPerUsedPerMonth = analyze.analyzeIssuesOpenClosedPerUserPerMonth
43 |
44 | @printableData = analyze.analyzeIssuesPrintableTable
45 |
46 | # Add inlcudeUnassigned = false to the analyzeIssuesAssignedCountPerUser Method arguments to not show unassigned issues
47 | # TODO Convert to Hash
48 | @issueAssignedPerUserOpenCount, @issueAssignedPerUserClosedCount = analyze.analyzeIssuesAssignedCountPerUser()
49 |
50 | # @timelineTest = [
51 | # ['Washington',0,23],
52 | # ['Adams',0,5],
53 | # ['Jefferson',0,50]]
54 |
55 |
56 |
57 |
58 | erb :index
59 | end
--------------------------------------------------------------------------------
/Ruby/src/gh-issue-download.rb:
--------------------------------------------------------------------------------
1 | require 'octokit'
2 | require 'mongo'
3 | require 'gchart'
4 | require 'date'
5 | require 'JSON'
6 |
7 | include Mongo
8 |
9 | class IssueDownload
10 |
11 | def initialize (repository, optionsDefaultsOverrides = {})
12 | optionsDefaults = {
13 | :mongoURL => "localhost",
14 | :mongoPort => 27017,
15 | :mongoDBName => "GitHub-Analytics",
16 | :mongoIssuesColl => "Issues",
17 | :mongoRepoEventsColl => "Repo-Events",
18 | :mongoIssueEventsColl => "Issue-Events",
19 | :mongoOrgMembersColl => "Org-Members",
20 | :mongoOrgTeamsInfoColl => "Org-Teams-Info",
21 | :mongoRepoLabelsColl => "Repo-Labels",
22 | :mongoRepoMilestonesColl => "Repo-Milestones",
23 | :mongoClearRecords => false
24 | }
25 | optionsDefaults.merge!(optionsDefaultsOverrides)
26 |
27 | @repository = repository.to_s
28 | @organization = @repository.slice(0..(repository.index('/')-1 ))
29 |
30 | # MongoDB Database Connect
31 | @client = MongoClient.new(optionsDefaults[:mongoURL], optionsDefaults[:mongoPort])
32 | @db = @client[optionsDefaults[:mongoDBName]]
33 |
34 | @collIssues = @db[optionsDefaults[:mongoIssuesColl]]
35 | @collRepoEvents = @db[optionsDefaults[:mongoRepoEventsColl]]
36 | @collRepoIssueEvents = @db[optionsDefaults[:mongoIssueEventsColl]]
37 | @collOrgMembers = @db[optionsDefaults[:mongoOrgMembersColl]]
38 | @collOrgTeamsInfoAllList = @db[optionsDefaults[:mongoOrgTeamsInfoColl]]
39 | @collRepoLabelsList = @db[optionsDefaults[:mongoRepoLabelsColl]]
40 | @collRepoMilestonesList = @db[optionsDefaults[:mongoRepoMilestonesColl]]
41 |
42 | # Debug code to empty out mongoDB records
43 | if optionsDefaults[:mongoClearRecords] == true
44 | @collIssues.remove
45 | @collRepoEvents.remove
46 | @collRepoIssueEvents.remove
47 | @collOrgMembers.remove
48 | @collRepoLabelsList.remove
49 | @collRepoMilestonesList.remove
50 | @collOrgTeamsInfoAllList.remove
51 | end
52 | end
53 |
54 | # TODO add authentication as a option for go live as Github Rate Limit is 60 hits per hour when unauthenticated by 5000 per hour when authenticated.
55 | # TODO PRIORITY username and password variables are not using "gets" correctly when used in terminal. When in terminal after typing in credentials github api returns a bad credentials alert. But when you type the credentials in directly in the code there is no issues.
56 | def ghAuthenticate (username, password)
57 | # puts "Enter GitHub Username:"
58 | # username = ""
59 | # puts "Enter GitHub Password:"
60 | # password = ""
61 | # Octokit.auto_paginate = true
62 | @ghClient = Octokit::Client.new(:login => username.to_s, :password => password.to_s, :per_page =>100)
63 | end
64 |
65 | def getIssues
66 | # TODO get list_issues working with options hash: Specifically need Open and Closed issued to be captured
67 | # Gets Open Issues List - Returns Sawyer::Resource
68 | issueResultsOpen = @ghClient.list_issues(@repository, {
69 | :state => :open
70 | })
71 | ghLastReponseOpen = @ghClient.last_response
72 |
73 | # Parses String body from last response/Closed Issues List into Proper Array in JSON format
74 | issueResultsOpenRaw = JSON.parse(@ghClient.last_response.body)
75 |
76 | while ghLastReponseOpen.rels.include?(:next) do
77 | ghLastReponseOpen = ghLastReponseOpen.rels[:next].get
78 | issueResultsOpenRaw.concat(JSON.parse(ghLastReponseOpen.body))
79 | end
80 |
81 |
82 |
83 |
84 | # Gets Closed Issues List - Returns Sawyer::Resource
85 | issueResultsClosed = @ghClient.list_issues(@repository.to_s, {
86 | :state => :closed
87 | })
88 |
89 | ghLastReponseClosed = @ghClient.last_response
90 |
91 | # # Parses String body from last response/Closed Issues List into Proper Array in JSON format
92 | issueResultsClosedRaw = JSON.parse(@ghClient.last_response.body)
93 |
94 | while ghLastReponseClosed.rels.include?(:next) do
95 | ghLastReponseClosed = ghLastReponseClosed.rels[:next].get
96 | issueResultsClosedRaw.concat(JSON.parse(ghLastReponseClosed.body))
97 | end
98 |
99 |
100 | # Open Issues
101 | if issueResultsOpenRaw.empty? == false
102 | issueResultsOpenRaw.each do |x|
103 | x["organization"] = @organization
104 | x["repo"] = @repository
105 | x["downloaded_at"] = Time.now
106 | if x["comments"] > 0
107 | openIssueComments = self.getIssueComments(x["number"])
108 | x["issue_comments"] = openIssueComments
109 | # puts "Processed Comments for Open issue number: #{x["number"]}"
110 | end
111 | xDatesFixed = self.convertIssueDatesForMongo(x)
112 | self.putIntoMongoCollIssues(xDatesFixed)
113 | self.getIssueEvents(x["number"])
114 | # puts "Processed Open issue number: #{x["number"]}"
115 |
116 | # if @ghClient.rate_limit.remaining < 100
117 | # end
118 | end
119 | end
120 |
121 | # Closed Issues
122 | if issueResultsClosedRaw.empty? == false
123 | issueResultsClosedRaw.each do |y|
124 | y["organization"] = @organization
125 | y["repo"] = @repository
126 | y["downloaded_at"] = Time.now
127 | if y["comments"] > 0
128 | closedIssueComments = self.getIssueComments(y["number"])
129 | y["issues_comments"] = closedIssueComments
130 | # puts "Processed Comments for Closed issue number: #{y["number"]}"
131 | end
132 | yDatesFixed = self.convertIssueDatesForMongo(y)
133 | self.putIntoMongoCollIssues(yDatesFixed)
134 | self.getIssueEvents(y["number"])
135 | # puts "Processed Closed issue number: #{y["number"]}"
136 |
137 | # if @ghClient.rate_limit.remaining < 100
138 | # end
139 | end
140 | end
141 |
142 | # Debug Code
143 | # puts "Got issues, Github raite limit remaining: " + @ghClient.rate_limit.remaining.to_s
144 | end
145 |
146 | # TODO preform DRY refactor for Mongodb insert
147 | def putIntoMongoCollIssues(mongoPayload)
148 | @collIssues.insert(mongoPayload)
149 | # puts "Issues Added, Count in Mongodb: " + @coll.count.to_s
150 | end
151 |
152 | def putIntoMongoCollRepoEvents(mongoPayload)
153 | @collRepoEvents.insert(mongoPayload)
154 | # puts "Repo Events Added, Count in Mongodb: " + @collRepoEvents.count.to_s
155 | end
156 |
157 | def putIntoMongoCollOrgMembers(mongoPayload)
158 | @collOrgMembers.insert(mongoPayload)
159 | # puts "Org Members Added, Count in Mongodb: " + @collOrgMembers.count.to_s
160 | end
161 |
162 | def putIntoMongoCollRepoIssuesEvents(mongoPayload)
163 | @collRepoIssueEvents.insert(mongoPayload)
164 | # puts "Repo Issue Events Added, Count in Mongodb: " + @collRepoIssueEvents.count.to_s
165 | end
166 |
167 | def putIntoMongoCollRepoLabelsList(mongoPayload)
168 | @collRepoLabelsList.insert(mongoPayload)
169 | # puts "Repo Labels List Added, Count in Mongodb: " + @collRepoIssueEvents.count.to_s
170 | end
171 |
172 | def putIntoMongoCollRepoMilestonesList(mongoPayload)
173 | @collRepoMilestonesList.insert(mongoPayload)
174 | # puts "Repo Labels List Added, Count in Mongodb: " + @collRepoIssueEvents.count.to_s
175 | end
176 |
177 | def putIntoMongoCollOrgTeamsInfoAllList(mongoPayload)
178 | @collOrgTeamsInfoAllList.insert(mongoPayload)
179 | # puts "Org Tema Repos List Added, Count in Mongodb: " + @collRepoIssueEvents.count.to_s
180 | end
181 |
182 |
183 | # find records in Mongodb that have a comments field value of 1 or higher
184 | # returns only the number field
185 | # TODO ***rebuild in option to not have to call MongoDB and add option to pull issues to get comments from directly from getIssues method
186 | def getIssueComments(issueNumber)
187 | # issuesWithComments = @coll.find({"comments" => {"$gt" => 0}},
188 | # {:fields => {"_id" => 0, "number" => 1}}
189 | # ).to_a
190 |
191 | issueComments = @ghClient.issue_comments(@repository.to_s, issueNumber.to_s)
192 | issueCommentsRaw = JSON.parse(@ghClient.last_response.body)
193 |
194 | ghLastReponse = @ghClient.last_response
195 |
196 | while ghLastReponse.rels.include?(:next) do
197 | ghLastReponse = ghLastReponse.rels[:next].get
198 | issueCommentsRaw.concat(JSON.parse(ghLastReponse.body))
199 | end
200 |
201 | issueCommentsRaw.each do |x|
202 | x["organizaion"] = @organization
203 | x["repo"] = @repository
204 | x["downloaded_at"] = Time.now
205 | x["issue_number"] = issueNumber
206 | self.convertIssueCommentDatesInMongo(x)
207 | end
208 | return issueCommentsRaw
209 | # @coll.update(
210 | # { "number" => x["number"]},
211 | # { "$push" => {"comments_Text" => self.convertIssueCommentDatesInMongo(commentDetails)}}
212 | # )
213 | end
214 |
215 | # TODO Setup so it will get all repo events since the last time a request was made
216 | def getRepositoryEvents
217 | respositoryEvents = @ghClient.repository_events(@repository.to_s)
218 | respositoryEventsRaw = JSON.parse(@ghClient.last_response.body)
219 |
220 | ghLastReponse = @ghClient.last_response
221 |
222 | while ghLastReponse.rels.include?(:next) do
223 | ghLastReponse = ghLastReponse.rels[:next].get
224 | respositoryEventsRaw.concat(JSON.parse(ghLastReponse.body))
225 | end
226 |
227 | # Debug Code
228 | # puts "Got Repository Events, GitHub rate limit remaining: " + @ghClient.rate_limit.remaining.to_s
229 |
230 | if respositoryEventsRaw.empty? == false
231 | respositoryEventsRaw.each do |y|
232 | y["organization"] = @organization
233 | y["repo"] = @repository
234 | y["downloaded_at"] = Time.now
235 | yDatesFixed = self.convertRepoEventsDates(y)
236 | self.putIntoMongoCollRepoEvents(yDatesFixed)
237 | end
238 | end
239 | end
240 |
241 | # TODO Setup so will get issues events since the last time they were downloaded
242 | # TODO Consider adding Issue Events directly into the Issue Object in Mongo
243 | def getIssueEvents (issueNumber)
244 |
245 | # issueNumbers = @coll.aggregate([
246 | # { "$project" => {number: 1}},
247 | # { "$group" => {_id: {number: "$number"}}},
248 | # { "$sort" => {"_id.number" => 1}}
249 | # ])
250 | issueEvents = @ghClient.issue_events(@repository, issueNumber)
251 | issueEventsRaw = JSON.parse(@ghClient.last_response.body)
252 |
253 | ghLastReponse = @ghClient.last_response
254 |
255 | while ghLastReponse.rels.include?(:next) do
256 | ghLastReponse = ghLastReponse.rels[:next].get
257 | issueEventsRaw.concat(JSON.parse(ghLastReponse.body))
258 | end
259 |
260 | if issueEventsRaw.empty? == false
261 | # Adds Repo and Issue number information into the hash of each event so multiple Repos can be stored in the same DB.
262 | # This was done becauase Issue Events do not have Issue number and Repo information.
263 | issueEventsRaw.each do |y|
264 | y["organization"] = @organization
265 | y["repo"] = @repository
266 | y["issue_number"] = issueNumber
267 | y["downloaded_at"] = Time.now
268 | yCorrectedDates = self.convertIssueEventsDates(y)
269 | self.putIntoMongoCollRepoIssuesEvents(yCorrectedDates)
270 | end
271 | end
272 |
273 | # Debug Code
274 | # puts "Got Repository Issue Events, GitHub rate limit remaining: " + @ghClient.rate_limit.remaining.to_s
275 | end
276 |
277 | # TODO This still needs work to function correctly. Need to add new collection in db and a way to handle variable for the specific org to get data from
278 | def getOrgMemberList
279 | orgMemberList = @ghClient.org_members(@organization.to_s)
280 | orgMemberListRaw = JSON.parse(@ghClient.last_response.body)
281 |
282 | # Debug Code
283 | # puts "Got Organization member list, Github rate limit remaining: " + @ghClient.rate_limit.remaining.to_s
284 |
285 | if orgMemberListRaw.empty? == false
286 | orgMemberListRaw.each do |y|
287 | y["organization"] = @organization
288 | y["repo"] = @repository
289 | y["downloaded_at"] = Time.now
290 | end
291 | orgMemberListRaw = self.putIntoMongoCollOrgMembers(orgMemberListRaw)
292 | return orgMemberListRaw
293 | end
294 | end
295 |
296 | def getOrgTeamsInfoAllList
297 | orgTeamsList = @ghClient.organization_teams(@organization.to_s)
298 | orgTeamsListRaw = JSON.parse(@ghClient.last_response.body)
299 |
300 | # Debug Code
301 | # puts " Got Organization Teams list, Github rate limit remaining: " + @ghClient.rate_limit.remaining.to_s
302 |
303 | if orgTeamsListRaw.empty? == false
304 | orgTeamsListRaw.each do |y|
305 | y["organization"] = @organization
306 | y["repo"] = @repository
307 | y["downloaded_at"] = Time.now
308 | y["team_info"] = self.getOrgTeamInfo(y["id"])
309 | y["team_members"] = self.getOrgTeamMembers(y["id"])
310 | y["team_repos"] = self.getOrgTeamRepos(y["id"])
311 | end
312 | orgTeamsListRaw = self.putIntoMongoCollOrgTeamsInfoAllList(orgTeamsListRaw)
313 | return orgTeamsListRaw
314 | end
315 | end
316 |
317 | def getOrgTeamInfo(teamId)
318 | orgTeamInfo = @ghClient.team(teamId)
319 | orgTeamsInfoRaw = JSON.parse(@ghClient.last_response.body)
320 |
321 | #Debug Code
322 | # puts "Got Team info for Team: #{teamId}, Github rate limit remaining: " + @ghClient.rate_limit.remaining.to_s
323 |
324 | if orgTeamsInfoRaw.empty? == false
325 | orgTeamsInfoRaw["organization"] = @organization
326 | orgTeamsInfoRaw["repo"] = @repository
327 | orgTeamsInfoRaw["downloaded_at"] = Time.now
328 | end
329 | return orgTeamsInfoRaw
330 | end
331 |
332 | def getOrgTeamMembers(teamId)
333 | orgTeamMembers = @ghClient.team_members(teamId)
334 | orgTeamMembersRaw = JSON.parse(@ghClient.last_response.body)
335 |
336 | # Debug Code
337 | # puts "Got members list of team: #{teamId}, Github rate limit remaining: " + @ghClient.rate_limit.remaining.to_s
338 |
339 | if orgTeamMembersRaw.empty? == false
340 | orgTeamMembersRaw.each do |y|
341 | y["organization"] = @organization
342 | y["repo"] = @repository
343 | y["downloaded_at"] = Time.now
344 | end
345 | end
346 | return orgTeamMembersRaw
347 | end
348 |
349 | def getOrgTeamRepos(teamId)
350 | orgTeamRepos = @ghClient.team_repositories(teamId)
351 | orgTeamReposRaw = JSON.parse(@ghClient.last_response.body)
352 | # Debug Code
353 | # puts "Got list of repos for team: #{teamId}, Github rate limit remaining: " + @ghClient.rate_limit.remaining.to_s
354 |
355 | if orgTeamReposRaw.empty? == false
356 | orgTeamReposRaw.each do |y|
357 | y["organization"] = @organization
358 | y["repo"] = @repository
359 | y["downloaded_at"] = Time.now
360 |
361 | end
362 | orgTeamReposRaw = self.convertTeamReposDates(orgTeamReposRaw)
363 | return orgTeamReposRaw
364 | end
365 |
366 | end
367 |
368 | def convertIssueCommentDatesInMongo(issueComments)
369 | issueComments["created_at"] = Time.strptime(issueComments["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
370 | issueComments["updated_at"] = Time.strptime(issueComments["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
371 | return issueComments
372 | end
373 |
374 | def convertIssueDatesForMongo(issues)
375 | issues["created_at"] = Time.strptime(issues["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
376 | issues["updated_at"] = Time.strptime(issues["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
377 | if issues["closed_at"] != nil
378 | issues["closed_at"] = Time.strptime(issues["closed_at"], '%Y-%m-%dT%H:%M:%S%z').utc
379 | end
380 | return issues
381 | end
382 |
383 | def convertRepoEventsDates(repoEvents)
384 | repoEvents["created_at"] = Time.strptime(repoEvents["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
385 | return repoEvents
386 | end
387 |
388 | def convertIssueEventsDates(issueEvents)
389 | issueEvents["created_at"] = Time.strptime(issueEvents["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
390 | return issueEvents
391 | end
392 |
393 | def convertMilestoneDates(milestone)
394 | milestone["created_at"] = Time.strptime(milestone["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
395 | milestone["updated_at"] = Time.strptime(milestone["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
396 | if milestone["due_on"]!= nil
397 | milestone["due_on"] = Time.strptime(milestone["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
398 | end
399 | return milestone
400 | end
401 |
402 | def convertTeamReposDates(teamRepos)
403 | teamRepos.each do |x|
404 | if x["created_at"] != nil
405 | x["created_at"] = Time.strptime(x["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
406 | end
407 | if x["updated_at"]!= nil
408 | x["updated_at"] = Time.strptime(x["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
409 | end
410 | if x["pushed_at"] != nil
411 | x["pushed_at"] = Time.strptime(x["pushed_at"], '%Y-%m-%dT%H:%M:%S%z').utc
412 | end
413 | end
414 | return teamRepos
415 | end
416 |
417 | def getMilestonesListforRepo
418 | # TODO build call to github to get list of milestones in a specific issue queue.
419 | # This will be used as part of the web app to select a milestone and return specific details filtered for that specific milestone.
420 | # Second option for cases were no Github.com access is avaliable will be to query mongodb to get a list of milestones from mongodb data.
421 | # This will be good for future needs when historical tracking is used to track changes in milestones or when milestone names are
422 | # changed or even deleted.
423 |
424 | repoOpenMilestoneList = @ghClient.list_milestones(@repository, :state => :open)
425 | repoOpenMilestoneListRaw = JSON.parse(@ghClient.last_response.body)
426 |
427 | ghLastReponseOpen = @ghClient.last_response
428 |
429 | while ghLastReponseOpen.rels.include?(:next) do
430 | ghLastReponseOpen = ghLastReponseOpen.rels[:next].get
431 | repoOpenMilestoneListRaw.concat(JSON.parse(ghLastReponseOpen.body))
432 | end
433 |
434 |
435 | repoClosedMilestoneList = @ghClient.list_milestones(@repository, :state => :closed)
436 | repoClosedMilestoneListRaw = JSON.parse(@ghClient.last_response.body)
437 |
438 | ghLastReponseClosed = @ghClient.last_response
439 |
440 | while ghLastReponseClosed.rels.include?(:next) do
441 | ghLastReponseClosed = ghLastReponseClosed.rels[:next].get
442 | repoClosedMilestoneListRaw.concat(JSON.parse(ghLastReponseClosed.body))
443 | end
444 |
445 | # Debug Code
446 | # puts "Got Open and Closed Milestones list, Github rate limit remaining: " + @ghClient.rate_limit.remaining.to_s
447 |
448 | if repoOpenMilestoneListRaw.empty? == false
449 | repoOpenMilestoneListRaw.each do |x|
450 | x["organization"] = @organization
451 | x["repo"] = @repository
452 | x["downloaded_at"] = Time.now
453 | xDatesFixed = self.convertMilestoneDates(x)
454 | self.putIntoMongoCollRepoMilestonesList(xDatesFixed)
455 | end
456 | end
457 | if repoClosedMilestoneListRaw.empty? == false
458 | repoClosedMilestoneListRaw.each do |y|
459 | y["organization"] = @organization
460 | y["repo"] = @repository
461 | y["downloaded_at"] = Time.now
462 | yDatesFixed = self.convertMilestoneDates(y)
463 | self.putIntoMongoCollRepoMilestonesList(yDatesFixed)
464 | end
465 | end
466 |
467 | # Debug Code but will eventually become output for web app
468 | if repoOpenMilestoneListRaw.empty? == true and repoClosedMilestoneListRaw.empty? == true
469 | puts "No Open or Closed Milestones"
470 | end
471 | end
472 |
473 | # Gets list of all Repos
474 | def getRepoLabelsList
475 | repoLabelsList = @ghClient.labels(@repository)
476 | repoLabelsListRaw = JSON.parse(@ghClient.last_response.body)
477 |
478 | # Debug Code
479 | # puts "Got Repo Labels list, Github rate limit remaining: " + @ghClient.rate_limit.remaining.to_s
480 |
481 | ghLastReponse = @ghClient.last_response
482 |
483 | while ghLastReponse.rels.include?(:next) do
484 | ghLastReponse = ghLastReponse.rels[:next].get
485 | repoLabelsListRaw.concat(JSON.parse(ghLastReponse.body))
486 | end
487 |
488 | if repoLabelsListRaw.empty? == false
489 | repoLabelsListRaw.each do |y|
490 | y["organization"] = @organization
491 | y["repo"] = @repository
492 | y["downloaded_at"] = Time.now
493 | end
494 | repoLabelsListRaw = self.putIntoMongoCollRepoLabelsList(repoLabelsListRaw)
495 | end
496 | return repoLabelsListRaw
497 | end
498 | end
499 |
500 | start = IssueDownload.new("wet-boew/wet-boew-wpss", :mongoClearRecords => true)
501 | # start = IssueDownload.new("stephenOTT/test1", :mongoClearRecords => true)
502 | # start = IssueDownload.new("wet-boew/wet-boew-drupal", :mongoClearRecords => true)
503 | # start = IssueDownload.new("StephenOTT/Test1", true)
504 | # start = IssueDownload.new("wet-boew/wet-boew-drupal")
505 |
506 | start.ghAuthenticate("USERNAME", "PASSWORD")
507 | start.getIssues
508 | # start.getRepositoryEvents
509 | # start.getOrgMemberList
510 | # start.getOrgTeamsInfoAllList
511 | # start.getRepoLabelsList
512 | # start.getMilestonesListforRepo
513 |
--------------------------------------------------------------------------------
/Ruby/src/gh-issue-orchestrator.rb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitHub-Analytics/7f464fa06da3d44da5f415a1d606b59d63b7a854/Ruby/src/gh-issue-orchestrator.rb
--------------------------------------------------------------------------------
/Ruby/src/public/chartkick.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Chartkick.js
3 | * Create beautiful Javascript charts with minimal code
4 | * https://github.com/ankane/chartkick.js
5 | * v1.1.0
6 | * MIT License
7 | */
8 |
9 | /*jslint browser: true, indent: 2, plusplus: true */
10 | /*global google, $*/
11 |
12 | (function() {
13 | 'use strict';
14 |
15 | // helpers
16 |
17 | function isArray(variable) {
18 | return Object.prototype.toString.call(variable) === "[object Array]";
19 | }
20 |
21 | function isPlainObject(variable) {
22 | return variable instanceof Object;
23 | }
24 |
25 | // https://github.com/madrobby/zepto/blob/master/src/zepto.js
26 | function extend(target, source) {
27 | var key;
28 | for (key in source) {
29 | if (isPlainObject(source[key]) || isArray(source[key])) {
30 | if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
31 | target[key] = {};
32 | }
33 | if (isArray(source[key]) && !isArray(target[key])) {
34 | target[key] = [];
35 | }
36 | extend(target[key], source[key]);
37 | }
38 | else if (source[key] !== undefined) {
39 | target[key] = source[key];
40 | }
41 | }
42 | }
43 |
44 | function merge(obj1, obj2) {
45 | var target = {};
46 | extend(target, obj1);
47 | extend(target, obj2);
48 | return target;
49 | }
50 |
51 | // https://github.com/Do/iso8601.js
52 | var ISO8601_PATTERN = /(\d\d\d\d)(\-)?(\d\d)(\-)?(\d\d)(T)?(\d\d)(:)?(\d\d)?(:)?(\d\d)?([\.,]\d+)?($|Z|([\+\-])(\d\d)(:)?(\d\d)?)/i;
53 | var DECIMAL_SEPARATOR = String(1.5).charAt(1);
54 |
55 | function parseISO8601(input) {
56 | var day, hour, matches, milliseconds, minutes, month, offset, result, seconds, type, year;
57 | type = Object.prototype.toString.call(input);
58 | if (type === '[object Date]') {
59 | return input;
60 | }
61 | if (type !== '[object String]') {
62 | return;
63 | }
64 | if (matches = input.match(ISO8601_PATTERN)) {
65 | year = parseInt(matches[1], 10);
66 | month = parseInt(matches[3], 10) - 1;
67 | day = parseInt(matches[5], 10);
68 | hour = parseInt(matches[7], 10);
69 | minutes = matches[9] ? parseInt(matches[9], 10) : 0;
70 | seconds = matches[11] ? parseInt(matches[11], 10) : 0;
71 | milliseconds = matches[12] ? parseFloat(DECIMAL_SEPARATOR + matches[12].slice(1)) * 1000 : 0;
72 | result = Date.UTC(year, month, day, hour, minutes, seconds, milliseconds);
73 | if (matches[13] && matches[14]) {
74 | offset = matches[15] * 60;
75 | if (matches[17]) {
76 | offset += parseInt(matches[17], 10);
77 | }
78 | offset *= matches[14] === '-' ? -1 : 1;
79 | result -= offset * 60 * 1000;
80 | }
81 | return new Date(result);
82 | }
83 | }
84 | // end iso8601.js
85 |
86 | function negativeValues(series) {
87 | var i, j, data;
88 | for (i = 0; i < series.length; i++) {
89 | data = series[i].data;
90 | for (j = 0; j < data.length; j++) {
91 | if (data[j][1] < 0) {
92 | return true;
93 | }
94 | }
95 | }
96 | return false;
97 | }
98 |
99 | function jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax) {
100 | return function(series, opts, chartOptions) {
101 | var options = merge({}, defaultOptions);
102 | options = merge(options, chartOptions || {});
103 |
104 | // hide legend
105 | // this is *not* an external option!
106 | if (opts.hideLegend) {
107 | hideLegend(options);
108 | }
109 |
110 | // min
111 | if ("min" in opts) {
112 | setMin(options, opts.min);
113 | }
114 | else if (!negativeValues(series)) {
115 | setMin(options, 0);
116 | }
117 |
118 | // max
119 | if ("max" in opts) {
120 | setMax(options, opts.max);
121 | }
122 |
123 | // merge library last
124 | options = merge(options, opts.library || {});
125 |
126 | return options;
127 | };
128 | }
129 |
130 | // only functions that need defined specific to charting library
131 | var renderLineChart, renderPieChart, renderColumnChart, renderBarChart, renderAreaChart;
132 |
133 | if ("Highcharts" in window) {
134 |
135 | var defaultOptions = {
136 | chart: {},
137 | xAxis: {
138 | labels: {
139 | style: {
140 | fontSize: "12px"
141 | }
142 | }
143 | },
144 | yAxis: {
145 | title: {
146 | text: null
147 | },
148 | labels: {
149 | style: {
150 | fontSize: "12px"
151 | }
152 | }
153 | },
154 | title: {
155 | text: null
156 | },
157 | credits: {
158 | enabled: false
159 | },
160 | legend: {
161 | borderWidth: 0
162 | },
163 | tooltip: {
164 | style: {
165 | fontSize: "12px"
166 | }
167 | },
168 | plotOptions: {
169 | areaspline: {},
170 | series: {
171 | marker: {}
172 | }
173 | }
174 | };
175 |
176 | var hideLegend = function(options) {
177 | options.legend.enabled = false;
178 | };
179 |
180 | var setMin = function(options, min) {
181 | options.yAxis.min = min;
182 | };
183 |
184 | var setMax = function(options, max) {
185 | options.yAxis.max = max;
186 | };
187 |
188 | var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax);
189 |
190 | renderLineChart = function(element, series, opts, chartType) {
191 | chartType = chartType || "spline";
192 | var chartOptions = {};
193 | if (chartType === "areaspline") {
194 | chartOptions = {
195 | plotOptions: {
196 | areaspline: {
197 | stacking: "normal"
198 | },
199 | series: {
200 | marker: {
201 | enabled: false
202 | }
203 | }
204 | }
205 | };
206 | }
207 | var options = jsOptions(series, opts, chartOptions), data, i, j;
208 | options.xAxis.type = "datetime";
209 | options.chart.type = chartType;
210 | options.chart.renderTo = element.id;
211 |
212 | for (i = 0; i < series.length; i++) {
213 | data = series[i].data;
214 | for (j = 0; j < data.length; j++) {
215 | data[j][0] = data[j][0].getTime();
216 | }
217 | series[i].marker = {symbol: "circle"};
218 | }
219 | options.series = series;
220 | new Highcharts.Chart(options);
221 | };
222 |
223 | renderPieChart = function(element, series, opts) {
224 | var options = merge(defaultOptions, opts.library || {});
225 | options.chart.renderTo = element.id;
226 | options.series = [{
227 | type: "pie",
228 | name: "Value",
229 | data: series
230 | }];
231 | new Highcharts.Chart(options);
232 | };
233 |
234 | renderColumnChart = function(element, series, opts, chartType) {
235 | chartType = chartType || "column";
236 | var options = jsOptions(series, opts), i, j, s, d, rows = [];
237 | options.chart.type = chartType;
238 | options.chart.renderTo = element.id;
239 |
240 | for (i = 0; i < series.length; i++) {
241 | s = series[i];
242 |
243 | for (j = 0; j < s.data.length; j++) {
244 | d = s.data[j];
245 | if (!rows[d[0]]) {
246 | rows[d[0]] = new Array(series.length);
247 | }
248 | rows[d[0]][i] = d[1];
249 | }
250 | }
251 |
252 | var categories = [];
253 | for (i in rows) {
254 | if (rows.hasOwnProperty(i)) {
255 | categories.push(i);
256 | }
257 | }
258 | options.xAxis.categories = categories;
259 |
260 | var newSeries = [];
261 | for (i = 0; i < series.length; i++) {
262 | d = [];
263 | for (j = 0; j < categories.length; j++) {
264 | d.push(rows[categories[j]][i] || 0);
265 | }
266 |
267 | newSeries.push({
268 | name: series[i].name,
269 | data: d
270 | });
271 | }
272 | options.series = newSeries;
273 |
274 | new Highcharts.Chart(options);
275 | };
276 |
277 | renderBarChart = function(element, series, opts) {
278 | renderColumnChart(element, series, opts, "bar");
279 | };
280 |
281 | renderAreaChart = function(element, series, opts) {
282 | renderLineChart(element, series, opts, "areaspline");
283 | };
284 | } else if ("google" in window) { // Google charts
285 | // load from google
286 | var loaded = false;
287 | google.setOnLoadCallback(function() {
288 | loaded = true;
289 | });
290 | google.load("visualization", "1.0", {"packages": ["corechart"]});
291 |
292 | var waitForLoaded = function(callback) {
293 | google.setOnLoadCallback(callback); // always do this to prevent race conditions (watch out for other issues due to this)
294 | if (loaded) {
295 | callback();
296 | }
297 | };
298 |
299 | // Set chart options
300 | var defaultOptions = {
301 | chartArea: {},
302 | fontName: "'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, Helvetica, sans-serif",
303 | pointSize: 6,
304 | legend: {
305 | textStyle: {
306 | fontSize: 12,
307 | color: "#444"
308 | },
309 | alignment: "center",
310 | position: "right"
311 | },
312 | curveType: "function",
313 | hAxis: {
314 | textStyle: {
315 | color: "#666",
316 | fontSize: 12
317 | },
318 | gridlines: {
319 | color: "transparent"
320 | },
321 | baselineColor: "#ccc",
322 | viewWindow: {}
323 | },
324 | vAxis: {
325 | textStyle: {
326 | color: "#666",
327 | fontSize: 12
328 | },
329 | baselineColor: "#ccc",
330 | viewWindow: {}
331 | },
332 | tooltip: {
333 | textStyle: {
334 | color: "#666",
335 | fontSize: 12
336 | }
337 | }
338 | };
339 |
340 | var hideLegend = function(options) {
341 | options.legend.position = "none";
342 | };
343 |
344 | var setMin = function(options, min) {
345 | options.vAxis.viewWindow.min = min;
346 | };
347 |
348 | var setMax = function(options, max) {
349 | options.vAxis.viewWindow.max = max;
350 | };
351 |
352 | var setBarMin = function(options, min) {
353 | options.hAxis.viewWindow.min = min;
354 | };
355 |
356 | var setBarMax = function(options, max) {
357 | options.hAxis.viewWindow.max = max;
358 | };
359 |
360 | var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax);
361 |
362 | // cant use object as key
363 | var createDataTable = function(series, columnType) {
364 | var data = new google.visualization.DataTable();
365 | data.addColumn(columnType, "");
366 |
367 | var i, j, s, d, key, rows = [];
368 | for (i = 0; i < series.length; i++) {
369 | s = series[i];
370 | data.addColumn("number", s.name);
371 |
372 | for (j = 0; j < s.data.length; j++) {
373 | d = s.data[j];
374 | key = (columnType === "datetime") ? d[0].getTime() : d[0];
375 | if (!rows[key]) {
376 | rows[key] = new Array(series.length);
377 | }
378 | rows[key][i] = toFloat(d[1]);
379 | }
380 | }
381 |
382 | var rows2 = [];
383 | for (i in rows) {
384 | if (rows.hasOwnProperty(i)) {
385 | rows2.push([(columnType === "datetime") ? new Date(toFloat(i)) : i].concat(rows[i]));
386 | }
387 | }
388 | if (columnType === "datetime") {
389 | rows2.sort(sortByTime);
390 | }
391 | data.addRows(rows2);
392 |
393 | return data;
394 | };
395 |
396 | var resize = function(callback) {
397 | if (window.attachEvent) {
398 | window.attachEvent("onresize", callback);
399 | }
400 | else if (window.addEventListener) {
401 | window.addEventListener("resize", callback, true);
402 | }
403 | callback();
404 | };
405 |
406 | renderLineChart = function(element, series, opts) {
407 | waitForLoaded(function() {
408 | var options = jsOptions(series, opts);
409 | var data = createDataTable(series, "datetime");
410 | var chart = new google.visualization.LineChart(element);
411 | resize( function() {
412 | chart.draw(data, options);
413 | });
414 | });
415 | };
416 |
417 | renderPieChart = function(element, series, opts) {
418 | waitForLoaded(function() {
419 | var chartOptions = {
420 | chartArea: {
421 | top: "10%",
422 | height: "80%"
423 | }
424 | };
425 | var options = merge(merge(defaultOptions, chartOptions), opts.library || {});
426 |
427 | var data = new google.visualization.DataTable();
428 | data.addColumn("string", "");
429 | data.addColumn("number", "Value");
430 | data.addRows(series);
431 |
432 | var chart = new google.visualization.PieChart(element);
433 | resize( function() {
434 | chart.draw(data, options);
435 | });
436 | });
437 | };
438 |
439 | renderColumnChart = function(element, series, opts) {
440 | waitForLoaded(function() {
441 | var options = jsOptions(series, opts);
442 | var data = createDataTable(series, "string");
443 | var chart = new google.visualization.ColumnChart(element);
444 | resize( function() {
445 | chart.draw(data, options);
446 | });
447 | });
448 | };
449 |
450 | renderBarChart = function(element, series, opts) {
451 | waitForLoaded(function() {
452 | var chartOptions = {
453 | hAxis: {
454 | gridlines: {
455 | color: "#ccc"
456 | }
457 | }
458 | };
459 | var options = jsOptionsFunc(defaultOptions, hideLegend, setBarMin, setBarMax)(series, opts, chartOptions);
460 | var data = createDataTable(series, "string");
461 | var chart = new google.visualization.BarChart(element);
462 | resize( function() {
463 | chart.draw(data, options);
464 | });
465 | });
466 | };
467 |
468 | renderAreaChart = function(element, series, opts) {
469 | waitForLoaded(function() {
470 | var chartOptions = {
471 | isStacked: true,
472 | pointSize: 0,
473 | areaOpacity: 0.5
474 | };
475 | var options = jsOptions(series, opts, chartOptions);
476 | var data = createDataTable(series, "datetime");
477 | var chart = new google.visualization.AreaChart(element);
478 | resize( function() {
479 | chart.draw(data, options);
480 | });
481 | });
482 | };
483 | } else { // no chart library installed
484 | renderLineChart = renderPieChart = renderColumnChart = renderBarChart = renderAreaChart = function() {
485 | throw new Error("Please install Google Charts or Highcharts");
486 | };
487 | }
488 |
489 | function setText(element, text) {
490 | if (document.body.innerText) {
491 | element.innerText = text;
492 | } else {
493 | element.textContent = text;
494 | }
495 | }
496 |
497 | function chartError(element, message) {
498 | setText(element, "Error Loading Chart: " + message);
499 | element.style.color = "#ff0000";
500 | }
501 |
502 | function getJSON(element, url, success) {
503 | $.ajax({
504 | dataType: "json",
505 | url: url,
506 | success: success,
507 | error: function(jqXHR, textStatus, errorThrown) {
508 | var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message;
509 | chartError(element, message);
510 | }
511 | });
512 | }
513 |
514 | function errorCatcher(element, data, opts, callback) {
515 | try {
516 | callback(element, data, opts);
517 | } catch (err) {
518 | chartError(element, err.message);
519 | throw err;
520 | }
521 | }
522 |
523 | function fetchDataSource(element, dataSource, opts, callback) {
524 | if (typeof dataSource === "string") {
525 | getJSON(element, dataSource, function(data, textStatus, jqXHR) {
526 | errorCatcher(element, data, opts, callback);
527 | });
528 | } else {
529 | errorCatcher(element, dataSource, opts, callback);
530 | }
531 | }
532 |
533 | // type conversions
534 |
535 | function toStr(n) {
536 | return "" + n;
537 | }
538 |
539 | function toFloat(n) {
540 | return parseFloat(n);
541 | }
542 |
543 | function toDate(n) {
544 | if (typeof n !== "object") {
545 | if (typeof n === "number") {
546 | n = new Date(n * 1000); // ms
547 | } else { // str
548 | // try our best to get the str into iso8601
549 | // TODO be smarter about this
550 | var str = n.replace(/ /, "T").replace(" ", "").replace("UTC", "Z");
551 | n = parseISO8601(str) || new Date(n);
552 | }
553 | }
554 | return n;
555 | }
556 |
557 | function toArr(n) {
558 | if (!isArray(n)) {
559 | var arr = [], i;
560 | for (i in n) {
561 | if (n.hasOwnProperty(i)) {
562 | arr.push([i, n[i]]);
563 | }
564 | }
565 | n = arr;
566 | }
567 | return n;
568 | }
569 |
570 | // process data
571 |
572 | function sortByTime(a, b) {
573 | return a[0].getTime() - b[0].getTime();
574 | }
575 |
576 | function processSeries(series, opts, time) {
577 | var i, j, data, r, key;
578 |
579 | // see if one series or multiple
580 | if (!isArray(series) || typeof series[0] !== "object" || isArray(series[0])) {
581 | series = [{name: "Value", data: series}];
582 | opts.hideLegend = true;
583 | } else {
584 | opts.hideLegend = false;
585 | }
586 |
587 | // right format
588 | for (i = 0; i < series.length; i++) {
589 | data = toArr(series[i].data);
590 | r = [];
591 | for (j = 0; j < data.length; j++) {
592 | key = data[j][0];
593 | key = time ? toDate(key) : toStr(key);
594 | r.push([key, toFloat(data[j][1])]);
595 | }
596 | if (time) {
597 | r.sort(sortByTime);
598 | }
599 | series[i].data = r;
600 | }
601 |
602 | return series;
603 | }
604 |
605 | function processLineData(element, data, opts) {
606 | renderLineChart(element, processSeries(data, opts, true), opts);
607 | }
608 |
609 | function processColumnData(element, data, opts) {
610 | renderColumnChart(element, processSeries(data, opts, false), opts);
611 | }
612 |
613 | function processPieData(element, data, opts) {
614 | var perfectData = toArr(data), i;
615 | for (i = 0; i < perfectData.length; i++) {
616 | perfectData[i] = [toStr(perfectData[i][0]), toFloat(perfectData[i][1])];
617 | }
618 | renderPieChart(element, perfectData, opts);
619 | }
620 |
621 | function processBarData(element, data, opts) {
622 | renderBarChart(element, processSeries(data, opts, false), opts);
623 | }
624 |
625 | function processAreaData(element, data, opts) {
626 | renderAreaChart(element, processSeries(data, opts, true), opts);
627 | }
628 |
629 | function setElement(element, data, opts, callback) {
630 | if (typeof element === "string") {
631 | element = document.getElementById(element);
632 | }
633 | fetchDataSource(element, data, opts || {}, callback);
634 | }
635 |
636 | // define classes
637 |
638 | var Chartkick = {
639 | LineChart: function(element, dataSource, opts) {
640 | setElement(element, dataSource, opts, processLineData);
641 | },
642 | PieChart: function(element, dataSource, opts) {
643 | setElement(element, dataSource, opts, processPieData);
644 | },
645 | ColumnChart: function(element, dataSource, opts) {
646 | setElement(element, dataSource, opts, processColumnData);
647 | },
648 | BarChart: function(element, dataSource, opts) {
649 | setElement(element, dataSource, opts, processBarData);
650 | },
651 | AreaChart: function(element, dataSource, opts) {
652 | setElement(element, dataSource, opts, processAreaData);
653 | }
654 | };
655 |
656 | window.Chartkick = Chartkick;
657 | })();
658 |
--------------------------------------------------------------------------------
/Ruby/src/views/index.erb:
--------------------------------------------------------------------------------
1 | <%= @eventstext %>
2 |
3 |
4 | This is <%= @foo %>
5 |
6 |
7 |
8 |
9 | Events Types Count
10 | <%= pie_chart(@eventTypesCount) %>
11 |
12 |
13 | Issue Events Types Count
14 | <%= pie_chart(@issueEventTypesCount) %>
15 |
16 |
17 | Issue Event Types Timeline
18 | <%= line_chart([
19 | {:name => "Closed", :data => @issuesClosedEventHash},
20 | {:name => "Reopened", :data => @issuesReopenedEventHash},
21 | {:name => "Subscribed", :data => @issuesSubscribedEventHash},
22 | {:name => "Merged", :data => @issuesMergedEventHash},
23 | {:name => "Referenced", :data => @issuesReferencedEventHash},
24 | {:name => "Mentioned", :data => @issuesMentionedEventHash},
25 | {:name => "Assigned", :data => @issuesAssignedEventHash},
26 | ],:library => {:hAxis => {:format => 'MMM y'}})%>
27 |
28 |
29 | Events Occurance Timeline
30 | <%= line_chart([
31 | {:name => "Create", :data => @createEvent},
32 | {:name => "Fork", :data => @forkEvent},
33 | {:name => "Release", :data => @releaseEvent},
34 | {:name => "Issue Comment", :data => @issueCommentEvent},
35 | {:name => "Watch", :data => @watchEvent},
36 | {:name => "Issues", :data => @issuesEvent},
37 | {:name => "Push", :data => @pushEvent},
38 | {:name => "Commit Comment", :data => @commitCommentEvent},
39 | {:name => "Pull Request", :data => @pullRequestEvent},
40 | ],:library => {:hAxis => {:format => 'MMM y'}})
41 | %>
42 |
43 |
44 | Issues Created and Closed Per Month Count
45 | <%= line_chart([
46 | {:name => "Open", :data => @issuesCreatedMonthCount},
47 | {:name => "Closed", :data => @issuesClosedMonthCount},
48 | ],:library => {:hAxis => {:format => 'MMM y'}}) # TODO write blog post about dealing with the library function. Add doc notes to Chartkick about accessing subfunctions.
49 | %>
50 | <%= @issuesOpenCount %> open issues remain unaccounted in the Closed data series because these issues don't have a closed date value
51 |
52 |
53 |
54 | Issues Count Assigned to each user
55 | <%= column_chart([
56 | {:name => "Open", :data => @issueAssignedPerUserOpenCount},
57 | {:name => "Closed", :data => @issueAssignedPerUserClosedCount},
58 | ]) %>
59 |
60 |
61 | Printable Issues List
62 |
63 |
64 | State |
65 | Issue Number |
66 | Milestone |
67 | Issue Title |
68 | Assignee |
69 | Created Date |
70 | Closed Date |
71 | Created By |
72 | Comment Count |
73 | Year to Date Comment Activity |
74 |
75 |
76 | <% @printableData.each do |elem| %>
77 |
78 |
79 | <%= elem["issueCurrentState"] %>
80 | |
81 |
82 | <%= elem["issueNumber"] %>
83 | |
84 |
85 | <%= elem["issueAssignedMilestone"] %>
86 | |
87 |
88 | <%= elem["issueTitle"] %>
89 | |
90 |
91 | <%= elem["issueCurrentAssignee"] %>
92 | |
93 |
94 | <%= elem["created_at"] %>
95 | |
96 |
97 | <%= elem["closed_at"] %>
98 | |
99 |
100 | <%= elem["createdBy"] %> " height="30" width="30">
101 | |
102 |
103 | <%= elem["commentsCount"] %>
104 | |
105 |
106 | ">
107 | |
108 |
109 | <% end %>
110 |
111 |
--------------------------------------------------------------------------------
/Ruby/src/views/layout.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Sinatra Sorcery
5 |
6 |
7 |
8 |
9 |
10 | <%= yield %>
11 |
12 |
--------------------------------------------------------------------------------
/app/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in sinatra_auth_github.gemspec
4 | gemspec
5 |
6 | # vim:ft=ruby
7 |
--------------------------------------------------------------------------------
/app/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | GitHub-Analytics Web App (1.0.0)
5 | activesupport (~> 4.1.0)
6 | bson_ext
7 | chartkick
8 | chronic_duration
9 | mongo
10 | sinatra (~> 1.0)
11 | sinatra-flash (~> 0.3.0)
12 | warden-github (~> 1.0)
13 |
14 | GEM
15 | remote: http://rubygems.org/
16 | specs:
17 | activesupport (4.1.0)
18 | i18n (~> 0.6, >= 0.6.9)
19 | json (~> 1.7, >= 1.7.7)
20 | minitest (~> 5.1)
21 | thread_safe (~> 0.1)
22 | tzinfo (~> 1.1)
23 | addressable (2.3.6)
24 | bson (1.10.0)
25 | bson_ext (1.10.0)
26 | bson (~> 1.10.0)
27 | chartkick (1.2.4)
28 | chronic_duration (0.10.4)
29 | numerizer (~> 0.1.1)
30 | diff-lcs (1.1.3)
31 | faraday (0.9.0)
32 | multipart-post (>= 1.2, < 3)
33 | i18n (0.6.9)
34 | json (1.8.1)
35 | minitest (5.3.3)
36 | mongo (1.10.0)
37 | bson (~> 1.10.0)
38 | multipart-post (2.0.0)
39 | numerizer (0.1.1)
40 | octokit (3.0.0)
41 | sawyer (~> 0.5.3)
42 | rack (1.5.2)
43 | rack-protection (1.5.3)
44 | rack
45 | rack-test (0.5.7)
46 | rack (>= 1.0)
47 | rake (10.1.1)
48 | randexp (0.1.7)
49 | rspec (2.4.0)
50 | rspec-core (~> 2.4.0)
51 | rspec-expectations (~> 2.4.0)
52 | rspec-mocks (~> 2.4.0)
53 | rspec-core (2.4.0)
54 | rspec-expectations (2.4.0)
55 | diff-lcs (~> 1.1.2)
56 | rspec-mocks (2.4.0)
57 | sawyer (0.5.4)
58 | addressable (~> 2.3.5)
59 | faraday (~> 0.8, < 0.10)
60 | shotgun (0.9)
61 | rack (>= 1.0)
62 | sinatra (1.4.5)
63 | rack (~> 1.4)
64 | rack-protection (~> 1.4)
65 | tilt (~> 1.3, >= 1.3.4)
66 | sinatra-flash (0.3.0)
67 | sinatra (>= 1.0.0)
68 | thread_safe (0.3.3)
69 | tilt (1.4.1)
70 | tzinfo (1.1.0)
71 | thread_safe (~> 0.1)
72 | warden (1.2.3)
73 | rack (>= 1.0)
74 | warden-github (1.0.1)
75 | octokit (> 2.1.0)
76 | warden (> 1.0)
77 |
78 | PLATFORMS
79 | ruby
80 |
81 | DEPENDENCIES
82 | GitHub-Analytics Web App!
83 | rack-test (~> 0.5.3)
84 | rake
85 | randexp (~> 0.1.5)
86 | rspec (~> 2.4.0)
87 | shotgun
88 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # How to run the Web App:
2 |
3 | 1. Register/Create a Application at https://github.com/settings/applications/new. Set your fields to the following:
4 |
5 | 1.1. Homepage URL: `http://localhost:9292`
6 |
7 | 1.2. Authorization callback URL: `http://localhost:9292/auth/github/callback`
8 |
9 | 1.3. Application Name: `GitHub-Analytics` or whatever you want to call your application.
10 |
11 | 2. Install MongoDB (typically: `brew update`, followed by: `brew install mongodb`)
12 |
13 | 3. `cd` into the `app` folder and run the following commands in the `app` folder:
14 |
15 | 3.1. Run `mongod` in terminal
16 |
17 | 3.2. Open a second terminal window and run: `bundle install`
18 |
19 | 3.3.`GITHUB_CLIENT_ID="YOUR CLIENT ID" GITHUB_CLIENT_SECRET="YOUR CLIENT SECRET" bundle exec rackup`
20 | Get the Client ID and Client Secret from the settings of your created/registered GitHub Application in Step 1.
21 |
22 | 4. Go to `http://localhost:9292`
--------------------------------------------------------------------------------
/app/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems/package_task'
2 | require 'rubygems/specification'
3 | require 'date'
4 | require 'bundler'
5 |
6 | task :default => [:spec]
7 |
8 | require 'rspec/core/rake_task'
9 | desc "Run specs"
10 | RSpec::Core::RakeTask.new do |t|
11 | t.pattern = 'spec/**/*_spec.rb'
12 | end
13 |
--------------------------------------------------------------------------------
/app/app.rb:
--------------------------------------------------------------------------------
1 | require_relative 'sinatra_helpers'
2 | require 'chartkick'
3 |
4 | module Example
5 | class App < Sinatra::Base
6 | enable :sessions
7 | register Sinatra::Flash
8 |
9 | set :github_options, {
10 | :scopes => ["repo, user"],
11 | :secret => ENV['GITHUB_CLIENT_SECRET'],
12 | :client_id => ENV['GITHUB_CLIENT_ID'],
13 | }
14 |
15 | register Sinatra::Auth::Github
16 |
17 | helpers do
18 |
19 | def get_auth_info
20 | authInfo = {:username => github_user.login, :userID => github_user.id}
21 | end
22 |
23 | def flash_types
24 | [:danger, :warning, :info, :success]
25 | end
26 | end
27 |
28 | get '/' do
29 | # authenticate!
30 | if authenticated? == true
31 | @username = github_user.login
32 | @gravatar_id = github_user.gravatar_id
33 | @fullName = github_user.name
34 | @userID = github_user.id
35 |
36 | erb :index
37 |
38 | else
39 | erb :unauthenticated
40 | end
41 |
42 | end
43 |
44 | get '/repos' do
45 | if authenticated? == true
46 | @reposList = Sinatra_Helpers.get_all_repos_for_logged_user(get_auth_info)
47 | erb :repos_listing
48 | else
49 | flash[:danger] = "You must be logged in"
50 | erb :unauthenticated
51 | end
52 | end
53 |
54 | get '/download' do
55 | if authenticated? == true
56 | erb :download
57 | else
58 | # TODO: This needs work as it is not loading the message by the time the page loads.
59 | flash[:danger] = "You must be logged in"
60 | redirect '/'
61 |
62 | end
63 | end
64 |
65 | get '/download/:user/:repo' do
66 | # authenticate!
67 | if authenticated? == true
68 | @username = github_user.login
69 | @userID = github_user.id
70 |
71 | Sinatra_Helpers.download_github_analytics_data(params['user'], params['repo'], github_api, get_auth_info )
72 | flash[:success] = "GitHub Data downloaded successfully"
73 | redirect '/download'
74 | else
75 | redirect '/download'
76 | end
77 | end
78 |
79 | get '/analyze/issues/:user/:repo' do
80 | # authenticate!
81 | if authenticated? == true
82 |
83 | @issuesOpenedPerUser = Sinatra_Helpers.analyze_issues_opened_per_user(params['user'], params['repo'], get_auth_info )
84 | @issuesOpenedPerUserChartReady ={}
85 |
86 | @issuesOpenedPerUser.each do |i|
87 | @issuesOpenedPerUserChartReady[i["user"]] = i["issues_opened_count"]
88 | end
89 |
90 | erb :analyze_issues_opened_per_user
91 | else
92 | redirect '/'
93 | end
94 | end
95 |
96 | get '/analyze/labels/:user/:repo' do
97 | # authenticate!
98 | if authenticated? == true
99 |
100 | @labelsCountForRepo = Sinatra_Helpers.analyze_labels_count_per_repo(params['user'], params['repo'], get_auth_info )
101 | @labelsCountForRepoChartReady ={}
102 |
103 | @labelsCountForRepo.each do |l|
104 | @labelsCountForRepoChartReady[l["label"]] = l["count"]
105 | end
106 |
107 | erb :analyze_labels_for_repo
108 | else
109 | redirect '/'
110 | end
111 | end
112 |
113 |
114 | get '/analyze/issues/events/:user/:repo' do
115 | # authenticate!
116 | if authenticated? == true
117 |
118 | @repoIssueEvents = Sinatra_Helpers.analyze_repo_issues_Events_per_month(params['user'], params['repo'], get_auth_info )
119 | # @repoIssueEventsChartReady ={}
120 |
121 | # @repoIssueEvents.each do |l|
122 | # @repoIssueEventsChartReady[l["events"]] = l["count"]
123 | # end
124 |
125 | erb :analyze_repo_issue_events
126 | else
127 | redirect '/'
128 | end
129 | end
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | get '/analyze/issues/statetimeline/:user/:repo' do
138 | # authenticate!
139 | if authenticated? == true
140 |
141 | @issuesOpenedPerMonth = Sinatra_Helpers.analyze_issues_opened_per_month(params['user'], params['repo'], get_auth_info)
142 | @issuesClosedPerMonth = Sinatra_Helpers.analyze_issues_closed_per_month(params['user'], params['repo'], get_auth_info)
143 | @issuesOpenedPerMonthChartReady ={}
144 | @issuesClosedPerMonthChartReady ={}
145 |
146 | @issuesOpenedPerMonth.each do |i|
147 | @issuesOpenedPerMonthChartReady[i["converted_date"].strftime("%b %Y")] = i["count"]
148 | end
149 |
150 | @issuesClosedPerMonth.each do |i|
151 | @issuesClosedPerMonthChartReady[i["converted_date"].strftime("%b %Y")] = i["count"]
152 | end
153 |
154 |
155 |
156 | @issuesOpenedPerWeek = Sinatra_Helpers.analyze_issues_opened_per_week(params['user'], params['repo'], get_auth_info)
157 | @issuesClosedPerWeek = Sinatra_Helpers.analyze_issues_closed_per_week(params['user'], params['repo'], get_auth_info)
158 | @issuesOpenedPerWeekChartReady ={}
159 | @issuesClosedPerWeekChartReady ={}
160 |
161 | @issuesOpenedPerWeek.each do |i|
162 | @issuesOpenedPerWeekChartReady["Week #{i["converted_date"].strftime("%U, %Y")}"] = i["count"]
163 | end
164 |
165 | @issuesClosedPerWeek.each do |i|
166 | @issuesClosedPerWeekChartReady["Week #{i["converted_date"].strftime("%U, %Y")}"] = i["count"]
167 | end
168 |
169 |
170 |
171 |
172 |
173 | erb :analyze_issues_opened_closed_per_month
174 | else
175 | redirect '/'
176 | end
177 | end
178 |
179 |
180 |
181 | get '/logout' do
182 | logout!
183 | redirect '/'
184 | end
185 | get '/login' do
186 | authenticate!
187 | redirect '/'
188 | end
189 | end
190 | end
--------------------------------------------------------------------------------
/app/config.ru:
--------------------------------------------------------------------------------
1 | ENV['RACK_ENV'] ||= 'development'
2 | require "rubygems"
3 | require "bundler/setup"
4 |
5 | $LOAD_PATH << File.dirname(__FILE__) + '/lib'
6 | require File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'sinatra_auth_github'))
7 | require File.expand_path(File.join(File.dirname(__FILE__), 'app'))
8 |
9 | use Rack::Static, :urls => ["/css", "/img", "/js"], :root => "public"
10 |
11 | run Example::App
12 |
13 | # vim:ft=ruby
14 |
--------------------------------------------------------------------------------
/app/lib/sinatra/auth/github.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra/base'
2 | require 'sinatra/flash'
3 | require 'warden/github'
4 |
5 | module Sinatra
6 | module Auth
7 | module Github
8 | # Simple way to serve an image early in the stack and not get blocked by
9 | # application level before filters
10 | class AccessDenied < Sinatra::Base
11 | enable :raise_errors
12 | disable :show_exceptions
13 |
14 | get '/_images/securocat.png' do
15 | send_file(File.join(File.dirname(__FILE__), "views", "securocat.png"))
16 | end
17 | end
18 |
19 | # The default failure application, this is overridable from the extension config
20 | class BadAuthentication < Sinatra::Base
21 | enable :raise_errors
22 | disable :show_exceptions
23 |
24 | helpers do
25 | def unauthorized_template
26 | @unauthenticated_template ||= File.read(File.join(File.dirname(__FILE__), "views", "401.html"))
27 | end
28 | end
29 |
30 | get '/unauthenticated' do
31 | status 403
32 | unauthorized_template
33 | end
34 | end
35 |
36 | module Helpers
37 | def warden
38 | env['warden']
39 | end
40 |
41 | def authenticate!(*args)
42 | warden.authenticate!(*args)
43 | end
44 |
45 | def authenticated?(*args)
46 | warden.authenticated?(*args)
47 | end
48 |
49 | def logout!
50 | warden.logout
51 | end
52 |
53 | # The authenticated user object
54 | #
55 | # Supports a variety of methods, name, full_name, email, etc
56 | def github_user
57 | warden.user
58 | end
59 |
60 | def github_api
61 | github_user.api
62 | end
63 |
64 | # Send a V3 API GET request to path
65 | #
66 | # path - the path on api.github.com to hit
67 | #
68 | # Returns a rest client response object
69 | #
70 | # Examples
71 | # github_raw_request("/user")
72 | # # => RestClient::Response
73 | def github_raw_request(path)
74 | github_user.github_raw_request(path)
75 | end
76 |
77 | # Send a V3 API GET request to path and parse the response body
78 | #
79 | # path - the path on api.github.com to hit
80 | #
81 | # Returns a parsed JSON response
82 | #
83 | # Examples
84 | # github_request("/user")
85 | # # => { 'login' => 'atmos', ... }
86 | def github_request(path)
87 | github_user.github_request(path)
88 | end
89 |
90 | # See if the user is a public member of the named organization
91 | #
92 | # name - the organization name
93 | #
94 | # Returns: true if the user is public access, false otherwise
95 | def github_public_organization_access?(name)
96 | github_user.publicized_organization_member?(name)
97 | end
98 |
99 | # See if the user is a member of the named organization
100 | #
101 | # name - the organization name
102 | #
103 | # Returns: true if the user has access, false otherwise
104 | def github_organization_access?(name)
105 | github_user.organization_member?(name)
106 | end
107 |
108 | # See if the user is a member of the team id
109 | #
110 | # team_id - the team's id
111 | #
112 | # Returns: true if the user has access, false otherwise
113 | def github_team_access?(team_id)
114 | github_user.team_member?(team_id)
115 | end
116 |
117 | # Enforce user membership to the named organization
118 | #
119 | # name - the organization to test membership against
120 | #
121 | # Returns an execution halt if the user is not a member of the named org
122 | def github_public_organization_authenticate!(name)
123 | authenticate!
124 | halt([401, "Unauthorized User"]) unless github_public_organization_access?(name)
125 | end
126 |
127 | # Enforce user membership to the named organization if membership is publicized
128 | #
129 | # name - the organization to test membership against
130 | #
131 | # Returns an execution halt if the user is not a member of the named org
132 | def github_organization_authenticate!(name)
133 | authenticate!
134 | halt([401, "Unauthorized User"]) unless github_organization_access?(name)
135 | end
136 |
137 | # Enforce user membership to the team id
138 | #
139 | # team_id - the team_id to test membership against
140 | #
141 | # Returns an execution halt if the user is not a member of the team
142 | def github_team_authenticate!(team_id)
143 | authenticate!
144 | halt([401, "Unauthorized User"]) unless github_team_access?(team_id)
145 | end
146 |
147 | def _relative_url_for(path)
148 | request.script_name + path
149 | end
150 | end
151 |
152 | def self.registered(app)
153 | app.use AccessDenied
154 | app.use BadAuthentication
155 |
156 | app.use Warden::Manager do |manager|
157 | manager.default_strategies :github
158 |
159 | manager.failure_app = app.github_options[:failure_app] || BadAuthentication
160 |
161 | manager.scope_defaults :default, :config => {
162 | :client_id => app.github_options[:client_id] || ENV['GITHUB_CLIENT_ID'],
163 | :client_secret => app.github_options[:secret] || ENV['GITHUB_CLIENT_SECRET'],
164 | :scope => app.github_options[:scopes] || '',
165 | :redirect_uri => app.github_options[:callback_url] || '/auth/github/callback'
166 | }
167 | end
168 |
169 | app.helpers Helpers
170 |
171 | app.get '/auth/github/callback' do
172 | if params["error"]
173 | redirect "/unauthenticated"
174 | else
175 | authenticate!
176 | return_to = session.delete('return_to') || _relative_url_for('/')
177 | redirect return_to
178 | end
179 | end
180 | end
181 | end
182 | end
183 | end
184 |
--------------------------------------------------------------------------------
/app/lib/sinatra/auth/github/version.rb:
--------------------------------------------------------------------------------
1 | module Sinatra
2 | module Auth
3 | module Github
4 | VERSION = "1.0.0"
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/lib/sinatra/auth/views/401.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Denied
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/lib/sinatra/auth/views/securocat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitHub-Analytics/7f464fa06da3d44da5f415a1d606b59d63b7a854/app/lib/sinatra/auth/views/securocat.png
--------------------------------------------------------------------------------
/app/lib/sinatra_auth_github.rb:
--------------------------------------------------------------------------------
1 | require_relative 'sinatra/auth/github'
2 |
--------------------------------------------------------------------------------
/app/public/vendor/bootstrap/css/bootstrap-theme.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.0.3 (http://getbootstrap.com)
3 | * Copyright 2013 Twitter, Inc.
4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0
5 | */
6 |
7 | .btn-default,
8 | .btn-primary,
9 | .btn-success,
10 | .btn-info,
11 | .btn-warning,
12 | .btn-danger {
13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
16 | }
17 |
18 | .btn-default:active,
19 | .btn-primary:active,
20 | .btn-success:active,
21 | .btn-info:active,
22 | .btn-warning:active,
23 | .btn-danger:active,
24 | .btn-default.active,
25 | .btn-primary.active,
26 | .btn-success.active,
27 | .btn-info.active,
28 | .btn-warning.active,
29 | .btn-danger.active {
30 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
31 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
32 | }
33 |
34 | .btn:active,
35 | .btn.active {
36 | background-image: none;
37 | }
38 |
39 | .btn-default {
40 | text-shadow: 0 1px 0 #fff;
41 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);
42 | background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%);
43 | background-repeat: repeat-x;
44 | border-color: #dbdbdb;
45 | border-color: #ccc;
46 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
47 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
48 | }
49 |
50 | .btn-default:hover,
51 | .btn-default:focus {
52 | background-color: #e0e0e0;
53 | background-position: 0 -15px;
54 | }
55 |
56 | .btn-default:active,
57 | .btn-default.active {
58 | background-color: #e0e0e0;
59 | border-color: #dbdbdb;
60 | }
61 |
62 | .btn-primary {
63 | background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
64 | background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
65 | background-repeat: repeat-x;
66 | border-color: #2b669a;
67 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
68 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
69 | }
70 |
71 | .btn-primary:hover,
72 | .btn-primary:focus {
73 | background-color: #2d6ca2;
74 | background-position: 0 -15px;
75 | }
76 |
77 | .btn-primary:active,
78 | .btn-primary.active {
79 | background-color: #2d6ca2;
80 | border-color: #2b669a;
81 | }
82 |
83 | .btn-success {
84 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
85 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
86 | background-repeat: repeat-x;
87 | border-color: #3e8f3e;
88 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
89 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
90 | }
91 |
92 | .btn-success:hover,
93 | .btn-success:focus {
94 | background-color: #419641;
95 | background-position: 0 -15px;
96 | }
97 |
98 | .btn-success:active,
99 | .btn-success.active {
100 | background-color: #419641;
101 | border-color: #3e8f3e;
102 | }
103 |
104 | .btn-warning {
105 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
106 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
107 | background-repeat: repeat-x;
108 | border-color: #e38d13;
109 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
110 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
111 | }
112 |
113 | .btn-warning:hover,
114 | .btn-warning:focus {
115 | background-color: #eb9316;
116 | background-position: 0 -15px;
117 | }
118 |
119 | .btn-warning:active,
120 | .btn-warning.active {
121 | background-color: #eb9316;
122 | border-color: #e38d13;
123 | }
124 |
125 | .btn-danger {
126 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
127 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
128 | background-repeat: repeat-x;
129 | border-color: #b92c28;
130 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
131 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
132 | }
133 |
134 | .btn-danger:hover,
135 | .btn-danger:focus {
136 | background-color: #c12e2a;
137 | background-position: 0 -15px;
138 | }
139 |
140 | .btn-danger:active,
141 | .btn-danger.active {
142 | background-color: #c12e2a;
143 | border-color: #b92c28;
144 | }
145 |
146 | .btn-info {
147 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
148 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
149 | background-repeat: repeat-x;
150 | border-color: #28a4c9;
151 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
152 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
153 | }
154 |
155 | .btn-info:hover,
156 | .btn-info:focus {
157 | background-color: #2aabd2;
158 | background-position: 0 -15px;
159 | }
160 |
161 | .btn-info:active,
162 | .btn-info.active {
163 | background-color: #2aabd2;
164 | border-color: #28a4c9;
165 | }
166 |
167 | .thumbnail,
168 | .img-thumbnail {
169 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
170 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
171 | }
172 |
173 | .dropdown-menu > li > a:hover,
174 | .dropdown-menu > li > a:focus {
175 | background-color: #e8e8e8;
176 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
177 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
178 | background-repeat: repeat-x;
179 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
180 | }
181 |
182 | .dropdown-menu > .active > a,
183 | .dropdown-menu > .active > a:hover,
184 | .dropdown-menu > .active > a:focus {
185 | background-color: #357ebd;
186 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
187 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
188 | background-repeat: repeat-x;
189 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
190 | }
191 |
192 | .navbar-default {
193 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
194 | background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);
195 | background-repeat: repeat-x;
196 | border-radius: 4px;
197 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
198 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
199 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
200 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
201 | }
202 |
203 | .navbar-default .navbar-nav > .active > a {
204 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
205 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
206 | background-repeat: repeat-x;
207 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
208 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
209 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
210 | }
211 |
212 | .navbar-brand,
213 | .navbar-nav > li > a {
214 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
215 | }
216 |
217 | .navbar-inverse {
218 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%);
219 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%);
220 | background-repeat: repeat-x;
221 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
222 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
223 | }
224 |
225 | .navbar-inverse .navbar-nav > .active > a {
226 | background-image: -webkit-linear-gradient(top, #222222 0%, #282828 100%);
227 | background-image: linear-gradient(to bottom, #222222 0%, #282828 100%);
228 | background-repeat: repeat-x;
229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
230 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
231 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
232 | }
233 |
234 | .navbar-inverse .navbar-brand,
235 | .navbar-inverse .navbar-nav > li > a {
236 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
237 | }
238 |
239 | .navbar-static-top,
240 | .navbar-fixed-top,
241 | .navbar-fixed-bottom {
242 | border-radius: 0;
243 | }
244 |
245 | .alert {
246 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
247 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
248 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
249 | }
250 |
251 | .alert-success {
252 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
253 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
254 | background-repeat: repeat-x;
255 | border-color: #b2dba1;
256 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
257 | }
258 |
259 | .alert-info {
260 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
261 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
262 | background-repeat: repeat-x;
263 | border-color: #9acfea;
264 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
265 | }
266 |
267 | .alert-warning {
268 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
269 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
270 | background-repeat: repeat-x;
271 | border-color: #f5e79e;
272 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
273 | }
274 |
275 | .alert-danger {
276 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
277 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
278 | background-repeat: repeat-x;
279 | border-color: #dca7a7;
280 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
281 | }
282 |
283 | .progress {
284 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
285 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
286 | background-repeat: repeat-x;
287 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
288 | }
289 |
290 | .progress-bar {
291 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
292 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
293 | background-repeat: repeat-x;
294 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
295 | }
296 |
297 | .progress-bar-success {
298 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
299 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
300 | background-repeat: repeat-x;
301 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
302 | }
303 |
304 | .progress-bar-info {
305 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
306 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
307 | background-repeat: repeat-x;
308 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
309 | }
310 |
311 | .progress-bar-warning {
312 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
313 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
314 | background-repeat: repeat-x;
315 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
316 | }
317 |
318 | .progress-bar-danger {
319 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
320 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
321 | background-repeat: repeat-x;
322 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
323 | }
324 |
325 | .list-group {
326 | border-radius: 4px;
327 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
328 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
329 | }
330 |
331 | .list-group-item.active,
332 | .list-group-item.active:hover,
333 | .list-group-item.active:focus {
334 | text-shadow: 0 -1px 0 #3071a9;
335 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
336 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
337 | background-repeat: repeat-x;
338 | border-color: #3278b3;
339 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
340 | }
341 |
342 | .panel {
343 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
344 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
345 | }
346 |
347 | .panel-default > .panel-heading {
348 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
349 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
350 | background-repeat: repeat-x;
351 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
352 | }
353 |
354 | .panel-primary > .panel-heading {
355 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
356 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
357 | background-repeat: repeat-x;
358 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
359 | }
360 |
361 | .panel-success > .panel-heading {
362 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
363 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
364 | background-repeat: repeat-x;
365 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
366 | }
367 |
368 | .panel-info > .panel-heading {
369 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
370 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
371 | background-repeat: repeat-x;
372 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
373 | }
374 |
375 | .panel-warning > .panel-heading {
376 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
377 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
378 | background-repeat: repeat-x;
379 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
380 | }
381 |
382 | .panel-danger > .panel-heading {
383 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
384 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
385 | background-repeat: repeat-x;
386 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
387 | }
388 |
389 | .well {
390 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
391 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
392 | background-repeat: repeat-x;
393 | border-color: #dcdcdc;
394 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
395 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
396 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
397 | }
--------------------------------------------------------------------------------
/app/public/vendor/bootstrap/css/bootstrap-theme.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.0.3 (http://getbootstrap.com)
3 | * Copyright 2013 Twitter, Inc.
4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0
5 | */
6 |
7 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)}
--------------------------------------------------------------------------------
/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitHub-Analytics/7f464fa06da3d44da5f415a1d606b59d63b7a854/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitHub-Analytics/7f464fa06da3d44da5f415a1d606b59d63b7a854/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitHub-Analytics/7f464fa06da3d44da5f415a1d606b59d63b7a854/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/app/public/vendor/chartkick.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Chartkick.js
3 | * Create beautiful Javascript charts with minimal code
4 | * https://github.com/ankane/chartkick.js
5 | * v1.2.1
6 | * MIT License
7 | */
8 |
9 | /*jslint browser: true, indent: 2, plusplus: true, vars: true */
10 |
11 | (function (window) {
12 | 'use strict';
13 |
14 | var Chartkick, ISO8601_PATTERN, DECIMAL_SEPARATOR, adapters = [];
15 |
16 | var $ = window.jQuery || window.Zepto || window.$;
17 |
18 | // helpers
19 |
20 | function isArray(variable) {
21 | return Object.prototype.toString.call(variable) === "[object Array]";
22 | }
23 |
24 | function isFunction(variable) {
25 | return variable instanceof Function;
26 | }
27 |
28 | function isPlainObject(variable) {
29 | return !isFunction(variable) && variable instanceof Object;
30 | }
31 |
32 | // https://github.com/madrobby/zepto/blob/master/src/zepto.js
33 | function extend(target, source) {
34 | var key;
35 | for (key in source) {
36 | if (isPlainObject(source[key]) || isArray(source[key])) {
37 | if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
38 | target[key] = {};
39 | }
40 | if (isArray(source[key]) && !isArray(target[key])) {
41 | target[key] = [];
42 | }
43 | extend(target[key], source[key]);
44 | } else if (source[key] !== undefined) {
45 | target[key] = source[key];
46 | }
47 | }
48 | }
49 |
50 | function merge(obj1, obj2) {
51 | var target = {};
52 | extend(target, obj1);
53 | extend(target, obj2);
54 | return target;
55 | }
56 |
57 | // https://github.com/Do/iso8601.js
58 | ISO8601_PATTERN = /(\d\d\d\d)(\-)?(\d\d)(\-)?(\d\d)(T)?(\d\d)(:)?(\d\d)?(:)?(\d\d)?([\.,]\d+)?($|Z|([\+\-])(\d\d)(:)?(\d\d)?)/i;
59 | DECIMAL_SEPARATOR = String(1.5).charAt(1);
60 |
61 | function parseISO8601(input) {
62 | var day, hour, matches, milliseconds, minutes, month, offset, result, seconds, type, year;
63 | type = Object.prototype.toString.call(input);
64 | if (type === '[object Date]') {
65 | return input;
66 | }
67 | if (type !== '[object String]') {
68 | return;
69 | }
70 | if (matches = input.match(ISO8601_PATTERN)) {
71 | year = parseInt(matches[1], 10);
72 | month = parseInt(matches[3], 10) - 1;
73 | day = parseInt(matches[5], 10);
74 | hour = parseInt(matches[7], 10);
75 | minutes = matches[9] ? parseInt(matches[9], 10) : 0;
76 | seconds = matches[11] ? parseInt(matches[11], 10) : 0;
77 | milliseconds = matches[12] ? parseFloat(DECIMAL_SEPARATOR + matches[12].slice(1)) * 1000 : 0;
78 | result = Date.UTC(year, month, day, hour, minutes, seconds, milliseconds);
79 | if (matches[13] && matches[14]) {
80 | offset = matches[15] * 60;
81 | if (matches[17]) {
82 | offset += parseInt(matches[17], 10);
83 | }
84 | offset *= matches[14] === '-' ? -1 : 1;
85 | result -= offset * 60 * 1000;
86 | }
87 | return new Date(result);
88 | }
89 | }
90 | // end iso8601.js
91 |
92 | function negativeValues(series) {
93 | var i, j, data;
94 | for (i = 0; i < series.length; i++) {
95 | data = series[i].data;
96 | for (j = 0; j < data.length; j++) {
97 | if (data[j][1] < 0) {
98 | return true;
99 | }
100 | }
101 | }
102 | return false;
103 | }
104 |
105 | function jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax, setStacked) {
106 | return function (series, opts, chartOptions) {
107 | var options = merge({}, defaultOptions);
108 | options = merge(options, chartOptions || {});
109 |
110 | // hide legend
111 | // this is *not* an external option!
112 | if (opts.hideLegend) {
113 | hideLegend(options);
114 | }
115 |
116 | // min
117 | if ("min" in opts) {
118 | setMin(options, opts.min);
119 | } else if (!negativeValues(series)) {
120 | setMin(options, 0);
121 | }
122 |
123 | // max
124 | if ("max" in opts) {
125 | setMax(options, opts.max);
126 | }
127 |
128 | if (opts.stacked) {
129 | setStacked(options);
130 | }
131 |
132 | if (opts.colors) {
133 | options.colors = opts.colors;
134 | }
135 |
136 | // merge library last
137 | options = merge(options, opts.library || {});
138 |
139 | return options;
140 | };
141 | }
142 |
143 | function setText(element, text) {
144 | if (document.body.innerText) {
145 | element.innerText = text;
146 | } else {
147 | element.textContent = text;
148 | }
149 | }
150 |
151 | function chartError(element, message) {
152 | setText(element, "Error Loading Chart: " + message);
153 | element.style.color = "#ff0000";
154 | }
155 |
156 | function getJSON(element, url, success) {
157 | $.ajax({
158 | dataType: "json",
159 | url: url,
160 | success: success,
161 | error: function (jqXHR, textStatus, errorThrown) {
162 | var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message;
163 | chartError(element, message);
164 | }
165 | });
166 | }
167 |
168 | function errorCatcher(chart, callback) {
169 | try {
170 | callback(chart);
171 | } catch (err) {
172 | chartError(chart.element, err.message);
173 | throw err;
174 | }
175 | }
176 |
177 | function fetchDataSource(chart, callback) {
178 | if (typeof chart.dataSource === "string") {
179 | getJSON(chart.element, chart.dataSource, function (data, textStatus, jqXHR) {
180 | chart.data = data;
181 | errorCatcher(chart, callback);
182 | });
183 | } else {
184 | chart.data = chart.dataSource;
185 | errorCatcher(chart, callback);
186 | }
187 | }
188 |
189 | // type conversions
190 |
191 | function toStr(n) {
192 | return "" + n;
193 | }
194 |
195 | function toFloat(n) {
196 | return parseFloat(n);
197 | }
198 |
199 | function toDate(n) {
200 | if (typeof n !== "object") {
201 | if (typeof n === "number") {
202 | n = new Date(n * 1000); // ms
203 | } else { // str
204 | // try our best to get the str into iso8601
205 | // TODO be smarter about this
206 | var str = n.replace(/ /, "T").replace(" ", "").replace("UTC", "Z");
207 | n = parseISO8601(str) || new Date(n);
208 | }
209 | }
210 | return n;
211 | }
212 |
213 | function toArr(n) {
214 | if (!isArray(n)) {
215 | var arr = [], i;
216 | for (i in n) {
217 | if (n.hasOwnProperty(i)) {
218 | arr.push([i, n[i]]);
219 | }
220 | }
221 | n = arr;
222 | }
223 | return n;
224 | }
225 |
226 | function sortByTime(a, b) {
227 | return a[0].getTime() - b[0].getTime();
228 | }
229 |
230 | if ("Highcharts" in window) {
231 | var HighchartsAdapter = new function () {
232 | var Highcharts = window.Highcharts;
233 |
234 | var defaultOptions = {
235 | chart: {},
236 | xAxis: {
237 | labels: {
238 | style: {
239 | fontSize: "12px"
240 | }
241 | }
242 | },
243 | yAxis: {
244 | title: {
245 | text: null
246 | },
247 | labels: {
248 | style: {
249 | fontSize: "12px"
250 | }
251 | }
252 | },
253 | title: {
254 | text: null
255 | },
256 | credits: {
257 | enabled: false
258 | },
259 | legend: {
260 | borderWidth: 0
261 | },
262 | tooltip: {
263 | style: {
264 | fontSize: "12px"
265 | }
266 | },
267 | plotOptions: {
268 | areaspline: {},
269 | series: {
270 | marker: {}
271 | }
272 | }
273 | };
274 |
275 | var hideLegend = function (options) {
276 | options.legend.enabled = false;
277 | };
278 |
279 | var setMin = function (options, min) {
280 | options.yAxis.min = min;
281 | };
282 |
283 | var setMax = function (options, max) {
284 | options.yAxis.max = max;
285 | };
286 |
287 | var setStacked = function (options) {
288 | options.plotOptions.series.stacking = "normal";
289 | };
290 |
291 | var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax, setStacked);
292 |
293 | this.renderLineChart = function (chart, chartType) {
294 | chartType = chartType || "spline";
295 | var chartOptions = {};
296 | if (chartType === "areaspline") {
297 | chartOptions = {
298 | plotOptions: {
299 | areaspline: {
300 | stacking: "normal"
301 | },
302 | series: {
303 | marker: {
304 | enabled: false
305 | }
306 | }
307 | }
308 | };
309 | }
310 | var options = jsOptions(chart.data, chart.options, chartOptions), data, i, j;
311 | options.xAxis.type = chart.options.discrete ? "category" : "datetime";
312 | options.chart.type = chartType;
313 | options.chart.renderTo = chart.element.id;
314 |
315 | var series = chart.data;
316 | for (i = 0; i < series.length; i++) {
317 | data = series[i].data;
318 | if (!chart.options.discrete) {
319 | for (j = 0; j < data.length; j++) {
320 | data[j][0] = data[j][0].getTime();
321 | }
322 | }
323 | series[i].marker = {symbol: "circle"};
324 | }
325 | options.series = series;
326 | new Highcharts.Chart(options);
327 | };
328 |
329 | this.renderPieChart = function (chart) {
330 | var chartOptions = {};
331 | if (chart.options.colors) {
332 | chartOptions.colors = chart.options.colors;
333 | }
334 | var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {});
335 | options.chart.renderTo = chart.element.id;
336 | options.series = [{
337 | type: "pie",
338 | name: "Value",
339 | data: chart.data
340 | }];
341 | new Highcharts.Chart(options);
342 | };
343 |
344 | this.renderColumnChart = function (chart, chartType) {
345 | var chartType = chartType || "column";
346 | var series = chart.data;
347 | var options = jsOptions(series, chart.options), i, j, s, d, rows = [];
348 | options.chart.type = chartType;
349 | options.chart.renderTo = chart.element.id;
350 |
351 | for (i = 0; i < series.length; i++) {
352 | s = series[i];
353 |
354 | for (j = 0; j < s.data.length; j++) {
355 | d = s.data[j];
356 | if (!rows[d[0]]) {
357 | rows[d[0]] = new Array(series.length);
358 | }
359 | rows[d[0]][i] = d[1];
360 | }
361 | }
362 |
363 | var categories = [];
364 | for (i in rows) {
365 | if (rows.hasOwnProperty(i)) {
366 | categories.push(i);
367 | }
368 | }
369 | options.xAxis.categories = categories;
370 |
371 | var newSeries = [];
372 | for (i = 0; i < series.length; i++) {
373 | d = [];
374 | for (j = 0; j < categories.length; j++) {
375 | d.push(rows[categories[j]][i] || 0);
376 | }
377 |
378 | newSeries.push({
379 | name: series[i].name,
380 | data: d
381 | });
382 | }
383 | options.series = newSeries;
384 |
385 | new Highcharts.Chart(options);
386 | };
387 |
388 | var self = this;
389 |
390 | this.renderBarChart = function (chart) {
391 | self.renderColumnChart(chart, "bar");
392 | };
393 |
394 | this.renderAreaChart = function (chart) {
395 | self.renderLineChart(chart, "areaspline");
396 | };
397 | };
398 | adapters.push(HighchartsAdapter);
399 | }
400 | if ("google" in window) {
401 | var GoogleChartsAdapter = new function () {
402 | var google = window.google;
403 |
404 | // load from google
405 | var loaded = false;
406 | google.setOnLoadCallback(function () {
407 | loaded = true;
408 | });
409 | google.load("visualization", "1.0", {"packages": ["corechart"]});
410 |
411 | var waitForLoaded = function (callback) {
412 | google.setOnLoadCallback(callback); // always do this to prevent race conditions (watch out for other issues due to this)
413 | if (loaded) {
414 | callback();
415 | }
416 | };
417 |
418 | // Set chart options
419 | var defaultOptions = {
420 | chartArea: {},
421 | fontName: "'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, Helvetica, sans-serif",
422 | pointSize: 6,
423 | legend: {
424 | textStyle: {
425 | fontSize: 12,
426 | color: "#444"
427 | },
428 | alignment: "center",
429 | position: "right"
430 | },
431 | curveType: "function",
432 | hAxis: {
433 | textStyle: {
434 | color: "#666",
435 | fontSize: 12
436 | },
437 | gridlines: {
438 | color: "transparent"
439 | },
440 | baselineColor: "#ccc",
441 | viewWindow: {}
442 | },
443 | vAxis: {
444 | textStyle: {
445 | color: "#666",
446 | fontSize: 12
447 | },
448 | baselineColor: "#ccc",
449 | viewWindow: {}
450 | },
451 | tooltip: {
452 | textStyle: {
453 | color: "#666",
454 | fontSize: 12
455 | }
456 | }
457 | };
458 |
459 | var hideLegend = function (options) {
460 | options.legend.position = "none";
461 | };
462 |
463 | var setMin = function (options, min) {
464 | options.vAxis.viewWindow.min = min;
465 | };
466 |
467 | var setMax = function (options, max) {
468 | options.vAxis.viewWindow.max = max;
469 | };
470 |
471 | var setBarMin = function (options, min) {
472 | options.hAxis.viewWindow.min = min;
473 | };
474 |
475 | var setBarMax = function (options, max) {
476 | options.hAxis.viewWindow.max = max;
477 | };
478 |
479 | var setStacked = function (options) {
480 | options.isStacked = true;
481 | };
482 |
483 | var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax, setStacked);
484 |
485 | // cant use object as key
486 | var createDataTable = function (series, columnType) {
487 | var data = new google.visualization.DataTable();
488 | data.addColumn(columnType, "");
489 |
490 | var i, j, s, d, key, rows = [];
491 | for (i = 0; i < series.length; i++) {
492 | s = series[i];
493 | data.addColumn("number", s.name);
494 |
495 | for (j = 0; j < s.data.length; j++) {
496 | d = s.data[j];
497 | key = (columnType === "datetime") ? d[0].getTime() : d[0];
498 | if (!rows[key]) {
499 | rows[key] = new Array(series.length);
500 | }
501 | rows[key][i] = toFloat(d[1]);
502 | }
503 | }
504 |
505 | var rows2 = [];
506 | for (i in rows) {
507 | if (rows.hasOwnProperty(i)) {
508 | rows2.push([(columnType === "datetime") ? new Date(toFloat(i)) : i].concat(rows[i]));
509 | }
510 | }
511 | if (columnType === "datetime") {
512 | rows2.sort(sortByTime);
513 | }
514 | data.addRows(rows2);
515 |
516 | return data;
517 | };
518 |
519 | var resize = function (callback) {
520 | if (window.attachEvent) {
521 | window.attachEvent("onresize", callback);
522 | } else if (window.addEventListener) {
523 | window.addEventListener("resize", callback, true);
524 | }
525 | callback();
526 | };
527 |
528 | this.renderLineChart = function (chart) {
529 | waitForLoaded(function () {
530 | var options = jsOptions(chart.data, chart.options);
531 | var data = createDataTable(chart.data, chart.options.discrete ? "string" : "datetime");
532 | chart.chart = new google.visualization.LineChart(chart.element);
533 | resize(function () {
534 | chart.chart.draw(data, options);
535 | });
536 | });
537 | };
538 |
539 | this.renderPieChart = function (chart) {
540 | waitForLoaded(function () {
541 | var chartOptions = {
542 | chartArea: {
543 | top: "10%",
544 | height: "80%"
545 | }
546 | };
547 | if (chart.options.colors) {
548 | chartOptions.colors = chart.options.colors;
549 | }
550 | var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {});
551 |
552 | var data = new google.visualization.DataTable();
553 | data.addColumn("string", "");
554 | data.addColumn("number", "Value");
555 | data.addRows(chart.data);
556 |
557 | chart.chart = new google.visualization.PieChart(chart.element);
558 | resize(function () {
559 | chart.chart.draw(data, options);
560 | });
561 | });
562 | };
563 |
564 | this.renderColumnChart = function (chart) {
565 | waitForLoaded(function () {
566 | var options = jsOptions(chart.data, chart.options);
567 | var data = createDataTable(chart.data, "string");
568 | chart.chart = new google.visualization.ColumnChart(chart.element);
569 | resize(function () {
570 | chart.chart.draw(data, options);
571 | });
572 | });
573 | };
574 |
575 | this.renderBarChart = function (chart) {
576 | waitForLoaded(function () {
577 | var chartOptions = {
578 | hAxis: {
579 | gridlines: {
580 | color: "#ccc"
581 | }
582 | }
583 | };
584 | var options = jsOptionsFunc(defaultOptions, hideLegend, setBarMin, setBarMax, setStacked)(chart.data, chart.options, chartOptions);
585 | var data = createDataTable(chart.data, "string");
586 | chart.chart = new google.visualization.BarChart(chart.element);
587 | resize(function () {
588 | chart.chart.draw(data, options);
589 | });
590 | });
591 | };
592 |
593 | this.renderAreaChart = function (chart) {
594 | waitForLoaded(function () {
595 | var chartOptions = {
596 | isStacked: true,
597 | pointSize: 0,
598 | areaOpacity: 0.5
599 | };
600 | var options = jsOptions(chart.data, chart.options, chartOptions);
601 | var data = createDataTable(chart.data, chart.options.discrete ? "string" : "datetime");
602 | chart.chart = new google.visualization.AreaChart(chart.element);
603 | resize(function () {
604 | chart.chart.draw(data, options);
605 | });
606 | });
607 | };
608 |
609 | this.renderGeoChart = function (chart) {
610 | waitForLoaded(function () {
611 | var chartOptions = {
612 | legend: "none",
613 | colorAxis: {
614 | colors: chart.options.colors || ["#f6c7b6", "#ce502d"]
615 | }
616 | };
617 | var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {});
618 |
619 | var data = new google.visualization.DataTable();
620 | data.addColumn("string", "");
621 | data.addColumn("number", "Value");
622 | data.addRows(chart.data);
623 |
624 | chart.chart = new google.visualization.GeoChart(chart.element);
625 | resize(function () {
626 | chart.chart.draw(data, options);
627 | });
628 | });
629 | };
630 | };
631 | adapters.push(GoogleChartsAdapter);
632 | }
633 |
634 | // TODO add adapter option
635 | // TODO remove chartType if cross-browser way
636 | // to get the name of the chart class
637 | function renderChart(chartType, chart) {
638 | var i, adapter, fnName;
639 | fnName = "render" + chartType + "Chart";
640 |
641 | for (i = 0; i < adapters.length; i++) {
642 | adapter = adapters[i];
643 | if (isFunction(adapter[fnName])) {
644 | return adapter[fnName](chart);
645 | }
646 | }
647 | throw new Error("No adapter found");
648 | }
649 |
650 | // process data
651 |
652 | function processSeries(series, opts, time) {
653 | var i, j, data, r, key;
654 |
655 | // see if one series or multiple
656 | if (!isArray(series) || typeof series[0] !== "object" || isArray(series[0])) {
657 | series = [{name: "Value", data: series}];
658 | opts.hideLegend = true;
659 | } else {
660 | opts.hideLegend = false;
661 | }
662 | if (opts.discrete) {
663 | time = false;
664 | }
665 |
666 | // right format
667 | for (i = 0; i < series.length; i++) {
668 | data = toArr(series[i].data);
669 | r = [];
670 | for (j = 0; j < data.length; j++) {
671 | key = data[j][0];
672 | key = time ? toDate(key) : toStr(key);
673 | r.push([key, toFloat(data[j][1])]);
674 | }
675 | if (time) {
676 | r.sort(sortByTime);
677 | }
678 | series[i].data = r;
679 | }
680 |
681 | return series;
682 | }
683 |
684 | function processSimple(data) {
685 | var perfectData = toArr(data), i;
686 | for (i = 0; i < perfectData.length; i++) {
687 | perfectData[i] = [toStr(perfectData[i][0]), toFloat(perfectData[i][1])];
688 | }
689 | return perfectData;
690 | }
691 |
692 | function processLineData(chart) {
693 | chart.data = processSeries(chart.data, chart.options, true);
694 | renderChart("Line", chart);
695 | }
696 |
697 | function processColumnData(chart) {
698 | chart.data = processSeries(chart.data, chart.options, false);
699 | renderChart("Column", chart);
700 | }
701 |
702 | function processPieData(chart) {
703 | chart.data = processSimple(chart.data);
704 | renderChart("Pie", chart);
705 | }
706 |
707 | function processBarData(chart) {
708 | chart.data = processSeries(chart.data, chart.options, false);
709 | renderChart("Bar", chart);
710 | }
711 |
712 | function processAreaData(chart) {
713 | chart.data = processSeries(chart.data, chart.options, true);
714 | renderChart("Area", chart);
715 | }
716 |
717 | function processGeoData(chart) {
718 | chart.data = processSimple(chart.data);
719 | renderChart("Geo", chart);
720 | }
721 |
722 | function setElement(chart, element, dataSource, opts, callback) {
723 | if (typeof element === "string") {
724 | element = document.getElementById(element);
725 | }
726 | chart.element = element;
727 | chart.options = opts || {};
728 | chart.dataSource = dataSource;
729 | Chartkick.charts[element.id] = chart;
730 | fetchDataSource(chart, callback);
731 | }
732 |
733 | // define classes
734 |
735 | Chartkick = {
736 | LineChart: function (element, dataSource, opts) {
737 | setElement(this, element, dataSource, opts, processLineData);
738 | },
739 | PieChart: function (element, dataSource, opts) {
740 | setElement(this, element, dataSource, opts, processPieData);
741 | },
742 | ColumnChart: function (element, dataSource, opts) {
743 | setElement(this, element, dataSource, opts, processColumnData);
744 | },
745 | BarChart: function (element, dataSource, opts) {
746 | setElement(this, element, dataSource, opts, processBarData);
747 | },
748 | AreaChart: function (element, dataSource, opts) {
749 | setElement(this, element, dataSource, opts, processAreaData);
750 | },
751 | GeoChart: function (element, dataSource, opts) {
752 | setElement(this, element, dataSource, opts, processGeoData);
753 | },
754 | charts: {}
755 | };
756 |
757 | window.Chartkick = Chartkick;
758 | }(window));
759 |
--------------------------------------------------------------------------------
/app/sinatra_auth_github.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "sinatra/auth/github/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "GitHub-Analytics-Web-App"
7 | s.version = Sinatra::Auth::Github::VERSION
8 | s.platform = Gem::Platform::RUBY
9 | s.authors = ["Stephen Russett"]
10 | s.email = ["stephenrussett@gmail.com"]
11 | s.homepage = "http://github.com/StephenOTT/GitHub-Analytics"
12 | s.summary = ""
13 | s.license = ""
14 | s.description = s.summary
15 |
16 | s.rubyforge_project = "sinatra_auth_github"
17 |
18 | s.add_dependency "sinatra", "~>1.0"
19 | s.add_dependency "warden-github", "~>1.0"
20 | s.add_dependency "chronic_duration"
21 | s.add_dependency "mongo"
22 | s.add_dependency "bson_ext"
23 | s.add_dependency 'sinatra-flash', '~> 0.3.0'
24 | s.add_dependency 'chartkick'
25 | s.add_dependency 'activesupport', '~> 4.1.0'
26 |
27 | s.add_development_dependency "rake"
28 | s.add_development_dependency "rspec", "~>2.4.0"
29 | s.add_development_dependency "shotgun" # Disable this when you want to test the Flash Messaging system
30 | s.add_development_dependency "randexp", "~>0.1.5"
31 | s.add_development_dependency "rack-test", "~>0.5.3"
32 |
33 | s.files = `git ls-files`.split("\n")
34 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
35 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
36 | s.require_paths = ["lib"]
37 | end
38 |
--------------------------------------------------------------------------------
/app/sinatra_helpers.rb:
--------------------------------------------------------------------------------
1 | require_relative '../github-analytics-data-download/controller'
2 | require_relative '../github-analytics-analyze-data/issues_processor'
3 | require_relative '../github-analytics-analyze-data/labels_processor'
4 | require_relative '../github-analytics-analyze-data/events_processor'
5 | require_relative '../github-analytics-analyze-data/system_wide_processor'
6 |
7 |
8 | module Sinatra_Helpers
9 |
10 | def self.get_all_repos_for_logged_user(githubAuthInfo)
11 | System_Wide_Processor.all_repos_for_logged_user(githubAuthInfo)
12 | end
13 |
14 | def self.download_github_analytics_data(user, repo, githubObject, githubAuthInfo)
15 | userRepo = "#{user}/#{repo}"
16 | Analytics_Download_Controller.controller(userRepo, githubObject, true, githubAuthInfo)
17 | end
18 |
19 | def self.analyze_issues_opened_per_user(user, repo, githubAuthInfo)
20 | userRepo = "#{user}/#{repo}"
21 | Issues_Processor.analyze_issues_opened_per_user(userRepo, githubAuthInfo)
22 | end
23 |
24 | def self.analyze_issues_opened_per_month(user, repo, githubAuthInfo)
25 | userRepo = "#{user}/#{repo}"
26 | Issues_Processor.analyze_issues_opened_per_month(userRepo, githubAuthInfo)
27 | end
28 |
29 | def self.analyze_issues_closed_per_month(user, repo, githubAuthInfo)
30 | userRepo = "#{user}/#{repo}"
31 | Issues_Processor.analyze_issues_closed_per_month(userRepo, githubAuthInfo)
32 | end
33 |
34 | def self.analyze_issues_opened_per_week(user, repo, githubAuthInfo)
35 | userRepo = "#{user}/#{repo}"
36 | Issues_Processor.analyze_issues_opened_per_week(userRepo, githubAuthInfo)
37 | end
38 |
39 | def self.analyze_issues_closed_per_week(user, repo, githubAuthInfo)
40 | userRepo = "#{user}/#{repo}"
41 | Issues_Processor.analyze_issues_closed_per_week(userRepo, githubAuthInfo)
42 | end
43 |
44 | def self.analyze_labels_count_per_repo(user, repo, githubAuthInfo)
45 | userRepo = "#{user}/#{repo}"
46 | Labels_Processor.analyze_labels_count_for_repo(userRepo, githubAuthInfo)
47 | end
48 |
49 |
50 | def self.analyze_repo_issues_Events_per_month(user, repo, githubAuthInfo)
51 | userRepo = "#{user}/#{repo}"
52 | Events_Processor.analyze_repo_issues_Events_per_month(userRepo, githubAuthInfo)
53 | end
54 |
55 |
56 | end
--------------------------------------------------------------------------------
/app/views/analyze_issues_opened_closed_per_month.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Issues Opened/Closed Timeline Analysis
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Repository Information
14 |
15 |
16 |
17 |
<%= @issuesOpenedPerMonth.first["repo"] %>
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Opened Issues Per Month
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Date |
42 | Count |
43 |
44 | <% @issuesOpenedPerMonth.each do |i| %>
45 |
46 | <%= i["converted_date"].strftime("%B %Y") %> |
47 | <%= i["count"] %> |
48 |
49 | <% end %>
50 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | Closed Issues Per Month
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Date |
72 | Count |
73 |
74 | <% @issuesClosedPerMonth.each do |i| %>
75 |
76 | <%= i["converted_date"].strftime("%B %Y") %> |
77 | <%= i["count"] %> |
78 |
79 | <% end %>
80 |
81 |
82 |
83 |
84 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | Column Chart of Issues Open/Closed Per Month
96 |
97 |
98 |
99 | <%= column_chart ([
100 | {name: "Opened", data: @issuesOpenedPerMonthChartReady},
101 | {name: "Closed", data: @issuesClosedPerMonthChartReady}
102 | ]),:library => { hAxis:{format:"#"},:isStacked => true}
103 | %>
104 |
105 |
106 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | Opened Issues Per Week
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | Date |
131 | Count |
132 |
133 | <% @issuesOpenedPerWeek.each do |i| %>
134 |
135 | <%= i["converted_date"].strftime("Week %U, %Y") %> |
136 | <%= i["count"] %> |
137 |
138 | <% end %>
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | Closed Issues Per Week
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | Date |
162 | Count |
163 |
164 | <% @issuesClosedPerWeek.each do |i| %>
165 |
166 | <%= i["converted_date"].strftime("Week %U, %Y") %> |
167 | <%= i["count"] %> |
168 |
169 | <% end %>
170 |
171 |
172 |
173 |
174 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 | Column Chart of Issues Open/Closed Per Week
187 |
188 |
189 |
190 | <%= column_chart ([
191 | {name: "Opened", data: @issuesOpenedPerWeekChartReady},
192 | {name: "Closed", data: @issuesClosedPerWeekChartReady}
193 | ]),:library => { :isStacked => true, hAxis: {showTextEvery:0, format:"#"}, chartArea: { height:"40%", top:"10%" }
194 | }
195 | %>
196 |
197 |
198 |
201 |
202 |
203 |
204 |
--------------------------------------------------------------------------------
/app/views/analyze_issues_opened_per_user.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Issues Opened Per User
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Repository Information
13 |
14 |
15 |
16 |
<%= @issuesOpenedPerUser.first["repo"] %>
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Issues Open Per User
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Username |
38 | Opened Issues Count |
39 |
40 | <% @issuesOpenedPerUser.each do |i| %>
41 |
42 | <%= i["user"] %> |
43 | <%= i["issues_opened_count"] %> |
44 |
45 | <% end %>
46 |
47 |
48 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Pie Chart
60 |
61 |
62 |
63 | <%= pie_chart(@issuesOpenedPerUserChartReady) %>
64 |
65 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/app/views/analyze_labels_for_repo.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Labels Assignment Analysis for Repo
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Repository Information
14 |
15 |
16 |
17 |
<%= @labelsCountForRepo.first["repo"] %>
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Labels Assignment Listing
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Label Name |
41 | Count |
42 |
43 | <% @labelsCountForRepo.each do |l| %>
44 |
45 | <%= l["label"] %> |
46 | <%= l["count"] %> |
47 |
48 | <% end %>
49 |
50 |
51 |
52 |
53 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | Labels Assignment Histogram
63 |
64 |
65 |
66 |
67 | <%= bar_chart (@labelsCountForRepoChartReady),:library => { hAxis: {showTextEvery:1}, chartArea: { left: "35%" } };
68 | %>
69 |
70 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | xxxx
82 |
83 |
84 |
85 | xxxxx
86 |
87 |
88 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/app/views/analyze_repo_issue_events.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Repo Issue Events Analysis
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Repository Information
14 |
15 |
16 |
17 |
<%= @repoIssueEvents.first["repo"] %>
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Repo Issue Events Per Month
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Event |
41 | Count |
42 | Date |
43 |
44 | <% @repoIssueEvents.each do |l| %>
45 |
46 | <%= l["event"] %> |
47 | <%= l["count"] %> |
48 | <%= l["converted_date"].strftime("%B %Y") %> |
49 |
50 | <% end %>
51 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | Repo Issue Events Assignment Histogram
65 |
66 |
67 |
68 |
69 | <%
70 | # Rebuild as a method for reuse.
71 | eventsArray =[]
72 |
73 | # Get all event Types in the Data
74 | eventTypes = @repoIssueEvents.map {|item| item["event"]}
75 |
76 | # @repoIssueEvents.each do |r|
77 | # eventTypes << r["event"]
78 | # end
79 | eventTypes = eventTypes.uniq
80 |
81 | eventTypes.each do |x|
82 | selectedEvents = @repoIssueEvents.select {|eventType| eventType["event"] == x }
83 |
84 | eventsData = {}
85 | selectedEvents.each do |y|
86 | eventsData[y["converted_date"].strftime("%B %Y")] = y["count"]
87 | end
88 |
89 | eventsArray << {name: x, data: eventsData }
90 | end
91 | %>
92 |
93 |
94 | <%= bar_chart (eventsArray),:library => { hAxis: {showTextEvery:1, format:"#"}, chartArea: {width:"60%", height:"90%", top:"0%"}};
95 | %>
96 |
97 |
98 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | xxxx
110 |
111 |
112 |
113 | xxxxx
114 |
115 |
116 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/app/views/download.erb:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | Provide your GitHub username/organization and Repository to download GitHub Issues data.
12 |
13 | Example: StephenOTT/Test1
14 |
31 | Download
--------------------------------------------------------------------------------
/app/views/index.erb:
--------------------------------------------------------------------------------
1 |
2 | Hello <%= @fullname %>
3 |
4 | <%= @username %>
5 |
6 |
7 | <%= @userID %>
--------------------------------------------------------------------------------
/app/views/layout.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | GitHub-Analytics
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
50 |
51 |
52 |
53 | <% flash_types.select{ |kind| flash.has_key?(kind) }.each do |kind| %>
54 |
55 |
56 | <%= flash[kind] %>
57 |
58 | <% end %>
59 |
60 |
61 |
62 | <%= yield %>
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/views/repos_listing.erb:
--------------------------------------------------------------------------------
1 | Repository Listing
2 |
3 |
4 |
5 |
Repositories
6 |
7 |
8 |
These are the repositories that you have downloaded
9 |
10 |
11 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/views/unauthenticated.erb:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/github-analytics-analyze-data/events_aggregation.rb:
--------------------------------------------------------------------------------
1 | require_relative './mongo'
2 | require 'date'
3 | require "active_support/core_ext"
4 |
5 |
6 | module Events_Aggregation
7 |
8 | def self.controller
9 |
10 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-Analytics", "Issues-Data")
11 |
12 | end
13 |
14 | def self.get_repo_issues_Events_per_month(repo, githubAuthInfo)
15 |
16 | repoIssueEvents = Mongo_Connection.aggregate_test([
17 | { "$match" => {type: "Repo Issue Event"}},
18 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }},
19 | {"$project" => {_id: 1,
20 | repo: 1,
21 | event: 1,
22 | created_month: {"$month" => "$created_at"},
23 | created_year: {"$year" => "$created_at"},
24 | }},
25 |
26 | { "$match" => { repo: repo }},
27 |
28 | { "$group" => { _id: {
29 | repo: "$repo",
30 | event: "$event",
31 | created_year: "$created_year",
32 | created_month: "$created_month"},
33 | count: { "$sum" => 1 }
34 | }},
35 | { "$sort" => {"_id.created_year" => 1, "_id.created_month" => 1}}
36 | ])
37 |
38 |
39 | output = []
40 |
41 | repoIssueEvents.each do |x|
42 | x["_id"]["count"] = x["count"]
43 | x["_id"]["converted_date"] = DateTime.new(x["_id"]["created_year"], x["_id"]["created_month"])
44 | output << x["_id"]
45 | end
46 |
47 | # TODO build this out into its own method
48 | if output.empty? == false
49 | # Get Missing Months/Years from Date Range
50 | a = []
51 | output.each do |x|
52 | a << x["converted_date"]
53 | end
54 | b = (output.first["converted_date"]..output.last["converted_date"]).to_a
55 | zeroValueDates = (b.map{ |date| date.strftime("%b %Y") } - a.map{ |date| date.strftime("%b %Y") }).uniq
56 |
57 | zeroValueDates.each do |zvd|
58 | zvd = DateTime.parse(zvd)
59 | output << {"repo"=> repo, "event" => "n/a", "created_year"=>zvd.strftime("%Y").to_i, "created_month"=>zvd.strftime("%m").to_i, "count"=>0, "converted_date"=>zvd}
60 | end
61 | # END of Get Missing Months/Years From Date Range
62 | end
63 |
64 | # Sorts the Output hash so the dates are in order
65 | output = output.sort_by { |hsh| hsh["converted_date"] }
66 | return output
67 | end
68 | end
69 |
70 |
71 | # Debug code
72 | # Events_Aggregation.controller
73 | # puts Events_Aggregation.get_repo_issues_Events_per_month("StephenOTT/Test1", {:username => "StephenOTT", :userID => 1994838})
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/github-analytics-analyze-data/events_processor.rb:
--------------------------------------------------------------------------------
1 | require_relative 'events_aggregation'
2 | require_relative 'helpers'
3 |
4 |
5 | module Events_Processor
6 |
7 | def self.analyze_repo_issues_Events_per_month(repo, githubAuthInfo)
8 | Events_Aggregation.controller
9 | repoIssueEventPerMonth = Events_Aggregation.get_repo_issues_Events_per_month(repo, githubAuthInfo)
10 |
11 | return repoIssueEventPerMonth
12 | end
13 |
14 | end
--------------------------------------------------------------------------------
/github-analytics-analyze-data/helpers.rb:
--------------------------------------------------------------------------------
1 | # require 'chronic_duration'
2 |
3 | module Helpers
4 |
5 |
6 |
7 | def self.get_date_formatter(dateGroup)
8 |
9 | case dateGroup
10 | when :month
11 | return "%b %Y" # Month Year
12 | when :week
13 | return "Week %U, %Y" # Week Year
14 | when :day
15 |
16 | end
17 |
18 | end
19 |
20 |
21 | def self.get_missing_dates(repo, output, dateFormater)
22 |
23 | # dateFormater = "%b %Y" # Month Year
24 | # dateFormater = "Week %U, %Y" # Week Year
25 |
26 |
27 | # Add all dates into a array and sort from oldest to newest
28 | a = []
29 | output.each do |x|
30 | a << x["converted_date"]
31 | end
32 | a.sort!
33 |
34 | # Create a array of dates based on the date at the start of the "a" array and the last item int eh "a" array
35 | b = (output.first["converted_date"]..output.last["converted_date"]).to_a
36 |
37 | # remove dates that are the same in both "a" and "b" array, then remove duplicate values - output is array of strings
38 | zeroValueDates = (b.map{ |date| date.strftime(dateFormater) } - a.map{ |date| date.strftime(dateFormater) }).uniq
39 |
40 | # Iterates through each zeroValueDates array of Strings and parses back into a date and adds to the output array
41 | zeroValueDates.each do |zvd|
42 | zvd = DateTime.parse(zvd)
43 | output << {"repo"=> repo , "created_year"=>zvd.strftime("%Y").to_i, "created_month"=>zvd.strftime("%m").to_i, "count"=>0, "converted_date"=>zvd}
44 | end
45 |
46 | return output
47 |
48 | end
49 |
50 |
51 | def self.sort_dates_array_hash(arrayofHashes, sortField)
52 |
53 | arrayofHashes = arrayofHashes.sort_by { |hsh| hsh[sortField] }
54 |
55 | end
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | # def self.budget_left?(large, small)
72 | # large - small
73 | # end
74 |
75 | # def self.convertSecondsToDurationFormat(timeInSeconds, outputFormat)
76 | # outputFormat = outputFormat.to_sym
77 | # return ChronicDuration.output(timeInSeconds, :format => outputFormat, :keep_zero => true)
78 | # end
79 |
80 |
81 | # def self.merge_issue_time_and_budget(issuesTime, issuesBudget)
82 |
83 | # issuesTime.each do |t|
84 |
85 | # issuesBudget.each do |b|
86 |
87 | # if b["issue_number"] == t["issue_number"]
88 | # t["budget_duration_sum"] = b["budget_duration_sum"]
89 | # t["budget_comment_count"] = b["budget_comment_count"]
90 | # break
91 | # end
92 | # end
93 | # if t.has_key?("budget_duration_sum") == false and t.has_key?("budget_comment_count") == false
94 | # t["budget_duration_sum"] = nil
95 | # t["budget_comment_count"] = nil
96 | # end
97 | # end
98 |
99 | # return issuesTime
100 | # end
101 |
102 |
103 |
104 |
105 |
106 | end
--------------------------------------------------------------------------------
/github-analytics-analyze-data/issues_date_aggregation.rb:
--------------------------------------------------------------------------------
1 | require_relative './mongo'
2 |
3 |
4 | module Issues_Date_Aggregation
5 |
6 | # def self.controller
7 |
8 | # Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-TimeTracking", "TimeTrackingCommits")
9 |
10 | # end
11 |
12 | # # gets the time durations of each repo for a specific yearr brokendown by month
13 | # def self.get_repo_time_month_year_sum(repo, filterYear, githubAuthInfo)
14 | # totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([
15 | # { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }},
16 | # { "$project" => { type: 1,
17 | # issue_number: 1,
18 | # _id: 1,
19 | # repo: 1,
20 | # milestone_number: 1,
21 | # issue_state: 1,
22 | # issue_title: 1,
23 | # time_tracking_commits:{ work_date: 1,
24 | # duration: 1,
25 | # type: 1,
26 | # comment_id: 1 }}},
27 |
28 | # { "$match" => { repo: repo }},
29 | # { "$match" => { type: "Issue" }},
30 | # { "$unwind" => "$time_tracking_commits" },
31 | # { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}},
32 | # { "$project" => {type: 1,
33 | # issue_number: 1,
34 | # _id: 1,
35 | # repo: 1,
36 | # milestone_number: 1,
37 | # issue_state: 1,
38 | # issue_title: 1,
39 | # time_tracking_commits:{ work_date_dayOfMonth: {"$dayOfMonth" => "$time_tracking_commits.work_date"},
40 | # work_date_month: {"$month" => "$time_tracking_commits.work_date"},
41 | # work_date_year: {"$year" => "$time_tracking_commits.work_date"},
42 | # work_date: 1,
43 | # duration: 1,
44 | # type: 1,
45 | # comment_id: 1 }}},
46 | # { "$match" => { "time_tracking_commits.work_date_year" => filterYear }},
47 | # { "$group" => { _id: {
48 | # repo_name: "$repo",
49 | # # milestone_number: "$milestone_number",
50 | # # issue_number: "$issue_number",
51 | # # issue_title: "$issue_title",
52 | # # issue_state: "$issue_state",
53 | # work_date_month: "$time_tracking_commits.work_date_month",
54 | # work_date_year: "$time_tracking_commits.work_date_year", },
55 | # time_duration_sum: { "$sum" => "$time_tracking_commits.duration" },
56 | # time_comment_count: { "$sum" => 1 }
57 | # }}
58 | # ])
59 | # output = []
60 | # totalIssueSpentHoursBreakdown.each do |x|
61 | # x["_id"]["time_duration_sum"] = x["time_duration_sum"]
62 | # x["_id"]["time_comment_count"] = x["time_comment_count"]
63 | # output << x["_id"]
64 | # end
65 | # return output
66 | # end
67 |
68 |
69 |
70 | # # gets the durations for a specific year for a specific issue number brokendown by month
71 | # def self.get_issue_time_month_year_sum(repo, filterYear, issueNumber, githubAuthInfo)
72 | # totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([
73 | # { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }},
74 | # { "$project" => { type: 1,
75 | # issue_number: 1,
76 | # _id: 1,
77 | # repo: 1,
78 | # milestone_number: 1,
79 | # issue_state: 1,
80 | # issue_title: 1,
81 | # time_tracking_commits:{ work_date: 1,
82 | # duration: 1,
83 | # type: 1,
84 | # comment_id: 1 }}},
85 |
86 | # { "$match" => { repo: repo }},
87 | # { "$match" => { type: "Issue" }},
88 | # { "$unwind" => "$time_tracking_commits" },
89 | # { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}},
90 | # { "$project" => {type: 1,
91 | # issue_number: 1,
92 | # _id: 1,
93 | # repo: 1,
94 | # milestone_number: 1,
95 | # issue_state: 1,
96 | # issue_title: 1,
97 | # time_tracking_commits:{ work_date_dayOfMonth: {"$dayOfMonth" => "$time_tracking_commits.work_date"},
98 | # work_date_month: {"$month" => "$time_tracking_commits.work_date"},
99 | # work_date_year: {"$year" => "$time_tracking_commits.work_date"},
100 | # work_date: 1,
101 | # duration: 1,
102 | # type: 1,
103 | # comment_id: 1 }}},
104 | # { "$match" => { "time_tracking_commits.work_date_year" => filterYear, "issue_number" =>issueNumber }},
105 | # { "$group" => { _id: {
106 | # repo_name: "$repo",
107 | # milestone_number: "$milestone_number",
108 | # issue_number: "$issue_number",
109 | # issue_title: "$issue_title",
110 | # issue_state: "$issue_state",
111 | # work_date_month: "$time_tracking_commits.work_date_month",
112 | # work_date_year: "$time_tracking_commits.work_date_year", },
113 | # time_duration_sum: { "$sum" => "$time_tracking_commits.duration" },
114 | # time_comment_count: { "$sum" => 1 }
115 | # }}
116 | # ])
117 | # output = []
118 | # totalIssueSpentHoursBreakdown.each do |x|
119 | # x["_id"]["time_duration_sum"] = x["time_duration_sum"]
120 | # x["_id"]["time_comment_count"] = x["time_comment_count"]
121 | # output << x["_id"]
122 | # end
123 | # return output
124 | # end
125 |
126 |
127 |
128 |
129 |
130 | end
131 |
132 | # Debug code
133 | # Issues_Date_Aggregation.controller
134 | # puts Issues_Date_Aggregation.get_repo_time_month_year_sum("StephenOTT/Test1", 2013, {:username => "StephenOTT", :userID => 1994838})
135 | # puts Issues_Date_Aggregation.get_issue_time_month_year_sum("StephenOTT/Test1", 2013, 8, {:username => "StephenOTT", :userID => 1994838})
136 |
--------------------------------------------------------------------------------
/github-analytics-analyze-data/issues_date_processor.rb:
--------------------------------------------------------------------------------
1 | require_relative 'issues_date_aggregation'
2 | require_relative 'helpers'
3 |
4 |
5 | module Issues_Date_Processor
6 |
7 | # def self.analyze_issues_date_year_repo(user, repo, filterYear, githubAuthInfo)
8 | # userRepo = "#{user}/#{repo}"
9 | # Issues_Date_Aggregation.controller
10 | # spentHours = Issues_Date_Aggregation.get_repo_time_month_year_sum(userRepo, filterYear, githubAuthInfo)
11 | # # issues = Helpers.merge_issue_time_and_budget(spentHours, budgetHours)
12 | # spentHours.each do |x|
13 | # if x["time_duration_sum"] != nil
14 | # x["time_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["time_duration_sum"], "long")
15 | # end
16 | # # if x["budget_duration_sum"] != nil
17 | # # x["budget_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["budget_duration_sum"], "long")
18 | # # end
19 | # end
20 |
21 | # # issues = self.process_issues_for_budget_left(issues)
22 |
23 | # # return issues
24 | # return spentHours
25 | # end
26 |
27 |
28 |
29 | # def self.process_issues_for_budget_left(issues)
30 | # issues.each do |i|
31 | # if i["budget_duration_sum"] != nil
32 | # # TODO Cleanup code for Budget left.
33 | # budgetLeftRaw = Helpers.budget_left?(i["budget_duration_sum"], i["time_duration_sum"])
34 | # budgetLeftHuman = Helpers.convertSecondsToDurationFormat(budgetLeftRaw, "long")
35 | # i["budget_left_raw"] = budgetLeftRaw
36 | # i["budget_left_human"] = budgetLeftHuman
37 | # end
38 | # end
39 | # return issues
40 | # end
41 | end
--------------------------------------------------------------------------------
/github-analytics-analyze-data/issues_processor.rb:
--------------------------------------------------------------------------------
1 | require_relative 'issues_aggregation'
2 | require_relative 'helpers'
3 |
4 |
5 | module Issues_Processor
6 |
7 | def self.analyze_issues_opened_per_user(repo, githubAuthInfo)
8 | Issues_Aggregation.controller
9 | issuesOpenedPerUser = Issues_Aggregation.get_issues_opened_per_user(repo, githubAuthInfo)
10 |
11 | return issuesOpenedPerUser
12 | end
13 |
14 | def self.analyze_issues_opened_per_month(repo, githubAuthInfo)
15 | Issues_Aggregation.controller
16 | issuesOpenedPerMonth = Issues_Aggregation.get_issues_created_per_month(repo, githubAuthInfo)
17 |
18 | issuesOpenedPerMonth = Helpers.get_missing_dates(repo, issuesOpenedPerMonth, Helpers.get_date_formatter(:month))
19 | issuesOpenedPerMonth = Helpers.sort_dates_array_hash(issuesOpenedPerMonth, "converted_date")
20 |
21 | return issuesOpenedPerMonth
22 | end
23 |
24 | def self.analyze_issues_closed_per_month(repo, githubAuthInfo)
25 | Issues_Aggregation.controller
26 | issuesClosedPerMonth = Issues_Aggregation.get_issues_closed_per_month(repo, githubAuthInfo)
27 |
28 | return issuesClosedPerMonth
29 | end
30 |
31 | def self.analyze_issues_opened_per_week(repo, githubAuthInfo)
32 | Issues_Aggregation.controller
33 | issuesOpenedPerWeek = Issues_Aggregation.get_issues_created_per_week(repo, githubAuthInfo)
34 |
35 | return issuesOpenedPerWeek
36 | end
37 |
38 | def self.analyze_issues_closed_per_week(repo, githubAuthInfo)
39 | Issues_Aggregation.controller
40 | issuesClosedPerWeek = Issues_Aggregation.get_issues_closed_per_week(repo, githubAuthInfo)
41 |
42 | return issuesClosedPerWeek
43 | end
44 |
45 |
46 |
47 | # def self.process_issues_for_budget_left(issues)
48 | # issues.each do |i|
49 | # if i["budget_duration_sum"] != nil
50 | # # TODO Cleanup code for Budget left.
51 | # budgetLeftRaw = Helpers.budget_left?(i["budget_duration_sum"], i["time_duration_sum"])
52 | # budgetLeftHuman = Helpers.convertSecondsToDurationFormat(budgetLeftRaw, "long")
53 | # i["budget_left_raw"] = budgetLeftRaw
54 | # i["budget_left_human"] = budgetLeftHuman
55 | # end
56 | # end
57 | # return issues
58 | # end
59 |
60 |
61 |
62 | # def self.get_issues_in_milestone(user, repo, milestoneNumber, githubAuthInfo)
63 | # userRepo = "#{user}/#{repo}"
64 | # Issues_Aggregation.controller
65 | # spentHours = Issues_Aggregation.get_all_issues_time_in_milestone(userRepo, milestoneNumber, githubAuthInfo)
66 | # budgetHours = Issues_Aggregation.get_all_issues_budget_in_milestone(userRepo, milestoneNumber, githubAuthInfo)
67 | # issues = Helpers.merge_issue_time_and_budget(spentHours, budgetHours)
68 | # issues.each do |x|
69 | # if x["time_duration_sum"] != nil
70 | # x["time_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["time_duration_sum"], "long")
71 | # end
72 | # if x["budget_duration_sum"] != nil
73 | # x["budget_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["budget_duration_sum"], "long")
74 | # end
75 | # end
76 |
77 | # issues = self.process_issues_for_budget_left(issues)
78 | # return issues
79 |
80 | # end
81 |
82 |
83 | end
--------------------------------------------------------------------------------
/github-analytics-analyze-data/labels_aggregation.rb:
--------------------------------------------------------------------------------
1 | require_relative './mongo'
2 |
3 | module Labels_Aggregation
4 |
5 | def self.controller
6 |
7 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-Analytics", "Issues-Data")
8 |
9 | end
10 |
11 | def self.get_labels_count_for_repo(repo, githubAuthInfo)
12 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([
13 | { "$match" => {type: "Issue"}},
14 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }},
15 | {"$project" => {_id: 1,
16 | repo: 1,
17 | labels: { name: 1},
18 | }},
19 | { "$match" => { repo: repo }},
20 | { "$unwind" => "$labels" },
21 | { "$group" => { _id: {
22 | repo: "$repo",
23 | label: "$labels.name",},
24 | count: { "$sum" => 1 }
25 | }},
26 | { "$sort" => {"count" => 1}}
27 |
28 | ])
29 | output = []
30 | totalIssueSpentHoursBreakdown.each do |x|
31 | # x["_id"]["time_duration_sum"] = x["time_duration_sum"]
32 | x["_id"]["count"] = x["count"]
33 | output << x["_id"]
34 | end
35 | return output
36 | end
37 |
38 |
39 |
40 |
41 | # def self.get_issues_time_for_label_and_milestone(repo, category, label, milestoneNumber)
42 | # totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([
43 | # {"$project" => {type: 1,
44 | # issue_number: 1,
45 | # _id: 1,
46 | # repo: 1,
47 | # milestone_number: 1,
48 | # issue_state: 1,
49 | # issue_title: 1,
50 | # time_tracking_commits: { duration: 1,
51 | # type: 1,
52 | # comment_id: 1 },
53 | # labels: { category: 1,
54 | # label: 1 },
55 | # }},
56 | # { "$match" => { repo: repo }},
57 | # { "$match" => { type: "Issue" }},
58 | # { "$match" => { milestone_number: milestoneNumber }},
59 | # { "$unwind" => "$labels" },
60 | # { "$unwind" => "$time_tracking_commits" },
61 | # { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}},
62 | # { "$match" => { "labels.category" => category }},
63 | # { "$match" => { "labels.label" => label }},
64 | # { "$group" => { _id: {
65 | # repo_name: "$repo",
66 | # category: "$labels.category",
67 | # label: "$labels.label",
68 | # milestone_number: "$milestone_number",
69 | # issue_number: "$issue_number",
70 | # issue_title: "$issue_title",
71 | # issue_state: "$issue_state", },
72 | # time_duration_sum: { "$sum" => "$time_tracking_commits.duration" },
73 | # time_comment_count: { "$sum" => 1 }
74 | # }}
75 | # ])
76 | # output = []
77 | # totalIssueSpentHoursBreakdown.each do |x|
78 | # x["_id"]["time_duration_sum"] = x["time_duration_sum"]
79 | # x["_id"]["time_comment_count"] = x["time_comment_count"]
80 | # output << x["_id"]
81 | # end
82 | # return output
83 | # end
84 |
85 | end
86 |
87 |
88 | # Debug Code
89 | # Labels_Aggregation.controller
90 | # puts Labels_Aggregation.get_labels_count_for_repo("StephenOTT/OPSEU", {:username => "StephenOTT", :userID => 1994838})
91 |
92 |
93 |
--------------------------------------------------------------------------------
/github-analytics-analyze-data/labels_processor.rb:
--------------------------------------------------------------------------------
1 | require_relative 'labels_aggregation'
2 | require_relative 'helpers'
3 |
4 |
5 | module Labels_Processor
6 |
7 | def self.analyze_labels_count_for_repo(repo, githubAuthInfo)
8 | Labels_Aggregation.controller
9 | labelsCountForRepo = Labels_Aggregation.get_labels_count_for_repo(repo, githubAuthInfo)
10 |
11 | return labelsCountForRepo
12 | end
13 |
14 |
15 | end
--------------------------------------------------------------------------------
/github-analytics-analyze-data/mongo.rb:
--------------------------------------------------------------------------------
1 | require 'mongo'
2 |
3 | module Mongo_Connection
4 |
5 | include Mongo
6 |
7 | def self.clear_mongo_collections
8 | @collTimeTrackingCommits.remove
9 | end
10 |
11 |
12 | def self.putIntoMongoCollTimeTrackingCommits(mongoPayload)
13 | @collTimeTrackingCommits.insert(mongoPayload)
14 | end
15 |
16 |
17 | def self.mongo_Connect(url, port, dbName, collName)
18 | # MongoDB Database Connect
19 | @client = MongoClient.new(url, port)
20 | # @client = MongoClient.new("localhost", 27017)
21 |
22 | # code for working with MongoLab
23 | # uri = "mongodb://USERNAME:PASSWORD@ds061268.mongolab.com:61268/TimeTrackingCommits"
24 | # @client = MongoClient.from_uri(uri)
25 |
26 | @db = @client[dbName]
27 |
28 | @collTimeTrackingCommits = @db[collName]
29 |
30 | end
31 |
32 | def self.aggregate_test(input1)
33 |
34 | @collTimeTrackingCommits.aggregate(input1)
35 |
36 | end
37 |
38 | end
--------------------------------------------------------------------------------
/github-analytics-analyze-data/system_wide_aggregation.rb:
--------------------------------------------------------------------------------
1 | require_relative './mongo'
2 |
3 | module System_Wide_Aggregation
4 |
5 | def self.controller
6 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-Analytics", "Issues-Data")
7 | end
8 |
9 | def self.get_all_repos_assigned_to_logged_user(githubAuthInfo)
10 | reposAssignedToLoggedUser = Mongo_Connection.aggregate_test([
11 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }},
12 | {"$project" => {_id: 1,
13 | repo: 1}},
14 | { "$group" => { _id: {
15 | repo: "$repo"
16 | }}}
17 | ])
18 |
19 |
20 | output = []
21 | reposAssignedToLoggedUser.each do |x|
22 | toParseString = x["_id"]["repo"]
23 | x["_id"]["username"] = toParseString.partition("/").first
24 | x["_id"]["repo"] = toParseString.partition("/").last
25 | x["_id"]["repo_name_full"] = toParseString
26 | output << x["_id"]
27 | end
28 | return output
29 | end
30 | end
31 |
32 | # Debug code
33 | # System_Wide_Aggregation.controller
34 | # puts System_Wide_Aggregation.get_all_repos_assigned_to_logged_user({:username => "StephenOTT", :userID => 1994838})
--------------------------------------------------------------------------------
/github-analytics-analyze-data/system_wide_processor.rb:
--------------------------------------------------------------------------------
1 | require_relative 'system_wide_aggregation'
2 | require_relative 'helpers'
3 |
4 |
5 | module System_Wide_Processor
6 |
7 | def self.all_repos_for_logged_user(githubAuthInfo)
8 | # userRepo = "#{user}/#{repo}"
9 |
10 | System_Wide_Aggregation.controller # makes mongo connection
11 |
12 | repos = System_Wide_Aggregation.get_all_repos_assigned_to_logged_user(githubAuthInfo)
13 |
14 | return repos
15 | end
16 | end
--------------------------------------------------------------------------------
/github-analytics-analyze-data/time_analyzer.rb:
--------------------------------------------------------------------------------
1 | require_relative './mongo'
2 |
3 | module Time_Analyzer
4 |
5 | def self.controller
6 |
7 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-TimeTracking", "TimeTrackingCommits")
8 |
9 | end
10 |
11 | end
12 |
13 |
14 |
--------------------------------------------------------------------------------
/github-analytics-data-download/code_commits.rb:
--------------------------------------------------------------------------------
1 | require_relative 'helpers'
2 | require_relative 'code_commit_messages'
3 | require_relative 'code_commit_comments'
4 |
5 | module GH_Commits
6 |
7 | def self.process_code_commit(repo, commitDetails, commitComments, githubAuthInfo)
8 |
9 | githubUserName = githubAuthInfo[:username]
10 | githubUserID = githubAuthInfo[:userID]
11 | commitMessage = commitDetails.attrs[:commit].attrs[:message]
12 |
13 | type = "Code Commit"
14 | recordCreationDate = Time.now.utc
15 |
16 | commitAuthorUsername = commitDetails.attrs[:author].attrs[:login]
17 | commitAuthorDate = commitDetails.attrs[:commit].attrs[:author].attrs[:date]
18 | commitCommitterUsername = commitDetails.attrs[:committer].attrs[:login]
19 | commitCommitterDate = commitDetails.attrs[:commit].attrs[:committer].attrs[:date]
20 | commitSha = commitDetails.attrs[:sha]
21 | commitTreeSha = commitDetails.attrs[:commit].attrs[:tree].attrs[:sha]
22 | commitParentsShas = []
23 |
24 | # TODO look to crate this if statement as a helper if it makes sense
25 | if commitDetails.attrs[:parents] != nil
26 | commitDetails.attrs[:parents].each do |x|
27 | commitParentsShas << x.attrs[:sha]
28 | end
29 | end
30 |
31 | parsedCommitMessage = Commit_Messages.process_commit_message_for_time(commitMessage)
32 | parsedCommitComments = []
33 |
34 | # commitComments = Helpers.get_commit_comments(repo, commitSha)
35 |
36 | commitComments.each do |x|
37 | parsedCommitComment = Commit_Comments.process_commit_comment_for_time(x)
38 | if parsedCommitComment.empty? == false
39 | parsedCommitComments << parsedCommitComment
40 | end
41 | end
42 |
43 |
44 | if parsedCommitMessage == nil and parsedCommitComments.empty? == true
45 | return []
46 | else
47 | timeCommitHash = { "downloaded_by_username" => githubUserName,
48 | "downloaded_by_userID" => githubUserID,
49 | "type" => type,
50 | "repo" => repo,
51 | "commit_author_username" => commitAuthorUsername,
52 | "commit_author_date" => commitAuthorDate,
53 | "commit_committer_username" => commitCommitterUsername,
54 | "commit_committer_date" => commitCommitterDate,
55 | "commit_sha" => commitSha,
56 | "commit_tree_sha" => commitTreeSha,
57 | "commit_parents_shas" => commitParentsShas,
58 | "record_creation_date" => recordCreationDate,
59 | "commit_message_time" => parsedCommitMessage,
60 | "commit_comments_time" => parsedCommitComments,
61 | }
62 | end
63 | end
64 | end
--------------------------------------------------------------------------------
/github-analytics-data-download/controller.rb:
--------------------------------------------------------------------------------
1 | require_relative 'github_data'
2 | require_relative 'mongo'
3 | require_relative 'convert_dates'
4 |
5 | module Analytics_Download_Controller
6 |
7 | def self.controller(repo, object1, clearCollections = false, githubAuthInfo = {})
8 | # GitHub_Data.gh_authenticate(username, password)
9 | GitHub_Data.gh_sinatra_auth(object1)
10 |
11 | # MongoDb connection: DB URL, Port, DB Name, Collection Name
12 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-Analytics", "Issues-Data")
13 |
14 | # Clears the DB collections if clearCollections var in controller argument is true
15 | if clearCollections == true
16 | Mongo_Connection.clear_mongo_collections
17 | end
18 |
19 | #======Start of Issues=======
20 | issues = GitHub_Data.get_Issues(repo)
21 |
22 |
23 | # goes through each issue returned from get_Issues method
24 | issues.each do |i|
25 | # puts i
26 | i = Dates_Convert_For_MongoDB.convertIssueDatesForMongo(i)
27 |
28 | # Gets the comments for the specific issue
29 | issueComments = GitHub_Data.get_Issue_Comments(repo, i["number"])
30 | issueComments.each do |ic|
31 | ic = Dates_Convert_For_MongoDB.convertIssueCommentDatesInMongo(ic)
32 | end
33 |
34 | i["comments"] = issueComments
35 | i["downloaded_by_username"] = githubAuthInfo[:username]
36 | i["downloaded_by_userID"] = githubAuthInfo[:userID]
37 | i["repo"] = repo
38 | i["type"] = "Issue"
39 | i["download_datetime"] = Time.now.utc
40 | # Parses the specific issue for time tracking information
41 | # processedIssues = Gh_Issue.process_issue(repo, i, issueComments, githubAuthInfo)
42 |
43 | # if data is returned from the parsing attempt, the data is passed into MongoDb
44 | # if i.empty? == false
45 | Mongo_Connection.putIntoMongoCollTimeTrackingCommits(i)
46 | # end
47 | end
48 | #======End of Issues=======
49 |
50 | #======Repo Issue Events======
51 | repoIssueEvents = GitHub_Data.get_repo_issue_events(repo)
52 | # puts repoIssueEvents.to_s
53 | repoIssueEvents.each do |rie|
54 |
55 | rie = Dates_Convert_For_MongoDB.convertRepoEventsDates(rie)
56 |
57 | rie["downloaded_by_username"] = githubAuthInfo[:username]
58 | rie["downloaded_by_userID"] = githubAuthInfo[:userID]
59 | rie["repo"] = repo
60 | rie["type"] = "Repo Issue Event"
61 | rie["download_datetime"] = Time.now.utc
62 |
63 | # if rie.empty? == false
64 | Mongo_Connection.putIntoMongoCollTimeTrackingCommits(rie)
65 | # end
66 | end
67 |
68 | end
69 | end
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/github-analytics-data-download/convert_dates.rb:
--------------------------------------------------------------------------------
1 | module Dates_Convert_For_MongoDB
2 |
3 |
4 | def self.convertIssueCommentDatesInMongo(issueComments)
5 | issueComments["created_at"] = Time.strptime(issueComments["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
6 | issueComments["updated_at"] = Time.strptime(issueComments["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
7 | return issueComments
8 | end
9 |
10 | def self.convertIssueDatesForMongo(issues)
11 | issues["created_at"] = Time.strptime(issues["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
12 | issues["updated_at"] = Time.strptime(issues["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
13 | if issues["closed_at"] != nil
14 | issues["closed_at"] = Time.strptime(issues["closed_at"], '%Y-%m-%dT%H:%M:%S%z').utc
15 | end
16 | return issues
17 | end
18 |
19 | def self.convertRepoEventsDates(repoEvents)
20 | if repoEvents["created_at"] != nil
21 | repoEvents["created_at"] = Time.strptime(repoEvents["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
22 | end
23 | return repoEvents
24 | end
25 |
26 | def self.convertIssueEventsDates(issueEvents)
27 | if issueEvents["created_at"] != nil
28 | issueEvents["created_at"] = Time.strptime(issueEvents["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
29 | end
30 | return issueEvents
31 | end
32 |
33 | def self.convertMilestoneDates(milestone)
34 | milestone["created_at"] = Time.strptime(milestone["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
35 | milestone["updated_at"] = Time.strptime(milestone["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
36 | if milestone["due_on"]!= nil
37 | milestone["due_on"] = Time.strptime(milestone["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
38 | end
39 | return milestone
40 | end
41 |
42 | def self.convertTeamReposDates(teamRepos)
43 | teamRepos.each do |x|
44 | if x["created_at"] != nil
45 | x["created_at"] = Time.strptime(x["created_at"], '%Y-%m-%dT%H:%M:%S%z').utc
46 | end
47 | if x["updated_at"]!= nil
48 | x["updated_at"] = Time.strptime(x["updated_at"], '%Y-%m-%dT%H:%M:%S%z').utc
49 | end
50 | if x["pushed_at"] != nil
51 | x["pushed_at"] = Time.strptime(x["pushed_at"], '%Y-%m-%dT%H:%M:%S%z').utc
52 | end
53 | end
54 | return teamRepos
55 | end
56 |
57 |
58 | end
--------------------------------------------------------------------------------
/github-analytics-data-download/github_data.rb:
--------------------------------------------------------------------------------
1 | require 'octokit'
2 | require 'pp'
3 | require 'json'
4 |
5 |
6 | # Monkey Patch code to make Sawyer::Response allow me to access the Raw Body and not
7 | # the Hypermedia version which is not does play friendly with MongoDB as it is not
8 | # JSON and currently no code exists to convert to JSON
9 | module Response
10 | attr_reader :response_body
11 |
12 | def initialize(agent, res, option = {})
13 | @response_body = res.body
14 | super
15 | end
16 | end
17 | # Prepend the module to override initialize
18 | ::Sawyer::Response.send :prepend, Response
19 |
20 |
21 | module GitHub_Data
22 |
23 | def self.gh_sinatra_auth(ghUser)
24 |
25 | @ghClient = ghUser
26 | # Octokit.auto_paginate = true
27 | Octokit.per_page = 100
28 | return @ghClient
29 |
30 | end
31 |
32 | def self.gh_authenticate(username, password)
33 | @ghClient = Octokit::Client.new(
34 | :login => username.to_s,
35 | :password => password.to_s,
36 | # :auto_paginate => true
37 | :per_page => 100
38 | )
39 | end
40 |
41 | def self.get_Issues(repo)
42 |
43 | puts "1: #{@ghClient.rate_limit.remaining}"
44 | issueResultsOpen = @ghClient.list_issues(repo, {
45 | :state => :open,
46 | :per_page => 100
47 | })
48 |
49 | ghLastReponseOpen = @ghClient.last_response
50 | responseOpen = JSON.parse(@ghClient.last_response.response_body)
51 |
52 | while ghLastReponseOpen.rels.include?(:next) do
53 | puts "2: #{@ghClient.rate_limit.remaining}"
54 | ghLastReponseOpen = ghLastReponseOpen.rels[:next].get
55 | responseOpen.concat(JSON.parse(ghLastReponseOpen.response_body))
56 | end
57 |
58 | puts "3: #{@ghClient.rate_limit.remaining}"
59 | issueResultsClosed = @ghClient.list_issues(repo, {
60 | :state => :closed,
61 | :per_page => 100
62 | })
63 |
64 | ghLastReponseClosed = @ghClient.last_response
65 | responseClosed = JSON.parse(@ghClient.last_response.response_body)
66 |
67 | while ghLastReponseClosed.rels.include?(:next) do
68 | puts "4: #{@ghClient.rate_limit.remaining}"
69 | ghLastReponseClosed = ghLastReponseClosed.rels[:next].get
70 | responseClosed.concat(JSON.parse(ghLastReponseClosed.response_body))
71 | end
72 |
73 | return mergedIssues = responseOpen + responseClosed
74 | end
75 |
76 | # def self.get_Milestones(repo)
77 | # milestonesResultsOpen = @ghClient.list_milestones(repo, {
78 | # :state => :open
79 | # })
80 | # milestonesResultsClosed = @ghClient.list_milestones(repo, {
81 | # :state => :closed
82 | # })
83 |
84 | # return mergedMilestones = milestonesResultsOpen + milestonesResultsClosed
85 | # end
86 |
87 | def self.get_Issue_Comments(repo, issueNumber)
88 | puts "5: #{@ghClient.rate_limit.remaining}"
89 | issueComments = @ghClient.issue_comments(repo, issueNumber, {
90 | :per_page => 100
91 | })
92 |
93 | ghLastReponseComments = @ghClient.last_response
94 | responseComments = JSON.parse(@ghClient.last_response.response_body)
95 |
96 | while ghLastReponseComments.rels.include?(:next) do
97 | puts "6: #{@ghClient.rate_limit.remaining}"
98 | ghLastReponseComments = ghLastReponseComments.rels[:next].get
99 | responseComments.concat(JSON.parse(ghLastReponseComments.response_body))
100 | end
101 |
102 | return responseComments
103 | end
104 |
105 | # def self.get_code_commits(repo)
106 | # repoCommits = @ghClient.commits(repo)
107 | # end
108 |
109 | # def self.get_commit_comments(repo, sha)
110 | # commitComments = @ghClient.commit_comments(repo, sha)
111 | # end
112 |
113 |
114 | def self.get_repo_issue_events(repo)
115 | issueResultsOpen = @ghClient.repository_issue_events(repo, {
116 | :per_page => 100
117 | })
118 | ghLastReponseRepoIssueEvents = @ghClient.last_response
119 | responseRepoEvents = JSON.parse(@ghClient.last_response.response_body)
120 |
121 | while ghLastReponseRepoIssueEvents.rels.include?(:next) do
122 | ghLastReponseRepoIssueEvents = ghLastReponseRepoIssueEvents.rels[:next].get
123 | responseRepoEvents.concat(JSON.parse(ghLastReponseRepoIssueEvents.response_body))
124 | end
125 | return responseRepoEvents
126 | end
127 |
128 |
129 |
130 | end
131 |
132 |
133 | # GitHub_Data.gh_authenticate("StephenOTT", "PASSWORD")
134 | # issues = GitHub_Data.get_Issues("StephenOTT/Test1")
135 | # repoIssueEvents = GitHub_Data.get_repo_events("StephenOTT/Test1")
136 | # pp issues
137 | # puts repoIssueEvents
138 |
--------------------------------------------------------------------------------
/github-analytics-data-download/helpers.rb:
--------------------------------------------------------------------------------
1 | # require 'chronic_duration'
2 | # require_relative 'accepted_emoji'
3 |
4 | module Helpers
5 |
6 | # def self.get_Issue_Budget_Emoji
7 | # return Accepted_Time_Tracking_Emoji.accepted_issue_budget_emoji
8 | # end
9 | # def self.get_Issue_Time_Emoji
10 | # return Accepted_Time_Tracking_Emoji.accepted_time_comment_emoji
11 | # end
12 | # def self.get_Milestone_Budget_Emoji
13 | # return Accepted_Time_Tracking_Emoji.accepted_milestone_budget_emoji
14 | # end
15 | # def self.get_Non_Billable_Emoji
16 | # return Accepted_Time_Tracking_Emoji.accepted_nonBillable_emoji
17 | # end
18 |
19 | # def self.get_duration(durationText)
20 | # return ChronicDuration.parse(durationText)
21 | # end
22 |
23 | # def self.get_time_work_date(parsedTimeComment)
24 | # begin
25 | # return Time.parse(parsedTimeComment).utc
26 | # rescue
27 | # return nil
28 | # end
29 | # end
30 |
31 | # def self.parse_non_billable_time_comment(timeComment, timeEmoji, nonBillableEmoji)
32 | # return timeComment.gsub("#{timeEmoji} #{nonBillableEmoji} ","").split(" | ")
33 | # end
34 |
35 | # def self.parse_billable_time_comment(timeComment, timeEmoji)
36 | # return timeComment.gsub("#{timeEmoji} ","").split(" | ")
37 | # end
38 |
39 | # def self.get_time_commit_comment(parsedTimeComment)
40 | # return parsedTimeComment.lstrip.gsub("\r\n", " ")
41 | # end
42 |
43 | # def self.budget_comment?(commentBody)
44 | # acceptedBudgetEmoji = Accepted_Time_Tracking_Emoji.accepted_milestone_budget_emoji
45 |
46 | # acceptedBudgetEmoji.any? { |w| commentBody =~ /\A#{w}/ }
47 | # end
48 |
49 | # def self.non_billable?(commentBody)
50 | # acceptedNonBilliableEmoji = Accepted_Time_Tracking_Emoji.accepted_nonBillable_emoji
51 | # return acceptedNonBilliableEmoji.any? { |b| commentBody =~ /#{b}/ }
52 | # end
53 |
54 | # # Is it a time comment? Returns True or False
55 | # def self.time_comment?(commentBody)
56 | # acceptedClockEmoji = Accepted_Time_Tracking_Emoji.accepted_time_comment_emoji
57 |
58 | # acceptedClockEmoji.any? { |w| commentBody =~ /\A#{w}/ }
59 | # end
60 |
61 | # # Gets the milestone ID number assigned to the issue
62 | # def self.get_issue_milestone_number(milestoneDetails)
63 | # if milestoneDetails != nil
64 | # return milestoneDetails.attrs[:number]
65 | # end
66 | # end
67 |
68 | # def self.convertSecondsToDurationFormat(timeInSeconds, outputFormat)
69 | # outputFormat = outputFormat.to_sym
70 | # return ChronicDuration.output(timeInSeconds, :format => outputFormat, :keep_zero => true)
71 | # end
72 |
73 | end
--------------------------------------------------------------------------------
/github-analytics-data-download/mongo.rb:
--------------------------------------------------------------------------------
1 | require 'mongo'
2 |
3 | module Mongo_Connection
4 |
5 | include Mongo
6 |
7 | def self.clear_mongo_collections
8 | @collTimeTrackingCommits.remove
9 | end
10 |
11 |
12 | def self.putIntoMongoCollTimeTrackingCommits(mongoPayload)
13 | @collTimeTrackingCommits.insert(mongoPayload)
14 | end
15 |
16 |
17 | def self.mongo_Connect(url, port, dbName, collName)
18 | # MongoDB Database Connect
19 | @client = MongoClient.new(url, port)
20 | # @client = MongoClient.new("localhost", 27017)
21 |
22 | # code for working with MongoLab
23 | # uri = "mongodb://USERNAME:PASSWORD@ds061268.mongolab.com:61268/TimeTrackingCommits"
24 | # @client = MongoClient.from_uri(uri)
25 |
26 | @db = @client[dbName]
27 |
28 | @collTimeTrackingCommits = @db[collName]
29 |
30 | end
31 |
32 | def self.aggregate_test(input1)
33 |
34 | @collTimeTrackingCommits.aggregate(input1)
35 |
36 | end
37 |
38 | end
--------------------------------------------------------------------------------