Commit 45f0d6aa authored by David Barr's avatar David Barr

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

Shift a user's contribution calendar to be based on their local time
zone (if set in their profile), rather than the server's timezone.

Changelog: changed
parent 7898e25d
......@@ -5,6 +5,7 @@ import { last } from 'lodash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import { formatDate } from '~/lib/utils/datetime/date_format_utility';
import { n__, s__, __ } from '~/locale';
const d3 = { select };
......@@ -294,7 +295,15 @@ export default class ActivityCalendar {
},
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(() =>
createFlash({
message: __('An error occurred while retrieving calendar activity'),
......
......@@ -32,10 +32,16 @@ module TimeZoneHelper
end
end
def local_time_instance(timezone)
return Time.zone if timezone.blank?
ActiveSupport::TimeZone.new(timezone) || Time.zone
end
def local_time(timezone)
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")
end
end
......@@ -2,7 +2,7 @@
.row.d-none.d-sm-flex
.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
.user-calendar-error.invisible
= _('There was an error loading users activity calendar.')
......
......@@ -5,9 +5,9 @@
%ul.bordered-list
- @events.sort_by(&:created_at).each do |event|
%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')
= 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.push_action?
#{event.action_name} #{event.ref_type}
......
......@@ -2,12 +2,15 @@
module Gitlab
class ContributionsCalendar
include TimeZoneHelper
attr_reader :contributor
attr_reader :current_user
attr_reader :projects
def initialize(contributor, current_user = nil)
@contributor = contributor
@contributor_time_instance = local_time_instance(contributor.timezone)
@current_user = current_user
@projects = if @contributor.include_private_contributions?
ContributedProjectsFinder.new(@contributor).execute(@contributor)
......@@ -22,7 +25,7 @@ module Gitlab
# Can't use Event.contributions here because we need to check 3 different
# 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)
.having(action: :pushed)
issue_events = event_counts(date_from, :issues)
......@@ -47,19 +50,21 @@ module Gitlab
def events_by_date(date)
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)
.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)
.with_associations
end
# rubocop: enable CodeReuse/ActiveRecord
def starting_year
1.year.ago.year
@contributor_time_instance.now.years_ago(1).year
end
def starting_month
Date.current.month
@contributor_time_instance.today.month
end
private
......@@ -82,10 +87,10 @@ module Gitlab
.select(:id)
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))
date_interval = "INTERVAL '#{Time.zone.now.utc_offset} seconds'"
date_interval = "INTERVAL '#{@contributor_time_instance.now.utc_offset} seconds'"
Event.reorder(nil)
.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
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
......@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::ContributionsCalendar do
let(:contributor) { create(:user) }
let(:user) { create(:user) }
let(:travel_time) { nil }
let(:private_project) do
create(:project, :private) do |project|
......@@ -31,7 +32,7 @@ RSpec.describe Gitlab::ContributionsCalendar do
let(:last_year) { today - 1.year }
before do
travel_to Time.now.utc.end_of_day
travel_to travel_time || Time.now.utc.end_of_day
end
after do
......@@ -89,7 +90,7 @@ RSpec.describe Gitlab::ContributionsCalendar do
expect(calendar(contributor).activity_dates[today]).to eq(2)
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
create_event(public_project, today, 1)
create_event(public_project, today, 4)
......@@ -116,6 +117,37 @@ RSpec.describe Gitlab::ContributionsCalendar do
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
describe '#events_by_date' do
......@@ -152,14 +184,38 @@ RSpec.describe Gitlab::ContributionsCalendar do
end
describe '#starting_year' do
it "is the start of last year" do
expect(calendar.starting_year).to eq(last_year.year)
let(:travel_time) { Time.find_zone('UTC').local(2020, 12, 31, 19, 0, 0) }
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
describe '#starting_month' do
it "is the start of this month" do
expect(calendar.starting_month).to eq(today.month)
let(:travel_time) { Time.find_zone('UTC').local(2020, 12, 31, 19, 0, 0) }
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
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