Commit fd23cb31 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '335343-contributor-timezone-on-contribution-calendar' into 'master'

Shift the user's contribution calendar based on their timezone setting

See merge request gitlab-org/gitlab!72938
parents 127e49ed 45f0d6aa
...@@ -5,6 +5,7 @@ import { last } from 'lodash'; ...@@ -5,6 +5,7 @@ import { last } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility'; import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import { formatDate } from '~/lib/utils/datetime/date_format_utility';
import { n__, s__, __ } from '~/locale'; import { n__, s__, __ } from '~/locale';
const d3 = { select }; const d3 = { select };
...@@ -294,7 +295,15 @@ export default class ActivityCalendar { ...@@ -294,7 +295,15 @@ export default class ActivityCalendar {
}, },
responseType: 'text', responseType: 'text',
}) })
.then(({ data }) => $(this.activitiesContainer).html(data)) .then(({ data }) => {
$(this.activitiesContainer).html(data);
document
.querySelector(this.activitiesContainer)
.querySelectorAll('.js-localtime')
.forEach((el) => {
el.setAttribute('title', formatDate(el.getAttribute('data-datetime')));
});
})
.catch(() => .catch(() =>
createFlash({ createFlash({
message: __('An error occurred while retrieving calendar activity'), message: __('An error occurred while retrieving calendar activity'),
......
...@@ -32,10 +32,16 @@ module TimeZoneHelper ...@@ -32,10 +32,16 @@ module TimeZoneHelper
end end
end end
def local_time_instance(timezone)
return Time.zone if timezone.blank?
ActiveSupport::TimeZone.new(timezone) || Time.zone
end
def local_time(timezone) def local_time(timezone)
return if timezone.blank? return if timezone.blank?
time_zone_instance = ActiveSupport::TimeZone.new(timezone) || Time.zone time_zone_instance = local_time_instance(timezone)
time_zone_instance.now.strftime("%-l:%M %p") time_zone_instance.now.strftime("%-l:%M %p")
end end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.row.d-none.d-sm-flex .row.d-none.d-sm-flex
.col-12.calendar-block.gl-my-3 .col-12.calendar-block.gl-my-3
.user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } } .user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_time_instance(@user.timezone).now.utc_offset } }
.gl-spinner.gl-spinner-md.gl-my-8 .gl-spinner.gl-spinner-md.gl-my-8
.user-calendar-error.invisible .user-calendar-error.invisible
= _('There was an error loading users activity calendar.') = _('There was an error loading users activity calendar.')
......
...@@ -5,9 +5,9 @@ ...@@ -5,9 +5,9 @@
%ul.bordered-list %ul.bordered-list
- @events.sort_by(&:created_at).each do |event| - @events.sort_by(&:created_at).each do |event|
%li %li
%span.light %span.light.js-localtime{ :data => { :datetime => event.created_at.utc.strftime('%Y-%m-%dT%H:%M:%SZ'), :toggle => 'tooltip', :placement => 'top' } }
= sprite_icon('clock', css_class: 'gl-vertical-align-text-bottom') = sprite_icon('clock', css_class: 'gl-vertical-align-text-bottom')
= event.created_at.to_time.in_time_zone.strftime('%-I:%M%P') = event.created_at.to_time.in_time_zone(@user.timezone).strftime('%-I:%M%P')
- if event.visible_to_user?(current_user) - if event.visible_to_user?(current_user)
- if event.push_action? - if event.push_action?
#{event.action_name} #{event.ref_type} #{event.action_name} #{event.ref_type}
......
...@@ -2,12 +2,15 @@ ...@@ -2,12 +2,15 @@
module Gitlab module Gitlab
class ContributionsCalendar class ContributionsCalendar
include TimeZoneHelper
attr_reader :contributor attr_reader :contributor
attr_reader :current_user attr_reader :current_user
attr_reader :projects attr_reader :projects
def initialize(contributor, current_user = nil) def initialize(contributor, current_user = nil)
@contributor = contributor @contributor = contributor
@contributor_time_instance = local_time_instance(contributor.timezone)
@current_user = current_user @current_user = current_user
@projects = if @contributor.include_private_contributions? @projects = if @contributor.include_private_contributions?
ContributedProjectsFinder.new(@contributor).execute(@contributor) ContributedProjectsFinder.new(@contributor).execute(@contributor)
...@@ -22,7 +25,7 @@ module Gitlab ...@@ -22,7 +25,7 @@ module Gitlab
# Can't use Event.contributions here because we need to check 3 different # Can't use Event.contributions here because we need to check 3 different
# project_features for the (currently) 3 different contribution types # project_features for the (currently) 3 different contribution types
date_from = 1.year.ago date_from = @contributor_time_instance.now.years_ago(1)
repo_events = event_counts(date_from, :repository) repo_events = event_counts(date_from, :repository)
.having(action: :pushed) .having(action: :pushed)
issue_events = event_counts(date_from, :issues) issue_events = event_counts(date_from, :issues)
...@@ -47,19 +50,21 @@ module Gitlab ...@@ -47,19 +50,21 @@ module Gitlab
def events_by_date(date) def events_by_date(date)
return Event.none unless can_read_cross_project? return Event.none unless can_read_cross_project?
date_in_time_zone = date.in_time_zone(@contributor_time_instance)
Event.contributions.where(author_id: contributor.id) Event.contributions.where(author_id: contributor.id)
.where(created_at: date.beginning_of_day..date.end_of_day) .where(created_at: date_in_time_zone.beginning_of_day..date_in_time_zone.end_of_day)
.where(project_id: projects) .where(project_id: projects)
.with_associations .with_associations
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def starting_year def starting_year
1.year.ago.year @contributor_time_instance.now.years_ago(1).year
end end
def starting_month def starting_month
Date.current.month @contributor_time_instance.today.month
end end
private private
...@@ -82,10 +87,10 @@ module Gitlab ...@@ -82,10 +87,10 @@ module Gitlab
.select(:id) .select(:id)
conditions = t[:created_at].gteq(date_from.beginning_of_day) conditions = t[:created_at].gteq(date_from.beginning_of_day)
.and(t[:created_at].lteq(Date.current.end_of_day)) .and(t[:created_at].lteq(@contributor_time_instance.today.end_of_day))
.and(t[:author_id].eq(contributor.id)) .and(t[:author_id].eq(contributor.id))
date_interval = "INTERVAL '#{Time.zone.now.utc_offset} seconds'" date_interval = "INTERVAL '#{@contributor_time_instance.now.utc_offset} seconds'"
Event.reorder(nil) Event.reorder(nil)
.select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval}) AS date", 'count(id) as total_amount') .select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval}) AS date", 'count(id) as total_amount')
......
...@@ -100,4 +100,36 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do ...@@ -100,4 +100,36 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do
end end
end end
end end
describe '#local_time_instance' do
let_it_be(:timezone) { 'UTC' }
before do
travel_to Time.find_zone(timezone).local(2021, 7, 20, 15, 30, 45)
end
context 'when timezone is `nil`' do
it 'returns the system timezone instance' do
expect(helper.local_time_instance(nil).name).to eq(timezone)
end
end
context 'when timezone is blank' do
it 'returns the system timezone instance' do
expect(helper.local_time_instance('').name).to eq(timezone)
end
end
context 'when a valid timezone is passed' do
it 'returns the local time instance' do
expect(helper.local_time_instance('America/Los_Angeles').name).to eq('America/Los_Angeles')
end
end
context 'when an invalid timezone is passed' do
it 'returns the system timezone instance' do
expect(helper.local_time_instance('Foo/Bar').name).to eq(timezone)
end
end
end
end end
...@@ -5,6 +5,7 @@ require 'spec_helper' ...@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::ContributionsCalendar do RSpec.describe Gitlab::ContributionsCalendar do
let(:contributor) { create(:user) } let(:contributor) { create(:user) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:travel_time) { nil }
let(:private_project) do let(:private_project) do
create(:project, :private) do |project| create(:project, :private) do |project|
...@@ -31,7 +32,7 @@ RSpec.describe Gitlab::ContributionsCalendar do ...@@ -31,7 +32,7 @@ RSpec.describe Gitlab::ContributionsCalendar do
let(:last_year) { today - 1.year } let(:last_year) { today - 1.year }
before do before do
travel_to Time.now.utc.end_of_day travel_to travel_time || Time.now.utc.end_of_day
end end
after do after do
...@@ -89,7 +90,7 @@ RSpec.describe Gitlab::ContributionsCalendar do ...@@ -89,7 +90,7 @@ RSpec.describe Gitlab::ContributionsCalendar do
expect(calendar(contributor).activity_dates[today]).to eq(2) expect(calendar(contributor).activity_dates[today]).to eq(2)
end end
context "when events fall under different dates depending on the time zone" do context "when events fall under different dates depending on the system time zone" do
before do before do
create_event(public_project, today, 1) create_event(public_project, today, 1)
create_event(public_project, today, 4) create_event(public_project, today, 4)
...@@ -116,6 +117,37 @@ RSpec.describe Gitlab::ContributionsCalendar do ...@@ -116,6 +117,37 @@ RSpec.describe Gitlab::ContributionsCalendar do
end end
end end
end end
context "when events fall under different dates depending on the contributor's time zone" do
before do
create_event(public_project, today, 1)
create_event(public_project, today, 4)
create_event(public_project, today, 10)
create_event(public_project, today, 16)
create_event(public_project, today, 23)
end
it "renders correct event counts within the UTC timezone" do
Time.use_zone('UTC') do
contributor.timezone = 'UTC'
expect(calendar.activity_dates).to eq(today => 5)
end
end
it "renders correct event counts within the Sydney timezone" do
Time.use_zone('UTC') do
contributor.timezone = 'Sydney'
expect(calendar.activity_dates).to eq(today => 3, tomorrow => 2)
end
end
it "renders correct event counts within the US Central timezone" do
Time.use_zone('UTC') do
contributor.timezone = 'Central Time (US & Canada)'
expect(calendar.activity_dates).to eq(yesterday => 2, today => 3)
end
end
end
end end
describe '#events_by_date' do describe '#events_by_date' do
...@@ -152,14 +184,38 @@ RSpec.describe Gitlab::ContributionsCalendar do ...@@ -152,14 +184,38 @@ RSpec.describe Gitlab::ContributionsCalendar do
end end
describe '#starting_year' do describe '#starting_year' do
it "is the start of last year" do let(:travel_time) { Time.find_zone('UTC').local(2020, 12, 31, 19, 0, 0) }
expect(calendar.starting_year).to eq(last_year.year)
context "when the contributor's timezone is not set" do
it "is the start of last year in the system timezone" do
expect(calendar.starting_year).to eq(2019)
end
end
context "when the contributor's timezone is set to Sydney" do
let(:contributor) { create(:user, { timezone: 'Sydney' }) }
it "is the start of last year in Sydney" do
expect(calendar.starting_year).to eq(2020)
end
end end
end end
describe '#starting_month' do describe '#starting_month' do
it "is the start of this month" do let(:travel_time) { Time.find_zone('UTC').local(2020, 12, 31, 19, 0, 0) }
expect(calendar.starting_month).to eq(today.month)
context "when the contributor's timezone is not set" do
it "is the start of this month in the system timezone" do
expect(calendar.starting_month).to eq(12)
end
end
context "when the contributor's timezone is set to Sydney" do
let(:contributor) { create(:user, { timezone: 'Sydney' }) }
it "is the start of this month in Sydney" do
expect(calendar.starting_month).to eq(1)
end
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment