├── .gitignore ├── Business └── Screenshots │ ├── ProjectInfo_and_TimeLog.png │ └── TimeLog.png ├── README.md └── app ├── Gemfile ├── Gemfile.lock ├── accepted_emoji.rb ├── aggregations ├── admin_queries.rb └── issue_stat_queries.rb ├── app.rb ├── config.ru ├── gitlab_downloader.rb ├── helpers.rb ├── issue_time.rb ├── issues.rb ├── milestone_budget.rb ├── milestones.rb ├── mongo_connection.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 ├── views ├── analyze.erb ├── index.erb └── layout.erb └── xlsx_exporter.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | # Gemfile.lock 30 | # .ruby-version 31 | # .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | -------------------------------------------------------------------------------- /Business/Screenshots/ProjectInfo_and_TimeLog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitLab-Time-Tracking/59ce1c127a90e66257b931d166291c541be554cd/Business/Screenshots/ProjectInfo_and_TimeLog.png -------------------------------------------------------------------------------- /Business/Screenshots/TimeLog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitLab-Time-Tracking/59ce1c127a90e66257b931d166291c541be554cd/Business/Screenshots/TimeLog.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitLab-Time-Tracking 2 | 3 | [![Join the chat at https://gitter.im/StephenOTT/GitLab-Time-Tracking](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/StephenOTT/GitLab-Time-Tracking?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Time Tracking application for GitLab Issue Queues built on Ruby Sinatra and MongoDB. 6 | 7 | 8 | ##System Requirements 9 | 1. GitLab Version 7.9+ 10 | 2. Ruby 2.x+ 11 | 3. MongoDB 2.x+ 12 | 13 | 14 | 15 | ##Installation 16 | 17 | 1. Register/Create an Application at https://gitlab.com/oauth/applications/new. Set your fields to the following: 18 | 19 | 1.1. Name: `GitLab-Time-Tracking` or whatever you want to call your application. 20 | 21 | 1.2. Redirect URI: `http://localhost:9292/auth/gitlab/callback` 22 | 23 | 2. Install MongoDB (typically: `brew update`, followed by: `brew install mongodb`) 24 | 25 | 3. `cd` into the repository's `app` folder and run the following commands in the `app` folder: 26 | 27 | 3.1. Run `mongod` in terminal 28 | 29 | 3.2. Open a second terminal window in the `app` folder and run: `bundle install` 30 | 31 | 3.3. Get the Client ID/Application ID and Client Secret/Application Secret from the settings of your created/registered GitLab Application in Step 1. 32 | 33 | 3.4. In the second terminal window copy the below, add your Client ID and Client Secret, and run: `GITLAB_ENDPOINT="https://gitlab.com" GITLAB_CLIENT_ID="APPLICATION_ID" GITLAB_CLIENT_SECRET="APPLICATION_SECRET" MONGODB_HOST="localhost" MONGODB_PORT="27017" MONGODB_DB="GitLab" MONGODB_COLL="Issues_Time_Tracking" bundle exec rackup` 34 | 35 | 36 | 4. Go to `http://localhost:9292` 37 | 38 | 39 | ## Current Features: 40 | 41 | 1. Download Issue Time Tracking Data from Multiple GitLab Projects. 42 | 2. Clear MongoDB Database Collection. 43 | 3. Download XLSX Version of Time Tracking Data. 44 | 4. Download of Milestone Budget data for Milestones that are attached to Issues. 45 | 5. For Each downloaded dataset you can view the time tracking logs related to the specific download. 46 | 6. Download specific XLSX file for each downloaded data set/snapshots. 47 | 48 | 49 | ## Notes 50 | 51 | 1. You can only connect to one GitLab Endpoint per application instance. If you wish to connect to multiple GitLab instances, then you must run multiple instances of the GitLab Time Tracking Application 52 | 2. Only Issues that have notes/comments with Time Tracking records are downloaded into MongoDB. 53 | 3. The same MongoDB database can be used by multiple instances of GitLab Time Tracking. 54 | 4. Design Philosophy: Ensure maximum ease of use per record for a business user. Each record contains all information needed to exist on its own without any other records. 55 | 56 | ## Current Limitations 57 | 58 | 1. The current code being used for converting human readable durations such as "1d"(1 day) or "1w"(1 week), calculates as "24 hours per day". A future update will allow you to control how you calculate a "Day". The current **workaround** for this issue/limitation is to always log your time and budgets using Hours or a small time duration (example: 15h, 15m, 15s). 59 | 60 | ## Time Tracking Usage Patterns 61 | 62 | ### Logging Time for an Issue 63 | 64 | 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. 65 | 66 | 67 | #### Examples 68 | 69 | 1. `:clock1: 2h` # => :clock1: 2h 70 | 71 | 2. `:clock1: 2h | 3pm` # => :clock1: 2h | 3pm 72 | 73 | 3. `:clock1: 2h | 3:20pm` # => :clock1: 2h | 3:20pm 74 | 75 | 4. `:clock1: 2h | Feb 26, 2014` # => :clock1: 2h | Feb 26, 2014 76 | 77 | 5. `:clock1: 2h | Feb 26, 2014 3pm` # => :clock1: 2h | Feb 26, 2014 3pm 78 | 79 | 6. `:clock1: 2h | Feb 26, 2014 3:20pm` # => :clock1: 2h | Feb 26, 2014 3:20pm 80 | 81 | 7. `:clock1: 2h | Installed security patch and restarted the server.` # => :clock1: 2h | Installed security patch and restarted the server. 82 | 83 | 8. `:clock1: 2h | 3pm | Installed security patch and restarted the server.` # => :clock1: 2h | 3pm | Installed security patch and restarted the server. 84 | 85 | 9. `:clock1: 2h | 3:20pm | Installed security patch and restarted the server.` # => :clock1: 2h | 3:20pm | Installed security patch and restarted the server. 86 | 87 | 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. 88 | 89 | 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. 90 | 91 | 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. 92 | 93 | 94 | - Dates and times can be provided in various formats, but the above formats are recommended for plain text readability. 95 | 96 | - Any GitHub.com supported `clock` Emoji is supported: 97 | ":clock130:", ":clock11:", ":clock1230:", ":clock3:", ":clock430:", ":clock6:", ":clock730:", ":clock9:", ":clock10:", ":clock1130:", ":clock2:", ":clock330:", ":clock5:", ":clock630:", ":clock8:", ":clock930:", ":clock1:", ":clock1030:", ":clock12:", ":clock230:", ":clock4:", ":clock530:", ":clock7:", ":clock830:" 98 | 99 | #### Sample 100 | ![screen shot 2013-12-15 at 8 41 35 pm](https://f.cloud.github.com/assets/1994838/1751599/b03deba6-65f3-11e3-9a4a-6e30ca750fd6.png) 101 | 102 | ### Non-Billable Time Indicator Usage Example 103 | 104 | #### Logging Non-Billable Time for an Issue 105 | 106 | ##### Examples 107 | 108 | 1. `:clock1: :free: 2h` # => :clock1: :free: 2h 109 | 110 | 111 | ### Logging Budgets for a Milestone 112 | 113 | 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. 114 | 115 | #### Examples 116 | 117 | 1. `:dart: 5d` # => :dart: 5d 118 | 119 | 2. `:dart: 5d | We cannot go over this time at all!` # => :dart: 5d | We cannot go over this time at all! 120 | 121 | #### Sample 122 | ![screen shot 2013-12-15 at 8 42 04 pm](https://f.cloud.github.com/assets/1994838/1751601/bb73ed86-65f3-11e3-9abb-4c47eabbc608.png) 123 | ![screen shot 2013-12-15 at 8 41 55 pm](https://f.cloud.github.com/assets/1994838/1751602/bb757d9a-65f3-11e3-9ac5-86dba26bc037.png) 124 | 125 | 126 | ### Tracking Non-Billable Time and Budgets 127 | 128 | 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. 129 | 130 | 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. 131 | 132 | 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. 133 | 134 | #### Non-Billable Time and Budget Tracking Indicator Usage Example 135 | 136 | 137 | ##### Logging Non-Billable Time for an Issue 138 | 139 | ###### Examples 140 | 141 | 1. `:clock1: :free: 2h` # => :clock1: :free: 2h 142 | 143 | ##### Logging Non-Billable Budgets for a Milestone 144 | 145 | ###### Examples 146 | 147 | 1. `:dart: :free: 5d` # => :dart: :free: 5d 148 | 149 | 150 | -------------------------------------------------------------------------------- /app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'mongo', '~> 1.12.0' 4 | gem 'awesome_print' 5 | # gem 'gitlab' 6 | gem "gitlab", :git=>"https://github.com/NARKOZ/gitlab.git" 7 | gem 'omniauth-gitlab' 8 | gem 'sinatra' 9 | gem 'omniauth' 10 | gem 'chronic_duration' 11 | gem 'bson_ext', '~> 1.12.0' 12 | 13 | gem 'rake' 14 | gem 'chronic' 15 | gem 'rack-flash3' 16 | gem 'axlsx' -------------------------------------------------------------------------------- /app/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/NARKOZ/gitlab.git 3 | revision: 8dcfec5aaaff9ef1c12687c5f9ca90f6aed0f912 4 | specs: 5 | gitlab (3.3.0) 6 | httparty 7 | terminal-table 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | awesome_print (1.6.1) 13 | axlsx (2.0.1) 14 | htmlentities (~> 4.3.1) 15 | nokogiri (>= 1.4.1) 16 | rubyzip (~> 1.0.0) 17 | bson (1.12.0) 18 | bson_ext (1.12.0) 19 | bson (~> 1.12.0) 20 | chronic (0.10.2) 21 | chronic_duration (0.10.6) 22 | numerizer (~> 0.1.1) 23 | faraday (0.9.1) 24 | multipart-post (>= 1.2, < 3) 25 | hashie (3.4.0) 26 | htmlentities (4.3.3) 27 | httparty (0.13.3) 28 | json (~> 1.8) 29 | multi_xml (>= 0.5.2) 30 | json (1.8.2) 31 | jwt (1.4.1) 32 | mini_portile (0.6.2) 33 | mongo (1.12.0) 34 | bson (= 1.12.0) 35 | multi_json (1.11.0) 36 | multi_xml (0.5.5) 37 | multipart-post (2.0.0) 38 | nokogiri (1.6.6.2) 39 | mini_portile (~> 0.6.0) 40 | numerizer (0.1.1) 41 | oauth2 (1.0.0) 42 | faraday (>= 0.8, < 0.10) 43 | jwt (~> 1.0) 44 | multi_json (~> 1.3) 45 | multi_xml (~> 0.5) 46 | rack (~> 1.2) 47 | omniauth (1.2.2) 48 | hashie (>= 1.2, < 4) 49 | rack (~> 1.0) 50 | omniauth-gitlab (1.0.0) 51 | omniauth (~> 1.0) 52 | omniauth-oauth2 (~> 1.0) 53 | omniauth-oauth2 (1.2.0) 54 | faraday (>= 0.8, < 0.10) 55 | multi_json (~> 1.3) 56 | oauth2 (~> 1.0) 57 | omniauth (~> 1.2) 58 | rack (1.6.0) 59 | rack-flash3 (1.0.5) 60 | rack 61 | rack-protection (1.5.3) 62 | rack 63 | rake (10.4.2) 64 | rubyzip (1.0.0) 65 | sinatra (1.4.6) 66 | rack (~> 1.4) 67 | rack-protection (~> 1.4) 68 | tilt (>= 1.3, < 3) 69 | terminal-table (1.4.5) 70 | tilt (2.0.1) 71 | 72 | PLATFORMS 73 | ruby 74 | 75 | DEPENDENCIES 76 | awesome_print 77 | axlsx 78 | bson_ext (~> 1.12.0) 79 | chronic 80 | chronic_duration 81 | gitlab! 82 | mongo (~> 1.12.0) 83 | omniauth 84 | omniauth-gitlab 85 | rack-flash3 86 | rake 87 | sinatra 88 | -------------------------------------------------------------------------------- /app/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 -------------------------------------------------------------------------------- /app/aggregations/admin_queries.rb: -------------------------------------------------------------------------------- 1 | # require_relative '../mongo_connection' 2 | # require "awesome_print" 3 | 4 | class Admin_Queries 5 | def initialize(mongoConnection) 6 | @mongoConnection = mongoConnection 7 | end 8 | 9 | def get_downloads 10 | # TODO add filtering and extra security around query 11 | output = @mongoConnection.aggregate([ 12 | # { "$match" => { project_id: projectID }}, 13 | 14 | {"$group" => {_id: { 15 | download_id: "$admin_info.download_id", 16 | download_date: "$admin_info.download_timestamp", 17 | downloaded_by: "$admin_info.downloaded_by_user", 18 | download_endpoint: "$admin_info.gitlab_endpoint", 19 | project_id: "$project_id", 20 | project_name: "$project_info.path_with_namespace" 21 | }}} 22 | ]) 23 | output = output.map { |e| e["_id"] } 24 | end 25 | end 26 | 27 | # Testing Code 28 | # m = Mongo_Connection.new("localhost", 27017, "GitLab", "Issues_Time_Tracking") 29 | # output = Admin_Queries.new(m) 30 | 31 | # ap output.get_downloads -------------------------------------------------------------------------------- /app/aggregations/issue_stat_queries.rb: -------------------------------------------------------------------------------- 1 | # require_relative '../mongo_connection' 2 | # require "awesome_print" 3 | 4 | 5 | class Issue_Stat_Queries 6 | def initialize(mongoConnection) 7 | @mongoConnection = mongoConnection 8 | end 9 | 10 | def get_downloads 11 | # TODO add filtering and extra security around query 12 | output = @mongoConnection.aggregate([ 13 | # { "$match" => { project_id: projectID }}, 14 | 15 | {"$project" => {_id: 0, 16 | download_id: "$admin_info.download_id", 17 | download_date: "$admin_info.download_timestamp", 18 | downloaded_by: "$admin_info.downloaded_by_user", 19 | download_endpoint: "$admin_info.gitlab_endpoint", 20 | project_id: "$project_id" 21 | }} 22 | ]) 23 | end 24 | 25 | def get_project_snapshot_info(downloadID) 26 | # TODO add filtering and extra security around query 27 | output = @mongoConnection.aggregate([ 28 | # { "$match" => { project_id: projectID }}, 29 | {"$project" => {_id: 0, 30 | download_id: "$admin_info.download_id", 31 | admin_info: "$admin_info", 32 | project_info: "$project_info", 33 | }}, 34 | { "$match" => {download_id: downloadID}}, 35 | {"$limit" => 1}, 36 | ]).first 37 | end 38 | 39 | 40 | def get_issues_time 41 | # TODO add filtering and extra security around query 42 | output = @mongoConnection.aggregate([ 43 | # { "$match" => { project_id: projectID }}, 44 | 45 | { "$unwind" => "$comments" }, 46 | {"$project" => {_id: 0, 47 | project_id: 1, 48 | issue_id: "$id", 49 | issue_number: "$iid", 50 | type: { "$ifNull" => [ "$comments.time_tracking_data.type", nil ] }, 51 | milestone_number: { "$ifNull" => [ "$milestone.iid", nil ] }, 52 | milestone_title: { "$ifNull" => [ "$milestone.title", nil ] }, 53 | milestone_budget_comment: { "$ifNull" => [ "$milestone.milestone_budget_data.budget_comment", nil ] }, 54 | milestone_state: { "$ifNull" => [ "$milestone.state", nil ] }, 55 | milestone_due_date: { "$ifNull" => [ "$milestone.due_date", nil ] }, 56 | issue_title: "$title", 57 | issue_state: "$state", 58 | issue_author: "$author.username", 59 | comment_id: "$comments.id", 60 | time_track_duration: "$comments.time_tracking_data.duration", 61 | time_track_non_billable: "$comments.time_tracking_data.non_billable", 62 | time_track_work_date: "$comments.time_tracking_data.work_date", 63 | time_track_time_comment: "$comments.time_tracking_data.time_comment", 64 | time_track_work_date_provided: "$comments.time_tracking_data.work_date_provided", 65 | time_track_work_logged_by: "$comments.time_tracking_data.work_logged_by"}}, 66 | # { "$unwind" => "$comments.time_tracking_data" }, 67 | 68 | 69 | # { "$match" => { "comments.time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 70 | # { "$group" => { _id: { 71 | # project_id: "$project_id", 72 | # id: "$id", 73 | # iid: "$iid", 74 | # title: "$title", 75 | # state: "$state", 76 | # issue_author: "$author.username", 77 | # comment_id: "$comment.id", 78 | # comment_author: "$comment.author.username", 79 | # time_track_duration: "$comment.time_tracking_data.duration", 80 | # time_track_non_billable: "$comment.time_tracking_data.non_billable", 81 | # time_track_work_date: "$comment.time_tracking_data.work_date", 82 | # time_track_time_comment: "$comment.time_tracking_data.time_comment", 83 | # }, 84 | 85 | # }} 86 | ]) 87 | # output = [] 88 | # totalIssueSpentHoursBreakdown.each do |x| 89 | # x["_id"]["time_duration_sum"] = x["time_duration_sum"] 90 | # x["_id"]["time_comment_count"] = x["time_comment_count"] 91 | # output << x["_id"] 92 | # end 93 | # return output 94 | end 95 | 96 | def get_all_issues_time(downloadID) 97 | output = @mongoConnection.aggregate([ 98 | # { "$match" => { downloaded_by_username: githubAuthInfo[:username], downloaded_by_userID: githubAuthInfo[:userID] }}, 99 | # { "$unwind" => "$comments" }, 100 | # { "$match" => { admin_info: {download_id: downloadID}}}, 101 | { "$unwind" => "$comments" }, 102 | {"$project" => {_id: 0, 103 | download_id: "$admin_info.download_id", 104 | project: "$project_info.path_with_namespace", 105 | issue_number: "$iid", 106 | issue_title: "$title", 107 | issue_state: "$state", 108 | duration: "$comments.time_tracking_data.duration" 109 | }}, 110 | { "$match" => {download_id: downloadID}}, 111 | # { "$unwind" => "$comments" }, 112 | 113 | { "$group" => { _id: { 114 | project: "$project", 115 | # milestone_number: "$milestone_number", 116 | issue_number: "$issue_number", 117 | issue_title: "$issue_title", 118 | issue_state: "$issue_state", 119 | }, 120 | time_duration_sum: { "$sum" => "$duration" }, 121 | time_comment_count: {"$sum" => 1} 122 | } 123 | }, 124 | # { "$match" => {download_id: downloadID}}, 125 | ]) 126 | output.each do |x| 127 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 128 | x["_id"]["time_comment_count"] = x["time_comment_count"] 129 | end 130 | return output.map { |e| e["_id"] } 131 | end 132 | 133 | 134 | def get_issues_for_milestone(downloadID, milestoneNumber) 135 | output = @mongoConnection.aggregate([ 136 | { "$unwind" => "$comments" }, 137 | {"$project" => {_id: 0, 138 | download_id: "$admin_info.download_id", 139 | # project: "$project_info.path_with_namespace", 140 | issue_number: "$iid", 141 | issue_title: "$title", 142 | issue_state: "$state", 143 | comment_duration: "$comments.time_tracking_data.duration", 144 | comment_time_comment: { "$ifNull" => [ "$comments.time_tracking_data.time_comment", nil ] }, 145 | milestone_number: { "$ifNull" => [ "$milestone.iid", nil ] }, 146 | }}, 147 | { "$match" => {download_id: downloadID}}, 148 | { "$match" => {milestone_number: milestoneNumber}}, 149 | { "$group" => { _id: { 150 | # project: "$project", 151 | issue_number: "$issue_number", 152 | issue_title: "$issue_title", 153 | issue_state: "$issue_state", 154 | }, 155 | time_duration_sum: { "$sum" => "$comment_duration" }, 156 | time_comment_count: {"$sum" => 1} 157 | } 158 | }, 159 | ]) 160 | output.each do |x| 161 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 162 | x["_id"]["time_comment_count"] = x["time_comment_count"] 163 | end 164 | return output.map { |e| e["_id"] } 165 | end 166 | 167 | def get_milestones(downloadID) 168 | output = @mongoConnection.aggregate([ 169 | # { "$unwind" => "$comments" }, 170 | {"$project" => {_id: 0, 171 | download_id: "$admin_info.download_id", 172 | project: "$project_info.path_with_namespace", 173 | # issue_number: "$iid", 174 | # issue_title: "$title", 175 | # issue_state: "$state", 176 | # comment_duration: "$comments.time_tracking_data.duration", 177 | # comment_time_comment: { "$ifNull" => [ "$comments.time_tracking_data.time_comment", nil ] }, 178 | milestone_number: { "$ifNull" => [ "$milestone.iid", nil ] }, 179 | milestone_title: { "$ifNull" => [ "$milestone.title", nil ] }, 180 | milestone_budget_comment: { "$ifNull" => [ "$milestone.milestone_budget_data.budget_comment", nil ] }, 181 | milestone_state: { "$ifNull" => [ "$milestone.state", nil ] }, 182 | milestone_due_date: { "$ifNull" => [ "$milestone.due_date", nil ] }, 183 | milestone_budget_duration: { "$ifNull" => [ "$milestone.milestone_budget_data.duration", nil ] }, 184 | 185 | 186 | }}, 187 | { "$match" => {download_id: downloadID}}, 188 | { "$group" => { _id: { 189 | download_id: "$download_id", 190 | project: "$project", 191 | milestone_number: "$milestone_number", 192 | milestone_title: "$milestone_title", 193 | milestone_budget_comment: "$milestone_budget_comment", 194 | milestone_state: "$milestone_state", 195 | milestone_due_date: "$milestone_due_date", 196 | milestone_budget_duration: "$milestone_budget_duration" 197 | 198 | }} 199 | }, 200 | ]) 201 | # output.each do |x| 202 | # x["_id"]["time_duration_sum"] = x["time_duration_sum"] 203 | # x["_id"]["time_comment_count"] = x["time_comment_count"] 204 | # end 205 | return output.map { |e| e["_id"] } 206 | end 207 | def get_milestone_sums(downloadID, milestoneNumber) 208 | output = @mongoConnection.aggregate([ 209 | { "$unwind" => "$comments" }, 210 | {"$project" => {_id: 0, 211 | download_id: "$admin_info.download_id", 212 | project: "$project_info.path_with_namespace", 213 | issue_number: "$iid", 214 | # issue_title: "$title", 215 | # issue_state: "$state", 216 | comment_duration: "$comments.time_tracking_data.duration", 217 | # comment_time_comment: { "$ifNull" => [ "$comments.time_tracking_data.time_comment", nil ] }, 218 | milestone_number: { "$ifNull" => [ "$milestone.iid", nil ] }, 219 | milestone_title: { "$ifNull" => [ "$milestone.title", nil ] }, 220 | milestone_budget_comment: { "$ifNull" => [ "$milestone.milestone_budget_data.budget_comment", nil ] }, 221 | milestone_state: { "$ifNull" => [ "$milestone.state", nil ] }, 222 | milestone_due_date: { "$ifNull" => [ "$milestone.due_date", nil ] }, 223 | milestone_budget_duration: { "$ifNull" => [ "$milestone.milestone_budget_data.duration", nil ] }, 224 | 225 | 226 | }}, 227 | { "$match" => {download_id: downloadID, milestone_number: milestoneNumber}}, 228 | { "$group" => { _id: { 229 | download_id: "$download_id", 230 | # project: "$project", 231 | milestone_number: "$milestone_number", 232 | # milestone_title: "$milestone_title", 233 | # milestone_budget_comment: "$milestone_budget_comment", 234 | # milestone_state: "$milestone_state", 235 | # milestone_due_date: "$milestone_due_date", 236 | # milestone_budget_duration: "$milestone_budget_duration", 237 | # comment_duration: "$comment_duration", 238 | 239 | }, 240 | time_duration_sum: { "$sum" => "$comment_duration" }, 241 | time_comment_count: {"$sum" => 1} 242 | }}, 243 | ]) 244 | output.each do |x| 245 | x["_id"]["time_duration_sum"] = x["time_duration_sum"] 246 | x["_id"]["time_comment_count"] = x["time_comment_count"] 247 | end 248 | return output.map { |e| e["_id"] }.first 249 | end 250 | 251 | end 252 | 253 | # Testing Code 254 | # m = Mongo_Connection.new("localhost", 27017, "GitLab", "Issues_Time_Tracking") 255 | # output = Issue_Stat_Queries.new(m) 256 | 257 | 258 | # ap output.get_issues_time 259 | # ap output.get_all_issues_time("abdd1c36-4cb0-4828-bbe0-34a1d679ca2f") 260 | # ap output.get_issues_for_milestone("57a364cb-9fb2-423c-a1aa-72ede9934325", 1) 261 | # ap output.get_milestones("8cec1dc7-2284-46d4-937b-b5ae937bf2c9", 1) 262 | # ap output.get_milestone_sums("8cec1dc7-2284-46d4-937b-b5ae937bf2c9", 1) 263 | # ap output.get_project_snapshot_info("8cec1dc7-2284-46d4-937b-b5ae937bf2c9") 264 | 265 | -------------------------------------------------------------------------------- /app/app.rb: -------------------------------------------------------------------------------- 1 | require 'awesome_print' 2 | require 'gitlab' 3 | require 'omniauth' 4 | require 'omniauth-gitlab' 5 | require 'sinatra' 6 | require 'rack-flash' 7 | require_relative 'gitlab_downloader' 8 | require_relative 'xlsx_exporter' 9 | require_relative 'mongo_connection' 10 | require_relative 'aggregations/admin_queries' 11 | require_relative 'aggregations/issue_stat_queries' 12 | 13 | set :logging, :true 14 | set :show_exceptions, true 15 | 16 | use Rack::Session::Pool 17 | set :session_secret, 'Password!' # TODO Change this to a ENV 18 | 19 | # TODO add the ability to set the endpoint api after the app has been initilized 20 | use OmniAuth::Builder do 21 | provider :gitlab, ENV["GITLAB_CLIENT_ID"], ENV["GITLAB_CLIENT_SECRET"], 22 | client_options: { 23 | site: ENV["GITLAB_ENDPOINT"] 24 | } 25 | end 26 | 27 | use Rack::Flash, :sweep => true 28 | 29 | # Testing code - Outputs the Client ID and Secrect to the Console to ensure that the ENV was taken 30 | # ap ENV["GITLAB_CLIENT_ID"] 31 | # ap ENV["GITLAB_CLIENT_SECRET"] 32 | # End of Testing Code 33 | 34 | helpers do 35 | def current_user 36 | session["current_user"] 37 | end 38 | 39 | def gitlab_instance 40 | if @gl == nil 41 | @gl = GitLab_Downloader.new(gitlab_endpoint, current_user["private_token"]) 42 | else 43 | @gl 44 | end 45 | end 46 | 47 | def admin_queries 48 | if @aq == nil 49 | @aq = Admin_Queries.new(mongoConnection) 50 | else 51 | @aq 52 | end 53 | end 54 | 55 | def get_issues_for_milestone(downloadID, milestoneNumber) 56 | issue_stat_queries.get_issues_for_milestone(downloadID,milestoneNumber) 57 | end 58 | 59 | def get_milestone_sums(downloadID, milestoneNumber) 60 | issue_stat_queries.get_milestone_sums(downloadID, milestoneNumber) 61 | end 62 | 63 | def issue_stat_queries 64 | if @isq == nil 65 | @isq = Issue_Stat_Queries.new(mongoConnection) 66 | else 67 | @isq 68 | end 69 | end 70 | 71 | def mongoConnection 72 | if @mongoConnection == nil 73 | @mongoConnection = Mongo_Connection.new(ENV["MONGODB_HOST"], ENV["MONGODB_PORT"].to_i, ENV["MONGODB_DB"], ENV["MONGODB_COLL"]) 74 | else 75 | @mongoConnection 76 | end 77 | end 78 | 79 | def gitlab_endpoint(style = "full") 80 | if style == "full" 81 | ENV["GITLAB_ENDPOINT"] + (ENV['ENDPOINT_API_ADDRESS'] || "/api/v3") 82 | elsif style == "short" 83 | ENV["GITLAB_ENDPOINT"] 84 | end 85 | end 86 | end 87 | 88 | get '/' do 89 | if current_user == nil 90 | flash[:warning] = ["You must Login to your GitLab Instance to continue"] 91 | else 92 | @projectList = gitlab_instance.user_projects 93 | @adminQueries = admin_queries.get_downloads 94 | end 95 | 96 | erb :index 97 | end 98 | 99 | get '/clear-mongo/:downloadID' do 100 | downloadID = params[:downloadID] 101 | 102 | mongoConnection.remove_mongo_records(downloadID) 103 | 104 | # mongoConnection.clear_mongo_collections 105 | flash[:success] = ["Snapshot has been cleared (#{downloadID})"] 106 | redirect '/' 107 | 108 | end 109 | 110 | get '/download-xlsx/:downloadID' do 111 | downloadID = params[:downloadID] 112 | 113 | if current_user == nil 114 | redirect '/' 115 | elsif current_user != nil and downloadID != nil 116 | 117 | dataExportConnection = XLSXExporter.new(mongoConnection) 118 | dataExportIssues = dataExportConnection.get_all_issues_time(downloadID) 119 | dataExportMilestones = dataExportConnection.get_all_milestone_budgets(downloadID) 120 | 121 | if dataExportIssues.empty? == false or dataExportMilestones.empty? == false 122 | 123 | content_type 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 124 | attachment 'time-tracking.xlsx' 125 | 126 | file = dataExportConnection.generateXLSX(dataExportIssues, dataExportMilestones) 127 | else 128 | flash[:danger] = ["Unable to generate a xlsx: No time tracking data has been downloaded"] 129 | redirect '/' 130 | end 131 | else 132 | flash[:danger] = ["Error Location 001: Something went wrong"] 133 | redirect '/' 134 | end 135 | 136 | end 137 | 138 | 139 | post '/gl-download' do 140 | post = params[:post] 141 | 142 | if current_user == nil 143 | flash[:warning] = ["You must log in to download data"] 144 | 145 | elsif current_user != nil and post != nil 146 | 147 | repoProjectID = post[:repo] 148 | issuesWithComments = gitlab_instance.downloadIssuesAndComments(repoProjectID) 149 | 150 | if issuesWithComments.length > 0 151 | mongoConnection.putIntoMongoCollTimeTrackingCommits(issuesWithComments) 152 | flash[:success] = ["Time Tracking Data has been Downloaded from #{gitlab_endpoint('short')}"] 153 | else 154 | flash[:danger] = ["Unable to download Time Tracking data from #{gitlab_endpoint('short')}: No Time Tracking Data was found"] 155 | end 156 | 157 | elsif post == nil 158 | flash[:warning] = ["You must select a Project to download, ensure you are the member or owner of a Project at #{gitlab_endpoint('short')}"] 159 | 160 | end 161 | redirect '/' 162 | 163 | end 164 | 165 | get '/analyze/:downloadID' do 166 | downloadID = params[:downloadID] 167 | 168 | if current_user == nil 169 | flash[:warning] = ["You must log in to download data"] 170 | redirect '/' 171 | elsif current_user != nil and downloadID != nil 172 | 173 | @milestones = issue_stat_queries.get_milestones(downloadID) 174 | @snapshot_info = issue_stat_queries.get_project_snapshot_info(downloadID) 175 | 176 | erb :analyze 177 | else 178 | flash[:danger] = ["Something went wrong: error location A1"] 179 | end 180 | 181 | end 182 | 183 | get '/auth/:name/callback' do 184 | auth = request.env["omniauth.auth"] 185 | 186 | username = auth["info"]["username"] 187 | private_token = auth["extra"]["raw_info"]["private_token"] 188 | userID = auth["uid"] 189 | 190 | session["current_user"] = {"username" => username, "user_id" => userID, "private_token" => private_token} 191 | 192 | redirect '/' 193 | end 194 | 195 | # any of the following routes should work to sign the user in: 196 | # /sign_up, /signup, /sign_in, /signin, /log_in, /login 197 | # TODO make only a single signin url 198 | ["/sign_in/?", "/signin/?", "/log_in/?", "/login/?", "/sign_up/?", "/signup/?"].each do |path| 199 | get path do 200 | redirect '/auth/gitlab' 201 | end 202 | end 203 | 204 | # either /log_out, /logout, /sign_out, or /signout will end the session and log the user out 205 | # TODO make only a single signout url 206 | ["/sign_out/?", "/signout/?", "/log_out/?", "/logout/?"].each do |path| 207 | get path do 208 | session["current_user"] = nil 209 | flash[:success] = ["You were Logged out"] 210 | redirect '/' 211 | end 212 | end -------------------------------------------------------------------------------- /app/config.ru: -------------------------------------------------------------------------------- 1 | 2 | require './app' 3 | 4 | use Rack::Static, :urls => ["/css", "/img", "/js", "/images"], :root => "public" 5 | 6 | run Sinatra::Application -------------------------------------------------------------------------------- /app/gitlab_downloader.rb: -------------------------------------------------------------------------------- 1 | require 'gitlab' 2 | # require 'mongo' 3 | require_relative 'issues' 4 | require_relative 'milestones' 5 | require_relative 'mongo_connection' 6 | # require 'awesome_print' 7 | # require 'secure_random' 8 | # require 'time' 9 | 10 | class GitLab_Downloader 11 | 12 | def initialize(gitlabURL, private_token) 13 | @glClient = Gitlab.client(endpoint: gitlabURL, private_token: private_token) 14 | end 15 | 16 | # Exposes the GitLab Client for easier access from the object 17 | def glClient 18 | @glClient 19 | end 20 | 21 | # Get user pojects 22 | def user_projects 23 | p = @glClient.projects({ "per_page" => 100 }) 24 | p.each do |x| 25 | x = x.to_h 26 | end 27 | return p 28 | end 29 | 30 | 31 | def add_admin_records 32 | creationTime = Time.now 33 | creationUser = @glClient.user.to_h 34 | 35 | # TODO use a variable from when this class is inilized to get the URL. 36 | # TODO make the enpoint that is displaed to the user more "Friendly" aka drop /api/v3. 37 | gitlabEndpoint = ENV["GITLAB_ENDPOINT"] + (ENV['ENDPOINT_API_ADDRESS'] || "/api/v3") 38 | downloadID = SecureRandom.uuid 39 | 40 | return adminRecords = { "download_timestamp" => creationTime, 41 | "downloaded_by_user" => creationUser["username"], 42 | "downloaded_by_user_id" => creationUser["id"], 43 | "gitlab_endpoint" => gitlabEndpoint, 44 | "download_id" => downloadID 45 | } 46 | end 47 | 48 | def downloadIssuesAndComments(projectID) 49 | projectInfo = @glClient.project(projectID) 50 | projectInfo = projectInfo.to_h 51 | 52 | admin_info = add_admin_records 53 | 54 | issuePageNum = 1 55 | issues = @glClient.issues(projectID, :per_page=>100, :page=>issuePageNum) 56 | issues2 = [] 57 | while issues.length > 0 do 58 | 59 | # Iterates through each issue and get the notes and merges the notes into the issue hash. 60 | issues.each do |x| 61 | x = x.to_h # Converts the ObjectifiedHash that is returned by GitLab into a Ruby Hash 62 | 63 | x["created_at"] = DateTime.strptime(x["created_at"], '%Y-%m-%dT%H:%M:%S.%L%z').to_time.utc 64 | x["updated_at"] = DateTime.strptime(x["updated_at"], '%Y-%m-%dT%H:%M:%S.%L%z').to_time.utc 65 | 66 | commentPageNum = 1 67 | issueComments = @glClient.issue_notes(x["project_id"], x["id"], :per_page=>100, :page=>commentPageNum) # Gets the notes for the current issue 68 | 69 | 70 | if issueComments.length > 0 # If there are notes in the issue then... 71 | comments2 = [] # Array used to hold comments with time tracking information. 72 | 73 | 74 | while issueComments.length > 0 do # Loop used to control api pagination 75 | issueComments.each do |y| # For Each Comment... 76 | y = y.to_h # Convert from ObjectifiableHash to Ruby Hash 77 | 78 | y["created_at"] = DateTime.strptime(y["created_at"], '%Y-%m-%dT%H:%M:%S.%L%z').to_time.utc 79 | 80 | # Parses each comment for Time Tracking Information. Then merges back into the Comment Hash 81 | timeTrack = Gl_Issue.process_comment(y) 82 | if timeTrack != nil # If Time Tracking data was found for the Comment then.... 83 | y = y.merge(timeTrack) # Merge time tracking hash into Comment Hash 84 | 85 | comments2 << y # Add the merged hash into the Comment2 Array which holds comments that have time tracking information 86 | end 87 | end 88 | 89 | # Part of the pagination process to go through as many pages of comments that may exist for the current issue 90 | commentPageNum += 1 91 | issueComments = @glClient.issue_notes(x["project_id"], x["id"], :per_page=>100, :page=>commentPageNum) # Gets the notes for the current issue 92 | end 93 | 94 | # if there are comments with time tracking information then.... 95 | if comments2.length > 0 96 | # If there is milestone data then.... 97 | if x["milestone"] != nil 98 | x["milestone"]["created_at"] = DateTime.strptime(x["milestone"]["created_at"], '%Y-%m-%dT%H:%M:%S.%L%z').to_time.utc 99 | x["milestone"]["updated_at"] = DateTime.strptime(x["milestone"]["updated_at"], '%Y-%m-%dT%H:%M:%S.%L%z').to_time.utc 100 | 101 | if x["milestone"]["due_date"] != nil 102 | x["milestone"]["due_date"] = DateTime.strptime(x["milestone"]["due_date"], '%Y-%m-%d').to_time.utc 103 | end 104 | 105 | 106 | milestoneBudgetTrack = Gl_Milestone.process_milestone(x["milestone"]) # Process the milestone data for budget tracking information. 107 | if milestoneBudgetTrack != nil 108 | x["milestone"]["milestone_budget_data"] = milestoneBudgetTrack 109 | end 110 | end 111 | 112 | x["project_info"] = projectInfo 113 | 114 | x["admin_info"] = admin_info 115 | x["comments"] = comments2 # Merges the comments/notes into the main Issues Hash for each issue 116 | 117 | issues2 << x 118 | end 119 | end 120 | end 121 | 122 | issuePageNum += 1 123 | issues = @glClient.issues(projectID, :per_page=>100, :page=>issuePageNum) 124 | end 125 | return issues2 126 | end # End of Method 127 | end # End of Class 128 | 129 | 130 | # class Mongo_Connection 131 | 132 | # include Mongo 133 | 134 | # def initialize(url, port, dbName, collName) 135 | # # MongoDB Database Connect 136 | # @client = MongoClient.new(url, port) 137 | 138 | # @db = @client[dbName] 139 | 140 | # @collTimeTracking = @db[collName] 141 | # end 142 | 143 | # def clear_mongo_collections 144 | # @collTimeTracking.remove 145 | # end 146 | 147 | # def putIntoMongoCollTimeTrackingCommits(mongoPayload) 148 | # @collTimeTracking.insert(mongoPayload) 149 | # end 150 | 151 | 152 | # end 153 | 154 | # Testing Code 155 | # m = Mongo_Connection.new("localhost", 27017, "GitLab-TimeTracking", "TimeTrackingCommits") 156 | # m.clear_mongo_collections 157 | 158 | # g = GitLab_Downloader.new("https://gitlab.com/api/v3", "GITLAB-TOKEN") 159 | # dog = g.downloadIssuesAndComments(153287) 160 | # dog = g.glClient.issue_notes(153287, 162495, :per_page=>1) 161 | # response = http_response_for(dog) 162 | # ap dog 163 | # issuesWithComments = g.downloadIssuesAndComments 164 | # m.putIntoMongoCollTimeTrackingCommits(issuesWithComments) -------------------------------------------------------------------------------- /app/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 | # Is it a budget comment? Returns True or False 44 | def self.budget_comment?(commentBody) 45 | acceptedBudgetEmoji = Accepted_Time_Tracking_Emoji.accepted_milestone_budget_emoji 46 | 47 | acceptedBudgetEmoji.any? { |w| commentBody =~ /\A#{w}/ } 48 | end 49 | 50 | # Is the time Non-Billable? Returns True or False 51 | def self.non_billable?(commentBody) 52 | acceptedNonBilliableEmoji = Accepted_Time_Tracking_Emoji.accepted_nonBillable_emoji 53 | return acceptedNonBilliableEmoji.any? { |b| commentBody =~ /#{b}/ } 54 | end 55 | 56 | # Is it a time comment? Returns True or False 57 | def self.time_comment?(commentBody) 58 | acceptedClockEmoji = Accepted_Time_Tracking_Emoji.accepted_time_comment_emoji 59 | 60 | acceptedClockEmoji.any? { |w| commentBody =~ /\A#{w}/ } 61 | end 62 | 63 | 64 | # TODO Rebuild for GitLab 65 | # Gets the milestone ID number assigned to the issue 66 | def self.get_issue_milestone_number(milestoneDetails) 67 | if milestoneDetails != nil 68 | return milestoneDetails.attrs[:number] 69 | end 70 | end 71 | 72 | 73 | # Not sure what this is used for - TODO figure out why i wrote this 74 | def self.convertSecondsToDurationFormat(timeInSeconds, outputFormat) 75 | outputFormat = outputFormat.to_sym 76 | return ChronicDuration.output(timeInSeconds, :format => outputFormat, :keep_zero => true) 77 | end 78 | 79 | end -------------------------------------------------------------------------------- /app/issue_time.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers' 2 | 3 | module Gl_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["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["id"], 18 | "work_logged_by" => issueComment["author"]["username"], 19 | # "comment_created_date" => issueComment["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["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 -------------------------------------------------------------------------------- /app/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 | # require 'pp' 7 | 8 | module Gl_Issue 9 | 10 | def self.process_comment(issueComment) 11 | bodyField = issueComment["body"] 12 | 13 | commentsTime = [] 14 | 15 | # cycles through each comment and returns time tracking 16 | # issueComments.each do |x| 17 | # checks to see if there is a time comment in the body field 18 | isTimeComment = Helpers.time_comment?(bodyField) 19 | isBudgetComment = Helpers.budget_comment?(bodyField) 20 | 21 | if isTimeComment == true 22 | # if true, the body field is parsed for time comment details 23 | parsedTime = Gl_Issue_Time.process_issue_comment_for_time(issueComment) 24 | if parsedTime != nil 25 | # assuming results are returned from the parse (aka the parse was preceived 26 | # by the code to be sucessful, the parsed time comment details array is put into 27 | # the commentsTime array) 28 | return output = {"time_tracking_data" => parsedTime} 29 | else 30 | return output = nil 31 | end 32 | # Buget Handling 33 | # elsif isBudgetComment == true 34 | # parsedBudget = Gh_Issue_Budget.process_issue_comment_for_budget(x) 35 | # if parsedBudget != nil 36 | # commentsTime << parsedBudget 37 | # end 38 | end 39 | # end # do not delete this 'end'. it is part of issueComments do block 40 | 41 | # if commentsTime.empty? == false 42 | # return output = { 43 | # "time_tracking_commits" => commentsTime 44 | # } 45 | # elsif commentsTime.empty? == true 46 | # return output = { 47 | # "time_tracking_commits" => nil 48 | # } 49 | # end 50 | end 51 | end -------------------------------------------------------------------------------- /app/milestone_budget.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers' 2 | 3 | module Gl_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, "budget_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 -------------------------------------------------------------------------------- /app/milestones.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers' 2 | require_relative 'milestone_budget' 3 | 4 | module Gl_Milestone 5 | 6 | def self.process_milestone(milestoneDetail) 7 | 8 | milestoneDescription = milestoneDetail["description"] 9 | recordCreationDate = Time.now.utc 10 | 11 | # cycles through each comment and returns time tracking 12 | # checks to see if there is a time comment in the body field 13 | isBudgetComment = Helpers.budget_comment?(milestoneDescription) 14 | if isBudgetComment == true 15 | # if true, the body field is parsed for time comment details 16 | parsedBudget = Gl_Milestone_Budget.process_budget_description_for_time(milestoneDescription) 17 | 18 | if parsedBudget != nil 19 | 20 | # return output = {"milestone_budget_tracking_data" => parsedBudget} 21 | return output = parsedBudget 22 | else 23 | return output = nil 24 | end 25 | 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /app/mongo_connection.rb: -------------------------------------------------------------------------------- 1 | require 'mongo' 2 | 3 | class Mongo_Connection 4 | 5 | include Mongo 6 | 7 | def initialize(url, port, dbName, collName) 8 | # MongoDB Database Connect 9 | @client = MongoClient.new(url, port) 10 | 11 | @db = @client[dbName] 12 | 13 | @collTimeTracking = @db[collName] 14 | end 15 | 16 | def clear_mongo_collections 17 | @collTimeTracking.remove 18 | puts "MongoDB Collection has been Cleared" 19 | end 20 | 21 | def remove_mongo_records(downloadID) 22 | @collTimeTracking.remove( { "admin_info.download_id"=> downloadID } ) 23 | end 24 | 25 | def putIntoMongoCollTimeTrackingCommits(mongoPayload) 26 | @collTimeTracking.insert(mongoPayload) 27 | end 28 | 29 | def aggregate(input1) 30 | @collTimeTracking.aggregate(input1) 31 | end 32 | 33 | 34 | end -------------------------------------------------------------------------------- /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/GitLab-Time-Tracking/59ce1c127a90e66257b931d166291c541be554cd/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitLab-Time-Tracking/59ce1c127a90e66257b931d166291c541be554cd/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenOTT/GitLab-Time-Tracking/59ce1c127a90e66257b931d166291c541be554cd/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/views/analyze.erb: -------------------------------------------------------------------------------- 1 | 2 |

Analysis:

3 | 4 |

Project Info:

5 | 6 |

Project: <%= @snapshot_info["project_info"]["path_with_namespace"] %>

7 |

Snapshot Created at: <%= @snapshot_info["admin_info"]["download_timestamp"] %>

8 |

Snapshot Created By: <%= @snapshot_info["admin_info"]["downloaded_by_user"] %>

9 | 10 |
11 | 12 |

Time Log Breakdown:

13 | <% @milestones.each do |m| %> 14 |

Milestone Number: <%= m["milestone_number"] %>

15 |

State: <%= m["milestone_state"] %>

16 |

Title: <%= m["milestone_title"] %>

17 |

Budget: <%= budget = m["milestone_budget_duration"].to_i %>

18 |

Due Date: <%= m["milestone_due_date"] %>

19 |

Sum of Time Logs assoicated with Milestone(Spent Time): <%= timeSum = get_milestone_sums(m["download_id"], m["milestone_number"])["time_duration_sum"] %> (<%= (timeSum / budget.to_f * 100).round(2) %> % spent)

20 |

Count of Time Logs assoicated with Milestone: <%= timeCount = get_milestone_sums(m["download_id"], m["milestone_number"])["time_comment_count"] %>

21 |

Budget Left: <%= budget - timeSum %> (<%= ((budget - timeSum) / budget.to_f * 100).round(2) %>% left)

22 | 23 |

Issue breakdown:

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | <% get_issues_for_milestone(m["download_id"], m["milestone_number"]).each do |i| %> 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | <% end %> 41 |
Issue NumberIssue TitleIssue StateTime Duration SumTime Log Count
<%= i["issue_number"] %><%= i["issue_title"] %><%= i["issue_state"] %><%= i["time_duration_sum"] %> (<%= (i["time_duration_sum"] / budget.to_f * 100).round(2)%>% of Budget)<%= i["time_comment_count"] %>
42 |
43 | <% end %> 44 | 45 | ">Download XSLX File: Includes Issues and Milestone Data 46 | 47 | 48 |
49 |

Milestone Stats Analysis:

50 |

User Stats Analysis:

51 |

Calendar/Dates Stats Analysis:

52 |

Workload Stats Analysis:

-------------------------------------------------------------------------------- /app/views/index.erb: -------------------------------------------------------------------------------- 1 | 2 | <% if current_user != nil %> 3 | 4 |

Avaliable Snapshots:

5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <% @adminQueries.each do |aq| %> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% end %> 29 |
ProjectDownload DateDownloaded ByActionAction
<%= aq["project_name"] %><%= aq["download_date"] %><%= aq["downloaded_by"] %>">View Time Logs">Delete Snapshot
30 |
31 |
32 | 37 |
38 | 39 |
40 |

Create Snapshot of Time Tracking Data from your GitLab Instance

41 | 42 |
43 | 44 |
45 | 46 |
47 | 53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 |
62 |
63 | <% end %> -------------------------------------------------------------------------------- /app/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GitLab Time Tracking 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 |
18 | 19 | 51 | 52 |

Endpoint: <%= gitlab_endpoint("short") %>

53 | <% if current_user != nil %> 54 |

Logged In as: <%= current_user["username"] %>

55 | <% end %> 56 | 57 | 58 | <%if flash[:danger] %> 59 | <% flash[:danger].each do |text| %> 60 |
61 | 62 | <%= text %> 63 |
64 | <% end %> 65 | <% end %> 66 | <% if flash[:warning] %> 67 | <% flash[:warning].each do |text| %> 68 |
69 | 70 | <%= text %> 71 |
72 | <% end %> 73 | <% end %> 74 | 75 | <%if flash[:info] %> 76 | <% flash[:info].each do |text| %> 77 |
78 | 79 | <%= text %> 80 |
81 | <% end %> 82 | <% end %> 83 | <%if flash[:success] %> 84 | <% flash[:success].each do |text| %> 85 |
86 | 87 | <%= text %> 88 |
89 | <% end %> 90 | <% end %> 91 | 92 |
93 | <%= yield %> 94 |
95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /app/xlsx_exporter.rb: -------------------------------------------------------------------------------- 1 | # require 'mongo' 2 | 3 | # require 'awesome_print' 4 | # require_relative 'mongo_connection' 5 | 6 | require 'axlsx' 7 | 8 | class XLSXExporter 9 | def initialize(mongoConnection) 10 | @mongoConnection = mongoConnection 11 | end 12 | 13 | # include Mongo 14 | 15 | # def initialize(mongoConnection) 16 | # # MongoDB Database Connect 17 | # @client = MongoClient.new(url, port) 18 | 19 | # @db = @client[dbName] 20 | 21 | # @collTimeTracking = @db[collName] 22 | # end 23 | 24 | # def aggregate(input1) 25 | 26 | # @collTimeTracking.aggregate(input1) 27 | 28 | # end 29 | 30 | def generateXLSX(issuesData, milestoneData) 31 | 32 | Axlsx::Package.new do |p| 33 | p.workbook.add_worksheet(:name => "Issues") do |sheet| 34 | sheet.add_row issuesData.first.keys 35 | 36 | issuesData.each do |hash| 37 | sheet.add_row hash.values 38 | end 39 | 40 | end 41 | p.workbook.add_worksheet(:name => "Milestones") do |sheet| 42 | if milestoneData.empty? == false 43 | sheet.add_row milestoneData.first.keys 44 | 45 | milestoneData.each do |hash| 46 | sheet.add_row hash.values 47 | end 48 | 49 | elsif milestoneData.empty? == true 50 | sheet.add_row ["No Milestone Data"] 51 | end 52 | end 53 | 54 | return p.to_stream 55 | end 56 | end 57 | 58 | def get_all_issues_time(downloadID) 59 | # TODO add filtering and extra security around query 60 | totalIssueSpentHoursBreakdown = @mongoConnection.aggregate([ 61 | 62 | { "$unwind" => "$comments"}, 63 | {"$project" => {_id: 0, 64 | download_id: "$admin_info.download_id", 65 | project_id: 1, 66 | id: 1, 67 | iid: 1, 68 | type: { "$ifNull" => [ "$comments.time_tracking_data.type", "n/a" ] }, 69 | milestone_number: { "$ifNull" => [ "$milestone.iid", "n/a" ] }, 70 | milestone_title: { "$ifNull" => [ "$milestone.title", "n/a" ] }, 71 | milestone_budget_comment: { "$ifNull" => [ "$milestone.milestone_budget_data.budget_comment", "n/a" ] }, 72 | milestone_state: { "$ifNull" => [ "$milestone.state", "n/a" ] }, 73 | milestone_due_date: { "$ifNull" => [ "$milestone.due_date", "n/a" ] }, 74 | issue_title: "$title", 75 | state: 1, 76 | issue_author: "$author.username", 77 | comment_id: "$comments.id", 78 | time_track_duration: "$comments.time_tracking_data.duration", 79 | time_track_non_billable: "$comments.time_tracking_data.non_billable", 80 | time_track_work_date: "$comments.time_tracking_data.work_date", 81 | time_track_time_comment: "$comments.time_tracking_data.time_comment", 82 | time_track_work_date_provided: "$comments.time_tracking_data.work_date_provided", 83 | time_track_work_logged_by: "$comments.time_tracking_data.work_logged_by"}}, 84 | 85 | { "$match" => {download_id: downloadID}}, 86 | 87 | # { "$unwind" => "$comments.time_tracking_data" }, 88 | 89 | 90 | # { "$match" => { "comments.time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 91 | # { "$group" => { _id: { 92 | # project_id: "$project_id", 93 | # id: "$id", 94 | # iid: "$iid", 95 | # title: "$title", 96 | # state: "$state", 97 | # issue_author: "$author.username", 98 | # comment_id: "$comment.id", 99 | # comment_author: "$comment.author.username", 100 | # time_track_duration: "$comment.time_tracking_data.duration", 101 | # time_track_non_billable: "$comment.time_tracking_data.non_billable", 102 | # time_track_work_date: "$comment.time_tracking_data.work_date", 103 | # time_track_time_comment: "$comment.time_tracking_data.time_comment", 104 | # }, 105 | 106 | # }} 107 | ]) 108 | # output = [] 109 | # totalIssueSpentHoursBreakdown.each do |x| 110 | # x["_id"]["time_duration_sum"] = x["time_duration_sum"] 111 | # x["_id"]["time_comment_count"] = x["time_comment_count"] 112 | # output << x["_id"] 113 | # end 114 | # return output 115 | end 116 | 117 | def get_all_milestone_budgets(downloadID) 118 | # TODO add filtering and extra security around query 119 | totalMileStoneBudgetHoursBreakdown = @mongoConnection.aggregate([ 120 | 121 | { "$match" => { milestone: { "$ne" => nil} }}, 122 | # { "$unwind" => "$comments" }, 123 | {"$project" => {_id: 0, 124 | download_id: "$admin_info.download_id", 125 | project_id: 1, 126 | id: 1, 127 | iid: 1, 128 | type: "$milestone.milestone_budget_data.type", 129 | milestone_number: "$milestone.iid", 130 | milestone_title: "$milestone.title", 131 | milestone_budget_comment: "$milestone.milestone_budget_data.budget_comment", 132 | milestone_state: "$milestone.state", 133 | milestone_due_date: "$milestone.due_date", 134 | milestone_budget_duration: "$milestone.milestone_budget_data.duration", 135 | }}, 136 | { "$match" => {download_id: downloadID}}, 137 | { "$group" => {_id: { 138 | download_id: "$download_id", 139 | type: "$type", 140 | milestone_number: "$milestone_number", 141 | project_id: "$project_id", 142 | id: "$id", 143 | iid: "$iid", 144 | milestone_title: "$milestone_title", 145 | milestone_budget_comment: "$milestone_budget_comment", 146 | milestone_state: "$milestone_state", 147 | milestone_due_date: "$milestone_due_date", 148 | milestone_budget_duration: "$milestone_budget_duration", 149 | }}} 150 | 151 | # { "$unwind" => "$comments.time_tracking_data" }, 152 | 153 | 154 | # { "$match" => { "comments.time_tracking_commits.type" => { "$in" => ["Issue Time"] }}}, 155 | # { "$group" => { _id: { 156 | # project_id: "$project_id", 157 | # id: "$id", 158 | # iid: "$iid", 159 | # title: "$title", 160 | # state: "$state", 161 | # issue_author: "$author.username", 162 | # comment_id: "$comment.id", 163 | # comment_author: "$comment.author.username", 164 | # time_track_duration: "$comment.time_tracking_data.duration", 165 | # time_track_non_billable: "$comment.time_tracking_data.non_billable", 166 | # time_track_work_date: "$comment.time_tracking_data.work_date", 167 | # time_track_time_comment: "$comment.time_tracking_data.time_comment", 168 | # }, 169 | 170 | # }} 171 | ]) 172 | output = [] 173 | totalMileStoneBudgetHoursBreakdown.each do |x| 174 | output << x["_id"] 175 | end 176 | return output 177 | end 178 | 179 | 180 | end 181 | 182 | 183 | # Testing Code 184 | 185 | # m = Mongo_Connection.new("localhost", 27017, "GitLab", "Issues_Time_Tracking") 186 | # export = XLSXExporter.new(m) 187 | 188 | 189 | # ap export.get_all_milestone_budgets("ddaed040-7c1a-4829-a07d-1d8608469ef4") 190 | # ap export.get_all_issues_time("ddaed040-7c1a-4829-a07d-1d8608469ef4") 191 | 192 | --------------------------------------------------------------------------------