mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 23:32:53 +00:00
Merge branch 'main' of github.com:hackclub/harbor
This commit is contained in:
commit
7c33ca11be
3 changed files with 146 additions and 106 deletions
|
|
@ -22,35 +22,37 @@ class StaticPagesController < ApplicationController
|
|||
@setup_social_proof = get_setup_social_proof if @show_wakatime_setup_notice
|
||||
|
||||
# Get languages and editors in a single query using window functions
|
||||
results = current_user.heartbeats.today
|
||||
.select(
|
||||
:language,
|
||||
:editor,
|
||||
"COUNT(*) OVER (PARTITION BY language) as language_count",
|
||||
"COUNT(*) OVER (PARTITION BY editor) as editor_count"
|
||||
)
|
||||
.distinct
|
||||
.to_a
|
||||
Time.use_zone(current_user.timezone) do
|
||||
results = current_user.heartbeats.today
|
||||
.select(
|
||||
:language,
|
||||
:editor,
|
||||
"COUNT(*) OVER (PARTITION BY language) as language_count",
|
||||
"COUNT(*) OVER (PARTITION BY editor) as editor_count"
|
||||
)
|
||||
.distinct
|
||||
.to_a
|
||||
|
||||
# Process results to get sorted languages and editors
|
||||
language_counts = results
|
||||
.map { |r| [ r.language, r.language_count ] }
|
||||
.reject { |lang, _| lang.nil? || lang.empty? }
|
||||
.uniq
|
||||
.sort_by { |_, count| -count }
|
||||
# Process results to get sorted languages and editors
|
||||
language_counts = results
|
||||
.map { |r| [ r.language, r.language_count ] }
|
||||
.reject { |lang, _| lang.nil? || lang.empty? }
|
||||
.uniq
|
||||
.sort_by { |_, count| -count }
|
||||
|
||||
editor_counts = results
|
||||
.map { |r| [ r.editor, r.editor_count ] }
|
||||
.reject { |ed, _| ed.nil? || ed.empty? }
|
||||
.uniq
|
||||
.sort_by { |_, count| -count }
|
||||
editor_counts = results
|
||||
.map { |r| [ r.editor, r.editor_count ] }
|
||||
.reject { |ed, _| ed.nil? || ed.empty? }
|
||||
.uniq
|
||||
.sort_by { |_, count| -count }
|
||||
|
||||
@todays_languages = language_counts.map(&:first)
|
||||
@todays_editors = editor_counts.map(&:first)
|
||||
@todays_duration = current_user.heartbeats.today.duration_seconds
|
||||
@todays_languages = language_counts.map(&:first)
|
||||
@todays_editors = editor_counts.map(&:first)
|
||||
@todays_duration = current_user.heartbeats.today.duration_seconds
|
||||
|
||||
if @todays_duration > 1.minute
|
||||
@show_logged_time_sentence = @todays_languages.any? || @todays_editors.any?
|
||||
if @todays_duration > 1.minute
|
||||
@show_logged_time_sentence = @todays_languages.any? || @todays_editors.any?
|
||||
end
|
||||
end
|
||||
|
||||
cached_data = filterable_dashboard_data
|
||||
|
|
@ -263,76 +265,78 @@ class StaticPagesController < ApplicationController
|
|||
Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
|
||||
result = {}
|
||||
# Load filter options
|
||||
filters.each do |filter|
|
||||
group_by_time = current_user.heartbeats.group(filter).duration_seconds
|
||||
result[filter] = group_by_time.sort_by { |k, v| v }
|
||||
.reverse.map(&:first)
|
||||
.compact_blank
|
||||
Time.use_zone(current_user.timezone) do
|
||||
filters.each do |filter|
|
||||
group_by_time = current_user.heartbeats.group(filter).duration_seconds
|
||||
result[filter] = group_by_time.sort_by { |k, v| v }
|
||||
.reverse.map(&:first)
|
||||
.compact_blank
|
||||
|
||||
if params[filter].present?
|
||||
filter_arr = params[filter].split(",")
|
||||
filtered_heartbeats = filtered_heartbeats.where(filter => filter_arr)
|
||||
if params[filter].present?
|
||||
filter_arr = params[filter].split(",")
|
||||
filtered_heartbeats = filtered_heartbeats.where(filter => filter_arr)
|
||||
|
||||
result["singular_#{filter}"] = filter_arr.length == 1
|
||||
result["singular_#{filter}"] = filter_arr.length == 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
result[:filtered_heartbeats] = filtered_heartbeats
|
||||
result[:filtered_heartbeats] = filtered_heartbeats
|
||||
|
||||
# Calculate stats for filtered data
|
||||
result[:total_time] = filtered_heartbeats.duration_seconds
|
||||
result[:total_heartbeats] = filtered_heartbeats.count
|
||||
# Calculate stats for filtered data
|
||||
result[:total_time] = filtered_heartbeats.duration_seconds
|
||||
result[:total_heartbeats] = filtered_heartbeats.count
|
||||
|
||||
filters.each do |filter|
|
||||
result["top_#{filter}"] = filtered_heartbeats.group(filter)
|
||||
.duration_seconds
|
||||
.max_by { |_, v| v }
|
||||
&.first
|
||||
end
|
||||
filters.each do |filter|
|
||||
result["top_#{filter}"] = filtered_heartbeats.group(filter)
|
||||
.duration_seconds
|
||||
.max_by { |_, v| v }
|
||||
&.first
|
||||
end
|
||||
|
||||
# Prepare project durations data
|
||||
result[:project_durations] = filtered_heartbeats
|
||||
.group(:project)
|
||||
.duration_seconds
|
||||
.sort_by { |_, duration| -duration }
|
||||
.first(10)
|
||||
.to_h unless result["singular_project"]
|
||||
|
||||
# Prepare pie chart data
|
||||
result[:language_stats] = filtered_heartbeats
|
||||
.group(:language)
|
||||
.duration_seconds
|
||||
.sort_by { |_, duration| -duration }
|
||||
.first(10)
|
||||
.map { |k, v| [ k.presence || "Unknown", v ] }
|
||||
.to_h unless result["singular_language"]
|
||||
|
||||
result[:editor_stats] = filtered_heartbeats
|
||||
.group(:editor)
|
||||
.duration_seconds
|
||||
.sort_by { |_, duration| -duration }
|
||||
.map { |k, v| [ k.presence || "Unknown", v ] }
|
||||
.to_h unless result["singular_editor"]
|
||||
|
||||
result[:operating_system_stats] = filtered_heartbeats
|
||||
.group(:operating_system)
|
||||
.duration_seconds
|
||||
.sort_by { |_, duration| -duration }
|
||||
.map { |k, v| [ k.presence || "Unknown", v ] }
|
||||
.to_h unless result["singular_operating_system"]
|
||||
|
||||
# Calculate weekly project stats for the last 6 months
|
||||
result[:weekly_project_stats] = {}
|
||||
(0..25).each do |week_offset| # 26 weeks = 6 months
|
||||
week_start = week_offset.weeks.ago.beginning_of_week
|
||||
week_end = week_offset.weeks.ago.end_of_week
|
||||
|
||||
week_stats = filtered_heartbeats
|
||||
.where(time: week_start.to_f..week_end.to_f)
|
||||
# Prepare project durations data
|
||||
result[:project_durations] = filtered_heartbeats
|
||||
.group(:project)
|
||||
.duration_seconds
|
||||
.sort_by { |_, duration| -duration }
|
||||
.first(10)
|
||||
.to_h unless result["singular_project"]
|
||||
|
||||
result[:weekly_project_stats][week_start.to_date.iso8601] = week_stats
|
||||
# Prepare pie chart data
|
||||
result[:language_stats] = filtered_heartbeats
|
||||
.group(:language)
|
||||
.duration_seconds
|
||||
.sort_by { |_, duration| -duration }
|
||||
.first(10)
|
||||
.map { |k, v| [ k.presence || "Unknown", v ] }
|
||||
.to_h unless result["singular_language"]
|
||||
|
||||
result[:editor_stats] = filtered_heartbeats
|
||||
.group(:editor)
|
||||
.duration_seconds
|
||||
.sort_by { |_, duration| -duration }
|
||||
.map { |k, v| [ k.presence || "Unknown", v ] }
|
||||
.to_h unless result["singular_editor"]
|
||||
|
||||
result[:operating_system_stats] = filtered_heartbeats
|
||||
.group(:operating_system)
|
||||
.duration_seconds
|
||||
.sort_by { |_, duration| -duration }
|
||||
.map { |k, v| [ k.presence || "Unknown", v ] }
|
||||
.to_h unless result["singular_operating_system"]
|
||||
|
||||
# Calculate weekly project stats for the last 6 months
|
||||
result[:weekly_project_stats] = {}
|
||||
(0..25).each do |week_offset| # 26 weeks = 6 months
|
||||
week_start = week_offset.weeks.ago.beginning_of_week
|
||||
week_end = week_offset.weeks.ago.end_of_week
|
||||
|
||||
week_stats = filtered_heartbeats
|
||||
.where(time: week_start.to_f..week_end.to_f)
|
||||
.group(:project)
|
||||
.duration_seconds
|
||||
|
||||
result[:weekly_project_stats][week_start.to_date.iso8601] = week_stats
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
|
|
|
|||
|
|
@ -21,10 +21,6 @@ class LeaderboardUpdateJob < ApplicationJob
|
|||
period_type: period_type
|
||||
)
|
||||
|
||||
# Get list of valid user IDs from our database
|
||||
valid_user_ids = User.pluck(:id)
|
||||
return if valid_user_ids.empty?
|
||||
|
||||
date_range = case period_type
|
||||
when :weekly
|
||||
(parsed_date.beginning_of_day...(parsed_date + 7.days).beginning_of_day)
|
||||
|
|
@ -59,8 +55,6 @@ class LeaderboardUpdateJob < ApplicationJob
|
|||
LeaderboardEntry.insert_all!(entries_data) if entries_data.any?
|
||||
end
|
||||
|
||||
Rails.logger.info "\nTiming breakdown:\n#{timing_info.join("\n")}"
|
||||
|
||||
leaderboard.finished_generating_at = Time.current
|
||||
leaderboard.save!
|
||||
|
||||
|
|
|
|||
|
|
@ -78,27 +78,69 @@ module Heartbeatable
|
|||
end
|
||||
|
||||
def daily_streaks_for_users(user_ids, start_date: 8.days.ago)
|
||||
require "set"
|
||||
|
||||
heartbeats = where(user_id: user_ids)
|
||||
# First get the raw durations using window function
|
||||
raw_durations = joins(:user)
|
||||
.where(user_id: user_ids)
|
||||
.coding_only
|
||||
.with_valid_timestamps
|
||||
.where(time: start_date..Time.current)
|
||||
.select(
|
||||
:user_id,
|
||||
"users.timezone as user_timezone",
|
||||
Arel.sql("DATE_TRUNC('day', to_timestamp(time) AT TIME ZONE users.timezone) as day_group"),
|
||||
Arel.sql("LEAST(EXTRACT(EPOCH FROM (to_timestamp(time) - to_timestamp(LAG(time) OVER (PARTITION BY user_id, DATE_TRUNC('day', to_timestamp(time) AT TIME ZONE users.timezone) ORDER BY time)))), #{heartbeat_timeout_duration.to_i}) as diff")
|
||||
)
|
||||
|
||||
user_ids.each_with_object(Hash.new(0)) do |user_id, hash|
|
||||
# Then aggregate the results
|
||||
daily_durations = connection.select_all(
|
||||
"SELECT user_id, user_timezone, day_group, COALESCE(SUM(diff), 0)::integer as duration
|
||||
FROM (#{raw_durations.to_sql}) AS diffs
|
||||
GROUP BY user_id, user_timezone, day_group"
|
||||
).group_by { |row| row["user_id"] }
|
||||
.transform_values do |rows|
|
||||
timezone = rows.first["user_timezone"]
|
||||
current_date = Time.current.in_time_zone(timezone).to_date
|
||||
{
|
||||
current_date: current_date,
|
||||
days: rows.map do |row|
|
||||
[ row["day_group"].to_date, row["duration"].to_i ]
|
||||
end.sort_by { |date, _| date }.reverse
|
||||
}
|
||||
end
|
||||
|
||||
# Initialize the result hash with zeros for all users
|
||||
result = user_ids.index_with { 0 }
|
||||
|
||||
# Then calculate streaks for each user
|
||||
daily_durations.each do |user_id, data|
|
||||
current_date = data[:current_date]
|
||||
days = data[:days]
|
||||
|
||||
# Calculate streak
|
||||
streak = 0
|
||||
days_for_user = heartbeats.where(user_id: user_id).daily_durations(start_date: start_date)
|
||||
days_for_user.sort_by { |date, _| date }
|
||||
.reverse
|
||||
.each do |_, duration|
|
||||
if duration >= 15 * 60
|
||||
streak += 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
hash[user_id] = streak
|
||||
days.each do |date, duration|
|
||||
# Skip if this day is in the future
|
||||
next if date > current_date
|
||||
|
||||
# If they didn't code enough today, just skip
|
||||
if date == current_date
|
||||
next unless duration >= 15 * 60
|
||||
streak += 1
|
||||
next
|
||||
end
|
||||
|
||||
# For previous days, check if it's the next day in the streak
|
||||
if date == current_date - streak.days && duration >= 15 * 60
|
||||
streak += 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
result[user_id] = streak
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def daily_durations(start_date: 365.days.ago, end_date: Time.current)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue