├── Business
└── Screenshots
│ ├── TimeLog.png
│ └── ProjectInfo_and_TimeLog.png
├── app
├── config.ru
├── public
│ └── vendor
│ │ └── bootstrap
│ │ ├── fonts
│ │ ├── glyphicons-halflings-regular.eot
│ │ ├── glyphicons-halflings-regular.ttf
│ │ ├── glyphicons-halflings-regular.woff
│ │ └── glyphicons-halflings-regular.svg
│ │ ├── css
│ │ ├── bootstrap-theme.min.css
│ │ └── bootstrap-theme.css
│ │ └── js
│ │ └── bootstrap.min.js
├── Gemfile
├── mongo_connection.rb
├── accepted_emoji.rb
├── milestones.rb
├── aggregations
│ ├── admin_queries.rb
│ └── issue_stat_queries.rb
├── issues.rb
├── milestone_budget.rb
├── views
│ ├── index.erb
│ ├── analyze.erb
│ └── layout.erb
├── Gemfile.lock
├── helpers.rb
├── issue_time.rb
├── gitlab_downloader.rb
├── app.rb
└── xlsx_exporter.rb
├── .gitignore
└── README.md
/Business/Screenshots/TimeLog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitLab-Time-Tracking/HEAD/Business/Screenshots/TimeLog.png
--------------------------------------------------------------------------------
/app/config.ru:
--------------------------------------------------------------------------------
1 |
2 | require './app'
3 |
4 | use Rack::Static, :urls => ["/css", "/img", "/js", "/images"], :root => "public"
5 |
6 | run Sinatra::Application
--------------------------------------------------------------------------------
/Business/Screenshots/ProjectInfo_and_TimeLog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitLab-Time-Tracking/HEAD/Business/Screenshots/ProjectInfo_and_TimeLog.png
--------------------------------------------------------------------------------
/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitLab-Time-Tracking/HEAD/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenOTT/GitLab-Time-Tracking/HEAD/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/HEAD/app/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/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'
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/views/index.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if current_user != nil %>
3 |
4 |
Avaliable Snapshots:
5 |
6 |
7 |
38 |
39 |
40 | Create Snapshot of Time Tracking Data from your GitLab Instance
41 |
42 |
62 |
63 | <% end %>
--------------------------------------------------------------------------------
/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/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 | | Issue Number |
27 | Issue Title |
28 | Issue State |
29 | Time Duration Sum |
30 | Time Log Count |
31 |
32 | <% get_issues_for_milestone(m["download_id"], m["milestone_number"]).each do |i| %>
33 |
34 | | <%= i["issue_number"] %> |
35 | <%= i["issue_title"] %> |
36 | <%= i["issue_state"] %> |
37 | <%= i["time_duration_sum"] %> (<%= (i["time_duration_sum"] / budget.to_f * 100).round(2)%>% of Budget) |
38 | <%= i["time_comment_count"] %> |
39 |
40 | <% end %>
41 |
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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitLab-Time-Tracking
2 |
3 | [](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 | 
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 | 
123 | 
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/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/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/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/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('').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h'}),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/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------