mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 23:32:53 +00:00
* Modals! New projects page! * Update modal close buttons * Make progress bar better * Various fixes + tests * Formatting * Fix tests?
156 lines
4.5 KiB
Ruby
156 lines
4.5 KiB
Ruby
class ProjectStatsQuery
|
|
def initialize(user:, params:, include_archived: false, default_discovery_start: 30.days.ago.beginning_of_day, default_discovery_end: Time.current, default_stats_start: 1.year.ago, default_stats_end: Time.current)
|
|
@user = user
|
|
@params = normalize_params(params)
|
|
@include_archived = include_archived
|
|
@default_discovery_start = default_discovery_start
|
|
@default_discovery_end = default_discovery_end
|
|
@default_stats_start = default_stats_start
|
|
@default_stats_end = default_stats_end
|
|
end
|
|
|
|
def project_names
|
|
names = scoped_heartbeats(discovery_start_time, discovery_end_time)
|
|
.select(:project)
|
|
.distinct
|
|
.pluck(:project)
|
|
.compact
|
|
|
|
return names if @include_archived
|
|
|
|
names.reject { |name| archived_project_names.include?(name) }
|
|
end
|
|
|
|
def project_details(names: nil)
|
|
names = normalize_names(names)
|
|
names = project_names if names.empty?
|
|
return [] if names.empty?
|
|
|
|
query = scoped_heartbeats(stats_start_time, stats_end_time)
|
|
.where(project: names)
|
|
return [] unless query.exists?
|
|
|
|
stats = query.group(:project)
|
|
.select("project, COUNT(*) AS heartbeat_count, MIN(time) AS first_heartbeat, MAX(time) AS last_heartbeat")
|
|
.index_by(&:project)
|
|
|
|
durations = query.group(:project).duration_seconds
|
|
|
|
languages_by_project = query.where.not(language: [ nil, "" ])
|
|
.group(:project, :language)
|
|
.pluck(:project, :language)
|
|
.group_by(&:first)
|
|
.transform_values { |pairs| pairs.map(&:last).uniq }
|
|
|
|
repo_mappings = @user.project_repo_mappings
|
|
.where(project_name: names)
|
|
.index_by(&:project_name)
|
|
|
|
names.filter_map do |name|
|
|
stat = stats[name]
|
|
next unless stat
|
|
|
|
repo_mapping = repo_mappings[name]
|
|
archived = repo_mapping&.archived? || false
|
|
next if archived && !@include_archived
|
|
|
|
first_heartbeat = format_heartbeat_time(stat.first_heartbeat)
|
|
last_heartbeat = format_heartbeat_time(stat.last_heartbeat)
|
|
|
|
{
|
|
name: name,
|
|
total_seconds: durations[name] || 0,
|
|
languages: languages_by_project[name] || [],
|
|
repo_url: repo_mapping&.repo_url,
|
|
total_heartbeats: stat.heartbeat_count || 0,
|
|
first_heartbeat: first_heartbeat,
|
|
last_heartbeat: last_heartbeat,
|
|
most_recent_heartbeat: last_heartbeat,
|
|
archived: archived
|
|
}
|
|
end.sort_by { |project| -project[:total_seconds] }
|
|
end
|
|
|
|
private
|
|
|
|
def normalize_params(params)
|
|
h = params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params.to_h
|
|
h.with_indifferent_access
|
|
end
|
|
|
|
def normalize_names(names)
|
|
raw = names.presence || parse_csv(@params[:projects])
|
|
Array(raw).map(&:to_s).map(&:strip).reject(&:blank?).uniq
|
|
end
|
|
|
|
def scoped_heartbeats(start_time, end_time)
|
|
start_timestamp = timestamp_value(start_time)
|
|
end_timestamp = timestamp_value(end_time)
|
|
return @user.heartbeats.none if start_timestamp.nil? || end_timestamp.nil?
|
|
|
|
@user.heartbeats
|
|
.with_valid_timestamps
|
|
.where.not(project: [ nil, "" ])
|
|
.where(time: start_timestamp..end_timestamp)
|
|
end
|
|
|
|
def archived_project_names
|
|
@archived_project_names ||= @user.project_repo_mappings.archived.pluck(:project_name)
|
|
end
|
|
|
|
def discovery_start_time
|
|
parse_time([ :since, :start, :start_date ], default: @default_discovery_start)
|
|
end
|
|
|
|
def discovery_end_time
|
|
parse_time([ :until, :until_date, :end, :end_date ], default: @default_discovery_end)
|
|
end
|
|
|
|
def stats_start_time
|
|
parse_time([ :start, :start_date ], default: @default_stats_start)
|
|
end
|
|
|
|
def stats_end_time
|
|
parse_time([ :end, :end_date ], default: @default_stats_end)
|
|
end
|
|
|
|
def parse_time(keys, default:)
|
|
keys.each do |key|
|
|
parsed = parse_datetime(@params[key])
|
|
return parsed if parsed
|
|
end
|
|
|
|
default
|
|
end
|
|
|
|
def parse_datetime(value)
|
|
return nil if value.blank?
|
|
|
|
value.to_datetime
|
|
rescue ArgumentError, TypeError
|
|
nil
|
|
end
|
|
|
|
def parse_csv(value)
|
|
return [] if value.blank?
|
|
|
|
value.to_s.split(",").map(&:strip).reject(&:blank?)
|
|
end
|
|
|
|
def format_heartbeat_time(time_value)
|
|
return nil if time_value.blank?
|
|
|
|
Time.at(time_value.to_f).utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
end
|
|
|
|
def timestamp_value(value)
|
|
case value
|
|
when Numeric
|
|
value.to_f
|
|
when Time, ActiveSupport::TimeWithZone
|
|
value.to_f
|
|
when DateTime
|
|
value.to_time.to_f
|
|
end
|
|
end
|
|
end
|