Add a commit calendar to the user profile
This commit is contained in:
parent
8eb365c0a0
commit
792ced2f41
16 changed files with 374 additions and 10 deletions
|
@ -29,7 +29,7 @@ v 7.8.0
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
-
|
- Add a commit calendar to the user profile (Hannes Rosenögger)
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -154,6 +154,9 @@ gem "slack-notifier", "~> 1.0.0"
|
||||||
# d3
|
# d3
|
||||||
gem "d3_rails", "~> 3.1.4"
|
gem "d3_rails", "~> 3.1.4"
|
||||||
|
|
||||||
|
#cal-heatmap
|
||||||
|
gem "cal-heatmap-rails", "~> 0.0.1"
|
||||||
|
|
||||||
# underscore-rails
|
# underscore-rails
|
||||||
gem "underscore-rails", "~> 1.4.4"
|
gem "underscore-rails", "~> 1.4.4"
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ GEM
|
||||||
sass (~> 3.2)
|
sass (~> 3.2)
|
||||||
browser (0.7.2)
|
browser (0.7.2)
|
||||||
builder (3.2.2)
|
builder (3.2.2)
|
||||||
|
cal-heatmap-rails (0.0.1)
|
||||||
capybara (2.2.1)
|
capybara (2.2.1)
|
||||||
mime-types (>= 1.16)
|
mime-types (>= 1.16)
|
||||||
nokogiri (>= 1.3.3)
|
nokogiri (>= 1.3.3)
|
||||||
|
@ -627,6 +628,7 @@ DEPENDENCIES
|
||||||
binding_of_caller
|
binding_of_caller
|
||||||
bootstrap-sass (~> 3.0)
|
bootstrap-sass (~> 3.0)
|
||||||
browser
|
browser
|
||||||
|
cal-heatmap-rails (~> 0.0.1)
|
||||||
capybara (~> 2.2.1)
|
capybara (~> 2.2.1)
|
||||||
carrierwave
|
carrierwave
|
||||||
coffee-rails
|
coffee-rails
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
#= require shortcuts_dashboard_navigation
|
#= require shortcuts_dashboard_navigation
|
||||||
#= require shortcuts_issueable
|
#= require shortcuts_issueable
|
||||||
#= require shortcuts_network
|
#= require shortcuts_network
|
||||||
|
#= require cal-heatmap
|
||||||
#= require_tree .
|
#= require_tree .
|
||||||
|
|
||||||
window.slugify = (text) ->
|
window.slugify = (text) ->
|
||||||
|
|
71
app/assets/javascripts/calendar.js.coffee
Normal file
71
app/assets/javascripts/calendar.js.coffee
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
class @calendar
|
||||||
|
options =
|
||||||
|
month: "short"
|
||||||
|
day: "numeric"
|
||||||
|
year: "numeric"
|
||||||
|
|
||||||
|
constructor: (timestamps,starting_year,starting_month,activities_path) ->
|
||||||
|
cal = new CalHeatMap()
|
||||||
|
cal.init
|
||||||
|
itemName: ["commit"]
|
||||||
|
data: timestamps
|
||||||
|
start: new Date(starting_year, starting_month)
|
||||||
|
domainLabelFormat: "%b"
|
||||||
|
id: "cal-heatmap"
|
||||||
|
domain: "month"
|
||||||
|
subDomain: "day"
|
||||||
|
range: 12
|
||||||
|
tooltip: true
|
||||||
|
domainDynamicDimension: false
|
||||||
|
colLimit: 4
|
||||||
|
label:
|
||||||
|
position: "top"
|
||||||
|
domainMargin: 1
|
||||||
|
legend: [
|
||||||
|
0
|
||||||
|
1
|
||||||
|
4
|
||||||
|
7
|
||||||
|
]
|
||||||
|
legendCellPadding: 3
|
||||||
|
onClick: (date, count) ->
|
||||||
|
$.ajax
|
||||||
|
url: activities_path
|
||||||
|
data:
|
||||||
|
date: date
|
||||||
|
|
||||||
|
dataType: "json"
|
||||||
|
success: (data) ->
|
||||||
|
$("#loading_commits").fadeIn()
|
||||||
|
calendar.calendarOnClick data, date, count
|
||||||
|
setTimeout (->
|
||||||
|
$("#calendar_onclick_placeholder").fadeIn 500
|
||||||
|
return
|
||||||
|
), 400
|
||||||
|
setTimeout (->
|
||||||
|
$("#loading_commits").hide()
|
||||||
|
return
|
||||||
|
), 400
|
||||||
|
return
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
|
@calendarOnClick: (data, date, nb)->
|
||||||
|
$("#calendar_onclick_placeholder").hide()
|
||||||
|
$("#calendar_onclick_placeholder").html ->
|
||||||
|
"<span class='calendar_onclick_second'><b>" +
|
||||||
|
((if nb is null then "no" else nb)) +
|
||||||
|
"</b><span class='calendar_commit_date'> commit" +
|
||||||
|
((if (nb isnt 1) then "s" else "")) + " " +
|
||||||
|
date.toLocaleDateString("en-US", options) +
|
||||||
|
"</span><hr class='calendar_onclick_hr'></span>"
|
||||||
|
$.each data, (key, data) ->
|
||||||
|
$.each data, (index, data) ->
|
||||||
|
$("#calendar_onclick_placeholder").append ->
|
||||||
|
"Pushed <b>" + ((if data is null then "no" else data)) + " commit" +
|
||||||
|
((if (data isnt 1) then "s" else "")) +
|
||||||
|
"</b> to <a href='/" + index + "'>" +
|
||||||
|
index + "</a><hr class='calendar_onclick_hr'>"
|
||||||
|
return
|
||||||
|
return
|
||||||
|
return
|
|
@ -8,6 +8,7 @@
|
||||||
*= require select2
|
*= require select2
|
||||||
*= require_self
|
*= require_self
|
||||||
*= require dropzone/basic
|
*= require dropzone/basic
|
||||||
|
*= require cal-heatmap
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import "main/*";
|
@import "main/*";
|
||||||
|
|
95
app/assets/stylesheets/generic/calendar.scss
Normal file
95
app/assets/stylesheets/generic/calendar.scss
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
.calendar_onclick_placeholder {
|
||||||
|
padding: 0 0 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar_commit_activity {
|
||||||
|
padding: 5px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar_onclick_second {
|
||||||
|
font-size: 14px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar_onclick_hr {
|
||||||
|
padding: 0;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar_commit_date {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar_activity_summary {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This overwrites the default values of the cal-heatmap gem
|
||||||
|
*/
|
||||||
|
.calendar {
|
||||||
|
.qi {
|
||||||
|
background-color: #999;
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q1 {
|
||||||
|
background-color: #dae289;
|
||||||
|
fill: #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q2 {
|
||||||
|
background-color: #cedb9c;
|
||||||
|
fill: #ACD5F2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q3 {
|
||||||
|
background-color: #b5cf6b;
|
||||||
|
fill: #7FA8D1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q4 {
|
||||||
|
background-color: #637939;
|
||||||
|
fill: #49729B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q5 {
|
||||||
|
background-color: #3b6427;
|
||||||
|
fill: #254E77;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-background {
|
||||||
|
fill: none;
|
||||||
|
shape-rendering: crispedges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ch-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
margin-top: 22px;
|
||||||
|
margin-left: 1px;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 3px;
|
||||||
|
font-weight: 550;
|
||||||
|
background-color: #222;
|
||||||
|
span {
|
||||||
|
position: absolute;
|
||||||
|
width: 200px;
|
||||||
|
text-align: center;
|
||||||
|
visibility: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -8px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 8px solid #000000;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
skip_before_filter :authenticate_user!, only: [:show]
|
skip_before_filter :authenticate_user!, only: [:show, :activities]
|
||||||
layout :determine_layout
|
layout :determine_layout
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -10,7 +10,8 @@ class UsersController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
# Projects user can view
|
# Projects user can view
|
||||||
authorized_projects_ids = ProjectsFinder.new.execute(current_user).pluck(:id)
|
visible_projects = ProjectsFinder.new.execute(current_user)
|
||||||
|
authorized_projects_ids = visible_projects.pluck(:id)
|
||||||
|
|
||||||
@projects = @user.personal_projects.
|
@projects = @user.personal_projects.
|
||||||
where(id: authorized_projects_ids)
|
where(id: authorized_projects_ids)
|
||||||
|
@ -24,12 +25,32 @@ class UsersController < ApplicationController
|
||||||
|
|
||||||
@title = @user.name
|
@title = @user.name
|
||||||
|
|
||||||
|
user_repositories = visible_projects.map(&:repository)
|
||||||
|
@timestamps = Gitlab::CommitsCalendar.create_timestamp(user_repositories,
|
||||||
|
@user, false)
|
||||||
|
@starting_year = Gitlab::CommitsCalendar.starting_year(@timestamps)
|
||||||
|
@starting_month = Gitlab::CommitsCalendar.starting_month(@timestamps)
|
||||||
|
@last_commit_date = Gitlab::CommitsCalendar.last_commit_date(@timestamps)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
format.atom { render layout: false }
|
format.atom { render layout: false }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def activities
|
||||||
|
user = User.find_by_username!(params[:username])
|
||||||
|
# Projects user can view
|
||||||
|
visible_projects = ProjectsFinder.new.execute(current_user)
|
||||||
|
|
||||||
|
user_repositories = visible_projects.map(&:repository)
|
||||||
|
user_activities = Gitlab::CommitsCalendar.create_timestamp(user_repositories,
|
||||||
|
user, true)
|
||||||
|
user_activities = Gitlab::CommitsCalendar.commit_activity_match(
|
||||||
|
user_activities, params[:date])
|
||||||
|
render json: user_activities.to_json
|
||||||
|
end
|
||||||
|
|
||||||
def determine_layout
|
def determine_layout
|
||||||
if current_user
|
if current_user
|
||||||
'navless'
|
'navless'
|
||||||
|
|
|
@ -139,21 +139,46 @@ class Repository
|
||||||
|
|
||||||
def graph_log
|
def graph_log
|
||||||
Rails.cache.fetch(cache_key(:graph_log)) do
|
Rails.cache.fetch(cache_key(:graph_log)) do
|
||||||
commits = raw_repository.log(limit: 6000, skip_merges: true,
|
|
||||||
ref: root_ref)
|
|
||||||
commits.map do |rugged_commit|
|
|
||||||
commit = Gitlab::Git::Commit.new(rugged_commit)
|
|
||||||
|
|
||||||
|
# handle empty repos that don't have a root_ref set yet
|
||||||
|
unless raw_repository.root_ref.present?
|
||||||
|
raw_repository.root_ref = 'refs/heads/master'
|
||||||
|
end
|
||||||
|
|
||||||
|
commits = raw_repository.log(limit: 6000, skip_merges: true,
|
||||||
|
ref: raw_repository.root_ref)
|
||||||
|
|
||||||
|
commits.map do |rugged_commit|
|
||||||
|
|
||||||
|
commit = Gitlab::Git::Commit.new(rugged_commit)
|
||||||
{
|
{
|
||||||
author_name: commit.author_name.force_encoding('UTF-8'),
|
author_name: commit.author_name.force_encoding('UTF-8'),
|
||||||
author_email: commit.author_email.force_encoding('UTF-8'),
|
author_email: commit.author_email.force_encoding('UTF-8'),
|
||||||
additions: commit.stats.additions,
|
additions: commit.stats.additions,
|
||||||
deletions: commit.stats.deletions
|
deletions: commit.stats.deletions,
|
||||||
|
date: commit.committed_date
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def graph_logs_by_user_email(user)
|
||||||
|
graph_log.select { |u_email| u_email[:author_email] == user.email }
|
||||||
|
end
|
||||||
|
|
||||||
|
def timestamps_by_user_from_graph_log(user)
|
||||||
|
graph_logs_by_user_email(user).map { |graph_log| graph_log[:date].to_time.to_i }
|
||||||
|
end
|
||||||
|
|
||||||
|
def commits_log_of_user_by_date(user)
|
||||||
|
timestamps_by_user_from_graph_log(user).
|
||||||
|
group_by { |commit_date| commit_date }.
|
||||||
|
inject({}) do |hash, (timestamp_date, commits)|
|
||||||
|
hash[timestamp_date] = commits.count
|
||||||
|
hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def cache_key(type)
|
def cache_key(type)
|
||||||
"#{type}:#{path_with_namespace}"
|
"#{type}:#{path_with_namespace}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,4 +12,3 @@
|
||||||
= render "events/event/note", event: event
|
= render "events/event/note", event: event
|
||||||
- else
|
- else
|
||||||
= render "events/event/common", event: event
|
= render "events/event/common", event: event
|
||||||
|
|
||||||
|
|
9
app/views/users/_calendar.html.haml
Normal file
9
app/views/users/_calendar.html.haml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#cal-heatmap.calendar
|
||||||
|
:javascript
|
||||||
|
new calendar(
|
||||||
|
#{@timestamps.to_json},
|
||||||
|
#{@starting_year},
|
||||||
|
#{@starting_month},
|
||||||
|
'#{user_activities_path}'
|
||||||
|
);
|
||||||
|
= render "calendar_onclick"
|
25
app/views/users/_calendar_onclick.html.haml
Normal file
25
app/views/users/_calendar_onclick.html.haml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#calendar_commit_activity.calendar_commit_activity
|
||||||
|
%h4.activity_title Commit Activity:
|
||||||
|
|
||||||
|
#loading_commits
|
||||||
|
%section.text-center
|
||||||
|
%h3
|
||||||
|
%i.icon-spinner.icon-spin
|
||||||
|
|
||||||
|
#calendar_onclick_placeholder.calendar_onclick_placeholder
|
||||||
|
%span.calendar_onclick_second.calendar_onclick_second
|
||||||
|
- if @timestamps.empty?
|
||||||
|
%span.calendar_activity_summary
|
||||||
|
%strong> #{@user.username}
|
||||||
|
has no activity
|
||||||
|
- else
|
||||||
|
%span.calendar_activity_summary
|
||||||
|
%strong> #{@user.username}
|
||||||
|
's last commit was on
|
||||||
|
%span.commit_date #{@last_commit_date}
|
||||||
|
|
||||||
|
%hr.calendar_onclick_hr
|
||||||
|
|
||||||
|
:javascript
|
||||||
|
$("#loading_commits").hide();
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
%h4 Groups:
|
%h4 Groups:
|
||||||
= render 'groups', groups: @groups
|
= render 'groups', groups: @groups
|
||||||
%hr
|
%hr
|
||||||
|
%h4 Calendar:
|
||||||
|
= render 'calendar'
|
||||||
%h4
|
%h4
|
||||||
User Activity:
|
User Activity:
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,10 @@ Gitlab::Application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# route for commits used by the cal-heatmap
|
||||||
|
get 'u/:username/activities' => 'users#activities', as: :user_activities,
|
||||||
|
constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
|
||||||
|
via: :get
|
||||||
get '/u/:username' => 'users#show', as: :user,
|
get '/u/:username' => 'users#show', as: :user,
|
||||||
constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
|
constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
|
||||||
|
|
||||||
|
|
79
lib/gitlab/commits_calendar.rb
Normal file
79
lib/gitlab/commits_calendar.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
module Gitlab
|
||||||
|
class CommitsCalendar
|
||||||
|
def self.create_timestamp(repositories, user, show_activity)
|
||||||
|
timestamps = {}
|
||||||
|
repositories.each do |raw_repository|
|
||||||
|
if raw_repository.exists?
|
||||||
|
commits_log = raw_repository.commits_log_of_user_by_date(user)
|
||||||
|
|
||||||
|
populated_timestamps =
|
||||||
|
if show_activity
|
||||||
|
populate_timestamps_by_project(
|
||||||
|
commits_log,
|
||||||
|
timestamps,
|
||||||
|
raw_repository
|
||||||
|
)
|
||||||
|
else
|
||||||
|
populate_timestamps(commits_log, timestamps)
|
||||||
|
end
|
||||||
|
timestamps.merge!(populated_timestamps)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.populate_timestamps(commits_log, timestamps)
|
||||||
|
commits_log.each do |timestamp_date, commits_count|
|
||||||
|
hash = { "#{timestamp_date}" => commits_count }
|
||||||
|
if timestamps.has_key?("#{timestamp_date}")
|
||||||
|
timestamps.merge!(hash) do |timestamp_date, commits_count,
|
||||||
|
new_commits_count| commits_count = commits_count.to_i +
|
||||||
|
new_commits_count
|
||||||
|
end
|
||||||
|
else
|
||||||
|
timestamps.merge!(hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.populate_timestamps_by_project(commits_log, timestamps,
|
||||||
|
project)
|
||||||
|
commits_log.each do |timestamp_date, commits_count|
|
||||||
|
if timestamps.has_key?("#{timestamp_date}")
|
||||||
|
timestamps["#{timestamp_date}"].
|
||||||
|
merge!(project.path_with_namespace => commits_count)
|
||||||
|
else
|
||||||
|
hash = { "#{timestamp_date}" => { project.path_with_namespace =>
|
||||||
|
commits_count } }
|
||||||
|
timestamps.merge!(hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.latest_commit_date(timestamps)
|
||||||
|
if timestamps.nil? || timestamps.empty?
|
||||||
|
DateTime.now.to_date
|
||||||
|
else
|
||||||
|
Time.at(timestamps.keys.first.to_i).to_date
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.starting_year(timestamps)
|
||||||
|
DateTime.now.to_date - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.starting_month(timestamps)
|
||||||
|
Date.today.strftime("%m").to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.last_commit_date(timestamps)
|
||||||
|
latest_commit_date(timestamps).to_formatted_s(:long).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.commit_activity_match(user_activities, date)
|
||||||
|
user_activities.select { |x| Time.at(x.to_i) == Time.parse(date) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
spec/controllers/users_controller_spec.rb
Normal file
27
spec/controllers/users_controller_spec.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe UsersController do
|
||||||
|
let(:user) { create(:user, username: "user1", name: "User 1", email: "user1@gitlab.com") }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET #show" do
|
||||||
|
render_views
|
||||||
|
before do
|
||||||
|
get :show, username: user.username
|
||||||
|
end
|
||||||
|
|
||||||
|
it "renders the show template" do
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response).to render_template("show")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "renders calendar" do
|
||||||
|
controller.prepend_view_path 'app/views/users'
|
||||||
|
expect(response).to render_template("_calendar")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
Loading…
Reference in a new issue