Files
gitlab-foss/lib/gitlab/contributions_calendar.rb
2025-04-09 18:13:01 +00:00

127 lines
4.4 KiB
Ruby

# frozen_string_literal: true
module Gitlab
class ContributionsCalendar
include TimeZoneHelper
include ::Gitlab::Utils::StrongMemoize
attr_reader :contributor
attr_reader :current_user
attr_reader :groups
attr_reader :projects
def initialize(contributor, current_user = nil)
@contributor = contributor
@contributor_time_instance = local_timezone_instance(contributor.timezone).now
@current_user = current_user
@groups = [] # Overriden in EE
@projects = ContributedProjectsFinder.new(
user: @contributor,
current_user: current_user,
params: {
ignore_visibility: @contributor.include_private_contributions?
}
).execute
end
def activity_dates
return {} if groups.blank? && projects.blank?
start_time = @contributor_time_instance.years_ago(1).beginning_of_day
end_time = @contributor_time_instance.end_of_day
date_interval = "INTERVAL '#{@contributor_time_instance.utc_offset} seconds'"
contributions_between(start_time, end_time).count_by_dates(date_interval)
end
def events_by_date(date)
return Event.none unless can_read_cross_project?
date_in_time_zone = date.in_time_zone(@contributor_time_instance.time_zone)
contributions_between(date_in_time_zone.beginning_of_day, date_in_time_zone.end_of_day).with_associations
end
private
def contributions_between(start_time, end_time)
Event.from_union(
collect_events_between(start_time, end_time),
remove_duplicates: false
)
end
def collect_events_between(start_time, end_time)
# Can't use Event.contributions here because we need to check 3 different
# project_features for the (currently) 4 different contribution types
repo_events =
project_events_created_between(start_time, end_time, features: :repository)
.for_action(:pushed)
issue_events =
project_events_created_between(start_time, end_time, features: :issues)
.for_issue
.for_action(%i[created closed])
design_events =
project_events_created_between(start_time, end_time, features: :issues)
.for_design
.for_action(%i[created updated destroyed])
mr_events =
project_events_created_between(start_time, end_time, features: :merge_requests)
.for_merge_request
.for_action(%i[merged created closed approved])
project_note_events =
project_events_created_between(start_time, end_time, features: %i[issues merge_requests])
.for_action(:commented)
[repo_events, issue_events, design_events, mr_events, project_note_events]
end
def can_read_cross_project?
Ability.allowed?(current_user, :read_cross_project)
end
# rubocop: disable CodeReuse/ActiveRecord -- no need to move this to ActiveRecord model
def project_events_created_between(start_time, end_time, features:)
Array(features).reduce(Event.none) do |events, feature|
events.or(contribution_events(start_time, end_time).where(project_id: authed_projects(feature)))
end
end
# rubocop: enable CodeReuse/ActiveRecord
def authed_projects(feature)
strong_memoize("#{feature}_projects") do
# no need to check features access of current user, if the contributor opted-in
# to show all private events anyway - otherwise they would get filtered out again
next contributed_project_ids if contributor.include_private_contributions?
# rubocop: disable CodeReuse/ActiveRecord -- no need to move this to ActiveRecord model
ProjectFeature
.with_feature_available_for_user(feature, current_user)
.where(project_id: contributed_project_ids)
.pluck(:project_id)
# rubocop: enable CodeReuse/ActiveRecord
end
end
# rubocop: disable CodeReuse/ActiveRecord -- no need to move this to ActiveRecord model
def contributed_project_ids
# re-running the contributed projects query in each union is expensive, so
# use IN(project_ids...) instead. It's the intersection of two users so
# the list will be (relatively) short
@contributed_project_ids ||= projects.distinct.pluck(:id)
end
# rubocop: enable CodeReuse/ActiveRecord
def contribution_events(start_time, end_time)
contributor.events.created_between(start_time, end_time)
end
end
end
Gitlab::ContributionsCalendar.prepend_mod