├── 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 | [![endorse](https://api.coderwall.com/stephenott/endorsecount.png)](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 | ![screen shot 2014-04-19 at 2 06 35 am](https://cloud.githubusercontent.com/assets/1994838/2747795/e1c498a4-c788-11e3-874d-29a6c54dae9f.png) 55 | 56 | 57 | ----- 58 | 59 | 60 | ![screen shot 2014-04-20 at 4 08 23 pm](https://cloud.githubusercontent.com/assets/1994838/2751355/a3b7bc9a-c8c7-11e3-8430-a2c065a2c98f.png) 61 | 62 | 63 | ##Process Overview: 64 | ![github-analytics process overview](https://f.cloud.github.com/assets/1994838/1708517/a88ca764-610f-11e3-8d0a-48500edb0d83.png) 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 | ![screen shot 2013-09-24 at 1 42 01 am](https://f.cloud.github.com/assets/1994838/1197485/553afd28-24dc-11e3-9d84-9c7b32bbe69b.png) 176 | 177 | -- 178 | 179 | ![screen shot 2013-09-24 at 1 34 07 am](https://f.cloud.github.com/assets/1994838/1197486/5559f91c-24dc-11e3-9792-0c884526fd60.png) 180 | 181 | -- 182 | 183 | Printable Issue queues for PMs with spikelines shows activity: 184 | ![screen shot 2013-10-17 at 2 04 24 pm](https://f.cloud.github.com/assets/1994838/1354563/d020bd84-3756-11e3-856b-34e29e3339c1.png) 185 | 186 | -- 187 | 188 | Issues Count Assigned to specific users + unassigned count: 189 | ![screen shot 2013-10-18 at 12 14 54 am](https://f.cloud.github.com/assets/1994838/1358033/0ecc7f12-37ae-11e3-9d14-1a6f369d047d.png) 190 | 191 | -- 192 | 193 | Issue Events Timeline (This is all issue events for all issues grouped together) 194 | ![screen shot 2013-10-19 at 2 04 15 am](https://f.cloud.github.com/assets/1994838/1365705/7af5e936-3884-11e3-85ae-0d404bc5c496.png) 195 | 196 | -- 197 | 198 | Pie chart of Issue Event Types - All Issue events for all issues 199 | ![screen shot 2013-10-19 at 2 08 05 am](https://f.cloud.github.com/assets/1994838/1365706/0cf044da-3885-11e3-842b-f68cbcc1b5b6.png) 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 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | <% @printableData.each do |elem| %> 77 | 78 | 81 | 84 | 87 | 90 | 93 | 96 | 99 | 102 | 105 | 108 | 109 | <% end %> 110 |
StateIssue NumberMilestoneIssue TitleAssigneeCreated DateClosed DateCreated ByComment CountYear to Date Comment Activity
79 | <%= elem["issueCurrentState"] %> 80 | 82 | <%= elem["issueNumber"] %> 83 | 85 | <%= elem["issueAssignedMilestone"] %> 86 | 88 | <%= elem["issueTitle"] %> 89 | 91 | <%= elem["issueCurrentAssignee"] %> 92 | 94 | <%= elem["created_at"] %> 95 | 97 | <%= elem["closed_at"] %> 98 | 100 | <%= elem["createdBy"] %> " height="30" width="30"> 101 | 103 | <%= elem["commentsCount"] %> 104 | 106 | "> 107 |
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 | 42 | 43 | 44 | <% @issuesOpenedPerMonth.each do |i| %> 45 | 46 | 47 | 48 | 49 | <% end %> 50 | 51 |
DateCount
<%= i["converted_date"].strftime("%B %Y") %><%= i["count"] %>
52 | 53 |
54 | 57 |
58 |
59 |
60 |
61 |
62 |

63 | Closed Issues Per Month 64 |

65 |
66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | <% @issuesClosedPerMonth.each do |i| %> 75 | 76 | 77 | 78 | 79 | <% end %> 80 | 81 |
DateCount
<%= i["converted_date"].strftime("%B %Y") %><%= i["count"] %>
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 | 131 | 132 | 133 | <% @issuesOpenedPerWeek.each do |i| %> 134 | 135 | 136 | 137 | 138 | <% end %> 139 | 140 |
DateCount
<%= i["converted_date"].strftime("Week %U, %Y") %><%= i["count"] %>
141 | 142 |
143 | 146 |
147 |
148 | 149 |
150 |
151 |
152 |

153 | Closed Issues Per Week 154 |

155 |
156 |
157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | <% @issuesClosedPerWeek.each do |i| %> 165 | 166 | 167 | 168 | 169 | <% end %> 170 | 171 |
DateCount
<%= i["converted_date"].strftime("Week %U, %Y") %><%= i["count"] %>
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 | 38 | 39 | 40 | <% @issuesOpenedPerUser.each do |i| %> 41 | 42 | 43 | 44 | 45 | <% end %> 46 | 47 |
UsernameOpened Issues Count
<%= i["user"] %><%= i["issues_opened_count"] %>
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 | 41 | 42 | 43 | <% @labelsCountForRepo.each do |l| %> 44 | 45 | 46 | 47 | 48 | <% end %> 49 | 50 |
Label NameCount
<%= l["label"] %><%= l["count"] %>
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 | 41 | 42 | 43 | 44 | <% @repoIssueEvents.each do |l| %> 45 | 46 | 47 | 48 | 49 | 50 | <% end %> 51 | 52 |
EventCountDate
<%= l["event"] %><%= l["count"] %><%= l["converted_date"].strftime("%B %Y") %>
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 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 | 24 |
25 |
26 | 29 |
30 |
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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% @reposList.each do |r| %> 20 | 21 | 22 | 23 | 24 | 30 | 31 | <% end %> 32 | 33 |
UsernameRepository NameFull Repository Name
<%= r["username"] %><%= r["repo"] %><%= r["repo_name_full"] %> 25 |

">Issues Opened/Closed Per User

26 |

">Issues Opened/Closed Analysis

27 |

">Labels Analysis

28 |

">Issue Events Analysis

29 |
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 --------------------------------------------------------------------------------