Skip to content

Commit 9d8cc0d

Browse files
Add heartbeat import (#469)
1 parent fac7758 commit 9d8cc0d

File tree

5 files changed

+192
-2
lines changed

5 files changed

+192
-2
lines changed

app/controllers/my/heartbeats_controller.rb

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def export
6464
end
6565
}
6666

67-
filename = "heartbeats_#{current_user.slack_uid}_#{start_date.strftime('%Y%m%d')}_#{end_date.strftime('%Y%m%d')}.json"
67+
filename = "heartbeats_#{current_user.slack_uid}_#{start_date.strftime("%Y%m%d")}_#{end_date.strftime("%Y%m%d")}.json"
6868

6969
respond_to do |format|
7070
format.json {
@@ -76,6 +76,56 @@ def export
7676
end
7777
end
7878

79+
def import
80+
unless Rails.env.development?
81+
redirect_to my_settings_path, alert: "Hey you! This is noit a dev env, STOP DOING THIS!!!!!) Also, idk why this is happning, you should not be able to see this button hmm...."
82+
return
83+
end
84+
85+
unless params[:heartbeat_file].present?
86+
redirect_to my_settings_path, alert: "pls select a file to import"
87+
return
88+
end
89+
90+
file = params[:heartbeat_file]
91+
92+
unless file.content_type == "application/json" || file.original_filename.ends_with?(".json")
93+
redirect_to my_settings_path, alert: "pls upload only json (download from the button above it)"
94+
return
95+
end
96+
97+
begin
98+
file_content = file.read.force_encoding("UTF-8")
99+
rescue => e
100+
redirect_to my_settings_path, alert: "error reading file: #{e.message}"
101+
return
102+
end
103+
104+
result = HeartbeatImportService.import_from_file(file_content, current_user)
105+
106+
if result[:success]
107+
message = "Imported #{result[:imported_count]} out of #{result[:total_count]} heartbeats"
108+
if result[:skipped_count] > 0
109+
message += " (#{result[:skipped_count]} skipped cause they were duplicates)"
110+
end
111+
if result[:errors].any?
112+
error_count = result[:errors].length
113+
if error_count <= 3
114+
message += ". Errors occurred: #{result[:errors].join("; ")}"
115+
else
116+
message += ". #{error_count} errors occurred. First few: #{result[:errors].first(2).join("; ")}..."
117+
end
118+
end
119+
redirect_to root_path, notice: message
120+
else
121+
error_message = "Import failed: #{result[:error]}"
122+
if result[:errors].any? && result[:errors].length > 1
123+
error_message += "Errors: #{result[:errors][1..2].join("; ")}"
124+
end
125+
redirect_to my_settings_path, alert: error_message
126+
end
127+
end
128+
79129
private
80130

81131
def ensure_current_user
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
class HeartbeatImportService
2+
def self.import_from_file(file_content, user)
3+
unless Rails.env.development?
4+
raise StandardError, "Not dev env, not running"
5+
end
6+
7+
begin
8+
parsed_data = JSON.parse(file_content)
9+
rescue JSON::ParserError => e
10+
raise StandardError, "Not json: #{e.message}"
11+
end
12+
13+
unless parsed_data.is_a?(Hash) && parsed_data["heartbeats"].is_a?(Array)
14+
raise StandardError, "Not correct format, download from /my/settings on the offical hackatime then import here"
15+
end
16+
17+
heartbeats_data = parsed_data["heartbeats"]
18+
imported_count = 0
19+
skipped_count = 0
20+
errors = []
21+
cc = 817263
22+
heartbeats_data.each_slice(100) do |batch|
23+
records_to_upsert = []
24+
25+
batch.each_with_index do |heartbeat_data, index|
26+
begin
27+
time_value = if heartbeat_data["time"].is_a?(String)
28+
Time.parse(heartbeat_data["time"]).to_f
29+
else
30+
heartbeat_data["time"].to_f
31+
end
32+
33+
attrs = {
34+
user_id: user.id,
35+
time: time_value,
36+
entity: heartbeat_data["entity"],
37+
type: heartbeat_data["type"],
38+
category: heartbeat_data["category"] || "coding",
39+
project: heartbeat_data["project"],
40+
language: heartbeat_data["language"],
41+
editor: heartbeat_data["editor"],
42+
operating_system: heartbeat_data["operating_system"],
43+
machine: heartbeat_data["machine"],
44+
branch: heartbeat_data["branch"],
45+
user_agent: heartbeat_data["user_agent"],
46+
is_write: heartbeat_data["is_write"] || false,
47+
line_additions: heartbeat_data["line_additions"],
48+
line_deletions: heartbeat_data["line_deletions"],
49+
lineno: heartbeat_data["lineno"],
50+
lines: heartbeat_data["lines"],
51+
cursorpos: heartbeat_data["cursorpos"],
52+
dependencies: heartbeat_data["dependencies"] || [],
53+
project_root_count: heartbeat_data["project_root_count"],
54+
source_type: :wakapi_import,
55+
raw_data: heartbeat_data.slice(*Heartbeat.indexed_attributes)
56+
}
57+
58+
attrs[:fields_hash] = Heartbeat.generate_fields_hash(attrs)
59+
print(attrs[:fields_hash])
60+
print("\n")
61+
records_to_upsert << attrs
62+
63+
rescue => e
64+
errors << "Row #{index + 1}: #{e.message}"
65+
next
66+
end
67+
end
68+
69+
if records_to_upsert.any?
70+
print("importing!!!!!!!!!!!!!!!!!!!!!!")
71+
print("\n")
72+
begin
73+
# Copied from migrate user from hackatime (app\jobs\migrate_user_from_hackatime_job.rb)
74+
records_to_upsert = records_to_upsert.group_by { |r| r[:fields_hash] }.map do |_, records|
75+
records.max_by { |r| r[:time] }
76+
end
77+
result = Heartbeat.upsert_all(records_to_upsert, unique_by: [ :fields_hash ])
78+
imported_count += result.length
79+
rescue => e
80+
errors << "Import error: #{e.message}"
81+
print(e.message)
82+
print("\n")
83+
end
84+
end
85+
end
86+
87+
{
88+
success: true,
89+
imported_count: imported_count,
90+
total_count: heartbeats_data.length,
91+
skipped_count: heartbeats_data.length - imported_count,
92+
errors: errors
93+
}
94+
95+
rescue => e
96+
{
97+
success: false,
98+
error: e.message,
99+
imported_count: 0,
100+
total_count: 0,
101+
skipped_count: 0,
102+
errors: [ e.message ]
103+
}
104+
end
105+
end

app/views/users/edit.html.erb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,38 @@
420420
</div>
421421
</div>
422422

423+
<% dev_tool do %>
424+
<div class="p-6 bg-gray-800 border border-gray-600 rounded">
425+
<div class="flex items-center gap-3 mb-3">
426+
<div class="p-2 bg-green-600/10 rounded">
427+
<svg class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
428+
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
429+
</svg>
430+
</div>
431+
<h4 class="text-white font-medium">Import Heartbeat Data</h4>
432+
</div>
433+
<p class="text-gray-300 text-sm mb-4">Import ur data from real hackatime to test stuff with.</p>
434+
<p class="text-gray-300 text-sm mb-4">PS: your console will be spammed and might crash ur dev env so be carefull if the file is very big</p>
435+
<%= form_with url: import_my_heartbeats_path, method: :post, multipart: true, local: true, class: "space-y-4" do |form| %>
436+
<div>
437+
<label class="block text-sm font-medium text-gray-300 mb-2">Select JSON File</label>
438+
<%= form.file_field :heartbeat_file,
439+
accept: ".json,application/json",
440+
class: "w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-white hover:file:bg-red transition-colors",
441+
required: true %>
442+
</div>
443+
444+
<div class="flex gap-3">
445+
<%= form.submit "Import Heartbeats",
446+
class: "bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded font-medium transition-colors inline-flex items-center gap-2",
447+
data: { confirm: "Are you sure you want to import heartbeats? This will add new data to your account." } %>
448+
</div>
449+
<% end %>
450+
451+
452+
</div>
453+
<% end %>
454+
423455

424456
</div>
425457
</div>

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def matches?(request)
113113
resources :heartbeats, only: [] do
114114
collection do
115115
get :export
116+
post :import
116117
end
117118
end
118119
end

db/seeds.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
# Creating test user
88
test_user = User.find_or_create_by(slack_uid: 'TEST123456') do |user|
99
user.username = 'testuser'
10-
user.is_admin = true
10+
11+
# Before you had user.is_admin = true, does not work, changed it to that, looks like it works but idk how to use the admin pages so pls check this, i just guess coded this, the cmd to seed the db works without errors
12+
user.set_admin_level(:superadmin)
1113
# Ensure timezone is set to avoid nil timezone issues
1214
user.timezone = 'America/New_York'
1315
end

0 commit comments

Comments
 (0)