├── Business ├── GitHub Time Tracking Process Overview.graffle ├── GitHub Time Tracking Process Overview.png ├── Object Based Process Overview.graffle └── Object Based Process Overview.png ├── LICENSE.txt ├── Old Files to be processed ├── time_analyzer └── time_tracker ├── README.md ├── 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 │ │ └── jquery-1.9.1.min.js ├── sinatra_auth_github.gemspec ├── sinatra_helpers.rb └── views │ ├── calendar.erb │ ├── code_commits.erb │ ├── download_data.erb │ ├── index.erb │ ├── issue_time.erb │ ├── issues.erb │ ├── issues_in_milestone.erb │ ├── labels.erb │ ├── layout.erb │ ├── milestones.erb │ ├── repo_dates.erb │ ├── repo_user_dates.erb │ ├── repos_listing.erb │ └── unauthenticated.erb ├── controller.rb ├── gh_issue_task_aggregator.rb ├── time_analyzer ├── TODO.txt ├── code_commit_aggregation.rb ├── helpers.rb ├── issues_aggregation.rb ├── issues_date_aggregation.rb ├── issues_date_processor.rb ├── issues_processor.rb ├── labels_aggregation.rb ├── milestones_aggregation.rb ├── milestones_processor.rb ├── mongo.rb ├── system_wide_aggregation.rb ├── system_wide_processor.rb ├── time_analyzer.rb ├── users_aggregation.rb └── users_processor.rb └── time_tracker ├── accepted_emoji.rb ├── accepted_labels_categories.rb ├── code_commit_comments.rb ├── code_commit_messages.rb ├── code_commits.rb ├── github_data.rb ├── helpers.rb ├── issue_budget.rb ├── issue_comment_tasks.rb ├── issue_time.rb ├── issues.rb ├── labels_processor.rb ├── milestone_budget.rb ├── milestones.rb └── mongo.rb /Business/GitHub Time Tracking Process Overview.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitHub-Time-Tracking/2c1812075d4de6720e37a100ec63d507cf410e38/Business/GitHub Time Tracking Process Overview.graffle -------------------------------------------------------------------------------- /Business/GitHub Time Tracking Process Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitHub-Time-Tracking/2c1812075d4de6720e37a100ec63d507cf410e38/Business/GitHub Time Tracking Process Overview.png -------------------------------------------------------------------------------- /Business/Object Based Process Overview.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitHub-Time-Tracking/2c1812075d4de6720e37a100ec63d507cf410e38/Business/Object Based Process Overview.graffle -------------------------------------------------------------------------------- /Business/Object Based Process Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitHub-Time-Tracking/2c1812075d4de6720e37a100ec63d507cf410e38/Business/Object Based Process Overview.png -------------------------------------------------------------------------------- /Old Files to be processed/time_analyzer: -------------------------------------------------------------------------------- 1 | require 'mongo' 2 | 3 | class GitHubTimeTrackingAnalyzer 4 | include Mongo 5 | 6 | def controller 7 | 8 | self.mongo_Connect 9 | 10 | puts self.analyze_issue_spent_hours.to_s 11 | puts self.analyze_issue_budget_hours.to_s 12 | puts self.analyze_milestone_budget_hours.to_s 13 | 14 | end 15 | 16 | def mongo_Connect 17 | # MongoDB Database Connect 18 | @client = MongoClient.new("localhost", 27017) 19 | 20 | # code for working with MongoLab 21 | # uri = "mongodb://USERNAME:PASSWORD@ds061268.mongolab.com:61268/time_commits" 22 | # @client = MongoClient.from_uri(uri) 23 | 24 | @db = @client["GitHub-TimeTracking"] 25 | 26 | @collTimeTrackingCommits = @db["TimeTrackingCommits"] 27 | end 28 | 29 | def analyze_issue_spent_hours 30 | totalIssueSpentHoursBreakdown = @collTimeTrackingCommits.aggregate([ 31 | { "$match" => {type: "Issue Time"}}, 32 | { "$group" => {_id:{repo_name: "$repo_name", 33 | type: "$type", 34 | assigned_milestone_number: "$assigned_milestone_number", 35 | issue_number: "$issue_number", 36 | issue_state: "$issue_state" }, 37 | duration_sum: { "$sum" => "$duration" }, 38 | issue_count:{ "$sum" => 1 }}} 39 | ]) 40 | output = [] 41 | totalIssueSpentHoursBreakdown.each do |x| 42 | x["_id"]["duration_sum"] = x["duration_sum"] 43 | x["_id"]["issue_count"] = x["issue_count"] 44 | output << x["_id"] 45 | end 46 | return output 47 | end 48 | 49 | def analyze_issue_budget_hours 50 | totalIssueBudgetHoursBreakdown = @collTimeTrackingCommits.aggregate([ 51 | { "$match" => {type: "Issue Budget"}}, 52 | { "$group" => {_id:{repo_name: "$repo_name", 53 | type: "$type", 54 | issue_number:"$issue_number", 55 | assigned_milestone_number: "$assigned_milestone_number", 56 | issue_state: "$issue_state" }, 57 | duration_sum: { "$sum" => "$duration" }, 58 | issue_count:{ "$sum" => 1 }}} 59 | ]) 60 | output = [] 61 | totalIssueBudgetHoursBreakdown.each do |x| 62 | x["_id"]["duration_sum"] = x["duration_sum"] 63 | x["_id"]["issue_count"] = x["issue_count"] 64 | output << x["_id"] 65 | end 66 | return output 67 | end 68 | 69 | def analyze_milestone_budget_hours 70 | totalMilestoneBudgetHoursBreakdown = @collTimeTrackingCommits.aggregate([ 71 | { "$match" => {type: "Milestone Budget"}}, 72 | { "$group" => {_id:{repo_name: "$repo_name", 73 | type: "$type", 74 | milestone_number: "$milestone_number", 75 | milestone_state: "$milestone_state" }, 76 | duration_sum: { "$sum" => "$duration" }, 77 | milestone_count:{ "$sum" => 1 }}} 78 | ]) 79 | output = [] 80 | totalMilestoneBudgetHoursBreakdown.each do |x| 81 | x["_id"]["duration_sum"] = x["duration_sum"] 82 | x["_id"]["milestone_count"] = x["milestone_count"] 83 | output << x["_id"] 84 | end 85 | return output 86 | end 87 | end 88 | 89 | start = GitHubTimeTrackingAnalyzer.new 90 | start.controller 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub-Time-Tracking 2 | =========================== 3 | 4 | [![Join the chat at https://gitter.im/StephenOTT/GitHub-Time-Tracking](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/StephenOTT/GitHub-Time-Tracking?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | [![endorse](https://api.coderwall.com/stephenott/endorsecount.png)](https://coderwall.com/stephenott) 7 | 8 | Ruby app that analyzes GitHub Issue Comments, Milestones, and Code Commit Messages for Time Tracking and Budget Tracking information. 9 | 10 | GitHub-Time-Tracking is designed to offer maximum flexibility in the way you use GitHub to track your time and budgets, but provide a time and budget syntax that is intuitive to use and read. Any emoji that is used was specifically chosen to be intuitive to its purpose. Of course you can choose your own set of Emoji if you do not like the predefined ones. 11 | 12 | **If you like GitHub-Time-Tracking, be sure to check out GitHub-Analytics: https://github.com/StephenOTT/GitHub-Analytics** 13 | 14 | 15 | 16 | ## News 17 | 18 | **March 1, 2014** Sinatra support has been added and mongo aggregation queries have started to be produced for MVP development. Time Tracker will be turned into a gem and some code will be refactored to better support the gem. The Sinatra App is currently part of this repo, but will be pushed into a separate repo sometime in the future. The app will use basic bootstrap to provide a theme for the interface and base queries will be support to provide time and budget totals for issues, milestones, labels, and commit messages(+commit comments). The Sinatra app is fully functioning with GitHub OAuth2 support and will download your repo issue, milestone, and commit data (that has time and budget information) into MongoDB. Stay Tuned! 19 | 20 | 21 | 22 | **Feb 23, 2014:** Support has been re-added for tasks, milestones, and code commits, and code commit comments. The code still needs some cleanup in term of OO based structure, but it is fully functioning. All features listed below are supported. I will be updating the diagrams and images of the improved data structure in the next few days as time permits. 23 | 24 | **Feb 19, 2014:** Large changes are occurring with Time Tracker to make it more modular. All old code will be kept in the "Old Files to be Processed" folder until all functions have been transferred into the new modular structure. This will be a multi-phase transition so changes will occur. As of Feb 19, 2014, the Issue Time Tracking with NonBillable Hours support has been provided along with download into MongoDB. Next will be to get Issue Budgets working followed by Milestone Budgets, followed by a rebuild of the Advanced Label support which covers creating multiple label levels/categories. The Final stage will be the implementation of the Task level time and budget tracking. If anyone has a need for a feature sooner rather than later, please post the request in the issue queue. See the 0.5 Branch for the code changes 25 | 26 |
27 | 28 | 29 | ## How to run the Web App: 30 | 31 | 1. Register/Create a Application at https://github.com/settings/applications/new. Set your fields to the following: 32 | 33 | 1.1. Homepage URL: `http://localhost:9292` 34 | 35 | 1.2. Authorization callback URL: `http://localhost:9292/auth/github/callback` 36 | 37 | 1.3. Application Name: `GitHub-Time-Tracking` or whatever you want to call your application. 38 | 39 | 2. Install MongoDB (typically: `brew update`, followed by: `brew install mongodb`) 40 | 41 | 3. `cd` into the `app` folder and run the following commands in the `app` folder: 42 | 43 | 3.1. Run `mongod` in terminal 44 | 45 | 3.2. Open a second terminal window and run: `bundle install` 46 | 47 | 3.3.`GITHUB_CLIENT_ID="YOUR CLIENT ID" GITHUB_CLIENT_SECRET="YOUR CLIENT SECRET" bundle exec rackup` 48 | Get the Client ID and Client Secret from the settings of your created/registered GitHub Application in Step 1. 49 | 50 | 4. Go to `http://localhost:9292` 51 | 52 | 53 | NOTE: The web app is under development at the moment, so while the code will always be executable for demo purposes, there are many links that have hard coded variables at the moment. So if you want to test out on your own repo you will have to make a few modifications. 54 | 55 | -- 56 | 57 | 58 | ## Minimal Viable Product: Time Tracking Web App 59 | 60 | Some Initial same images for first iteration of development 61 | 62 | ![screen shot 2014-03-06 at 1 17 38 am](https://f.cloud.github.com/assets/1994838/2342453/294b23b4-a4f7-11e3-8d5a-44f532e5392e.png) 63 | - 64 | ![screen shot 2014-03-06 at 1 17 50 am](https://f.cloud.github.com/assets/1994838/2342455/2b732736-a4f7-11e3-98fc-cff944644e1e.png) 65 | - 66 | ![screen shot 2014-03-06 at 1 17 54 am](https://f.cloud.github.com/assets/1994838/2342457/3239fa9a-a4f7-11e3-9feb-19825d49e798.png) 67 | - 68 | ![screen shot 2014-03-06 at 1 17 29 am](https://f.cloud.github.com/assets/1994838/2342458/3c4cf2c6-a4f7-11e3-835a-7dde7a5a1dbd.png) 69 | - 70 | ![screen shot 2014-03-06 at 1 17 21 am](https://f.cloud.github.com/assets/1994838/2342459/45fdd74a-a4f7-11e3-8ad9-8321a62be2e5.png) 71 | - 72 | ![screen shot 2014-03-06 at 1 19 39 am](https://f.cloud.github.com/assets/1994838/2342462/527c893a-a4f7-11e3-82c5-cb51a2e3608e.png) 73 | - 74 | 75 | 76 | 77 | ## Time Tracking Usage Patterns 78 | 79 | ### Logging Time for an Issue 80 | 81 | Logging time for a specific issue should be done in its own comment. The comment should not include any data other than the time tracking information. 82 | **NOTE: The Body of the Issue (the text you write when you first open the issue), is not a comment. You must make a comment for a time-comment to function. The logic is that if you were to make a issue, the opening of the issue would contain the information about the issue, and then subsequent comments would have time-comments.** 83 | 84 | 85 | #### Examples 86 | 87 | 1. `:clock1: 2h` # => :clock1: 2h 88 | 89 | 2. `:clock1: 2h | 3pm` # => :clock1: 2h | 3pm 90 | 91 | 3. `:clock1: 2h | 3:20pm` # => :clock1: 2h | 3:20pm 92 | 93 | 4. `:clock1: 2h | Feb 26, 2014` # => :clock1: 2h | Feb 26, 2014 94 | 95 | 5. `:clock1: 2h | Feb 26, 2014 3pm` # => :clock1: 2h | Feb 26, 2014 3pm 96 | 97 | 6. `:clock1: 2h | Feb 26, 2014 3:20pm` # => :clock1: 2h | Feb 26, 2014 3:20pm 98 | 99 | 7. `:clock1: 2h | Installed security patch and restarted the server.` # => :clock1: 2h | Installed security patch and restarted the server. 100 | 101 | 8. `:clock1: 2h | 3pm | Installed security patch and restarted the server.` # => :clock1: 2h | 3pm | Installed security patch and restarted the server. 102 | 103 | 9. `:clock1: 2h | 3:20pm | Installed security patch and restarted the server.` # => :clock1: 2h | 3:20pm | Installed security patch and restarted the server. 104 | 105 | 10. `:clock1: 2h | Feb 26, 2014 | Installed security patch and restarted the server.` # => :clock1: 2h | Feb 26, 2014 | Installed security patch and restarted the server. 106 | 107 | 11. `:clock1: 2h | Feb 26, 2014 3pm | Installed security patch and restarted the server.` # => :clock1: 2h | Feb 26, 2014 3pm | Installed security patch and restarted the server. 108 | 109 | 12. `:clock1: 2h | Feb 26, 2014 3:20pm | Installed security patch and restarted the server.` # => :clock1: 2h | Feb 26, 2014 3:20pm | Installed security patch and restarted the server. 110 | 111 | 112 | - Dates and times can be provided in various formats, but the above formats are recommended for plain text readability. 113 | 114 | - Any GitHub.com supported `clock` Emoji is supported: 115 | ":clock130:", ":clock11:", ":clock1230:", ":clock3:", ":clock430:", ":clock6:", ":clock730:", ":clock9:", ":clock10:", ":clock1130:", ":clock2:", ":clock330:", ":clock5:", ":clock630:", ":clock8:", ":clock930:", ":clock1:", ":clock1030:", ":clock12:", ":clock230:", ":clock4:", ":clock530:", ":clock7:", ":clock830:" 116 | 117 | #### Sample 118 | ![screen shot 2013-12-15 at 8 41 35 pm](https://f.cloud.github.com/assets/1994838/1751599/b03deba6-65f3-11e3-9a4a-6e30ca750fd6.png) 119 | 120 | 121 | 122 | ### Logging Time for a Code Commit 123 | 124 | When logging time in a Code Commit, the code commit message should follow the usage pattern. The commit message that you would normally submit as part of the code commit comes after the time tracking information. See example 7 below for a typical usage pattern. Code Commit time logging can be done as part of the overall Git Commit Message, individual GitHub Commit Comment or Line Comment. 125 | 126 | #### Examples 127 | 128 | 1. `:clock1: 2h` # => :clock1: 2h 129 | 130 | 2. `:clock1: 2h | 3pm` # => :clock1: 2h | 3pm 131 | 132 | 3. `:clock1: 2h | 3:20pm` # => :clock1: 2h | 3:20pm 133 | 134 | 4. `:clock1: 2h | Feb 26, 2014` # => :clock1: 2h | Feb 26, 2014 135 | 136 | 5. `:clock1: 2h | Feb 26, 2014 3pm` # => :clock1: 2h | Feb 26, 2014 3pm 137 | 138 | 6. `:clock1: 2h | Feb 26, 2014 3:20pm` # => :clock1: 2h | Feb 26, 2014 3:20pm 139 | 140 | 7. `:clock1: 2h | Installed security patch and restarted the server.` # => :clock1: 2h | Installed security patch and restarted the server. 141 | 142 | 8. `:clock1: 2h | 3pm | Installed security patch and restarted the server.` # => :clock1: 2h | 3pm | Installed security patch and restarted the server. 143 | 144 | 9. `:clock1: 2h | 3:20pm | Installed security patch and restarted the server.` # => :clock1: 2h | 3:20pm | Installed security patch and restarted the server. 145 | 146 | 10. `:clock1: 2h | Feb 26, 2014 | Installed security patch and restarted the server.` # => :clock1: 2h | Feb 26, 2014 | Installed security patch and restarted the server. 147 | 148 | 11. `:clock1: 2h | Feb 26, 2014 3pm | Installed security patch and restarted the server.` # => :clock1: 2h | Feb 26, 2014 3pm | Installed security patch and restarted the server. 149 | 150 | 12. `:clock1: 2h | Feb 26, 2014 3:20pm | Installed security patch and restarted the server.` # => :clock1: 2h | Feb 26, 2014 3:20pm | Installed security patch and restarted the server. 151 | 152 | 153 | - Dates and times can be provided in various formats, but the above formats are recommended for plain text readability. 154 | 155 | - Any GitHub.com supported `clock` Emoji is supported: 156 | ":clock130:", ":clock11:", ":clock1230:", ":clock3:", ":clock430:", ":clock6:", ":clock730:", ":clock9:", ":clock10:", ":clock1130:", ":clock2:", ":clock330:", ":clock5:", ":clock630:", ":clock8:", ":clock930:", ":clock1:", ":clock1030:", ":clock12:", ":clock230:", ":clock4:", ":clock530:", ":clock7:", ":clock830:" 157 | 158 | #### Sample 159 | ##### Code Commit Message: 160 | ![screen shot 2013-12-15 at 8 42 55 pm](https://f.cloud.github.com/assets/1994838/1751603/ca03597c-65f3-11e3-82a8-0fa293f69d84.png) 161 | ![screen shot 2013-12-15 at 8 42 43 pm](https://f.cloud.github.com/assets/1994838/1751604/ca044274-65f3-11e3-9b60-4a912959c19b.png) 162 | 163 | ##### Code Commit Comment: 164 | ![screen shot 2013-12-16 at 10 20 22 am](https://f.cloud.github.com/assets/1994838/1757115/00735ccc-667c-11e3-8656-57caaae42e04.png) 165 | 166 | ##### Code Commit Line Comment: 167 | ![screen shot 2013-12-16 at 10 19 55 am](https://f.cloud.github.com/assets/1994838/1757116/00741680-667c-11e3-9a2f-ba9128a60515.png) 168 | 169 | 170 | ### Logging Budgets for an Issue 171 | 172 | Logging a budget for a specific issue should be done in its own comment. The comment should not include any data other than the budget tracking information. 173 | 174 | #### Examples 175 | 176 | 1. `:dart: 5d` # => :dart: 5d 177 | 178 | 2. `:dart: 5d | We cannot go over this time at all!` # => :dart: 5d | We cannot go over this time at all! 179 | 180 | #### Sample 181 | ![screen shot 2013-12-15 at 8 46 33 pm](https://f.cloud.github.com/assets/1994838/1751609/24b45bbe-65f4-11e3-8a5e-86b0cfb12a74.png) 182 | 183 | 184 | ### Logging Budgets for a Milestone 185 | 186 | Logging a budget for a milestone should be done at the beginning of the milestone description. The typical milestone description information comes after the budget information. See example 2 below for a typical usage pattern. 187 | 188 | #### Examples 189 | 190 | 1. `:dart: 5d` # => :dart: 5d 191 | 192 | 2. `:dart: 5d | We cannot go over this time at all!` # => :dart: 5d | We cannot go over this time at all! 193 | 194 | #### Sample 195 | ![screen shot 2013-12-15 at 8 42 04 pm](https://f.cloud.github.com/assets/1994838/1751601/bb73ed86-65f3-11e3-9abb-4c47eabbc608.png) 196 | ![screen shot 2013-12-15 at 8 41 55 pm](https://f.cloud.github.com/assets/1994838/1751602/bb757d9a-65f3-11e3-9ac5-86dba26bc037.png) 197 | 198 | 199 | ### Tracking Non-Billable Time and Budgets 200 | 201 | The ability to indicate where a Time Log and Budget is considered Non-Billable has been provided. This is typically used when staff are doing work that will not be billed to the client, but you want to track their time and indicate how much non-billable/free time has been allocated. The assumption is that all time logs and budgets are billable unless indicated to be Non-Billable. 202 | 203 | You may indicate when a time log or budget is non-billable time in any Issue Time Log, Issue Budget, Milestone Budget, Code Commit Message, and Code Commit Comment. 204 | 205 | To indicate if time or budgets are non-billable, you add the `:free:` :free: emoji right after your chosen `clock` emoji (like `:clock1:` :clock1:) or for budget you would place the `:free:` :free: emoji right after the `:dart:` :dart: emoji. 206 | 207 | #### Non-Billable Time and Budget Tracking Indicator Usage Example 208 | 209 | 210 | ##### Logging Non-Billable Time for an Issue 211 | 212 | ###### Examples 213 | 214 | 1. `:clock1: :free: 2h` # => :clock1: :free: 2h 215 | 216 | 217 | ##### Logging Non-Billable Time for a Code Commit Message 218 | 219 | ###### Examples 220 | 221 | 1. `:clock1: :free: 2h` # => :clock1: :free: 2h 222 | 223 | 224 | ##### Logging Non-Billable Time for a Code Commit Comment 225 | 226 | ###### Examples 227 | 228 | 1. `:clock1: :free: 2h` # => :clock1: :free: 2h 229 | 230 | 231 | ##### Logging Non-Billable Budgets for an Issue 232 | 233 | ###### Examples 234 | 235 | 1. `:dart: :free: 5d` # => :dart: :free: 5d 236 | 237 | 238 | ##### Logging Non-Billable Budgets for a Milestone 239 | 240 | ###### Examples 241 | 242 | 1. `:dart: :free: 5d` # => :dart: :free: 5d 243 | 244 | 245 | 246 | 247 | ## Sample Data Structure for Reporting 248 | 249 | **NOTE: These images are out of date. New data structures have been implemented and are in full use in the Time Tracker Gem and the Sinatra App. Sample data structures will be updated shortly.** 250 | 251 | ### Time Logging in a Issue 252 | ![screen shot 2013-12-17 at 2 10 36 pm](https://f.cloud.github.com/assets/1994838/1767347/81179704-6752-11e3-8783-e3e083f5cc30.png) 253 | 254 | ### Budget Logging in a Issue 255 | ![screen shot 2013-12-17 at 2 10 58 pm](https://f.cloud.github.com/assets/1994838/1767348/8117dcc8-6752-11e3-9a69-578f11cdf21b.png) 256 | 257 | ### Budget Logging in a Milestone 258 | ![screen shot 2013-12-17 at 2 16 30 pm](https://f.cloud.github.com/assets/1994838/1767346/811700dc-6752-11e3-924f-1340642b19bf.png) 259 | 260 | ### Code Commit Time Logging - Supports Time Logging in Commit Message and Commit Comments 261 | ![screen shot 2013-12-17 at 2 17 08 pm](https://f.cloud.github.com/assets/1994838/1767349/81191ae8-6752-11e3-9a06-236006fef16c.png) 262 | 263 | Notice the parent `Duration` field is empty. This is due to time being logged in the commit comments rather than the the Git Commit Message. A use case for this would be if the developer forgot to add the Time tracking information in their Git Commit Message, they can just add it to the Commit Comments after the commit has been pushed to GitHub without any issues or errors. 264 | 265 | 266 | ## Future Features 267 | 268 | 1. ~~Tracking of Billable and non-billable hours~~ Done 269 | 2. ~~Breakdown by Milestones~~ 270 | 3. Breakdown by User 271 | 4. ~~Breakdown by Labels~~ 272 | 5. Printable View 273 | 6. Import from CSV 274 | 7. Export to CSV 275 | 8. ~~Budget Tracking (What is the allocated budget of a issue, milestone, label, etc)~~ Done 276 | 9. ~~Code Commit Time Tracking~~ Done 277 | 10. Support Business Hours Time and Budget Logging. Example: 1 week will equal 5 days (1 Business Week) rather than 1 week equalling 7 days (1 Calendar Week). Most popular use case would be able to say 1 Day would equal 8 hours rather than 24 hours. This is upcoming as the Chronic_Duration Gem has merged a pull request to support this feature. 278 | 11. ~~Add Ability to parse Label grouping words out of labels. This will allow Web app to categorize beyond milestones and to categorize within a label. Example: Label = Project Management: Project Oversight. Label = Business Analysis: Requirements Definition.~~ Done 279 | 12. Add ability to track Size of Issues - Likely will use Labels as Size (something like Small, Med, Large) 280 | 13. Add ability to track estimated effort for an issue. Estimated effort and Budget are different. Budget is something that has been determined by the Project Management-like user. Estimated Effort is a duration that has been determined by the developer. Who this is submitted in the syntax still needs to be determined. Thinking maybe :8ball: or maybe Playing Cards emoji that is a relation to Agile Poker. **Labels support is already provided. So you can currently use labels to categorize level of effort estimates.** 281 | 14. Explore the use of Natural Language Processing Libraries such as OpenNPL for better text processing. 282 | 283 | 15. Add GitLab support. This is upcoming. Need to tweak data input structures and OmniOAuth support. But it looks like its very possible. 284 | 285 | ## Process Overview 286 | 287 | ![github time tracking process overview](https://f.cloud.github.com/assets/1994838/1757137/409bf8e0-667c-11e3-9576-14400457c2c1.png) 288 | 289 | ### Future Process rebuilt around Object Based design: 290 | ![object based process overview](https://f.cloud.github.com/assets/1994838/2003533/3eae6152-865d-11e3-96af-9380e3c77715.png) 291 | 292 | 293 | ## Data Analysis 294 | 295 | This section will grow as the data analysis / UI is developed for the application 296 | 297 | 298 | ### Data output from Data Analyzer and MongoDB 299 | 300 | Using the MongoDB Aggregation Framework a series of high level aggregations are preformed to provide the required data for the front-end to display needed Time Tracking information. 301 | 302 | #### Issue Time Output 303 | 304 | **NOTE: These images are out of date. New data structures have been implemented and are in full use in the Time Tracker Gem and the Sinatra App. Structures will be updated shortly.** 305 | 306 | ``` 307 | [ 308 | { 309 | "repo_name"=>"StephenOTT/Test1", 310 | "type"=>"Issue Time", 311 | "assigned_milestone_number"=>1, 312 | "issue_number"=>6, 313 | "issue_state"=>"open", 314 | "duration_sum"=>43200, 315 | "issue_count"=>3 316 | }, 317 | { 318 | "repo_name"=>"StephenOTT/Test1", 319 | "type"=>"Issue Time", 320 | "assigned_milestone_number"=>1, 321 | "issue_number"=>7, 322 | "issue_state"=>"open", 323 | "duration_sum"=>14400, 324 | "issue_count"=>1 325 | } 326 | ] 327 | ``` 328 | 329 | #### Issue Budget Output 330 | 331 | ``` 332 | [ 333 | { 334 | "repo_name"=>"StephenOTT/Test1", 335 | "type"=>"Issue Budget", 336 | "issue_number"=>7, 337 | "assigned_milestone_number"=>1, 338 | "issue_state"=>"open", 339 | "duration_sum"=>57600, 340 | "issue_count"=>1 341 | } 342 | ] 343 | ``` 344 | 345 | #### Milestone Budget Output 346 | 347 | ``` 348 | [ 349 | { 350 | "repo_name"=>"StephenOTT/Test1", 351 | "type"=>"Milestone Budget", 352 | "milestone_number"=>1, 353 | "milestone_state"=>"open", 354 | "duration_sum"=>604800, 355 | "milestone_count"=>1 356 | } 357 | ] 358 | ``` 359 | -------------------------------------------------------------------------------- /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 | sinatra_auth_github (1.0.0) 5 | awesome_print 6 | bson_ext 7 | chronic_duration 8 | mongo 9 | sinatra (~> 1.0) 10 | warden-github (~> 1.0) 11 | 12 | GEM 13 | remote: http://rubygems.org/ 14 | specs: 15 | addressable (2.3.5) 16 | awesome_print (1.6.1) 17 | bson (1.9.2) 18 | bson_ext (1.9.1) 19 | bson (~> 1.9.1) 20 | chronic_duration (0.10.2) 21 | numerizer (~> 0.1.1) 22 | diff-lcs (1.1.3) 23 | faraday (0.9.0) 24 | multipart-post (>= 1.2, < 3) 25 | mongo (1.9.2) 26 | bson (~> 1.9.2) 27 | multipart-post (2.0.0) 28 | numerizer (0.1.1) 29 | octokit (2.7.1) 30 | sawyer (~> 0.5.2) 31 | rack (1.5.2) 32 | rack-protection (1.5.2) 33 | rack 34 | rack-test (0.5.7) 35 | rack (>= 1.0) 36 | rake (10.1.1) 37 | randexp (0.1.7) 38 | rspec (2.4.0) 39 | rspec-core (~> 2.4.0) 40 | rspec-expectations (~> 2.4.0) 41 | rspec-mocks (~> 2.4.0) 42 | rspec-core (2.4.0) 43 | rspec-expectations (2.4.0) 44 | diff-lcs (~> 1.1.2) 45 | rspec-mocks (2.4.0) 46 | sawyer (0.5.3) 47 | addressable (~> 2.3.5) 48 | faraday (~> 0.8, < 0.10) 49 | shotgun (0.9) 50 | rack (>= 1.0) 51 | sinatra (1.4.4) 52 | rack (~> 1.4) 53 | rack-protection (~> 1.4) 54 | tilt (~> 1.3, >= 1.3.4) 55 | tilt (1.4.1) 56 | warden (1.2.3) 57 | rack (>= 1.0) 58 | warden-github (1.0.0) 59 | octokit (> 2.1.0) 60 | warden (> 1.0) 61 | 62 | PLATFORMS 63 | ruby 64 | 65 | DEPENDENCIES 66 | rack-test (~> 0.5.3) 67 | rake 68 | randexp (~> 0.1.5) 69 | rspec (~> 2.4.0) 70 | shotgun 71 | sinatra_auth_github! 72 | 73 | BUNDLED WITH 74 | 1.10.6 75 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | sinatra_auth_github 2 | =================== 3 | 4 | A sinatra extension that provides oauth authentication to github. Find out more about enabling your application at github's [oauth quickstart](http://developer.github.com/v3/oauth/). 5 | 6 | To test it out on localhost set your callback url to 'http://localhost:9393/auth/github/callback' 7 | 8 | The gist of this project is to provide a few things easily: 9 | 10 | * authenticate a user against github's oauth service 11 | * provide an easy way to make API requests for the authenticated user 12 | * optionally restrict users to a specific github organization 13 | * optionally restrict users to a specific github team 14 | 15 | Installation 16 | ============ 17 | 18 | % gem install sinatra_auth_github 19 | 20 | Running the Example 21 | =================== 22 | % gem install bundler 23 | % bundle install 24 | % GITHUB_CLIENT_ID="" GITHUB_CLIENT_SECRET="" bundle exec rackup -p9393 25 | 26 | There's an example app in [spec/app.rb](/spec/app.rb). 27 | 28 | Example App Functionality 29 | ========================= 30 | 31 | You can simply authenticate via GitHub by hitting http://localhost:9393 32 | 33 | You can check organization membership by hitting http://localhost:9393/orgs/github 34 | 35 | You can check team membership by hitting http://localhost:9393/teams/42 36 | 37 | All unsuccessful authentication requests get sent to the securocat denied page. 38 | 39 | API Access 40 | ============ 41 | 42 | The extension also provides a simple way to access the GitHub API, by providing an 43 | authenticated Octokit::Client for the user. 44 | 45 | def repos 46 | github_user.api.repositories 47 | end 48 | 49 | For more information on API access, refer to the [octokit documentation](http://rdoc.info/gems/octokit). 50 | 51 | Extension Options 52 | ================= 53 | 54 | * `:scopes` - The OAuth2 scopes you require, [Learn More](http://gist.github.com/419219) 55 | * `:secret` - The client secret that GitHub provides 56 | * `:client_id` - The client id that GitHub provides 57 | * `:failure_app` - A Sinatra::Base class that has a route for `/unauthenticated`, Useful for overriding the securocat default page. 58 | * `:callback_url` - The path that GitHub posts back to, defaults to `/auth/github/callback`. 59 | 60 | Enterprise Authentication 61 | ========================= 62 | 63 | Under the hood, the `warden-github` portion is powered by octokit. If you find yourself wanting to connect to a GitHub Enterprise installation you'll need to export two environmental variables. 64 | 65 | * OCTOKIT_WEB_ENDPOINT - The web endpoint for OAuth, defaults to https://github.com 66 | * OCTOKIT_API_ENDPOINT - The API endpoint for authenticated requests, defaults to https://api.github.com 67 | -------------------------------------------------------------------------------- /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 | 3 | module Example 4 | class App < Sinatra::Base 5 | enable :sessions 6 | 7 | set :github_options, { 8 | :scopes => "user", 9 | :secret => ENV['GITHUB_CLIENT_SECRET'], 10 | :client_id => ENV['GITHUB_CLIENT_ID'], 11 | } 12 | 13 | register Sinatra::Auth::Github 14 | 15 | helpers do 16 | 17 | def get_auth_info 18 | authInfo = {:username => github_user.login, :userID => github_user.id} 19 | end 20 | 21 | end 22 | 23 | get '/cal' do 24 | 25 | erb :calendar 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 | 37 | else 38 | # @dangerMessage = "Danger... Warning! Warning" 39 | @warningMessage = "Please login to continue" 40 | # @infoMessage = "Info 123" 41 | # @successMessage = "Success" 42 | end 43 | erb :index 44 | end 45 | 46 | get '/repos' do 47 | if authenticated? == true 48 | @reposList = Sinatra_Helpers.get_all_repos_for_logged_user(get_auth_info) 49 | erb :repos_listing 50 | else 51 | @warningMessage = "You must be logged in" 52 | erb :unauthenticated 53 | end 54 | end 55 | 56 | get '/timetrack' do 57 | if authenticated? == true 58 | erb :download_data 59 | else 60 | @warningMessage = "You must be logged in" 61 | erb :unauthenticated 62 | end 63 | end 64 | 65 | post '/download' do 66 | if authenticated? == true 67 | post = params[:post] 68 | if post['clearmongo'] == 'on' 69 | post['clearmongo'] = true 70 | else 71 | post['clearmongo'] = false 72 | end 73 | Sinatra_Helpers.download_time_tracking_data(post['username'], post['repository'], github_api, get_auth_info, post['clearmongo'] ) 74 | @successMessage = "Download Complete" 75 | redirect '/timetrack' 76 | else 77 | @warningMessage = "You must be logged in" 78 | erb :unauthenticated 79 | end 80 | end 81 | 82 | # get '/analyze-issues/:user/:repo' do 83 | # issuesRaw = Sinatra_Helpers.analyze_issues(params['user'], params['repo']) 84 | # issuesProcessed = Sinatra_Helpers.process_issues_for_budget_left(issuesRaw) 85 | 86 | 87 | # @issues = issuesProcessed 88 | # erb :issues 89 | 90 | # end 91 | 92 | get '/:user/:repo/issues' do 93 | if authenticated? == true 94 | @issues = Sinatra_Helpers.issues(params['user'], params['repo'], get_auth_info) 95 | erb :issues 96 | else 97 | @warningMessage = "You must be logged in" 98 | erb :unauthenticated 99 | end 100 | end 101 | 102 | 103 | get '/repo-dates' do 104 | if authenticated? == true 105 | @repoDates = Sinatra_Helpers.issues_date_repo_year("StephenOTT", "Test1", 2013, get_auth_info) 106 | erb :repo_dates 107 | else 108 | @warningMessage = "You must be logged in" 109 | erb :unauthenticated 110 | end 111 | end 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | get '/:user/:repo/milestones' do 120 | # milestones1 = Sinatra_Helpers.analyze_milestones(params['user'], params['repo']) 121 | # milestonesProcessed = Sinatra_Helpers.process_milestone_budget_left(milestones1) 122 | if authenticated? == true 123 | @milestones = Sinatra_Helpers.milestones(params['user'], params['repo'], get_auth_info) 124 | erb :milestones 125 | else 126 | @warningMessage = "You must be logged in" 127 | erb :unauthenticated 128 | end 129 | end 130 | 131 | get '/:user/:repo/milestone/:milestoneNumber/issues' do 132 | @issuesInMilestone = Sinatra_Helpers.milestone_issues(params['user'], params['repo'], params['milestoneNumber'], get_auth_info) 133 | erb :issues_in_milestone 134 | end 135 | 136 | # Old route: get '/issues-spent-hours/:user/:repo/:issueNumber' do 137 | get '/:user/:repo/issues/:issueNumber' do 138 | @issues_spent_hours = Sinatra_Helpers.issues_users(params['user'], params['repo'], params['issueNumber'], get_auth_info) 139 | erb :issue_time 140 | 141 | end 142 | 143 | 144 | 145 | 146 | # get '/analyze-issue-time/:user/:repo/:issueNumber' do 147 | # @issueTime = Sinatra_Helpers.analyze_issueTime(params['user'], params['repo'], params['issueNumber']) 148 | 149 | # erb :issue_time 150 | 151 | # end 152 | 153 | 154 | 155 | # get '/analyze-milestone-time/:user/:repo/:milestoneNumber' do 156 | # @issuesInMilestone = Sinatra_Helpers.analyze_issue_time_in_milestone(params['user'], params['repo'], params['milestoneNumber']) 157 | 158 | # erb :issues_in_milestone 159 | 160 | # end 161 | 162 | # TODO: Write better code/route to support multiple categories and labels 163 | get '/analyze-labels-time/:user/:repo/:category/:label' do 164 | category = [] 165 | label = [] 166 | 167 | category << params['category'] 168 | label << params['label'] 169 | 170 | @labelsTime = Sinatra_Helpers.analyze_labelTime(params['user'], params['repo'], category, label) 171 | 172 | erb :labels 173 | 174 | end 175 | 176 | get '/analyze-code-commits/:user/:repo' do 177 | @codeCommits = Sinatra_Helpers.analyze_codeCommits(params['user'], params['repo']) 178 | 179 | erb :code_commits 180 | 181 | end 182 | 183 | 184 | 185 | 186 | get '/logout' do 187 | logout! 188 | redirect '/' 189 | end 190 | get '/login' do 191 | authenticate! 192 | redirect '/' 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /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 'warden/github' 3 | require_relative '../../../../controller' 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-Time-Tracking/2c1812075d4de6720e37a100ec63d507cf410e38/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-Time-Tracking/2c1812075d4de6720e37a100ec63d507cf410e38/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitHub-Time-Tracking/2c1812075d4de6720e37a100ec63d507cf410e38/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitHub-Time-Tracking/2c1812075d4de6720e37a100ec63d507cf410e38/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/public/vendor/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 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 | if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /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 = "sinatra_auth_github" 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-Time-Tracking" 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 "awesome_print" 24 | 25 | 26 | s.add_development_dependency "rake" 27 | s.add_development_dependency "rspec", "~>2.4.0" 28 | s.add_development_dependency "shotgun" 29 | s.add_development_dependency "randexp", "~>0.1.5" 30 | s.add_development_dependency "rack-test", "~>0.5.3" 31 | 32 | s.files = `git ls-files`.split("\n") 33 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 34 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 35 | s.require_paths = ["lib"] 36 | end 37 | -------------------------------------------------------------------------------- /app/sinatra_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative '../controller' 2 | require_relative '../time_analyzer/time_analyzer' 3 | require_relative '../time_tracker/helpers' 4 | require_relative '../time_analyzer/milestones_processor' 5 | require_relative '../time_analyzer/issues_processor' 6 | require_relative '../time_analyzer/users_processor' 7 | require_relative '../time_analyzer/system_wide_processor' 8 | require_relative '../time_analyzer/issues_date_processor' 9 | 10 | module Sinatra_Helpers 11 | 12 | def self.get_all_repos_for_logged_user(githubAuthInfo) 13 | 14 | System_Wide_Processor.all_repos_for_logged_user(githubAuthInfo) 15 | 16 | end 17 | 18 | 19 | def self.download_time_tracking_data(user, repo, githubObject, githubAuthInfo, clearMongo) 20 | userRepo = "#{user}/#{repo}" 21 | Time_Tracking_Controller.controller(userRepo, githubObject, clearMongo, githubAuthInfo) 22 | end 23 | 24 | 25 | def self.issues(user, repo, githubAuthInfo) 26 | 27 | Issues_Processor.analyze_issues(user, repo, githubAuthInfo) 28 | 29 | end 30 | 31 | 32 | def self.milestones(user, repo, githubAuthInfo) 33 | 34 | Milestones_Processor.milestones_and_issue_sums(user, repo, githubAuthInfo) 35 | 36 | end 37 | 38 | def self.milestone_issues(user, repo, milestoneNumber, githubAuthInfo) 39 | 40 | Issues_Processor.get_issues_in_milestone(user, repo, milestoneNumber, githubAuthInfo) 41 | 42 | end 43 | 44 | 45 | 46 | def self.issues_users(user, repo, issueNumber, githubAuthInfo) 47 | 48 | Users_Processor.analyze_issues_users(user, repo, issueNumber, githubAuthInfo) 49 | 50 | end 51 | 52 | 53 | def self.issues_date_repo_year(user, repo, filterYear, githubAuthInfo) 54 | 55 | Issues_Date_Processor.analyze_issues_date_year_repo(user, repo, filterYear, githubAuthInfo) 56 | 57 | end 58 | 59 | 60 | def self.analyze_issueTime(user, repo, issueNumber) 61 | userRepo = "#{user}/#{repo}" 62 | Issues_Processor.controller 63 | issuesTime = Issues_Processor.analyze_issue_spent_hours_per_user(userRepo, issueNumber.to_i) 64 | issuesTime.each do |x| 65 | if x["time_duration_sum"] != nil 66 | x["time_duration_sum"] = Helpers.convertSecondsToDurationFormat(x["time_duration_sum"], "long") 67 | end 68 | # if x["budget_duration_sum"] != nil 69 | # x["budget_duration_sum"] = Helpers.chronic_convert(x["budget_duration_sum"], "long") 70 | # end 71 | end 72 | end 73 | 74 | def self.analyze_labelTime(user, repo, category, label) 75 | userRepo = "#{user}/#{repo}" 76 | Time_Analyzer.controller 77 | Time_Analyzer.analyze_issue_spent_hours_per_label(category, label) 78 | end 79 | 80 | def self.analyze_codeCommits(user, repo) 81 | userRepo = "#{user}/#{repo}" 82 | Time_Analyzer.controller 83 | Time_Analyzer.analyze_code_commits_spent_hours 84 | end 85 | 86 | # TODO come up with better way to call chronic duration 87 | def self.analyze_issue_time_in_milestone(user, repo, milestoneNumber) 88 | userRepo = "#{user}/#{repo}" 89 | puts milestoneNumber 90 | Time_Analyzer.controller 91 | issuesPerMilestone = Time_Analyzer.analyze_issue_spent_hours_per_milestone(milestoneNumber.to_i) 92 | issuesPerMilestone.each do |x| 93 | x["time_duration_sum"] = Helpers.convertSecondsToDurationFormat(x["time_duration_sum"], "long") 94 | end 95 | return issuesPerMilestone 96 | end 97 | 98 | 99 | end 100 | -------------------------------------------------------------------------------- /app/views/calendar.erb: -------------------------------------------------------------------------------- 1 | <%= calendrier(:year => 2012, :month => 5, :day => 25, :start_on_monday => true, :display => :week, :title => "My calendar title") %> -------------------------------------------------------------------------------- /app/views/code_commits.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <% @codeCommits.each do |c| %> 11 | 12 | 13 | 14 | 16 | 17 | <% end %> 18 | 19 |
Committer UsernameAuthor UsernameCommit SHATime Duration Sum
<%= c["commit_committer_username"] %><%= c["commit_author_username"] %><%= c["commit_sha"] %> 15 | <%= c["time_duration_sum"] %>
20 | -------------------------------------------------------------------------------- /app/views/download_data.erb: -------------------------------------------------------------------------------- 1 |

Provide your GitHub username/organization and Repository to download time tracking content.

2 | 3 |

Example: StephenOTT/Test1

4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /app/views/index.erb: -------------------------------------------------------------------------------- 1 | 2 |

Hello <%= @fullname %>

3 | 4 |

<%= @username %>

5 | 6 | 7 |

<%= @userID %>

-------------------------------------------------------------------------------- /app/views/issue_time.erb: -------------------------------------------------------------------------------- 1 |

Issue Time Breakdown

2 | 3 |
4 |
5 |

Repository: <%= @issues_spent_hours.first["repo_name"] %>

6 |
7 |
8 |

Issue Number: <%= @issues_spent_hours.first["issue_number"] %>

9 |

Issue Title: <%= @issues_spent_hours.first["issue_title"] %>

10 |

Issue State: <%= @issues_spent_hours.first["issue_state"] %>

11 |

Milestone Number: /milestone/<%= @issues_spent_hours.first["milestone_number"] %>/issues"><%= @issues_spent_hours.first["milestone_number"] %>

12 |
13 |
14 | 15 | 16 |
17 |
18 |

User Hours

19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | <% @issues_spent_hours.each do |i| %> 28 | 29 | 30 | 31 | 32 | 33 | <% end %> 34 | 35 |
Work Logged ByTime Duration SumTime Comment Count
<%= i["work_logged_by"] %><%= i["time_duration_sum_human"] %><%= i["time_comment_count"] %>
36 |
37 | -------------------------------------------------------------------------------- /app/views/issues.erb: -------------------------------------------------------------------------------- 1 |

Issues List

2 | 3 |
4 |
5 |

Repository

6 |
7 |
8 |

<%= @issues.first["repo_name"] %>

9 |
10 |
11 | 12 |
13 |
14 |

Issue List

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | <% @issues.each do |i| %> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | <% end %> 38 | 39 |
Issue #Issue StateIssue TitleMilestone #Time Duration Sum (Comment Count)Budget Duration Sum (Comment Count)Budget Left
/issues/<%= i["issue_number"] %>"><%= i["issue_number"] %><%= i["issue_state"] %><%= i["issue_title"] %>/milestone/<%= i["milestone_number"] %>/issues"><%= i["milestone_number"] %><%= i["time_duration_sum_human"] %> <%= i["time_comment_count"] %><%= i["budget_duration_sum_human"] %> <%= i["budget_comment_count"] %><%= i["budget_left_human"] %>
40 |
41 | -------------------------------------------------------------------------------- /app/views/issues_in_milestone.erb: -------------------------------------------------------------------------------- 1 |

Specific Issue Breakdown from Milestone

2 | 3 |
4 |
5 |

Repository: <%= @issuesInMilestone.first["repo_name"] %>

6 |
7 |
8 |

Milestone Number: <%= @issuesInMilestone.first["milestone_number"] %>

9 |
10 |
11 | 12 |
13 |
14 |

Issue List

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% @issuesInMilestone.each do |i| %> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | <% end %> 34 | 35 |
Issue #Issue TitleIssue StateTime Duration SumTime Comment Count
/issues/<%= i["issue_number"] %>"><%= i["issue_number"] %><%= i["issue_title"] %><%= i["issue_state"] %><%= i["time_duration_sum_human"] %><%= i["time_comment_count"] %>
36 |
-------------------------------------------------------------------------------- /app/views/labels.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% @labelsTime.each do |l| %> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% end %> 26 | 27 |
RepositoryCategoryLabelMilestone #Issue NumberIssue TitleIssue StateTime Duration Sum (Time Comment Count)
<%= l["repo_name"] %><%= l["category"] %><%= l["label"] %><%= l["milestone_number"] %><%= l["issue_number"] %><%= l["issue_title"] %><%= l["issue_state"] %><%= l["time_duration_sum"] %> (<%= l["time_comment_count"] %>)
28 | -------------------------------------------------------------------------------- /app/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GitHub Time tracking 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 |
18 | 19 | 20 | 52 | 53 | <%if @dangerMessage then %> 54 |
55 | 56 | <%=@dangerMessage%> 57 |
58 | <% end %> 59 | <%if @warningMessage then %> 60 |
61 | 62 | <%=@warningMessage%> 63 |
64 | <% end %> 65 | <%if @infoMessage then %> 66 |
67 | 68 | <%=@infoMessage%> 69 |
70 | <% end %> 71 | <%if @successMessage then %> 72 |
73 | 74 | <%=@successMessage%> 75 |
76 | <% end %> 77 | 78 |
79 | <%= yield %> 80 |
81 | 82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /app/views/milestones.erb: -------------------------------------------------------------------------------- 1 |

Milestones List

2 | 3 |
4 |
5 |

Repository

6 |
7 |
8 |

<% if @milestones.empty? == false %> 9 | <% @milestones.first["repo_name"] %> 10 | <% else %> 11 | No Milestones 12 | <% end %>

13 |
14 |
15 | 16 |
17 |
18 |

Milestones List

19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | <% @milestones.each do |m| %> 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | <% end %> 42 | 43 |
Milestone #Milestone StateMilestone TitleMilestone Open/Closed IssuesMilestone Duration SumSpent Hours on Issues for MilestoneMilestone Budget Remaining
/milestone/<%= m["milestone_number"] %>/issues"><%= m["milestone_number"] %><%= m["milestone_state"] %><%= m["milestone_title"] %><%= m["milestone_open_issue_count"] %> / <%= m["milestone_closed_issue_count"] %><%= m["milestone_duration_sum_human"] %><%= m["issues_duration_sum_human"] %><%= m["budget_left_human"] %> 40 |
44 |
45 | -------------------------------------------------------------------------------- /app/views/repo_dates.erb: -------------------------------------------------------------------------------- 1 | <%= @repoDates %> 2 | 3 | 4 |

Repo Time Spent Breakdown

5 |
6 |
7 |

Repository: <%= @repoDates.first["repo_name"] %>

8 |

Year: <%= @repoDates.first["work_date_year"] %>

9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @repoDates.each do |d| %> 18 | 19 | 20 | 21 | 22 | <% end %> 23 | 24 |
MonthTime Duration
<%= Date::MONTHNAMES[d["work_date_month"]] %><%= d["time_duration_sum_human"] %>
25 |
26 | -------------------------------------------------------------------------------- /app/views/repo_user_dates.erb: -------------------------------------------------------------------------------- 1 | <%= @repoDates %> 2 | 3 | 4 |

Repo Time Spent Breakdown

5 |
6 |
7 |

Repository: <%= @repoDates.first["repo_name"] %>

8 |

Year: <%= @repoDates.first["work_date_year"] %>

9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @repoDates.each do |d| %> 18 | 19 | 20 | 21 | 22 | <% end %> 23 | 24 |
MonthTime Duration
<%= Date::MONTHNAMES[d["work_date_month"]] %><%= d["time_duration_sum_human"] %>
25 |
26 | -------------------------------------------------------------------------------- /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 | 20 | <% @reposList.each do |r| %> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% end %> 29 | 30 |
UsernameRepository NameFull Repository Name
<%= r["username"] %><%= r["repo_name"] %><%= r["repo_name_full"] %>/issues">Issues/milestones">Milestones
31 | 32 |
33 | 34 | -------------------------------------------------------------------------------- /app/views/unauthenticated.erb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /controller.rb: -------------------------------------------------------------------------------- 1 | require_relative 'time_tracker/issues' 2 | require_relative 'time_tracker/github_data' 3 | require_relative 'time_tracker/mongo' 4 | require_relative 'time_tracker/milestones' 5 | require_relative 'time_tracker/issue_comment_tasks' 6 | require_relative 'time_tracker/code_commits' 7 | # require_relative 'time_tracker/time_analyzer' 8 | 9 | module Time_Tracking_Controller 10 | 11 | # def controller(repo, username, password, clearCollections = false) 12 | def self.controller(repo, object1, clearCollections = false, githubAuthInfo = {}) 13 | # GitHub_Data.gh_authenticate(username, password) 14 | GitHub_Data.gh_sinatra_auth(object1) 15 | 16 | # MongoDb connection: DB URL, Port, DB Name, Collection Name 17 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-TimeTracking", "TimeTrackingCommits") 18 | 19 | # Clears the DB collections if clearCollections var in controller argument is true 20 | if clearCollections == true 21 | Mongo_Connection.clear_mongo_collections 22 | end 23 | 24 | #======Start of Issues======= 25 | issues = GitHub_Data.get_Issues(repo) 26 | 27 | # goes through each issue returned from get_Issues method 28 | issues.each do |i| 29 | ap "downloading # #{i.attrs[:number]}" 30 | # Gets the comments for the specific issue 31 | issueComments = GitHub_Data.get_Issue_Comments(repo, i.attrs[:number]) 32 | 33 | # Parses the specific issue for time tracking information 34 | processedIssues = Gh_Issue.process_issue(repo, i, issueComments, githubAuthInfo) 35 | # if data is returned from the parsing attempt, the data is passed into MongoDb 36 | if processedIssues.empty? == false 37 | Mongo_Connection.putIntoMongoCollTimeTrackingCommits(processedIssues) 38 | end 39 | end 40 | #======End of Issues======= 41 | 42 | #======Start of Milestone======= 43 | milestones = GitHub_Data.get_Milestones(repo) 44 | 45 | milestones.each do |m| 46 | 47 | processedMilestones = Gh_Milestone.process_milestone(repo, m, githubAuthInfo) 48 | 49 | if processedMilestones.empty? == false 50 | Mongo_Connection.putIntoMongoCollTimeTrackingCommits(processedMilestones) 51 | end 52 | end 53 | #======End of Milestone======= 54 | 55 | #======Start of Code Commits======= 56 | codeCommits = GitHub_Data.get_code_commits(repo) 57 | 58 | codeCommits.each do |c| 59 | commitComments = GitHub_Data.get_commit_comments(repo, c.attrs[:sha]) 60 | 61 | processedCodeCommits = GH_Commits.process_code_commit(repo, c, commitComments, githubAuthInfo) 62 | if processedCodeCommits.empty? == false 63 | Mongo_Connection.putIntoMongoCollTimeTrackingCommits(processedCodeCommits) 64 | end 65 | end 66 | #======End of Code Commits======= 67 | end 68 | end 69 | 70 | # start = Time_Tracking_Controller.new 71 | 72 | # # GitHubUsername/RepoName, GitHubUsername, GitHubPassword, ClearCollectionsTFValue 73 | # start.controller("StephenOTT/Test1", "USERNAME", "PASSWORD", true) 74 | -------------------------------------------------------------------------------- /gh_issue_task_aggregator.rb: -------------------------------------------------------------------------------- 1 | # require_relative 'time_tracker/helpers' 2 | 3 | # Beta code for getting lists of tasks in issues broken down by each comment 4 | 5 | module GH_Issue_Task_Aggregator 6 | 7 | def self.comment_has_tasks?(commentBody) 8 | tasks = self.get_tasks_from_comment(commentBody) 9 | if tasks[:incomplete].empty? == true and tasks[:complete].empty? == true 10 | return false 11 | else 12 | return true 13 | end 14 | end 15 | 16 | def self.get_issue_details(repo, issueDetailsRaw) 17 | issueDetails = { "repo" => repo, 18 | "issue_number" => issueDetailsRaw.attrs[:number], 19 | "issue_title" => issueDetailsRaw.attrs[:title], 20 | "issue_state" => issueDetailsRaw.attrs[:state], 21 | } 22 | end 23 | 24 | def self.get_comment_details(commentDetailsRaw) 25 | overviewDetails = { "comment_id" => commentDetailsRaw.attrs[:id], 26 | "comment_created_by" => commentDetailsRaw.attrs[:user].attrs[:login], 27 | "comment_created_date" => commentDetailsRaw.attrs[:created_at], 28 | "comment_last_updated_date" =>commentDetailsRaw.attrs[:updated_at], 29 | "record_creation_date" => Time.now.utc, 30 | } 31 | end 32 | 33 | def self.get_comment_body(commentDetailsRaw) 34 | body = commentDetailsRaw.attrs[:body] 35 | end 36 | 37 | 38 | def self.merge_details_and_tasks(overviewDetails, tasks) 39 | mergedHash = overviewDetails.merge(tasks) 40 | end 41 | 42 | def self.get_tasks_from_comment(commentBody) 43 | 44 | tasks = {:complete => nil, :incomplete => nil } 45 | completeTasks = [] 46 | incompleteTasks = [] 47 | 48 | startStringIncomplete = /\-\s\[\s\]\s/ 49 | startStringComplete = /\-\s\[x\]\s/ 50 | 51 | endString = /[\r\n]|\z/ 52 | 53 | tasksInBody = commentBody.scan(/#{startStringIncomplete}(.*?)#{endString}/) 54 | tasksInBody.each do |x| 55 | incompleteTasks << x[0] 56 | end 57 | 58 | tasksInBody = commentBody.scan(/#{startStringComplete}(.*?)#{endString}/) 59 | tasksInBody.each do |x| 60 | completeTasks << x[0] 61 | end 62 | 63 | tasks[:complete] = completeTasks 64 | tasks[:incomplete] = incompleteTasks 65 | 66 | return tasks 67 | end 68 | 69 | def self.add_task_status(tasks) 70 | finalTasks = [] 71 | 72 | tasks[:complete].each do |t| 73 | parsedTimeDetails["task_status"] = "complete" 74 | finalTasks << parsedTimeDetails 75 | end 76 | 77 | tasks[:incomplete].each do |t| 78 | parsedTimeDetails["task_status"] = "incomplete" 79 | finalTasks << parsedTimeDetails 80 | end 81 | return finalTasks 82 | end 83 | end -------------------------------------------------------------------------------- /time_analyzer/TODO.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Get all Issues Time 5 | Get all Issues Budget 6 | Get all issues Time and Budget 7 | 8 | Get a Issue Time 9 | Get a Issue Budget 10 | Get a Issue Time and Budget 11 | 12 | Get all Issues Time for a Milestone 13 | Get all Issues Budget for a Milestone 14 | Get all Issues Time and Budget for a Milestone 15 | 16 | 17 | 18 | Get all Milestones Budget 19 | Get a Milestone Budget 20 | 21 | 22 | Get User Time sum for all/each Issues 23 | Get User Time sum for a Issue Time 24 | 25 | 26 | Get User Time sum for all/each Milestone 27 | Get User Time sum for a Milestone 28 | 29 | 30 | Get all Issues with selected labels 31 | Get all Issues with selected labels for a Milestone 32 | 33 | 34 | Get all Code Commits 35 | Get all Comments for a Code Commit 36 | -------------------------------------------------------------------------------- /time_analyzer/code_commit_aggregation.rb: -------------------------------------------------------------------------------- 1 | 2 | require_relative './mongo' 3 | 4 | 5 | module Code_Commit_Aggregation 6 | 7 | def self.controller 8 | 9 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-TimeTracking", "TimeTrackingCommits") 10 | 11 | end 12 | 13 | # old name: analyze_code_commits_spent_hours 14 | def self.get_code_commits_time 15 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 16 | {"$project" => {type: 1, 17 | commit_author_username: 1, 18 | _id: 1, 19 | repo: 1, 20 | commit_committer_username: 1, 21 | commit_sha: 1, 22 | commit_message_time:{ duration: 1, 23 | type: 1}}}, 24 | { "$match" => { type: "Code Commit" }}, 25 | { "$match" => { commit_message_time: { "$ne" => nil } }}, 26 | { "$group" => { _id: { 27 | repo_name: "$repo", 28 | commit_committer_username: "$commit_committer_username", 29 | commit_author_username: "$commit_author_username", 30 | commit_sha: "$commit_sha", }, 31 | time_duration_sum: { "$sum" => "$commit_message_time.duration" } 32 | }} 33 | ]) 34 | output = [] 35 | totalIssueSpentHoursBreakdown.each do |x| 36 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 37 | output << x["_id"] 38 | end 39 | return output 40 | end 41 | 42 | def self.get_code_commit_time_comments(codeCommitSHA) 43 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 44 | {"$project" => {type: 1, 45 | _id: 1, 46 | repo: 1, 47 | commit_sha: 1, 48 | commit_comment_time:{ duration: 1, 49 | type: 1 }}}, 50 | { "$match" => { type: "Code Commit" }}, 51 | { "$match" => { commit_sha: codeCommitSHA }}, 52 | { "$match" => { commit_comment_time: { "$ne" => {"$size" => 0 }}}}, 53 | { "$group" => { _id: { 54 | repo_name: "$repo", 55 | commit_sha: "$commit_sha", 56 | type: "$commit_comment_time.type"}, 57 | time_duration_sum: { "$sum" => "$commit_comment_time.duration" }, 58 | time_comment_count: { "$sum" => 1 }, 59 | }} 60 | ]) 61 | output = [] 62 | totalIssueSpentHoursBreakdown.each do |x| 63 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 64 | output << x["_id"] 65 | end 66 | return output 67 | end 68 | 69 | end 70 | 71 | # Code_Commit_Aggregation.controller 72 | # puts Code_Commit_Aggregation.get_code_commits_time 73 | # puts Code_Commit_Aggregation.get_code_commit_time_comments("b4f3a58b6e44c6a99fc2fb9a8d27e098d13af45d") -------------------------------------------------------------------------------- /time_analyzer/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'chronic_duration' 2 | 3 | module Helpers 4 | 5 | def self.budget_left?(large, small) 6 | large - small 7 | end 8 | 9 | def self.convertSecondsToDurationFormat(timeInSeconds, outputFormat) 10 | outputFormat = outputFormat.to_sym 11 | return ChronicDuration.output(timeInSeconds, :format => outputFormat, :keep_zero => true) 12 | end 13 | 14 | 15 | def self.merge_issue_time_and_budget(issuesTime, issuesBudget) 16 | 17 | issuesTime.each do |t| 18 | 19 | issuesBudget.each do |b| 20 | 21 | if b["issue_number"] == t["issue_number"] 22 | t["budget_duration_sum"] = b["budget_duration_sum"] 23 | t["budget_comment_count"] = b["budget_comment_count"] 24 | break 25 | end 26 | end 27 | if t.has_key?("budget_duration_sum") == false and t.has_key?("budget_comment_count") == false 28 | t["budget_duration_sum"] = nil 29 | t["budget_comment_count"] = nil 30 | end 31 | end 32 | 33 | return issuesTime 34 | end 35 | 36 | 37 | 38 | 39 | 40 | end -------------------------------------------------------------------------------- /time_analyzer/issues_aggregation.rb: -------------------------------------------------------------------------------- 1 | require_relative './mongo' 2 | 3 | 4 | module Issues_Aggregation 5 | 6 | def self.controller 7 | 8 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-TimeTracking", "TimeTrackingCommits") 9 | 10 | end 11 | 12 | # old name: analyze_issue_spent_hours 13 | def self.get_all_issues_time(repo, 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:{ duration: 1, 24 | type: 1, 25 | comment_id: 1 }}}, 26 | { "$match" => { repo: repo }}, 27 | { "$match" => { type: "Issue" }}, 28 | { "$unwind" => "$time_tracking_commits" }, 29 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 30 | { "$group" => { _id: { 31 | repo_name: "$repo", 32 | milestone_number: "$milestone_number", 33 | issue_number: "$issue_number", 34 | issue_title: "$issue_title", 35 | issue_state: "$issue_state", }, 36 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 37 | time_comment_count: { "$sum" => 1 } 38 | }} 39 | ]) 40 | output = [] 41 | totalIssueSpentHoursBreakdown.each do |x| 42 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 43 | x["_id"]["time_comment_count"] = x["time_comment_count"] 44 | output << x["_id"] 45 | end 46 | return output 47 | end 48 | 49 | def self.get_issue_time(repo, issueNumber, githubAuthInfo) 50 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 51 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 52 | {"$project" => {type: 1, 53 | issue_number: 1, 54 | _id: 1, 55 | repo: 1, 56 | milestone_number: 1, 57 | issue_state: 1, 58 | issue_title: 1, 59 | time_tracking_commits:{ duration: 1, 60 | type: 1, 61 | comment_id: 1 }}}, 62 | { "$match" => { repo: repo }}, 63 | { "$match" => { type: "Issue" }}, 64 | { "$match" => {issue_number: issueNumber.to_i}}, 65 | { "$unwind" => "$time_tracking_commits" }, 66 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 67 | { "$group" => { _id: { 68 | repo_name: "$repo", 69 | milestone_number: "$milestone_number", 70 | issue_number: "$issue_number", 71 | issue_title: "$issue_title", 72 | issue_state: "$issue_state", }, 73 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 74 | time_comment_count: { "$sum" => 1 } 75 | }} 76 | ]) 77 | output = [] 78 | totalIssueSpentHoursBreakdown.each do |x| 79 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 80 | x["_id"]["time_comment_count"] = x["time_comment_count"] 81 | output << x["_id"] 82 | end 83 | return output 84 | end 85 | 86 | 87 | 88 | 89 | # old name: analyze_issue_budget_hours 90 | def self.get_all_issues_budget(repo, githubAuthInfo) 91 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 92 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 93 | {"$project" => {type: 1, 94 | issue_number: 1, 95 | _id: 1, 96 | repo: 1, 97 | milestone_number: 1, 98 | issue_state: 1, 99 | issue_title: 1, 100 | time_tracking_commits:{ duration: 1, 101 | type: 1, 102 | comment_id: 1 }}}, 103 | { "$match" => { repo: repo }}, 104 | { "$match" => { type: "Issue" }}, 105 | { "$unwind" => "$time_tracking_commits" }, 106 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Budget"] }}}, 107 | { "$group" => { _id: { 108 | repo_name: "$repo", 109 | milestone_number: "$milestone_number", 110 | issue_number: "$issue_number", 111 | issue_state: "$issue_state", 112 | issue_title: "$issue_title",}, 113 | budget_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 114 | budget_comment_count: { "$sum" => 1 } 115 | }} 116 | ]) 117 | output = [] 118 | totalIssueSpentHoursBreakdown.each do |x| 119 | x["_id"]["budget_duration_sum"] = x["budget_duration_sum"] 120 | x["_id"]["budget_comment_count"] = x["budget_comment_count"] 121 | output << x["_id"] 122 | end 123 | return output 124 | end 125 | 126 | 127 | def self.get_issue_budget(repo, issueNumber, githubAuthInfo) 128 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 129 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 130 | {"$project" => {type: 1, 131 | issue_number: 1, 132 | _id: 1, 133 | repo: 1, 134 | milestone_number: 1, 135 | issue_state: 1, 136 | issue_title: 1, 137 | time_tracking_commits:{ duration: 1, 138 | type: 1, 139 | comment_id: 1 }}}, 140 | { "$match" => { repo: repo }}, 141 | { "$match" => { type: "Issue" }}, 142 | { "$match" => {issue_number: issueNumber.to_i}}, 143 | { "$unwind" => "$time_tracking_commits" }, 144 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Budget"] }}}, 145 | { "$group" => { _id: { 146 | repo_name: "$repo", 147 | milestone_number: "$milestone_number", 148 | issue_number: "$issue_number", 149 | issue_state: "$issue_state", 150 | issue_title: "$issue_title",}, 151 | budget_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 152 | budget_comment_count: { "$sum" => 1 } 153 | }} 154 | ]) 155 | output = [] 156 | totalIssueSpentHoursBreakdown.each do |x| 157 | x["_id"]["budget_duration_sum"] = x["budget_duration_sum"] 158 | x["_id"]["budget_comment_count"] = x["budget_comment_count"] 159 | output << x["_id"] 160 | end 161 | return output 162 | end 163 | 164 | 165 | 166 | 167 | def self.get_all_issues_time_in_milestone(repo, milestoneNumber, githubAuthInfo) 168 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 169 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 170 | {"$project" => {type: 1, 171 | issue_number: 1, 172 | _id: 1, 173 | repo: 1, 174 | milestone_number: 1, 175 | issue_state: 1, 176 | issue_title: 1, 177 | time_tracking_commits:{ duration: 1, 178 | type: 1, 179 | comment_id: 1 }}}, 180 | { "$match" => { repo: repo }}, 181 | { "$match" => { type: "Issue" }}, 182 | { "$match" => { milestone_number: milestoneNumber.to_i }}, 183 | { "$unwind" => "$time_tracking_commits" }, 184 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 185 | { "$group" => { _id: { 186 | repo_name: "$repo", 187 | milestone_number: "$milestone_number", 188 | issue_number: "$issue_number", 189 | issue_title: "$issue_title", 190 | issue_state: "$issue_state", }, 191 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 192 | time_comment_count: { "$sum" => 1 } 193 | }} 194 | ]) 195 | output = [] 196 | totalIssueSpentHoursBreakdown.each do |x| 197 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 198 | x["_id"]["time_comment_count"] = x["time_comment_count"] 199 | output << x["_id"] 200 | end 201 | return output 202 | end 203 | 204 | 205 | 206 | def self.get_total_issues_time_for_milestone(repo, milestoneNumber, githubAuthInfo) 207 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 208 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 209 | {"$project" => {type: 1, 210 | issue_number: 1, 211 | _id: 1, 212 | repo: 1, 213 | milestone_number: 1, 214 | issue_state: 1, 215 | issue_title: 1, 216 | time_tracking_commits:{ duration: 1, 217 | type: 1, 218 | comment_id: 1 }}}, 219 | { "$match" => { repo: repo }}, 220 | { "$match" => { type: "Issue" }}, 221 | { "$match" => { milestone_number: milestoneNumber.to_i }}, 222 | { "$unwind" => "$time_tracking_commits" }, 223 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 224 | { "$group" => { _id: { 225 | repo_name: "$repo", 226 | milestone_number: "$milestone_number"}, 227 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 228 | time_comment_count: { "$sum" => 1 } 229 | }} 230 | ]) 231 | output = [] 232 | totalIssueSpentHoursBreakdown.each do |x| 233 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 234 | x["_id"]["time_comment_count"] = x["time_comment_count"] 235 | output << x["_id"] 236 | end 237 | return output 238 | end 239 | 240 | 241 | 242 | 243 | 244 | def self.get_all_issues_budget_in_milestone(repo, milestoneNumber, githubAuthInfo) 245 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 246 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 247 | {"$project" => {type: 1, 248 | issue_number: 1, 249 | _id: 1, 250 | repo: 1, 251 | milestone_number: 1, 252 | issue_state: 1, 253 | issue_title: 1, 254 | time_tracking_commits:{ duration: 1, 255 | type: 1, 256 | comment_id: 1 }}}, 257 | { "$match" => { repo: repo }}, 258 | { "$match" => { type: "Issue" }}, 259 | { "$match" => { milestone_number: milestoneNumber.to_i }}, 260 | { "$unwind" => "$time_tracking_commits" }, 261 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Budget"] }}}, 262 | { "$group" => { _id: { 263 | repo_name: "$repo", 264 | milestone_number: "$milestone_number", 265 | issue_number: "$issue_number", 266 | issue_state: "$issue_state", 267 | issue_title: "$issue_title",}, 268 | budget_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 269 | budget_comment_count: { "$sum" => 1 } 270 | }} 271 | ]) 272 | output = [] 273 | totalIssueSpentHoursBreakdown.each do |x| 274 | x["_id"]["budget_duration_sum"] = x["budget_duration_sum"] 275 | x["_id"]["budget_comment_count"] = x["budget_comment_count"] 276 | output << x["_id"] 277 | end 278 | return output 279 | end 280 | 281 | 282 | # Get repo sum of issue time 283 | def self.get_repo_time_from_issues(repo, githubAuthInfo) 284 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 285 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 286 | {"$project" => {type: 1, 287 | issue_number: 1, 288 | _id: 1, 289 | repo: 1, 290 | milestone_number: 1, 291 | issue_state: 1, 292 | issue_title: 1, 293 | time_tracking_commits:{ duration: 1, 294 | type: 1, 295 | comment_id: 1 }}}, 296 | { "$match" => { repo: repo }}, 297 | { "$match" => { type: "Issue" }}, 298 | { "$unwind" => "$time_tracking_commits" }, 299 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 300 | { "$group" => { _id: { 301 | repo_name: "$repo"}, 302 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 303 | time_comment_count: { "$sum" => 1 } 304 | }} 305 | ]) 306 | output = [] 307 | totalIssueSpentHoursBreakdown.each do |x| 308 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 309 | x["_id"]["time_comment_count"] = x["time_comment_count"] 310 | output << x["_id"] 311 | end 312 | return output 313 | end 314 | 315 | # Sums all issue budgets for the repo and outputs the total budget based on issues 316 | def self.get_repo_budget_from_issues(repo, githubAuthInfo) 317 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 318 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 319 | {"$project" => {type: 1, 320 | issue_number: 1, 321 | _id: 1, 322 | repo: 1, 323 | milestone_number: 1, 324 | issue_state: 1, 325 | issue_title: 1, 326 | time_tracking_commits:{ duration: 1, 327 | type: 1, 328 | comment_id: 1 }}}, 329 | { "$match" => { repo: repo }}, 330 | { "$match" => { type: "Issue" }}, 331 | { "$unwind" => "$time_tracking_commits" }, 332 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Budget"] }}}, 333 | { "$group" => { _id: { 334 | repo_name: "$repo"}, 335 | budget_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 336 | budget_comment_count: { "$sum" => 1 } 337 | }} 338 | ]) 339 | output = [] 340 | totalIssueSpentHoursBreakdown.each do |x| 341 | x["_id"]["budget_duration_sum"] = x["budget_duration_sum"] 342 | x["_id"]["budget_comment_count"] = x["budget_comment_count"] 343 | output << x["_id"] 344 | end 345 | return output 346 | end 347 | end 348 | 349 | 350 | # Debug code 351 | # Issues_Aggregation.controller 352 | # puts Issues_Aggregation.get_all_issues_time_in_milestone("StephenOTT/Test1", 1, {:username => "StephenOTT", :userID => 1994838}) 353 | 354 | 355 | 356 | -------------------------------------------------------------------------------- /time_analyzer/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 | -------------------------------------------------------------------------------- /time_analyzer/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 -------------------------------------------------------------------------------- /time_analyzer/issues_processor.rb: -------------------------------------------------------------------------------- 1 | require_relative 'issues_aggregation' 2 | require_relative 'helpers' 3 | 4 | 5 | module Issues_Processor 6 | 7 | def self.analyze_issues(user, repo, githubAuthInfo) 8 | userRepo = "#{user}/#{repo}" 9 | Issues_Aggregation.controller 10 | spentHours = Issues_Aggregation.get_all_issues_time(userRepo, githubAuthInfo) 11 | budgetHours = Issues_Aggregation.get_all_issues_budget(userRepo, githubAuthInfo) 12 | issues = Helpers.merge_issue_time_and_budget(spentHours, budgetHours) 13 | issues.each do |x| 14 | if x["time_duration_sum"] != nil 15 | x["time_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["time_duration_sum"], "long") 16 | end 17 | if x["budget_duration_sum"] != nil 18 | x["budget_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["budget_duration_sum"], "long") 19 | end 20 | end 21 | 22 | issues = self.process_issues_for_budget_left(issues) 23 | 24 | return issues 25 | 26 | end 27 | 28 | 29 | 30 | def self.process_issues_for_budget_left(issues) 31 | issues.each do |i| 32 | if i["budget_duration_sum"] != nil 33 | # TODO Cleanup code for Budget left. 34 | budgetLeftRaw = Helpers.budget_left?(i["budget_duration_sum"], i["time_duration_sum"]) 35 | budgetLeftHuman = Helpers.convertSecondsToDurationFormat(budgetLeftRaw, "long") 36 | i["budget_left_raw"] = budgetLeftRaw 37 | i["budget_left_human"] = budgetLeftHuman 38 | end 39 | end 40 | return issues 41 | end 42 | 43 | 44 | 45 | def self.get_issues_in_milestone(user, repo, milestoneNumber, githubAuthInfo) 46 | userRepo = "#{user}/#{repo}" 47 | Issues_Aggregation.controller 48 | spentHours = Issues_Aggregation.get_all_issues_time_in_milestone(userRepo, milestoneNumber, githubAuthInfo) 49 | budgetHours = Issues_Aggregation.get_all_issues_budget_in_milestone(userRepo, milestoneNumber, githubAuthInfo) 50 | issues = Helpers.merge_issue_time_and_budget(spentHours, budgetHours) 51 | issues.each do |x| 52 | if x["time_duration_sum"] != nil 53 | x["time_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["time_duration_sum"], "long") 54 | end 55 | if x["budget_duration_sum"] != nil 56 | x["budget_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["budget_duration_sum"], "long") 57 | end 58 | end 59 | 60 | issues = self.process_issues_for_budget_left(issues) 61 | return issues 62 | 63 | end 64 | 65 | 66 | end -------------------------------------------------------------------------------- /time_analyzer/labels_aggregation.rb: -------------------------------------------------------------------------------- 1 | require_relative './mongo' 2 | 3 | module Labels_Analyzer 4 | 5 | def self.controller 6 | 7 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-TimeTracking", "TimeTrackingCommits") 8 | 9 | end 10 | 11 | #old method name: analyze_issue_spent_hours_per_label 12 | def self.get_issues_time_for_label(repo, category, label) 13 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 14 | {"$project" => {type: 1, 15 | issue_number: 1, 16 | _id: 1, 17 | repo: 1, 18 | milestone_number: 1, 19 | issue_state: 1, 20 | issue_title: 1, 21 | time_tracking_commits: { duration: 1, 22 | type: 1, 23 | comment_id: 1 }, 24 | labels: { category: 1, 25 | label: 1 }, 26 | }}, 27 | { "$match" => { repo: repo }}, 28 | { "$match" => { type: "Issue" }}, 29 | { "$unwind" => "$labels" }, 30 | { "$unwind" => "$time_tracking_commits" }, 31 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 32 | { "$match" => { "labels.category" => category }}, 33 | { "$match" => { "labels.label" => label }}, 34 | { "$group" => { _id: { 35 | repo_name: "$repo", 36 | category: "$labels.category", 37 | label: "$labels.label", 38 | milestone_number: "$milestone_number", 39 | issue_number: "$issue_number", 40 | issue_title: "$issue_title", 41 | issue_state: "$issue_state", }, 42 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 43 | time_comment_count: { "$sum" => 1 } 44 | }} 45 | ]) 46 | output = [] 47 | totalIssueSpentHoursBreakdown.each do |x| 48 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 49 | x["_id"]["time_comment_count"] = x["time_comment_count"] 50 | output << x["_id"] 51 | end 52 | return output 53 | end 54 | 55 | 56 | 57 | 58 | def self.get_issues_time_for_label_and_milestone(repo, category, label, milestoneNumber) 59 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 60 | {"$project" => {type: 1, 61 | issue_number: 1, 62 | _id: 1, 63 | repo: 1, 64 | milestone_number: 1, 65 | issue_state: 1, 66 | issue_title: 1, 67 | time_tracking_commits: { duration: 1, 68 | type: 1, 69 | comment_id: 1 }, 70 | labels: { category: 1, 71 | label: 1 }, 72 | }}, 73 | { "$match" => { repo: repo }}, 74 | { "$match" => { type: "Issue" }}, 75 | { "$match" => { milestone_number: milestoneNumber }}, 76 | { "$unwind" => "$labels" }, 77 | { "$unwind" => "$time_tracking_commits" }, 78 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 79 | { "$match" => { "labels.category" => category }}, 80 | { "$match" => { "labels.label" => label }}, 81 | { "$group" => { _id: { 82 | repo_name: "$repo", 83 | category: "$labels.category", 84 | label: "$labels.label", 85 | milestone_number: "$milestone_number", 86 | issue_number: "$issue_number", 87 | issue_title: "$issue_title", 88 | issue_state: "$issue_state", }, 89 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 90 | time_comment_count: { "$sum" => 1 } 91 | }} 92 | ]) 93 | output = [] 94 | totalIssueSpentHoursBreakdown.each do |x| 95 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 96 | x["_id"]["time_comment_count"] = x["time_comment_count"] 97 | output << x["_id"] 98 | end 99 | return output 100 | end 101 | 102 | 103 | 104 | end -------------------------------------------------------------------------------- /time_analyzer/milestones_aggregation.rb: -------------------------------------------------------------------------------- 1 | require_relative './mongo' 2 | 3 | 4 | module Milestones_Aggregation 5 | 6 | def self.controller 7 | 8 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-TimeTracking", "TimeTrackingCommits") 9 | 10 | end 11 | 12 | 13 | 14 | # old names: self.analyze_milestones 15 | def self.get_all_milestones_budget(repo, githubAuthInfo) 16 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 17 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 18 | {"$project" => {type: 1, 19 | milestone_number: 1, 20 | _id: 1, 21 | repo: 1, 22 | milestone_state: 1, 23 | milestone_title: 1, 24 | milestone_open_issue_count: 1, 25 | milestone_closed_issue_count: 1, 26 | budget_tracking_commits:{ duration: 1, 27 | type: 1}}}, 28 | { "$match" => { repo: repo }}, 29 | { "$match" => { type: "Milestone" }}, 30 | { "$unwind" => "$budget_tracking_commits" }, 31 | { "$match" => { "budget_tracking_commits.type" => { "$in" => ["Milestone Budget"] }}}, 32 | { "$group" => { _id: { 33 | repo_name: "$repo", 34 | milestone_number: "$milestone_number", 35 | milestone_state: "$milestone_state", 36 | milestone_title: "$milestone_title", 37 | milestone_open_issue_count: "$milestone_open_issue_count", 38 | milestone_closed_issue_count: "$milestone_closed_issue_count",}, 39 | milestone_duration_sum: { "$sum" => "$budget_tracking_commits.duration" } 40 | }} 41 | ]) 42 | output = [] 43 | totalIssueSpentHoursBreakdown.each do |x| 44 | x["_id"]["milestone_duration_sum"] = x["milestone_duration_sum"] 45 | output << x["_id"] 46 | end 47 | return output 48 | end 49 | 50 | 51 | def self.get_milestone_budget(repo, milestoneNumber, githubAuthInfo) 52 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 53 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 54 | {"$project" => {type: 1, 55 | milestone_number: 1, 56 | _id: 1, 57 | repo: 1, 58 | milestone_state: 1, 59 | milestone_title: 1, 60 | milestone_open_issue_count: 1, 61 | milestone_closed_issue_count: 1, 62 | budget_tracking_commits:{ duration: 1, 63 | type: 1}}}, 64 | { "$match" => { repo: repo }}, 65 | { "$match" => { type: "Milestone" }}, 66 | { "$match" => { milestone_number: milestoneNumber }}, 67 | { "$unwind" => "$budget_tracking_commits" }, 68 | { "$match" => { "budget_tracking_commits.type" => { "$in" => ["Milestone Budget"] }}}, 69 | { "$group" => { _id: { 70 | repo_name: "$repo", 71 | milestone_number: "$milestone_number", 72 | milestone_state: "$milestone_state", 73 | milestone_title: "$milestone_title", 74 | milestone_open_issue_count: "$milestone_open_issue_count", 75 | milestone_closed_issue_count: "$milestone_closed_issue_count",}, 76 | milestone_duration_sum: { "$sum" => "$budget_tracking_commits.duration" } 77 | }} 78 | ]) 79 | output = [] 80 | totalIssueSpentHoursBreakdown.each do |x| 81 | x["_id"]["milestone_duration_sum"] = x["milestone_duration_sum"] 82 | output << x["_id"] 83 | end 84 | return output 85 | end 86 | 87 | # Gets the sum of all milestones budgets for a repo 88 | def self.get_repo_budget_from_milestones(repo, githubAuthInfo) 89 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 90 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 91 | {"$project" => {type: 1, 92 | milestone_number: 1, 93 | _id: 1, 94 | repo: 1, 95 | milestone_state: 1, 96 | milestone_title: 1, 97 | milestone_open_issue_count: 1, 98 | milestone_closed_issue_count: 1, 99 | budget_tracking_commits:{ duration: 1, 100 | type: 1}}}, 101 | { "$match" => { repo: repo }}, 102 | { "$match" => { type: "Milestone" }}, 103 | { "$unwind" => "$budget_tracking_commits" }, 104 | { "$match" => { "budget_tracking_commits.type" => { "$in" => ["Milestone Budget"] }}}, 105 | { "$group" => { _id: { 106 | repo_name: "$repo"}, 107 | milestone_duration_sum: { "$sum" => "$budget_tracking_commits.duration" }, 108 | milestone_state: { "$sum" => 1 }, 109 | }} 110 | ]) 111 | output = [] 112 | totalIssueSpentHoursBreakdown.each do |x| 113 | x["_id"]["milestone_duration_sum"] = x["milestone_duration_sum"] 114 | output << x["_id"] 115 | end 116 | return output 117 | end 118 | end -------------------------------------------------------------------------------- /time_analyzer/milestones_processor.rb: -------------------------------------------------------------------------------- 1 | require_relative 'milestones_aggregation' 2 | require_relative 'issues_aggregation' 3 | require_relative 'helpers' 4 | 5 | 6 | module Milestones_Processor 7 | 8 | def self.milestones_and_issue_sums(user, repo, githubAuthInfo) 9 | userRepo = "#{user}/#{repo}" 10 | 11 | Milestones_Aggregation.controller # makes mongo connection 12 | 13 | milestones = Milestones_Aggregation.get_all_milestones_budget(userRepo, githubAuthInfo) 14 | 15 | milestones.each do |x| 16 | x["milestone_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["milestone_duration_sum"], "long") 17 | 18 | # get the total hours of time spent on issues assigned to the milestone(x) 19 | issuesSpentHours = Issues_Aggregation.get_total_issues_time_for_milestone(userRepo, x["milestone_number"], githubAuthInfo) 20 | 21 | if issuesSpentHours.empty? == false # array would be empty if there was no time allocated to the issues in the milestone 22 | issuesSpentHoursHuman = Helpers.convertSecondsToDurationFormat(issuesSpentHours[0]["time_duration_sum"], "long") 23 | x["issues_duration_sum"] = issuesSpentHours[0]["time_duration_sum"] 24 | x["issues_duration_sum_human"] = issuesSpentHoursHuman 25 | else 26 | issuesSpentHoursHuman = Helpers.convertSecondsToDurationFormat(0, "long") 27 | x["issues_duration_sum"] = 0 28 | x["issues_duration_sum_human"] = issuesSpentHoursHuman 29 | end 30 | 31 | budgetLeftRaw = Helpers.budget_left?(x["milestone_duration_sum"], x["issues_duration_sum"]) 32 | budgetLeftHuman = Helpers.convertSecondsToDurationFormat(budgetLeftRaw, "long") 33 | x["budget_left"] = budgetLeftRaw 34 | x["budget_left_human"] = budgetLeftHuman 35 | 36 | end 37 | return milestones 38 | end 39 | end -------------------------------------------------------------------------------- /time_analyzer/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 -------------------------------------------------------------------------------- /time_analyzer/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-TimeTracking", "TimeTrackingCommits") 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_name: "$repo" 16 | }}} 17 | ]) 18 | 19 | 20 | output = [] 21 | reposAssignedToLoggedUser.each do |x| 22 | toParseString = x["_id"]["repo_name"] 23 | x["_id"]["username"] = toParseString.partition("/").first 24 | x["_id"]["repo_name"] = 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}) -------------------------------------------------------------------------------- /time_analyzer/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 -------------------------------------------------------------------------------- /time_analyzer/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 | 12 | 13 | # TODO: Review code for better structure. 14 | # TODO: Review code to work out problematic selections. 15 | # Not 100% sure this query wont cause bad results in certain situations. 16 | def self.analyze_issue_spent_hours_per_label(category, label) 17 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 18 | {"$project" => {type: 1, 19 | issue_number: 1, 20 | _id: 1, 21 | repo: 1, 22 | milestone_number: 1, 23 | issue_state: 1, 24 | issue_title: 1, 25 | time_tracking_commits: { duration: 1, 26 | type: 1, 27 | comment_id: 1 }, 28 | labels: { category: 1, 29 | label: 1 }, 30 | }}, 31 | { "$match" => { type: "Issue" }}, 32 | { "$unwind" => "$labels" }, 33 | { "$unwind" => "$time_tracking_commits" }, 34 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 35 | { "$match" => { "labels.category" => { "$in" => category }}}, 36 | { "$match" => { "labels.label" => { "$in" => label }}}, 37 | { "$group" => { _id: { 38 | repo_name: "$repo", 39 | category: "$labels.category", 40 | label: "$labels.label", 41 | milestone_number: "$milestone_number", 42 | issue_number: "$issue_number", 43 | issue_title: "$issue_title", 44 | issue_state: "$issue_state", }, 45 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 46 | time_comment_count: { "$sum" => 1 } 47 | }} 48 | ]) 49 | output = [] 50 | totalIssueSpentHoursBreakdown.each do |x| 51 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 52 | x["_id"]["time_comment_count"] = x["time_comment_count"] 53 | output << x["_id"] 54 | end 55 | return output 56 | end 57 | 58 | 59 | 60 | def self.analyze_code_commits_spent_hours 61 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 62 | {"$project" => {type: 1, 63 | commit_author_username: 1, 64 | _id: 1, 65 | repo: 1, 66 | commit_committer_username: 1, 67 | commit_sha: 1, 68 | commit_message_time:{ duration: 1, 69 | type: 1}}}, 70 | { "$match" => { type: "Code Commit" }}, 71 | { "$match" => { commit_message_time: { "$ne" => nil } }}, 72 | { "$group" => { _id: { 73 | repo_name: "$repo", 74 | commit_committer_username: "$commit_committer_username", 75 | commit_author_username: "$commit_author_username", 76 | commit_sha: "$commit_sha", }, 77 | time_duration_sum: { "$sum" => "$commit_message_time.duration" } 78 | }} 79 | ]) 80 | output = [] 81 | totalIssueSpentHoursBreakdown.each do |x| 82 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 83 | output << x["_id"] 84 | end 85 | return output 86 | end 87 | 88 | def self.budget_left?(large, small) 89 | large - small 90 | end 91 | 92 | 93 | 94 | end 95 | 96 | 97 | # DEBUG CODE for testing output of methods without the need of Sinatra. 98 | 99 | # Time_Analyzer.controller 100 | 101 | # puts Time_Analyzer.analyze_issue_spent_hours_per_user 102 | # puts Time_Analyzer.analyze_issue_spent_hours_per_milestone(1) 103 | # puts Time_Analyzer.analyze_issue_spent_hours_per_label(["Priority"], ["High", "Medium"]) 104 | # puts Time_Analyzer.analyze_code_commits_spent_hours 105 | # puts Time_Analyzer.analyze_issue_spent_hours_for_milestone([1]) 106 | 107 | -------------------------------------------------------------------------------- /time_analyzer/users_aggregation.rb: -------------------------------------------------------------------------------- 1 | require_relative './mongo' 2 | 3 | 4 | module Users_Aggregation 5 | 6 | def self.controller 7 | 8 | Mongo_Connection.mongo_Connect("localhost", 27017, "GitHub-TimeTracking", "TimeTrackingCommits") 9 | 10 | end 11 | 12 | # old name: analyze_user_spent_hours_on_issue 13 | # get all users time for a issue 14 | def self.get_users_time_for_issue(repo, issueNumber, githubAuthInfo) 15 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 16 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 17 | {"$project" => {type: 1, 18 | issue_number: 1, 19 | _id: 1, 20 | repo: 1, 21 | milestone_number: 1, 22 | issue_state: 1, 23 | issue_title: 1, 24 | time_tracking_commits:{ duration: 1, 25 | type: 1, 26 | work_logged_by: 1, 27 | comment_id: 1 }}}, 28 | { "$match" => { repo: repo }}, 29 | { "$match" => { type: "Issue" }}, 30 | { "$match" => { issue_number: issueNumber }}, 31 | { "$unwind" => "$time_tracking_commits" }, 32 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 33 | { "$group" => { _id: { 34 | repo_name: "$repo", 35 | milestone_number: "$milestone_number", 36 | issue_number: "$issue_number", 37 | issue_title: "$issue_title", 38 | issue_state: "$issue_state", 39 | work_logged_by: "$time_tracking_commits.work_logged_by"}, 40 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 41 | time_comment_count: { "$sum" => 1 } 42 | }} 43 | ]) 44 | output = [] 45 | totalIssueSpentHoursBreakdown.each do |x| 46 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 47 | x["_id"]["time_comment_count"] = x["time_comment_count"] 48 | output << x["_id"] 49 | end 50 | return output 51 | end 52 | 53 | # get time for a specific user in a specific issue 54 | def self.get_user_time_for_issue(repo, issueNumber, username, githubAuthInfo) 55 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 56 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 57 | {"$project" => {type: 1, 58 | issue_number: 1, 59 | _id: 1, 60 | repo: 1, 61 | milestone_number: 1, 62 | issue_state: 1, 63 | issue_title: 1, 64 | time_tracking_commits:{ duration: 1, 65 | type: 1, 66 | work_logged_by: 1, 67 | comment_id: 1 }}}, 68 | { "$match" => { repo: repo }}, 69 | { "$match" => { type: "Issue" }}, 70 | { "$match" => { issue_number: issueNumber }}, 71 | { "$unwind" => "$time_tracking_commits" }, 72 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 73 | { "$match" => { "time_tracking_commits.work_logged_by" => username }}, 74 | { "$group" => { _id: { 75 | repo_name: "$repo", 76 | milestone_number: "$milestone_number", 77 | issue_number: "$issue_number", 78 | issue_title: "$issue_title", 79 | issue_state: "$issue_state", 80 | work_logged_by: "$time_tracking_commits.work_logged_by"}, 81 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 82 | time_comment_count: { "$sum" => 1 } 83 | }} 84 | ]) 85 | output = [] 86 | totalIssueSpentHoursBreakdown.each do |x| 87 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 88 | x["_id"]["time_comment_count"] = x["time_comment_count"] 89 | output << x["_id"] 90 | end 91 | return output 92 | end 93 | 94 | 95 | 96 | # get all users time for all issues (and milestones) 97 | def self.get_users_time_for_issues(repo, githubAuthInfo) 98 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 99 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 100 | {"$project" => {type: 1, 101 | issue_number: 1, 102 | _id: 1, 103 | repo: 1, 104 | milestone_number: 1, 105 | issue_state: 1, 106 | issue_title: 1, 107 | time_tracking_commits:{ duration: 1, 108 | type: 1, 109 | work_logged_by: 1, 110 | comment_id: 1 }}}, 111 | { "$match" => { repo: repo }}, 112 | { "$match" => { type: "Issue" }}, 113 | { "$unwind" => "$time_tracking_commits" }, 114 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 115 | { "$group" => { _id: { 116 | repo_name: "$repo", 117 | milestone_number: "$milestone_number", 118 | issue_number: "$issue_number", 119 | issue_title: "$issue_title", 120 | issue_state: "$issue_state", 121 | work_logged_by: "$time_tracking_commits.work_logged_by"}, 122 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 123 | time_comment_count: { "$sum" => 1 } 124 | }} 125 | ]) 126 | output = [] 127 | totalIssueSpentHoursBreakdown.each do |x| 128 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 129 | x["_id"]["time_comment_count"] = x["time_comment_count"] 130 | output << x["_id"] 131 | end 132 | return output 133 | end 134 | 135 | # get all time for all issues for a specific user 136 | def self.get_user_time_for_issues(repo, username, githubAuthInfo) 137 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 138 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 139 | {"$project" => {type: 1, 140 | issue_number: 1, 141 | _id: 1, 142 | repo: 1, 143 | milestone_number: 1, 144 | issue_state: 1, 145 | issue_title: 1, 146 | time_tracking_commits:{ duration: 1, 147 | type: 1, 148 | work_logged_by: 1, 149 | comment_id: 1 }}}, 150 | { "$match" => { repo: repo }}, 151 | { "$match" => { type: "Issue" }}, 152 | { "$unwind" => "$time_tracking_commits" }, 153 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 154 | { "$match" => { "time_tracking_commits.work_logged_by" => username }}, 155 | { "$group" => { _id: { 156 | repo_name: "$repo", 157 | milestone_number: "$milestone_number", 158 | issue_number: "$issue_number", 159 | issue_title: "$issue_title", 160 | issue_state: "$issue_state", 161 | work_logged_by: "$time_tracking_commits.work_logged_by"}, 162 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 163 | time_comment_count: { "$sum" => 1 } 164 | }} 165 | ]) 166 | output = [] 167 | totalIssueSpentHoursBreakdown.each do |x| 168 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 169 | x["_id"]["time_comment_count"] = x["time_comment_count"] 170 | output << x["_id"] 171 | end 172 | return output 173 | end 174 | 175 | 176 | 177 | 178 | # milestones 179 | 180 | 181 | # get all users time for all issues assigned to a specific milestone 182 | def self.get_users_issue_time_for_milestone(repo, milestoneNumber, githubAuthInfo) 183 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 184 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 185 | {"$project" => {type: 1, 186 | issue_number: 1, 187 | _id: 1, 188 | repo: 1, 189 | milestone_number: 1, 190 | issue_state: 1, 191 | issue_title: 1, 192 | time_tracking_commits:{ duration: 1, 193 | type: 1, 194 | work_logged_by: 1, 195 | comment_id: 1 }}}, 196 | { "$match" => { repo: repo }}, 197 | { "$match" => { type: "Issue" }}, 198 | { "$match" => { milestone_number: milestoneNumber }}, 199 | { "$unwind" => "$time_tracking_commits" }, 200 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 201 | { "$group" => { _id: { 202 | repo_name: "$repo", 203 | milestone_number: "$milestone_number", 204 | issue_number: "$issue_number", 205 | issue_title: "$issue_title", 206 | issue_state: "$issue_state", 207 | work_logged_by: "$time_tracking_commits.work_logged_by"}, 208 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 209 | time_comment_count: { "$sum" => 1 } 210 | }} 211 | ]) 212 | output = [] 213 | totalIssueSpentHoursBreakdown.each do |x| 214 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 215 | x["_id"]["time_comment_count"] = x["time_comment_count"] 216 | output << x["_id"] 217 | end 218 | return output 219 | end 220 | 221 | 222 | # get time for a specific user accross all issues assigned to a specific milestone 223 | def self.get_user_time_for_issue(repo, milestoneNumber, username, githubAuthInfo) 224 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 225 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 226 | {"$project" => {type: 1, 227 | issue_number: 1, 228 | _id: 1, 229 | repo: 1, 230 | milestone_number: 1, 231 | issue_state: 1, 232 | issue_title: 1, 233 | time_tracking_commits:{ duration: 1, 234 | type: 1, 235 | work_logged_by: 1, 236 | comment_id: 1 }}}, 237 | { "$match" => { repo: repo }}, 238 | { "$match" => { type: "Issue" }}, 239 | { "$match" => { milestone_number: milestoneNumber }}, 240 | { "$unwind" => "$time_tracking_commits" }, 241 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 242 | { "$match" => { "time_tracking_commits.work_logged_by" => username }}, 243 | { "$group" => { _id: { 244 | repo_name: "$repo", 245 | milestone_number: "$milestone_number", 246 | issue_number: "$issue_number", 247 | issue_title: "$issue_title", 248 | issue_state: "$issue_state", 249 | work_logged_by: "$time_tracking_commits.work_logged_by"}, 250 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 251 | time_comment_count: { "$sum" => 1 } 252 | }} 253 | ]) 254 | output = [] 255 | totalIssueSpentHoursBreakdown.each do |x| 256 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 257 | x["_id"]["time_comment_count"] = x["time_comment_count"] 258 | output << x["_id"] 259 | end 260 | return output 261 | end 262 | 263 | 264 | # get all time from issues for a specific user for all milestones 265 | def self.get_user_time_for_issues(repo, username, githubAuthInfo) 266 | totalIssueSpentHoursBreakdown = Mongo_Connection.aggregate_test([ 267 | { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 268 | {"$project" => {type: 1, 269 | issue_number: 1, 270 | _id: 1, 271 | repo: 1, 272 | milestone_number: 1, 273 | issue_state: 1, 274 | issue_title: 1, 275 | time_tracking_commits:{ duration: 1, 276 | type: 1, 277 | work_logged_by: 1, 278 | comment_id: 1 }}}, 279 | { "$match" => { repo: repo }}, 280 | { "$match" => { type: "Issue" }}, 281 | { "$unwind" => "$time_tracking_commits" }, 282 | { "$match" => { "time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 283 | { "$match" => { "time_tracking_commits.work_logged_by" => username }}, 284 | { "$group" => { _id: { 285 | repo_name: "$repo", 286 | milestone_number: "$milestone_number", 287 | work_logged_by: "$time_tracking_commits.work_logged_by"}, 288 | time_duration_sum: { "$sum" => "$time_tracking_commits.duration" }, 289 | time_comment_count: { "$sum" => 1 } 290 | }} 291 | ]) 292 | output = [] 293 | totalIssueSpentHoursBreakdown.each do |x| 294 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 295 | x["_id"]["time_comment_count"] = x["time_comment_count"] 296 | output << x["_id"] 297 | end 298 | return output 299 | end 300 | end 301 | 302 | # Debug code 303 | # Users_Aggregation.controller 304 | # puts Users_Aggregation.analyze_user_spent_hours_on_issue(8) -------------------------------------------------------------------------------- /time_analyzer/users_processor.rb: -------------------------------------------------------------------------------- 1 | require_relative 'users_aggregation' 2 | require_relative 'helpers' 3 | 4 | 5 | module Users_Processor 6 | 7 | def self.analyze_issues_users(user, repo, issueNumber, githubAuthInfo) 8 | userRepo = "#{user}/#{repo}" 9 | Users_Aggregation.controller 10 | spentHours = Users_Aggregation.get_users_time_for_issue(userRepo, issueNumber.to_i, githubAuthInfo) 11 | # budgetHours = Users_Aggregation.analyze_issue_budget_hours 12 | # issues = Helpers.merge_issue_time_and_budget(spentHours, budgetHours) 13 | spentHours.each do |x| 14 | if x["time_duration_sum"] != nil 15 | x["time_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["time_duration_sum"], "long") 16 | end 17 | # if x["budget_duration_sum"] != nil 18 | # x["budget_duration_sum_human"] = Helpers.convertSecondsToDurationFormat(x["budget_duration_sum"], "long") 19 | # end 20 | end 21 | 22 | # issues = self.process_issues_for_budget_left(issues) 23 | 24 | return spentHours 25 | 26 | end 27 | 28 | 29 | 30 | def self.process_issues_for_budget_left(issues) 31 | issues.each do |i| 32 | if i["budget_duration_sum"] != nil 33 | # TODO Cleanup code for Budget left. 34 | budgetLeftRaw = Helpers.budget_left?(i["budget_duration_sum"], i["time_duration_sum"]) 35 | budgetLeftHuman = Helpers.convertSecondsToDurationFormat(budgetLeftRaw, "long") 36 | i["budget_left_raw"] = budgetLeftRaw 37 | i["budget_left_human"] = budgetLeftHuman 38 | end 39 | end 40 | return issues 41 | end 42 | 43 | 44 | end -------------------------------------------------------------------------------- /time_tracker/accepted_emoji.rb: -------------------------------------------------------------------------------- 1 | module Accepted_Time_Tracking_Emoji 2 | 3 | def self.accepted_time_comment_emoji(*acceptedTimeCommentEmoji) 4 | acceptedTimeCommentEmoji = [":clock130:", ":clock11:", ":clock1230:", ":clock3:", ":clock430:", 5 | ":clock6:", ":clock730:", ":clock9:", ":clock10:", ":clock1130:", 6 | ":clock2:", ":clock330:", ":clock5:", ":clock630:", ":clock8:", 7 | ":clock930:", ":clock1:", ":clock1030:", ":clock12:", ":clock230:", 8 | ":clock4:", ":clock530:", ":clock7:", ":clock830:"] 9 | end 10 | 11 | def self.accepted_nonBillable_emoji(*acceptedNonBilliableEmoji) 12 | acceptedNonBilliableEmoji = [":free:"] 13 | end 14 | 15 | def self.accepted_milestone_budget_emoji 16 | acceptedNonBilliableEmoji = [":dart:"] 17 | end 18 | 19 | def self.accepted_issue_budget_emoji 20 | acceptedNonBilliableEmoji = [":dart:"] 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /time_tracker/accepted_labels_categories.rb: -------------------------------------------------------------------------------- 1 | 2 | module Accepted_Labels_Categories 3 | 4 | def self.accepted_labels_categories 5 | acceptedLabels = [ 6 | {:category => "Priority:", :label => "Low"}, 7 | {:category => "Priority:", :label => "Medium"}, 8 | {:category => "Priority:", :label => "High"}, 9 | {:category => "Size:", :label => "Small"}, 10 | {:category => "Size:", :label => "Medium"}, 11 | {:category => "Size:", :label => "Large"}, 12 | {:category => "Version:", :label => "1.0"}, 13 | {:category => "Version:", :label => "1.5"}, 14 | {:category => "Version:", :label => "2.0"}, 15 | {:category => "Task:", :label => "Medium"}, 16 | {:category => "Size:", :label => "Medium"}, 17 | ] 18 | end 19 | end -------------------------------------------------------------------------------- /time_tracker/code_commit_comments.rb: -------------------------------------------------------------------------------- 1 | require_relative "helpers" 2 | 3 | module Commit_Comments 4 | 5 | def self.process_commit_comment_for_time(commitCommentRaw) 6 | 7 | 8 | commentBody = commitCommentRaw.attrs[:body] 9 | nonBillable = Helpers.non_billable?(commentBody) 10 | parsedTimeDetails = parse_time_commit(commentBody, nonBillable) 11 | 12 | 13 | 14 | commentId = commitCommentRaw.attrs[:id] 15 | userCreated = commitCommentRaw.attrs[:user].attrs[:login] 16 | createdAtDate = commitCommentRaw.attrs[:created_at] 17 | updatedAtDate = commitCommentRaw.attrs[:updated_at] 18 | 19 | recordCreationDate = Time.now.utc 20 | commentForPath = commitCommentRaw.attrs[:path] 21 | commentForLine = commitCommentRaw.attrs[:line] 22 | 23 | if commentForLine == nil and commentForLine == nil 24 | type = "Code Commit Comment Time" 25 | else 26 | type = "Code Commit Line Comment Time" 27 | end 28 | 29 | 30 | if parsedTimeDetails == nil 31 | return [] 32 | else 33 | overviewDetails = { "type" => type, 34 | "comment_id" => commentId, 35 | "comment_created_date" => createdAtDate, 36 | "work_logged_by" => userCreated, 37 | "path" => commentForPath, 38 | "line" => commentForLine, 39 | "record_creation_date" => Time.now.utc} 40 | mergedHash = parsedTimeDetails.merge(overviewDetails) 41 | return mergedHash 42 | end 43 | end 44 | 45 | def self.parse_time_commit(timeComment, nonBillableTime) 46 | acceptedClockEmoji = Helpers.get_Issue_Time_Emoji 47 | acceptedNonBilliableEmoji = Helpers.get_Non_Billable_Emoji 48 | 49 | parsedCommentHash = { "duration" => nil, "non_billable" => nil, "work_date" => nil, "time_comment" => nil} 50 | parsedComment = [] 51 | 52 | acceptedClockEmoji.each do |x| 53 | if nonBillableTime == true 54 | acceptedNonBilliableEmoji.each do |b| 55 | if timeComment =~ /\A#{x}\s#{b}/ 56 | parsedComment = Helpers.parse_non_billable_time_comment(timeComment,x,b) 57 | parsedCommentHash["non_billable"] = true 58 | break 59 | end 60 | end 61 | elsif nonBillableTime == false 62 | if timeComment =~ /\A#{x}/ 63 | parsedComment = Helpers.parse_billable_time_comment(timeComment,x) 64 | parsedCommentHash["non_billable"] = false 65 | break 66 | end 67 | end 68 | end 69 | 70 | if parsedComment.empty? == true 71 | return nil 72 | end 73 | 74 | if parsedComment[0] != nil 75 | parsedCommentHash["duration"] = Helpers.get_duration(parsedComment[0]) 76 | end 77 | 78 | if parsedComment[1] != nil 79 | workDate = Helpers.get_time_work_date(parsedComment[1]) 80 | if workDate != nil 81 | parsedCommentHash["work_date"] = workDate 82 | elsif workDate == nil 83 | parsedCommentHash["time_comment"] = Helpers.get_time_commit_comment(parsedComment[1]) 84 | end 85 | end 86 | 87 | if parsedComment[2] != nil 88 | parsedCommentHash["time_comment"] = Helpers.get_time_commit_comment(parsedComment[2]) 89 | end 90 | 91 | return parsedCommentHash 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /time_tracker/code_commit_messages.rb: -------------------------------------------------------------------------------- 1 | require_relative "helpers" 2 | 3 | module Commit_Messages 4 | 5 | def self.process_commit_message_for_time(commitMessageBody) 6 | 7 | type = "Code Commit Time" 8 | nonBillable = Helpers.non_billable?(commitMessageBody) 9 | parsedTimeDetails = parse_time_commit(commitMessageBody, nonBillable) 10 | if parsedTimeDetails == nil 11 | return nil 12 | else 13 | overviewDetails = {"type" => type, 14 | "record_creation_date" => Time.now.utc} 15 | mergedHash = parsedTimeDetails.merge(overviewDetails) 16 | return mergedHash 17 | end 18 | end 19 | 20 | def self.parse_time_commit(timeComment, nonBillableTime) 21 | acceptedClockEmoji = Helpers.get_Issue_Time_Emoji 22 | acceptedNonBilliableEmoji = Helpers.get_Non_Billable_Emoji 23 | 24 | parsedCommentHash = { "duration" => nil, "non_billable" => nil, "work_date" => nil, "time_comment" => nil} 25 | parsedComment = [] 26 | 27 | acceptedClockEmoji.each do |x| 28 | if nonBillableTime == true 29 | acceptedNonBilliableEmoji.each do |b| 30 | if timeComment =~ /\A#{x}\s#{b}/ 31 | parsedComment = Helpers.parse_non_billable_time_comment(timeComment,x,b) 32 | parsedCommentHash["non_billable"] = true 33 | break 34 | end 35 | end 36 | elsif nonBillableTime == false 37 | if timeComment =~ /\A#{x}/ 38 | parsedComment = Helpers.parse_billable_time_comment(timeComment,x) 39 | parsedCommentHash["non_billable"] = false 40 | break 41 | end 42 | end 43 | end 44 | 45 | if parsedComment.empty? == true 46 | return nil 47 | end 48 | 49 | if parsedComment[0] != nil 50 | parsedCommentHash["duration"] = Helpers.get_duration(parsedComment[0]) 51 | end 52 | 53 | if parsedComment[1] != nil 54 | workDate = Helpers.get_time_work_date(parsedComment[1]) 55 | if workDate != nil 56 | parsedCommentHash["work_date"] = workDate 57 | elsif workDate == nil 58 | parsedCommentHash["time_comment"] = Helpers.get_time_commit_comment(parsedComment[1]) 59 | end 60 | end 61 | 62 | if parsedComment[2] != nil 63 | parsedCommentHash["time_comment"] = Helpers.get_time_commit_comment(parsedComment[2]) 64 | end 65 | 66 | return parsedCommentHash 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /time_tracker/code_commits.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers' 2 | require_relative 'code_commit_messages' 3 | require_relative 'code_commit_comments' 4 | require 'awesome_print' 5 | 6 | module GH_Commits 7 | 8 | def self.process_code_commit(repo, commitDetails, commitComments, githubAuthInfo) 9 | 10 | # ap commitDetails.attrs[:author].attrs[:login] 11 | ap commitDetails 12 | githubUserName = githubAuthInfo[:username] 13 | githubUserID = githubAuthInfo[:userID] 14 | commitMessage = commitDetails.attrs[:commit].attrs[:message] 15 | 16 | type = "Code Commit" 17 | recordCreationDate = Time.now.utc 18 | if commitDetails.attrs[:author] != nil 19 | commitAuthorUsername = commitDetails.attrs[:author].attrs[:login] 20 | commitAuthorDate = commitDetails.attrs[:commit].attrs[:author].attrs[:date] 21 | else 22 | commitAuthorUsername = nil 23 | end 24 | if commitDetails.attrs[:committer] != nil 25 | commitCommitterUsername = commitDetails.attrs[:committer].attrs[:login] 26 | else 27 | commitCommitterUsername = nil 28 | end 29 | 30 | 31 | commitCommitterDate = commitDetails.attrs[:commit].attrs[:committer].attrs[:date] 32 | commitSha = commitDetails.attrs[:sha] 33 | commitTreeSha = commitDetails.attrs[:commit].attrs[:tree].attrs[:sha] 34 | commitParentsShas = [] 35 | 36 | # TODO look to crate this if statement as a helper if it makes sense 37 | if commitDetails.attrs[:parents] != nil 38 | commitDetails.attrs[:parents].each do |x| 39 | commitParentsShas << x.attrs[:sha] 40 | end 41 | end 42 | 43 | parsedCommitMessage = Commit_Messages.process_commit_message_for_time(commitMessage) 44 | parsedCommitComments = [] 45 | 46 | # commitComments = Helpers.get_commit_comments(repo, commitSha) 47 | 48 | commitComments.each do |x| 49 | parsedCommitComment = Commit_Comments.process_commit_comment_for_time(x) 50 | if parsedCommitComment.empty? == false 51 | parsedCommitComments << parsedCommitComment 52 | end 53 | end 54 | 55 | 56 | if parsedCommitMessage == nil and parsedCommitComments.empty? == true 57 | return [] 58 | else 59 | timeCommitHash = { "downloaded_by_username" => githubUserName, 60 | "downloaded_by_userID" => githubUserID, 61 | "type" => type, 62 | "repo" => repo, 63 | "commit_author_username" => commitAuthorUsername, 64 | "commit_author_date" => commitAuthorDate, 65 | "commit_committer_username" => commitCommitterUsername, 66 | "commit_committer_date" => commitCommitterDate, 67 | "commit_sha" => commitSha, 68 | "commit_tree_sha" => commitTreeSha, 69 | "commit_parents_shas" => commitParentsShas, 70 | "record_creation_date" => recordCreationDate, 71 | "commit_message_time" => parsedCommitMessage, 72 | "commit_comments_time" => parsedCommitComments, 73 | } 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /time_tracker/github_data.rb: -------------------------------------------------------------------------------- 1 | require 'octokit' 2 | 3 | module GitHub_Data 4 | 5 | def self.gh_sinatra_auth(ghUser) 6 | 7 | @ghClient = ghUser 8 | Octokit.auto_paginate = true 9 | return @ghClient 10 | 11 | end 12 | 13 | def self.gh_authenticate(username, password) 14 | @ghClient = Octokit::Client.new( 15 | :login => username.to_s, 16 | :password => password.to_s, 17 | :auto_paginate => true 18 | ) 19 | end 20 | 21 | def self.get_Issues(repo) 22 | issueResultsOpen = @ghClient.list_issues(repo, { 23 | :state => :open 24 | }) 25 | issueResultsClosed = @ghClient.list_issues(repo, { 26 | :state => :closed 27 | }) 28 | 29 | return mergedIssues = issueResultsOpen + issueResultsClosed 30 | end 31 | 32 | def self.get_Milestones(repo) 33 | milestonesResultsOpen = @ghClient.list_milestones(repo, { 34 | :state => :open 35 | }) 36 | milestonesResultsClosed = @ghClient.list_milestones(repo, { 37 | :state => :closed 38 | }) 39 | 40 | return mergedMilestones = milestonesResultsOpen + milestonesResultsClosed 41 | end 42 | 43 | def self.get_Issue_Comments(repo, issueNumber) 44 | issueComments = @ghClient.issue_comments(repo, issueNumber) 45 | end 46 | 47 | def self.get_code_commits(repo) 48 | repoCommits = @ghClient.commits(repo) 49 | end 50 | 51 | def self.get_commit_comments(repo, sha) 52 | commitComments = @ghClient.commit_comments(repo, sha) 53 | end 54 | end -------------------------------------------------------------------------------- /time_tracker/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 -------------------------------------------------------------------------------- /time_tracker/issue_budget.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers' 2 | 3 | module Gh_Issue_Budget 4 | 5 | # processes a comment for time comment information 6 | def self.process_issue_comment_for_budget(issueComment) 7 | 8 | type = "Issue Budget" 9 | issueCommentBody = issueComment.attrs[:body] 10 | nonBillable = Helpers.non_billable?(issueCommentBody) 11 | parsedTimeDetails = parse_time_commit_for_budget(issueCommentBody, nonBillable) 12 | if parsedTimeDetails == nil 13 | return nil 14 | else 15 | overviewDetails = {"type" => type, 16 | "comment_id" => issueComment.attrs[:id], 17 | "work_logged_by" => issueComment.attrs[:user].attrs[:login], 18 | "comment_created_date" => issueComment.attrs[:created_at], 19 | "comment_last_updated_date" =>issueComment.attrs[:updated_at], 20 | "record_creation_date" => Time.now.utc} 21 | mergedHash = parsedTimeDetails.merge(overviewDetails) 22 | return mergedHash 23 | end 24 | end 25 | 26 | def self.parse_time_commit_for_budget(timeComment, nonBillableTime) 27 | acceptedBudgetEmoji = Helpers.get_Issue_Budget_Emoji 28 | acceptedNonBilliableEmoji = Helpers.get_Non_Billable_Emoji 29 | 30 | parsedCommentHash = { "duration" => nil, "non_billable" => nil, "time_comment" => nil} 31 | parsedComment = [] 32 | 33 | acceptedBudgetEmoji.each do |x| 34 | if nonBillableTime == true 35 | acceptedNonBilliableEmoji.each do |b| 36 | if timeComment =~ /\A#{x}\s#{b}/ 37 | parsedComment = Helpers.parse_non_billable_time_comment(timeComment,x,b) 38 | parsedCommentHash["non_billable"] = true 39 | break 40 | end 41 | end 42 | elsif nonBillableTime == false 43 | if timeComment =~ /\A#{x}/ 44 | parsedComment = Helpers.parse_billable_time_comment(timeComment,x) 45 | parsedCommentHash["non_billable"] = false 46 | break 47 | end 48 | end 49 | end 50 | 51 | if parsedComment.empty? == true 52 | return nil 53 | end 54 | 55 | if parsedComment[0] != nil 56 | parsedCommentHash["duration"] = Helpers.get_duration(parsedComment[0]) 57 | end 58 | 59 | if parsedComment[1] != nil 60 | parsedCommentHash["budget_comment"] = Helpers.get_time_commit_comment(parsedComment[1]) 61 | end 62 | 63 | return parsedCommentHash 64 | end 65 | end -------------------------------------------------------------------------------- /time_tracker/issue_comment_tasks.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers' 2 | require_relative 'issue_time' 3 | 4 | module Gh_Issue_Comment_Tasks 5 | 6 | def self.process_issue_comment_for_task_time(commentRaw) 7 | 8 | type = "Task Time" 9 | commentBody = commentRaw.attrs[:body] 10 | rawTasks = get_comment_tasks(commentBody) 11 | 12 | if rawTasks[:complete] == nil and rawTasks[:incomplete] == nil 13 | return nil 14 | end 15 | 16 | processedTasks = process_comment_task_for_time(rawTasks) 17 | 18 | # Checks the hash of arrays items to see if the work date provided YN value is 19 | # false and if it is then makes the work date value the same value as the comment 20 | # created date 21 | processedTasks["tasks"].each do |t| 22 | if t["work_date_provided"] == false 23 | t["work_date"] = commentRaw.attrs[:created_at] 24 | end 25 | end 26 | 27 | 28 | 29 | if processedTasks.empty? == false 30 | overviewDetails = {"type" => type, 31 | "comment_id" => commentRaw.attrs[:id], 32 | "work_logged_by" => commentRaw.attrs[:user].attrs[:login], 33 | "comment_created_date" => commentRaw.attrs[:created_at], 34 | "comment_last_updated_date" =>commentRaw.attrs[:updated_at], 35 | "record_creation_date" => Time.now.utc} 36 | 37 | mergedHash = processedTasks.merge(overviewDetails) 38 | return mergedHash 39 | else 40 | return nil 41 | end 42 | end 43 | 44 | def self.get_comment_tasks(commentBody) 45 | 46 | tasks = {:complete => nil, :incomplete => nil } 47 | completeTasks = [] 48 | incompleteTasks = [] 49 | 50 | startStringIncomplete = /\-\s\[\s\]\s/ 51 | startStringComplete = /\-\s\[x\]\s/ 52 | 53 | endString = /[\r\n]|\z/ 54 | 55 | tasksInBody = commentBody.scan(/#{startStringIncomplete}(.*?)#{endString}/) 56 | tasksInBody.each do |x| 57 | incompleteTasks << x[0] 58 | end 59 | 60 | tasksInBody = commentBody.scan(/#{startStringComplete}(.*?)#{endString}/) 61 | tasksInBody.each do |x| 62 | completeTasks << x[0] 63 | end 64 | 65 | tasks[:complete] = completeTasks 66 | tasks[:incomplete] = incompleteTasks 67 | 68 | return tasks 69 | end 70 | 71 | def self.process_comment_task_for_time(tasks) 72 | finalTasks = [] 73 | 74 | tasks[:complete].each do |t| 75 | nonBillable = Helpers.non_billable?(t) 76 | parsedTimeDetails = Gh_Issue_Time.parse_time_commit(t, nonBillable) 77 | 78 | if parsedTimeDetails != nil 79 | parsedTimeDetails["task_status"] = "complete" 80 | finalTasks << parsedTimeDetails 81 | end 82 | end 83 | 84 | tasks[:incomplete].each do |t| 85 | nonBillable = Helpers.non_billable?(t) 86 | parsedTimeDetails = Gh_Issue_Time.parse_time_commit(t, nonBillable) 87 | 88 | if parsedTimeDetails != nil 89 | parsedTimeDetails["task_status"] = "incomplete" 90 | finalTasks << parsedTimeDetails 91 | end 92 | end 93 | tasksHash = {"tasks" => finalTasks} 94 | 95 | return tasksHash 96 | end 97 | 98 | 99 | end 100 | -------------------------------------------------------------------------------- /time_tracker/issue_time.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers' 2 | 3 | module Gh_Issue_Time 4 | 5 | # processes a comment for time comment information 6 | def self.process_issue_comment_for_time(issueComment) 7 | 8 | type = "Issue Time" 9 | issueCommentBody = issueComment.attrs[:body] 10 | nonBillable = Helpers.non_billable?(issueCommentBody) 11 | parsedTimeDetails = parse_time_commit(issueCommentBody, nonBillable) 12 | 13 | if parsedTimeDetails == nil 14 | return nil 15 | else 16 | overviewDetails = {"type" => type, 17 | "comment_id" => issueComment.attrs[:id], 18 | "work_logged_by" => issueComment.attrs[:user].attrs[:login], 19 | "comment_created_date" => issueComment.attrs[:created_at], 20 | "comment_last_updated_date" =>issueComment.attrs[:updated_at], 21 | "record_creation_date" => Time.now.utc} 22 | mergedHash = parsedTimeDetails.merge(overviewDetails) 23 | if mergedHash["work_date_provided"] == false 24 | mergedHash["work_date"] = issueComment.attrs[:created_at] 25 | end 26 | return mergedHash 27 | end 28 | end 29 | 30 | def self.parse_time_commit(timeComment, nonBillableTime) 31 | acceptedClockEmoji = Helpers.get_Issue_Time_Emoji 32 | acceptedNonBilliableEmoji = Helpers.get_Non_Billable_Emoji 33 | 34 | parsedCommentHash = { "duration" => nil, "non_billable" => nil, "work_date" => nil, "time_comment" => nil, "work_date_provided" => false} 35 | parsedComment = [] 36 | 37 | acceptedClockEmoji.each do |x| 38 | if nonBillableTime == true 39 | acceptedNonBilliableEmoji.each do |b| 40 | if timeComment =~ /\A#{x}\s#{b}/ 41 | parsedComment = Helpers.parse_non_billable_time_comment(timeComment,x,b) 42 | parsedCommentHash["non_billable"] = true 43 | break 44 | end 45 | end 46 | elsif nonBillableTime == false 47 | if timeComment =~ /\A#{x}/ 48 | parsedComment = Helpers.parse_billable_time_comment(timeComment,x) 49 | parsedCommentHash["non_billable"] = false 50 | break 51 | end 52 | end 53 | end 54 | 55 | if parsedComment.empty? == true 56 | return nil 57 | end 58 | 59 | if parsedComment[0] != nil 60 | parsedCommentHash["duration"] = Helpers.get_duration(parsedComment[0]) 61 | end 62 | 63 | if parsedComment[1] != nil 64 | workDate = Helpers.get_time_work_date(parsedComment[1]) 65 | if workDate != nil 66 | parsedCommentHash["work_date"] = workDate 67 | parsedCommentHash["work_date_provided"] = true 68 | elsif workDate == nil 69 | parsedCommentHash["time_comment"] = Helpers.get_time_commit_comment(parsedComment[1]) 70 | 71 | end 72 | end 73 | 74 | if parsedComment[2] != nil 75 | parsedCommentHash["time_comment"] = Helpers.get_time_commit_comment(parsedComment[2]) 76 | end 77 | 78 | return parsedCommentHash 79 | end 80 | end -------------------------------------------------------------------------------- /time_tracker/issues.rb: -------------------------------------------------------------------------------- 1 | require_relative 'labels_processor' 2 | require_relative 'helpers' 3 | require_relative 'issue_budget' 4 | require_relative 'issue_time' 5 | require_relative '../gh_issue_task_aggregator' 6 | 7 | module Gh_Issue 8 | 9 | def self.process_issue(repo, issueDetails, issueComments, githubAuthInfo) 10 | githubUserName = githubAuthInfo[:username] 11 | githubUserID = githubAuthInfo[:userID] 12 | issueState = issueDetails.attrs[:state] 13 | issueTitle = issueDetails.attrs[:title] 14 | issueNumber = issueDetails.attrs[:number] 15 | issueCreatedAt = issueDetails.attrs[:created_at] 16 | issueClosedAt = issueDetails.attrs[:closed_at] 17 | issueLastUpdatedAt = issueDetails.attrs[:updated_at] 18 | recordCreationDate = Time.now.utc 19 | 20 | # gets the milestone number assigned to a issue. Is not milestone assigned then returns nil 21 | milestoneNumber = Helpers.get_issue_milestone_number(issueDetails.attrs[:milestone]) 22 | 23 | # gets labels data for issue and returns array of label strings 24 | labelNames = Labels_Processor.get_label_names(issueDetails.attrs[:labels]) 25 | 26 | # runs the label names through a parser to create Label categories. 27 | # used for advanced label grouping 28 | labels = Labels_Processor.process_issue_labels(labelNames) 29 | 30 | commentsTime = [] 31 | 32 | # Part of the Task Listing code 33 | gotIssueDetails = {} 34 | commentsArray = [] 35 | # end of part of the task listing code 36 | 37 | # cycles through each comment and returns time tracking 38 | issueComments.each do |x| 39 | # checks to see if there is a time comment in the body field 40 | isTimeComment = Helpers.time_comment?(x.attrs[:body]) 41 | isBudgetComment = Helpers.budget_comment?(x.attrs[:body]) 42 | if isTimeComment == true 43 | # if true, the body field is parsed for time comment details 44 | parsedTime = Gh_Issue_Time.process_issue_comment_for_time(x) 45 | if parsedTime != nil 46 | # assuming results are returned from the parse (aka the parse was preceived 47 | # by the code to be sucessful, the parsed time comment details array is put into 48 | # the commentsTime array) 49 | commentsTime << parsedTime 50 | end 51 | elsif isBudgetComment == true 52 | parsedBudget = Gh_Issue_Budget.process_issue_comment_for_budget(x) 53 | if parsedBudget != nil 54 | commentsTime << parsedBudget 55 | end 56 | end 57 | 58 | parsedTasks = Gh_Issue_Comment_Tasks.process_issue_comment_for_task_time(x) 59 | if parsedTasks["tasks"].empty? == false 60 | commentsTime << parsedTasks 61 | end 62 | 63 | # Beta Code======== Tests for Tasks Listing - Provides a lists of tasks for each issue 64 | commentBody1 = GH_Issue_Task_Aggregator.get_comment_body(x) 65 | commentHasTasksTF = GH_Issue_Task_Aggregator.comment_has_tasks?(commentBody1) 66 | if commentHasTasksTF == true 67 | gotTasks = GH_Issue_Task_Aggregator.get_tasks_from_comment(commentBody1) 68 | gotCommentDetails = GH_Issue_Task_Aggregator.get_comment_details(x) 69 | 70 | mergedDetails = GH_Issue_Task_Aggregator.merge_details_and_tasks(gotCommentDetails, gotTasks) 71 | 72 | commentsArray << mergedDetails 73 | end 74 | 75 | end # do not delete this 'end'. it is part of issueComments do block 76 | 77 | if commentsArray.empty? == false 78 | gotIssueDetails = GH_Issue_Task_Aggregator.get_issue_details(repo, issueDetails) 79 | gotIssueDetails["comments_with_tasks"] = commentsArray 80 | # pp gotIssueDetails 81 | end 82 | #====== End of Tests for Task Listings 83 | 84 | if commentsTime.empty? == false 85 | return output = { "downloaded_by_username" => githubUserName, 86 | "downloaded_by_userID" => githubUserID, 87 | "repo" => repo, 88 | "type" => "Issue", 89 | "issue_state" => issueState, 90 | "issue_title" => issueTitle, 91 | "issue_number" => issueNumber, 92 | "milestone_number" => milestoneNumber, 93 | "labels" => labels, 94 | "issue_created_at" => issueCreatedAt, 95 | "issue_closed_at" => issueClosedAt, 96 | "issue_last_updated_at" => issueLastUpdatedAt, 97 | "record_creation_date" => recordCreationDate, 98 | "time_tracking_commits" => commentsTime, } 99 | elsif commentsTime.empty? == true 100 | return output = {} 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /time_tracker/labels_processor.rb: -------------------------------------------------------------------------------- 1 | require_relative 'accepted_labels_categories' 2 | 3 | module Labels_Processor 4 | 5 | # parse through GitHub labels and return label names in an array 6 | def self.get_label_names(labelsData) 7 | issueLabels = [] 8 | if labelsData != nil 9 | labelsData.each do |x| 10 | issueLabels << x["name"] 11 | end 12 | end 13 | return issueLabels 14 | end 15 | 16 | def self.process_issue_labels(ghLabels, options = {}) 17 | output = [] 18 | 19 | if options[:acceptedLabels] == nil 20 | # Exaple/Default labels. 21 | acceptedLabels = Accepted_Labels_Categories.accepted_labels_categories 22 | end 23 | 24 | if ghLabels != nil 25 | ghLabels.each do |x| 26 | # clears the outputHash variable every time a new Label is inspected 27 | outputHash = {} 28 | 29 | # does a check to see if the label is one of the Accepted Labels. 30 | anyAcceptedLabelsTF = acceptedLabels.any? { |b| [b[:category],b[:label]].join(" ") == x } 31 | 32 | # If the label is a accepted label then process 33 | if anyAcceptedLabelsTF == true 34 | acceptedLabels.each do |y| 35 | if [y[:category], y[:label]].join(" ") == x 36 | # Add the Category to the Cateogry field and Removes the colon character from category name 37 | outputHash["category"] = y[:category][0..-2] 38 | 39 | outputHash["label"] = y[:label] 40 | output << outputHash 41 | end 42 | end 43 | 44 | # If the label is not an accepted label then make the category field nil 45 | elsif anyAcceptedLabelsTF == false 46 | outputHash["category"] = nil 47 | outputHash["label"] = x 48 | output << outputHash 49 | end 50 | end 51 | else 52 | output = [] 53 | end 54 | return output 55 | end 56 | end -------------------------------------------------------------------------------- /time_tracker/milestone_budget.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers' 2 | 3 | module Gh_Milestone_Budget 4 | 5 | # processes a budget description for time comment information 6 | def self.process_budget_description_for_time(budgetComment) 7 | 8 | type = "Milestone Budget" 9 | nonBillable = Helpers.non_billable?(budgetComment) 10 | parsedTimeDetails = parse_time_commit(budgetComment, nonBillable) 11 | if parsedTimeDetails == nil 12 | return nil 13 | else 14 | overviewDetails = {"type" => type, 15 | "record_creation_date" => Time.now.utc} 16 | mergedHash = parsedTimeDetails.merge(overviewDetails) 17 | return mergedHash 18 | end 19 | end 20 | 21 | def self.parse_time_commit(timeComment, nonBillableTime) 22 | acceptedBudgetEmoji = Helpers.get_Milestone_Budget_Emoji 23 | acceptedNonBilliableEmoji = Helpers.get_Non_Billable_Emoji 24 | 25 | parsedCommentHash = { "duration" => nil, "non_billable" => nil, "time_comment" => nil} 26 | parsedComment = [] 27 | acceptedBudgetEmoji.each do |x| 28 | if nonBillableTime == true 29 | acceptedNonBilliableEmoji.each do |b| 30 | if timeComment =~ /\A#{x}\s#{b}/ 31 | parsedComment = Helpers.parse_non_billable_time_comment(timeComment,x,b) 32 | parsedCommentHash["non_billable"] = true 33 | break 34 | end 35 | end 36 | elsif nonBillableTime == false 37 | if timeComment =~ /\A#{x}/ 38 | parsedComment = Helpers.parse_billable_time_comment(timeComment,x) 39 | parsedCommentHash["non_billable"] = false 40 | break 41 | end 42 | end 43 | end 44 | if parsedComment.empty? == true 45 | return nil 46 | end 47 | 48 | if parsedComment[0] != nil 49 | parsedCommentHash["duration"] = Helpers.get_duration(parsedComment[0]) 50 | end 51 | 52 | if parsedComment[1] != nil 53 | parsedCommentHash["budget_comment"] = Helpers.get_time_commit_comment(parsedComment[1]) 54 | end 55 | 56 | return parsedCommentHash 57 | end 58 | end -------------------------------------------------------------------------------- /time_tracker/milestones.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers' 2 | require_relative 'milestone_budget' 3 | 4 | module Gh_Milestone 5 | 6 | def self.process_milestone(repo, milestoneDetail, githubAuthInfo) 7 | 8 | githubUserName = githubAuthInfo[:username] 9 | githubUserID = githubAuthInfo[:userID] 10 | milestoneState = milestoneDetail.attrs[:state] 11 | milestoneTitle = milestoneDetail.attrs[:title] 12 | milestoneNumber = milestoneDetail.attrs[:number] 13 | 14 | milestoneCreatedAt = milestoneDetail.attrs[:created_at] 15 | milestoneClosedAt = milestoneDetail.attrs[:closed_at] 16 | milestoneDueDate = milestoneDetail.attrs[:due_on] 17 | milestoneOpenIssueCount = milestoneDetail.attrs[:open_issues] 18 | milestoneClosedIssueCount = milestoneDetail.attrs[:closed_issues] 19 | 20 | milestoneDescription = milestoneDetail.attrs[:description] 21 | 22 | recordCreationDate = Time.now.utc 23 | 24 | 25 | budgetTime = [] 26 | 27 | # cycles through each comment and returns time tracking 28 | # checks to see if there is a time comment in the body field 29 | isBudgetComment = Helpers.budget_comment?(milestoneDescription) 30 | if isBudgetComment == true 31 | # if true, the body field is parsed for time comment details 32 | parsedBudget = Gh_Milestone_Budget.process_budget_description_for_time(milestoneDescription) 33 | if parsedBudget != nil 34 | # assuming results are returned from the parse (aka the parse was preceived 35 | # by the code to be sucessful, the parsed time comment details array is put into 36 | # the commentsTime array) 37 | budgetTime << parsedBudget 38 | end 39 | end 40 | 41 | if budgetTime.empty? == false 42 | return output = { "downloaded_by_username" => githubUserName, 43 | "downloaded_by_userID" => githubUserID, 44 | "repo" => repo, 45 | "type" => "Milestone", 46 | "milestone_state" => milestoneState, 47 | "milestone_title" => milestoneTitle, 48 | "milestone_number" => milestoneNumber, 49 | "milestone_due_date" => milestoneDueDate, 50 | "milestone_created_at" => milestoneCreatedAt, 51 | "milestone_closed_at" => milestoneClosedAt, 52 | "milestone_open_issue_count" => milestoneOpenIssueCount, 53 | "milestone_closed_issue_count" => milestoneClosedIssueCount, 54 | "record_creation_date" => recordCreationDate, 55 | "budget_tracking_commits" => budgetTime, } 56 | elsif commentsTime.empty? == true 57 | return output = {} 58 | end 59 | end 60 | end -------------------------------------------------------------------------------- /time_tracker/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 --------------------------------------------------------------------------------