From 4a470667b6290853dc553e734eeac2a9aff2956a Mon Sep 17 00:00:00 2001
From: ShyMike <122023566+ImShyMike@users.noreply.github.com>
Date: Sat, 18 Oct 2025 18:40:52 +0100
Subject: [PATCH 1/7] allow username overwriting
---
app/models/user.rb | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/app/models/user.rb b/app/models/user.rb
index 407e6a4b..65367cb6 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -372,8 +372,7 @@ def self.from_slack_token(code, redirect_uri)
end
user.slack_uid = data.dig("authed_user", "id")
- user.username ||= user_data.dig("user", "profile", "username")
- user.username ||= user_data.dig("user", "profile", "display_name_normalized")
+ user.username = user_data.dig("user", "profile", "username") || user_data.dig("user", "profile", "display_name_normalized")
user.slack_username = user_data.dig("user", "profile", "username")
user.slack_avatar_url = user_data.dig("user", "profile", "image_192") || user_data.dig("user", "profile", "image_72")
@@ -427,7 +426,7 @@ def self.from_github_token(code, redirect_uri, current_user)
# Update GitHub-specific fields
current_user.github_uid = github_uid
- current_user.username ||= user_data["login"]
+ current_user.username = user_data["login"]
current_user.github_username = user_data["login"]
current_user.github_avatar_url = user_data["avatar_url"]
current_user.github_access_token = data["access_token"]
From 7664daba45b06b612339bfddf1056ce4889f2df5 Mon Sep 17 00:00:00 2001
From: ShyMike <122023566+ImShyMike@users.noreply.github.com>
Date: Sun, 19 Oct 2025 10:40:41 +0100
Subject: [PATCH 2/7] add custom display name support
---
app/controllers/admin/timeline_controller.rb | 10 ++--
.../trust_level_audit_logs_controller.rb | 4 +-
.../api/admin/v1/admin_controller.rb | 16 +++----
.../api/v1/external_slack_controller.rb | 4 +-
app/controllers/sessions_controller.rb | 2 +-
app/controllers/static_pages_controller.rb | 2 +-
app/controllers/users_controller.rb | 41 ++++++++++------
app/jobs/cache/currently_hacking_job.rb | 4 +-
app/models/user.rb | 47 +++++++++++++++----
app/views/admin/timeline/show.html.erb | 2 +-
app/views/shared/_user_mention.html.erb | 22 +++++----
app/views/users/edit.html.erb | 34 ++++++++++++--
db/migrate/20251018181955_add_custom_name.rb | 5 ++
db/schema.rb | 3 +-
db/seeds.rb | 5 +-
lib/flavor_text.rb | 4 +-
lib/test_wakatime_service.rb | 2 +-
lib/wakatime_service.rb | 2 +-
18 files changed, 145 insertions(+), 64 deletions(-)
create mode 100644 db/migrate/20251018181955_add_custom_name.rb
diff --git a/app/controllers/admin/timeline_controller.rb b/app/controllers/admin/timeline_controller.rb
index 64ae526c..846a2d55 100644
--- a/app/controllers/admin/timeline_controller.rb
+++ b/app/controllers/admin/timeline_controller.rb
@@ -135,7 +135,7 @@ def show
# For Stimulus: provide initial selected users with details
@initial_selected_user_objects = User.where(id: @selected_user_ids)
- .select(:id, :username, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
+ .select(:id, :custom_name, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
.map { |u| { id: u.id, display_name: "#{u.display_name}", avatar_url: u.avatar_url } }
.sort_by { |u_obj| @selected_user_ids.index(u_obj[:id]) || Float::INFINITY } # Preserve order
@@ -185,10 +185,10 @@ def search_users
avatar_url: user_id_match.avatar_url
} ]
else
- users = User.where("LOWER(username) LIKE :query OR LOWER(slack_username) LIKE :query OR CAST(id AS TEXT) LIKE :query OR EXISTS (SELECT 1 FROM email_addresses WHERE email_addresses.user_id = users.id AND LOWER(email_addresses.email) LIKE :query)", query: "%#{query_term}%")
- .order(Arel.sql("CASE WHEN LOWER(username) = #{ActiveRecord::Base.connection.quote(query_term)} THEN 0 ELSE 1 END, username ASC")) # Prioritize exact match
+ users = User.where("LOWER(custom_name) LIKE :query OR LOWER(slack_username) LIKE :query OR CAST(id AS TEXT) LIKE :query OR EXISTS (SELECT 1 FROM email_addresses WHERE email_addresses.user_id = users.id AND LOWER(email_addresses.email) LIKE :query)", query: "%#{query_term}%")
+ .order(Arel.sql("CASE WHEN LOWER(custom_name) = #{ActiveRecord::Base.connection.quote(query_term)} THEN 0 ELSE 1 END, username ASC")) # Prioritize exact match
.limit(20)
- .select(:id, :username, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
+ .select(:id, :custom_name, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
results = users.map do |user|
{
@@ -218,7 +218,7 @@ def leaderboard_users
all_ids_to_fetch.unshift(current_user.id).uniq!
users_data = User.where(id: all_ids_to_fetch)
- .select(:id, :username, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
+ .select(:id, :custom_name, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
.index_by(&:id)
final_user_objects = []
diff --git a/app/controllers/admin/trust_level_audit_logs_controller.rb b/app/controllers/admin/trust_level_audit_logs_controller.rb
index a9257983..60959f55 100644
--- a/app/controllers/admin/trust_level_audit_logs_controller.rb
+++ b/app/controllers/admin/trust_level_audit_logs_controller.rb
@@ -25,7 +25,7 @@ def index
if params[:user_search].present?
search_term = params[:user_search].strip
user_ids = User.joins(:email_addresses)
- .where("LOWER(users.username) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
+ .where("LOWER(users.custom_name) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
"%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term}%")
.pluck(:id)
@audit_logs = @audit_logs.where(user_id: user_ids)
@@ -35,7 +35,7 @@ def index
if params[:admin_search].present?
search_term = params[:admin_search].strip
admin_ids = User.joins(:email_addresses)
- .where("LOWER(users.username) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
+ .where("LOWER(users.custom_name) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
"%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term}%")
.pluck(:id)
@audit_logs = @audit_logs.where(changed_by_id: admin_ids)
diff --git a/app/controllers/api/admin/v1/admin_controller.rb b/app/controllers/api/admin/v1/admin_controller.rb
index b6a2f2ea..f95a8de6 100644
--- a/app/controllers/api/admin/v1/admin_controller.rb
+++ b/app/controllers/api/admin/v1/admin_controller.rb
@@ -15,7 +15,7 @@ def check
},
creator: {
id: creator.id,
- username: creator.username,
+ username: creator.name,
display_name: creator.display_name,
admin_level: creator.admin_level
}
@@ -75,7 +75,7 @@ def user_info
render json: {
user: {
id: user.id,
- username: user.username,
+ username: user.name,
display_name: user.display_name,
slack_uid: user.slack_uid,
slack_username: user.slack_username,
@@ -118,7 +118,7 @@ def user_stats
render json: {
user_id: user.id,
- username: user.username,
+ username: user.name,
date: date.iso8601,
timezone: user.timezone,
heartbeats: heartbeats.map do |hb|
@@ -184,7 +184,7 @@ def user_projects
render json: {
user_id: user.id,
- username: user.username,
+ username: user.name,
projects: project_data,
total_projects: project_data.count
}
@@ -223,12 +223,12 @@ def user_convict
message: "gotcha, updated to #{trust_level}",
user: {
id: user.id,
- username: user.username,
+ username: user.name,
trust_level: user.trust_level,
updated_at: user.updated_at
},
audit_log: {
- changed_by: current_user.username,
+ changed_by: current_user.name,
reason: reason,
notes: notes,
timestamp: Time.current
@@ -276,7 +276,7 @@ def execute
columns: columns,
rows: rows,
row_count: rows.count,
- executed_by: current_user.username,
+ executed_by: current_user.name,
executed_at: Time.current
}
rescue => e
@@ -299,7 +299,7 @@ def trust_logs
new_trust_level: log.new_trust_level,
changed_by: {
id: log.changed_by.id,
- username: log.changed_by.username,
+ username: log.changed_by.name,
display_name: log.changed_by.display_name,
admin_level: log.changed_by.admin_level
},
diff --git a/app/controllers/api/v1/external_slack_controller.rb b/app/controllers/api/v1/external_slack_controller.rb
index 5218dda3..cc42dfc9 100644
--- a/app/controllers/api/v1/external_slack_controller.rb
+++ b/app/controllers/api/v1/external_slack_controller.rb
@@ -23,7 +23,7 @@ def create_user
if user.persisted?
return render json: {
user_id: user.id,
- username: user.username,
+ username: user.name,
email: user.email_addresses.first&.email
}, status: :ok
end
@@ -46,7 +46,7 @@ def create_user
if user.save
render json: {
user_id: user.id,
- username: user.username,
+ username: user.name,
email: email
}, status: :created
else
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 2e632d9e..cf4e9fee 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -191,7 +191,7 @@ def impersonate
session[:impersonater_user_id] ||= current_user.id
session[:user_id] = user.id
- redirect_to root_path, notice: "Impersonating #{user.username}"
+ redirect_to root_path, notice: "Impersonating #{user.name}"
end
def stop_impersonating
diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb
index 34f7f60f..224ff312 100644
--- a/app/controllers/static_pages_controller.rb
+++ b/app/controllers/static_pages_controller.rb
@@ -170,7 +170,7 @@ def currently_hacking
json_response = locals[:users].map do |user|
{
id: user.id,
- username: user.username,
+ username: user.name,
slack_username: user.slack_username,
github_username: user.github_username,
display_name: user.display_name,
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 267ab0e7..6a450b6d 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -6,17 +6,7 @@ class UsersController < ApplicationController
before_action :require_admin, only: [ :update_trust_level ]
def edit
- @can_enable_slack_status = @user.slack_access_token.present? && @user.slack_scopes.include?("users.profile:write")
-
- @enabled_sailors_logs = SailorsLogNotificationPreference.where(
- slack_uid: @user.slack_uid,
- enabled: true,
- ).where.not(slack_channel_id: SailorsLog::DEFAULT_CHANNELS)
-
- @heartbeats_migration_jobs = @user.data_migration_jobs
-
- @projects = @user.project_repo_mappings.distinct.pluck(:project_name)
- @work_time_stats_url = "https://hackatime-badge.hackclub.com/#{@user.slack_uid}/#{@projects.first || 'example'}"
+ prepare_settings_page
end
def update
@@ -29,8 +19,9 @@ def update
redirect_to is_own_settings? ? my_settings_path : settings_user_path(@user),
notice: "Settings updated successfully"
else
- flash[:error] = "Failed to update settings"
- render :settings, status: :unprocessable_entity
+ flash.now[:error] = @user.errors.full_messages.to_sentence.presence || "Failed to update settings"
+ prepare_settings_page
+ render :edit, status: :unprocessable_entity
end
elsif params[:default_timezone_leaderboard].present?
if @user.update(default_timezone_leaderboard: params[:default_timezone_leaderboard] == "1")
@@ -126,6 +117,21 @@ def require_current_user
end
end
+ def prepare_settings_page
+ @is_own_settings = is_own_settings?
+ @can_enable_slack_status = @user.slack_access_token.present? && @user.slack_scopes.include?("users.profile:write")
+
+ @enabled_sailors_logs = SailorsLogNotificationPreference.where(
+ slack_uid: @user.slack_uid,
+ enabled: true,
+ ).where.not(slack_channel_id: SailorsLog::DEFAULT_CHANNELS)
+
+ @heartbeats_migration_jobs = @user.data_migration_jobs
+
+ @projects = @user.project_repo_mappings.distinct.pluck(:project_name)
+ @work_time_stats_url = "https://hackatime-badge.hackclub.com/#{@user.slack_uid}/#{@projects.first || 'example'}"
+ end
+
def set_user
@user = if params["id"].present?
User.find(params["id"])
@@ -141,6 +147,13 @@ def is_own_settings?
end
def user_params
- params.require(:user).permit(:uses_slack_status, :hackatime_extension_text_type, :timezone, :allow_public_stats_lookup, :default_timezone_leaderboard)
+ params.require(:user).permit(
+ :uses_slack_status,
+ :hackatime_extension_text_type,
+ :timezone,
+ :allow_public_stats_lookup,
+ :default_timezone_leaderboard,
+ :custom_name,
+ )
end
end
diff --git a/app/jobs/cache/currently_hacking_job.rb b/app/jobs/cache/currently_hacking_job.rb
index 74f2e7bd..bd4be025 100644
--- a/app/jobs/cache/currently_hacking_job.rb
+++ b/app/jobs/cache/currently_hacking_job.rb
@@ -29,9 +29,7 @@ def calculate
users = users.sort_by do |user|
[
active_projects[user.id].present? ? 0 : 1,
- user.username.present? ? 0 : 1,
- user.slack_username.present? ? 0 : 1,
- user.github_username.present? ? 0 : 1
+ user.name.present? ? 0 : 1
]
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 65367cb6..f014ce86 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,15 +2,23 @@ class User < ApplicationRecord
include TimezoneRegions
include PublicActivity::Model
+ CUSTOM_NAME_MAX_LENGTH = 21 # going over 21 overflows the navbar
+
has_paper_trail
after_create :create_signup_activity
+ before_validation :normalize_custom_name
encrypts :slack_access_token, :github_access_token
validates :slack_uid, uniqueness: true, allow_nil: true
validates :github_uid, uniqueness: { conditions: -> { where.not(github_access_token: nil) } }, allow_nil: true
validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers }, allow_nil: false
validates :country_code, inclusion: { in: ISO3166::Country.codes }, allow_nil: true
+ validates :custom_name,
+ length: { maximum: CUSTOM_NAME_MAX_LENGTH },
+ format: { with: /\A[A-Za-z0-9_-]+\z/, message: "may only include letters, numbers, '-', and '_'" },
+ allow_nil: true
+ validate :custom_name_must_be_visible
attribute :allow_public_stats_lookup, :boolean, default: true
attribute :default_timezone_leaderboard, :boolean, default: true
@@ -242,8 +250,6 @@ def update_from_slack
self.slack_username = profile.dig("username").presence
self.slack_username ||= profile.dig("display_name_normalized").presence
self.slack_username ||= profile.dig("real_name_normalized").presence
-
- self.username.blank? && self.username = self.slack_username
end
def update_slack_status
@@ -372,8 +378,9 @@ def self.from_slack_token(code, redirect_uri)
end
user.slack_uid = data.dig("authed_user", "id")
- user.username = user_data.dig("user", "profile", "username") || user_data.dig("user", "profile", "display_name_normalized")
- user.slack_username = user_data.dig("user", "profile", "username")
+ user.slack_username = user_data.dig("username").presence
+ user.slack_username ||= user_data.dig("display_name_normalized").presence
+ user.slack_username ||= user_data.dig("real_name_normalized").presence
user.slack_avatar_url = user_data.dig("user", "profile", "image_192") || user_data.dig("user", "profile", "image_72")
user.parse_and_set_timezone(user_data.dig("user", "tz"))
@@ -426,7 +433,6 @@ def self.from_github_token(code, redirect_uri, current_user)
# Update GitHub-specific fields
current_user.github_uid = github_uid
- current_user.username = user_data["login"]
current_user.github_username = user_data["login"]
current_user.github_avatar_url = user_data["avatar_url"]
current_user.github_access_token = data["access_token"]
@@ -459,9 +465,7 @@ def avatar_url
end
def display_name
- return slack_username.presence.truncate(10) if slack_username.present?
- return github_username.presence.truncate(10) if github_username.present?
- return username.presence.truncate(10) if username.present?
+ return name.presence.truncate(10) if name.present?
# "zach@hackclub.com" -> "zach (email sign-up)"
email = email_addresses&.first&.email
@@ -470,6 +474,10 @@ def display_name
email.split("@")&.first.truncate(10) + " (email sign-up)"
end
+ def name
+ custom_name || slack_username || github_username
+ end
+
def most_recent_direct_entry_heartbeat
heartbeats.where(source_type: :direct_entry).order(time: :desc).first
end
@@ -508,4 +516,27 @@ def invalidate_activity_graph_cache
def create_signup_activity
create_activity :first_signup, owner: self
end
+
+ def normalize_custom_name
+ original = custom_name
+ @custom_name_cleared_for_invisible = false
+
+ return if original.nil?
+
+ cleaned = original.gsub(/\p{Cf}/, "")
+ stripped = cleaned.strip
+
+ if stripped.empty?
+ self.custom_name = nil
+ @custom_name_cleared_for_invisible = original.length.positive?
+ else
+ self.custom_name = stripped
+ end
+ end
+
+ def custom_name_must_be_visible
+ if instance_variable_defined?(:@custom_name_cleared_for_invisible) && @custom_name_cleared_for_invisible
+ errors.add(:custom_name, "must include visible characters")
+ end
+ end
end
diff --git a/app/views/admin/timeline/show.html.erb b/app/views/admin/timeline/show.html.erb
index da881341..a7c0e722 100644
--- a/app/views/admin/timeline/show.html.erb
+++ b/app/views/admin/timeline/show.html.erb
@@ -165,7 +165,7 @@
+ title="User ID: <%= user.id %> - <%= user.respond_to?(:name) && user.name.present? ? h(user.name) : h(user.email_addresses.first&.email) %> | Total Coded: <%= total_coded_time_seconds && total_coded_time_seconds > 0 ? short_time_detailed(total_coded_time_seconds) : '0m' %> | TZ: <%= h(user.timezone) %>">
<%= render "shared/user_mention", user: user %>
diff --git a/app/views/shared/_user_mention.html.erb b/app/views/shared/_user_mention.html.erb
index b89b2d4a..869cc9c7 100644
--- a/app/views/shared/_user_mention.html.erb
+++ b/app/views/shared/_user_mention.html.erb
@@ -1,14 +1,23 @@
-
+
<%= image_tag user.avatar_url,
size: "32x32",
class: "rounded-full aspect-square border border-gray-300",
- alt: "#{h(user.username)}'s avatar" if user.avatar_url %>
+ alt: "#{h(user.name)}'s avatar" if user.avatar_url %>
<% if local_assigns.fetch(:show, []).include?(:slack) && user.slack_uid.present? %>
- <%= link_to "@#{h(user.display_name)}", "https://hackclub.slack.com/team/#{user.slack_uid}", target: "_blank", class: "text-blue-500 hover:underline" %>
+ <%= link_to "@#{h(user.name)}", "https://hackclub.slack.com/team/#{user.slack_uid}", target: "_blank", class: "text-blue-500 hover:underline" %>
<% else %>
- <%= h(user.display_name) %>
+ <%= h(user.name) %>
<% end %>
+ <% if user.country_code.present? %>
+
+ <%= country_to_emoji(user.country_code) %>
+
+ <% end %>
<%# GitHub profile link - shown if :github_profile_link is in the :show array %>
<% if github_url = user.github_profile_url # Assign to variable to avoid calling method twice %>
<% if local_assigns.fetch(:show, []).include?(:github_profile_link) %>
@@ -18,11 +27,6 @@
<% end %>
<% end %>
- <% if user.country_code.present? %>
-
- <%= country_to_emoji(user.country_code) %>
-
- <% end %>
<% if local_assigns.fetch(:show, []).include?(:neighborhood) && user.slack_neighborhood_channel.present? %>
<%= link_to "🏘️", "https://hackclub.slack.com/archives/#{user.slack_neighborhood_channel}", target: "_blank" %>
<% end %>
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index 23f1fe08..6abce4e1 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -1,17 +1,17 @@
<% content_for :title do %>
- <%= @is_own_settings ? "My Settings" : "Settings | #{@user.username}" %>
+ <%= @is_own_settings ? "My Settings" : "Settings | #{@user.name}" %>
<% end %>
- <%= @is_own_settings ? "My Settings" : "Settings for #{@user.username}" %>
+ <%= @is_own_settings ? "My Settings" : "Settings for #{@user.name}" %>
Change your Hackatime experience and preferences
-
+
🚀
@@ -68,6 +68,34 @@
<% end %>
+
+
+ <%= form_with model: @user,
+ url: @is_own_settings ? my_settings_path : settings_user_path(@user),
+ method: :patch, local: false,
+ class: "space-y-4" do |f| %>
+
+ <%= f.label :custom_name, "Custom display name", class: "block text-sm font-medium text-gray-200 mb-2" %>
+ <%= f.text_field :custom_name,
+ class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-primary focus:ring-1 focus:ring-primary",
+ placeholder: "HackClubber",
+ maxlength: User::CUSTOM_NAME_MAX_LENGTH %>
+ <% if @user.errors[:custom_name].present? %>
+
<%= @user.errors[:custom_name].to_sentence %>
+ <% end %>
+
+
+ Choose a name to use in Hackatime. This will take priority over Slack or GitHub names when possible. Letters, numbers, "-" and "_" only, max <%= User::CUSTOM_NAME_MAX_LENGTH %> chars.
+
+ <%= f.submit "Save Settings", class: "w-full px-4 py-2 bg-primary text-white font-medium rounded transition-colors duration-200 cursor-pointer" %>
+ <% end %>
+
+
diff --git a/db/migrate/20251018181955_add_custom_name.rb b/db/migrate/20251018181955_add_custom_name.rb
new file mode 100644
index 00000000..8883d893
--- /dev/null
+++ b/db/migrate/20251018181955_add_custom_name.rb
@@ -0,0 +1,5 @@
+class AddCustomName < ActiveRecord::Migration[8.0]
+ def change
+ add_column :users, :custom_name, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ef386860..ce29e3e2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2025_10_03_215127) do
+ActiveRecord::Schema[8.0].define(version: 2025_10_18_181955) do
create_schema "pganalyze"
# These are extensions that must be enabled in order to support this database
@@ -562,6 +562,7 @@
t.boolean "allow_public_stats_lookup", default: true, null: false
t.boolean "default_timezone_leaderboard", default: true, null: false
t.integer "admin_level", default: 0, null: false
+ t.string "custom_name"
t.index ["github_uid", "github_access_token"], name: "index_users_on_github_uid_and_access_token"
t.index ["github_uid"], name: "index_users_on_github_uid"
t.index ["slack_uid"], name: "index_users_on_slack_uid", unique: true
diff --git a/db/seeds.rb b/db/seeds.rb
index 11a31e74..40bfad35 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -14,7 +14,8 @@
if Rails.env.development?
# Creating test user
test_user = User.find_or_create_by(slack_uid: 'TEST123456') do |user|
- user.username = 'testuser'
+ user.custom_name = 'testuser'
+ user.slack_username = 'testuser'
# 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
user.set_admin_level(:superadmin)
@@ -38,7 +39,7 @@
end
puts "Created test user:"
- puts " Username: #{test_user.username}"
+ puts " Username: #{test_user.name}"
puts " Email: #{email.email}"
puts " API Key: #{api_key.token}"
puts " Sign-in Token: #{token.token}"
diff --git a/lib/flavor_text.rb b/lib/flavor_text.rb
index 61c80ca1..a7ed1c14 100644
--- a/lib/flavor_text.rb
+++ b/lib/flavor_text.rb
@@ -246,8 +246,8 @@ def self.conditional_mottos(user)
r = []
r << "quit slacking off!" if user.slack_uid.present?
- r << "in the nick of time!" if %w[nick nicholas nickolas].include?(user.username)
- r << "just-in time!" if %w[justin justine].include?(user.username)
+ r << "in the nick of time!" if %w[nick nicholas nickolas].include?(user.name)
+ r << "just-in time!" if %w[justin justine].include?(user.name)
minutes_logged = Cache::MinutesLoggedJob.perform_now
r << "in the past hour, #{minutes_logged} minutes have passed" if minutes_logged > 0
diff --git a/lib/test_wakatime_service.rb b/lib/test_wakatime_service.rb
index 9295e6b0..722d44fe 100644
--- a/lib/test_wakatime_service.rb
+++ b/lib/test_wakatime_service.rb
@@ -33,7 +33,7 @@ def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, st
def generate_summary
summary = {}
- summary[:username] = @user.username if @user.present?
+ summary[:username] = @user.name if @user.present?
summary[:user_id] = @user.id.to_s if @user.present?
summary[:is_coding_activity_visible] = true if @user.present?
summary[:is_other_usage_visible] = true if @user.present?
diff --git a/lib/wakatime_service.rb b/lib/wakatime_service.rb
index 908de9b9..53bca297 100644
--- a/lib/wakatime_service.rb
+++ b/lib/wakatime_service.rb
@@ -26,7 +26,7 @@ def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, st
def generate_summary
summary = {}
- summary[:username] = @user.username if @user.present?
+ summary[:username] = @user.name if @user.present?
summary[:user_id] = @user.id.to_s if @user.present?
summary[:is_coding_activity_visible] = true if @user.present?
summary[:is_other_usage_visible] = true if @user.present?
From 9dcca8f8d89bd11277acf161d115b015f993fa48 Mon Sep 17 00:00:00 2001
From: ShyMike <122023566+ImShyMike@users.noreply.github.com>
Date: Sun, 19 Oct 2025 11:04:18 +0100
Subject: [PATCH 3/7] fix username overwriting
---
app/models/user.rb | 27 +++++++++++++++------------
1 file changed, 15 insertions(+), 12 deletions(-)
diff --git a/app/models/user.rb b/app/models/user.rb
index f014ce86..ed88ec44 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -243,13 +243,13 @@ def update_from_slack
return unless user_data.present?
- profile = user_data.dig("profile")
+ profile = user_data["profile"] || {}
- self.slack_avatar_url = profile.dig("image_192") || profile.dig("image_72")
+ self.slack_avatar_url = profile["image_192"] || profile["image_72"]
- self.slack_username = profile.dig("username").presence
- self.slack_username ||= profile.dig("display_name_normalized").presence
- self.slack_username ||= profile.dig("real_name_normalized").presence
+ self.slack_username = user_data["name"].presence
+ self.slack_username ||= profile["display_name_normalized"].presence
+ self.slack_username ||= profile["real_name_normalized"].presence
end
def update_slack_status
@@ -365,7 +365,10 @@ def self.from_slack_token(code, redirect_uri)
return nil unless user_data["ok"]
- email = user_data.dig("user", "profile", "email")&.downcase
+ slack_user = user_data["user"] || {}
+ profile = slack_user["profile"] || {}
+
+ email = profile["email"]&.downcase
email_address = EmailAddress.find_or_initialize_by(email: email)
email_address.source ||= :slack
user = email_address.user
@@ -378,12 +381,12 @@ def self.from_slack_token(code, redirect_uri)
end
user.slack_uid = data.dig("authed_user", "id")
- user.slack_username = user_data.dig("username").presence
- user.slack_username ||= user_data.dig("display_name_normalized").presence
- user.slack_username ||= user_data.dig("real_name_normalized").presence
- user.slack_avatar_url = user_data.dig("user", "profile", "image_192") || user_data.dig("user", "profile", "image_72")
+ user.slack_username = slack_user["name"].presence
+ user.slack_username ||= profile["display_name_normalized"].presence
+ user.slack_username ||= profile["real_name_normalized"].presence
+ user.slack_avatar_url = profile["image_192"] || profile["image_72"]
- user.parse_and_set_timezone(user_data.dig("user", "tz"))
+ user.parse_and_set_timezone(slack_user["tz"])
user.slack_access_token = data["authed_user"]["access_token"]
user.slack_scopes = data["authed_user"]["scope"]&.split(/,\s*/)
@@ -433,7 +436,7 @@ def self.from_github_token(code, redirect_uri, current_user)
# Update GitHub-specific fields
current_user.github_uid = github_uid
- current_user.github_username = user_data["login"]
+ current_user.github_username = user_data["login"].presence || user_data["name"].presence
current_user.github_avatar_url = user_data["avatar_url"]
current_user.github_access_token = data["access_token"]
From e2401f1677ea73736cca66bb3563b34d847531f4 Mon Sep 17 00:00:00 2001
From: ShyMike <122023566+ImShyMike@users.noreply.github.com>
Date: Sun, 19 Oct 2025 11:16:43 +0100
Subject: [PATCH 4/7] move country code flag back to where it was
---
app/views/shared/_user_mention.html.erb | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/views/shared/_user_mention.html.erb b/app/views/shared/_user_mention.html.erb
index 869cc9c7..79c701b1 100644
--- a/app/views/shared/_user_mention.html.erb
+++ b/app/views/shared/_user_mention.html.erb
@@ -13,11 +13,6 @@ end %>">
<% else %>
<%= h(user.name) %>
<% end %>
- <% if user.country_code.present? %>
-
- <%= country_to_emoji(user.country_code) %>
-
- <% end %>
<%# GitHub profile link - shown if :github_profile_link is in the :show array %>
<% if github_url = user.github_profile_url # Assign to variable to avoid calling method twice %>
<% if local_assigns.fetch(:show, []).include?(:github_profile_link) %>
@@ -27,6 +22,11 @@ end %>">
<% end %>
<% end %>
+ <% if user.country_code.present? %>
+
+ <%= country_to_emoji(user.country_code) %>
+
+ <% end %>
<% if local_assigns.fetch(:show, []).include?(:neighborhood) && user.slack_neighborhood_channel.present? %>
<%= link_to "🏘️", "https://hackclub.slack.com/archives/#{user.slack_neighborhood_channel}", target: "_blank" %>
<% end %>
From 109ae988a0a649d053e1045a0dd15b4d578bbeb6 Mon Sep 17 00:00:00 2001
From: ShyMike <122023566+ImShyMike@users.noreply.github.com>
Date: Tue, 21 Oct 2025 21:28:35 +0100
Subject: [PATCH 5/7] deprecate username + name cleanup
---
app/controllers/api/admin/v1/admin_controller.rb | 14 +++++++-------
.../api/v1/external_slack_controller.rb | 4 ++--
app/controllers/sessions_controller.rb | 2 +-
app/controllers/static_pages_controller.rb | 2 +-
app/jobs/cache/currently_hacking_job.rb | 2 +-
app/models/user.rb | 7 ++-----
app/views/admin/timeline/show.html.erb | 2 +-
app/views/shared/_user_mention.html.erb | 6 +++---
app/views/users/edit.html.erb | 4 ++--
db/migrate/20251021202329_deprecate_username.rb | 5 +++++
db/schema.rb | 4 ++--
db/seeds.rb | 2 +-
lib/flavor_text.rb | 4 ++--
lib/test_wakatime_service.rb | 2 +-
lib/wakatime_service.rb | 2 +-
15 files changed, 32 insertions(+), 30 deletions(-)
create mode 100644 db/migrate/20251021202329_deprecate_username.rb
diff --git a/app/controllers/api/admin/v1/admin_controller.rb b/app/controllers/api/admin/v1/admin_controller.rb
index f95a8de6..42afaf27 100644
--- a/app/controllers/api/admin/v1/admin_controller.rb
+++ b/app/controllers/api/admin/v1/admin_controller.rb
@@ -15,7 +15,7 @@ def check
},
creator: {
id: creator.id,
- username: creator.name,
+ username: creator.custom_name,
display_name: creator.display_name,
admin_level: creator.admin_level
}
@@ -75,7 +75,7 @@ def user_info
render json: {
user: {
id: user.id,
- username: user.name,
+ username: user.custom_name,
display_name: user.display_name,
slack_uid: user.slack_uid,
slack_username: user.slack_username,
@@ -118,7 +118,7 @@ def user_stats
render json: {
user_id: user.id,
- username: user.name,
+ username: user.display_name,
date: date.iso8601,
timezone: user.timezone,
heartbeats: heartbeats.map do |hb|
@@ -184,7 +184,7 @@ def user_projects
render json: {
user_id: user.id,
- username: user.name,
+ username: user.display_name,
projects: project_data,
total_projects: project_data.count
}
@@ -223,12 +223,12 @@ def user_convict
message: "gotcha, updated to #{trust_level}",
user: {
id: user.id,
- username: user.name,
+ username: user.display_name,
trust_level: user.trust_level,
updated_at: user.updated_at
},
audit_log: {
- changed_by: current_user.name,
+ changed_by: current_user.display_name,
reason: reason,
notes: notes,
timestamp: Time.current
@@ -276,7 +276,7 @@ def execute
columns: columns,
rows: rows,
row_count: rows.count,
- executed_by: current_user.name,
+ executed_by: current_user.display_name,
executed_at: Time.current
}
rescue => e
diff --git a/app/controllers/api/v1/external_slack_controller.rb b/app/controllers/api/v1/external_slack_controller.rb
index cc42dfc9..cefaac59 100644
--- a/app/controllers/api/v1/external_slack_controller.rb
+++ b/app/controllers/api/v1/external_slack_controller.rb
@@ -23,7 +23,7 @@ def create_user
if user.persisted?
return render json: {
user_id: user.id,
- username: user.name,
+ username: user.display_name,
email: user.email_addresses.first&.email
}, status: :ok
end
@@ -46,7 +46,7 @@ def create_user
if user.save
render json: {
user_id: user.id,
- username: user.name,
+ username: user.display_name,
email: email
}, status: :created
else
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index cf4e9fee..073ca3c6 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -191,7 +191,7 @@ def impersonate
session[:impersonater_user_id] ||= current_user.id
session[:user_id] = user.id
- redirect_to root_path, notice: "Impersonating #{user.name}"
+ redirect_to root_path, notice: "Impersonating #{user.display_name}"
end
def stop_impersonating
diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb
index 224ff312..07f9cac1 100644
--- a/app/controllers/static_pages_controller.rb
+++ b/app/controllers/static_pages_controller.rb
@@ -170,7 +170,7 @@ def currently_hacking
json_response = locals[:users].map do |user|
{
id: user.id,
- username: user.name,
+ username: user.display_name,
slack_username: user.slack_username,
github_username: user.github_username,
display_name: user.display_name,
diff --git a/app/jobs/cache/currently_hacking_job.rb b/app/jobs/cache/currently_hacking_job.rb
index bd4be025..a0ea360a 100644
--- a/app/jobs/cache/currently_hacking_job.rb
+++ b/app/jobs/cache/currently_hacking_job.rb
@@ -29,7 +29,7 @@ def calculate
users = users.sort_by do |user|
[
active_projects[user.id].present? ? 0 : 1,
- user.name.present? ? 0 : 1
+ user.display_name.present? ? 0 : 1
]
end
diff --git a/app/models/user.rb b/app/models/user.rb
index ed88ec44..94514547 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -468,7 +468,8 @@ def avatar_url
end
def display_name
- return name.presence.truncate(10) if name.present?
+ name = custom_name || slack_username || github_username
+ return name if name.present?
# "zach@hackclub.com" -> "zach (email sign-up)"
email = email_addresses&.first&.email
@@ -477,10 +478,6 @@ def display_name
email.split("@")&.first.truncate(10) + " (email sign-up)"
end
- def name
- custom_name || slack_username || github_username
- end
-
def most_recent_direct_entry_heartbeat
heartbeats.where(source_type: :direct_entry).order(time: :desc).first
end
diff --git a/app/views/admin/timeline/show.html.erb b/app/views/admin/timeline/show.html.erb
index a7c0e722..06be2b27 100644
--- a/app/views/admin/timeline/show.html.erb
+++ b/app/views/admin/timeline/show.html.erb
@@ -165,7 +165,7 @@
+ title="User ID: <%= user.id %> - <%= user.respond_to?(:display_name) && user.display_name.present? ? h(user.display_name) : h(user.email_addresses.first&.email) %> | Total Coded: <%= total_coded_time_seconds && total_coded_time_seconds > 0 ? short_time_detailed(total_coded_time_seconds) : '0m' %> | TZ: <%= h(user.timezone) %>">
<%= render "shared/user_mention", user: user %>
diff --git a/app/views/shared/_user_mention.html.erb b/app/views/shared/_user_mention.html.erb
index 79c701b1..7e52bbfa 100644
--- a/app/views/shared/_user_mention.html.erb
+++ b/app/views/shared/_user_mention.html.erb
@@ -6,12 +6,12 @@ end %>">
<%= image_tag user.avatar_url,
size: "32x32",
class: "rounded-full aspect-square border border-gray-300",
- alt: "#{h(user.name)}'s avatar" if user.avatar_url %>
+ alt: "#{h(user.display_name)}'s avatar" if user.avatar_url %>
<% if local_assigns.fetch(:show, []).include?(:slack) && user.slack_uid.present? %>
- <%= link_to "@#{h(user.name)}", "https://hackclub.slack.com/team/#{user.slack_uid}", target: "_blank", class: "text-blue-500 hover:underline" %>
+ <%= link_to "@#{h(user.display_name)}", "https://hackclub.slack.com/team/#{user.slack_uid}", target: "_blank", class: "text-blue-500 hover:underline" %>
<% else %>
- <%= h(user.name) %>
+ <%= h(user.display_name) %>
<% end %>
<%# GitHub profile link - shown if :github_profile_link is in the :show array %>
<% if github_url = user.github_profile_url # Assign to variable to avoid calling method twice %>
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index 6abce4e1..73b0ae44 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -1,11 +1,11 @@
<% content_for :title do %>
- <%= @is_own_settings ? "My Settings" : "Settings | #{@user.name}" %>
+ <%= @is_own_settings ? "My Settings" : "Settings | #{@user.display_name}" %>
<% end %>
- <%= @is_own_settings ? "My Settings" : "Settings for #{@user.name}" %>
+ <%= @is_own_settings ? "My Settings" : "Settings for #{@user.display_name}" %>
Change your Hackatime experience and preferences
diff --git a/db/migrate/20251021202329_deprecate_username.rb b/db/migrate/20251021202329_deprecate_username.rb
new file mode 100644
index 00000000..bdd8c686
--- /dev/null
+++ b/db/migrate/20251021202329_deprecate_username.rb
@@ -0,0 +1,5 @@
+class DeprecateUsername < ActiveRecord::Migration[8.0]
+ def change
+ rename_column :users, :username, :deprecated_name
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ce29e3e2..135c13a0 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2025_10_18_181955) do
+ActiveRecord::Schema[8.0].define(version: 2025_10_21_202329) do
create_schema "pganalyze"
# These are extensions that must be enabled in order to support this database
@@ -543,7 +543,7 @@
t.string "slack_uid"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.string "username"
+ t.string "deprecated_name"
t.string "slack_avatar_url"
t.boolean "uses_slack_status", default: false, null: false
t.string "slack_scopes", default: [], array: true
diff --git a/db/seeds.rb b/db/seeds.rb
index 40bfad35..a821f48c 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -39,7 +39,7 @@
end
puts "Created test user:"
- puts " Username: #{test_user.name}"
+ puts " Username: #{test_user.display_name}"
puts " Email: #{email.email}"
puts " API Key: #{api_key.token}"
puts " Sign-in Token: #{token.token}"
diff --git a/lib/flavor_text.rb b/lib/flavor_text.rb
index a7ed1c14..7be37272 100644
--- a/lib/flavor_text.rb
+++ b/lib/flavor_text.rb
@@ -246,8 +246,8 @@ def self.conditional_mottos(user)
r = []
r << "quit slacking off!" if user.slack_uid.present?
- r << "in the nick of time!" if %w[nick nicholas nickolas].include?(user.name)
- r << "just-in time!" if %w[justin justine].include?(user.name)
+ r << "in the nick of time!" if %w[nick nicholas nickolas].include?(user.display_name)
+ r << "just-in time!" if %w[justin justine].include?(user.display_name)
minutes_logged = Cache::MinutesLoggedJob.perform_now
r << "in the past hour, #{minutes_logged} minutes have passed" if minutes_logged > 0
diff --git a/lib/test_wakatime_service.rb b/lib/test_wakatime_service.rb
index 722d44fe..22e17078 100644
--- a/lib/test_wakatime_service.rb
+++ b/lib/test_wakatime_service.rb
@@ -33,7 +33,7 @@ def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, st
def generate_summary
summary = {}
- summary[:username] = @user.name if @user.present?
+ summary[:username] = @user.display_name if @user.present?
summary[:user_id] = @user.id.to_s if @user.present?
summary[:is_coding_activity_visible] = true if @user.present?
summary[:is_other_usage_visible] = true if @user.present?
diff --git a/lib/wakatime_service.rb b/lib/wakatime_service.rb
index 53bca297..1f550682 100644
--- a/lib/wakatime_service.rb
+++ b/lib/wakatime_service.rb
@@ -26,7 +26,7 @@ def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, st
def generate_summary
summary = {}
- summary[:username] = @user.name if @user.present?
+ summary[:username] = @user.display_name if @user.present?
summary[:user_id] = @user.id.to_s if @user.present?
summary[:is_coding_activity_visible] = true if @user.present?
summary[:is_other_usage_visible] = true if @user.present?
From a1bb20ab3f28031efe77667507d1182e4541e4d9 Mon Sep 17 00:00:00 2001
From: ShyMike <122023566+ImShyMike@users.noreply.github.com>
Date: Tue, 21 Oct 2025 22:15:16 +0100
Subject: [PATCH 6/7] rename custom_name to username + fix audit logs
---
app/controllers/admin/timeline_controller.rb | 10 +++----
.../trust_level_audit_logs_controller.rb | 4 +--
.../api/admin/v1/admin_controller.rb | 6 ++--
app/controllers/users_controller.rb | 2 +-
app/models/user.rb | 30 +++++++++----------
app/views/users/edit.html.erb | 14 ++++-----
.../20251021211039_rename_custom_username.rb | 5 ++++
db/schema.rb | 4 +--
db/seeds.rb | 2 +-
9 files changed, 41 insertions(+), 36 deletions(-)
create mode 100644 db/migrate/20251021211039_rename_custom_username.rb
diff --git a/app/controllers/admin/timeline_controller.rb b/app/controllers/admin/timeline_controller.rb
index 846a2d55..64ae526c 100644
--- a/app/controllers/admin/timeline_controller.rb
+++ b/app/controllers/admin/timeline_controller.rb
@@ -135,7 +135,7 @@ def show
# For Stimulus: provide initial selected users with details
@initial_selected_user_objects = User.where(id: @selected_user_ids)
- .select(:id, :custom_name, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
+ .select(:id, :username, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
.map { |u| { id: u.id, display_name: "#{u.display_name}", avatar_url: u.avatar_url } }
.sort_by { |u_obj| @selected_user_ids.index(u_obj[:id]) || Float::INFINITY } # Preserve order
@@ -185,10 +185,10 @@ def search_users
avatar_url: user_id_match.avatar_url
} ]
else
- users = User.where("LOWER(custom_name) LIKE :query OR LOWER(slack_username) LIKE :query OR CAST(id AS TEXT) LIKE :query OR EXISTS (SELECT 1 FROM email_addresses WHERE email_addresses.user_id = users.id AND LOWER(email_addresses.email) LIKE :query)", query: "%#{query_term}%")
- .order(Arel.sql("CASE WHEN LOWER(custom_name) = #{ActiveRecord::Base.connection.quote(query_term)} THEN 0 ELSE 1 END, username ASC")) # Prioritize exact match
+ users = User.where("LOWER(username) LIKE :query OR LOWER(slack_username) LIKE :query OR CAST(id AS TEXT) LIKE :query OR EXISTS (SELECT 1 FROM email_addresses WHERE email_addresses.user_id = users.id AND LOWER(email_addresses.email) LIKE :query)", query: "%#{query_term}%")
+ .order(Arel.sql("CASE WHEN LOWER(username) = #{ActiveRecord::Base.connection.quote(query_term)} THEN 0 ELSE 1 END, username ASC")) # Prioritize exact match
.limit(20)
- .select(:id, :custom_name, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
+ .select(:id, :username, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
results = users.map do |user|
{
@@ -218,7 +218,7 @@ def leaderboard_users
all_ids_to_fetch.unshift(current_user.id).uniq!
users_data = User.where(id: all_ids_to_fetch)
- .select(:id, :custom_name, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
+ .select(:id, :username, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
.index_by(&:id)
final_user_objects = []
diff --git a/app/controllers/admin/trust_level_audit_logs_controller.rb b/app/controllers/admin/trust_level_audit_logs_controller.rb
index 60959f55..a9257983 100644
--- a/app/controllers/admin/trust_level_audit_logs_controller.rb
+++ b/app/controllers/admin/trust_level_audit_logs_controller.rb
@@ -25,7 +25,7 @@ def index
if params[:user_search].present?
search_term = params[:user_search].strip
user_ids = User.joins(:email_addresses)
- .where("LOWER(users.custom_name) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
+ .where("LOWER(users.username) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
"%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term}%")
.pluck(:id)
@audit_logs = @audit_logs.where(user_id: user_ids)
@@ -35,7 +35,7 @@ def index
if params[:admin_search].present?
search_term = params[:admin_search].strip
admin_ids = User.joins(:email_addresses)
- .where("LOWER(users.custom_name) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
+ .where("LOWER(users.username) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
"%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term}%")
.pluck(:id)
@audit_logs = @audit_logs.where(changed_by_id: admin_ids)
diff --git a/app/controllers/api/admin/v1/admin_controller.rb b/app/controllers/api/admin/v1/admin_controller.rb
index 42afaf27..8039545c 100644
--- a/app/controllers/api/admin/v1/admin_controller.rb
+++ b/app/controllers/api/admin/v1/admin_controller.rb
@@ -15,7 +15,7 @@ def check
},
creator: {
id: creator.id,
- username: creator.custom_name,
+ username: creator.username,
display_name: creator.display_name,
admin_level: creator.admin_level
}
@@ -75,7 +75,7 @@ def user_info
render json: {
user: {
id: user.id,
- username: user.custom_name,
+ username: user.username,
display_name: user.display_name,
slack_uid: user.slack_uid,
slack_username: user.slack_username,
@@ -299,7 +299,7 @@ def trust_logs
new_trust_level: log.new_trust_level,
changed_by: {
id: log.changed_by.id,
- username: log.changed_by.name,
+ username: log.changed_by.username,
display_name: log.changed_by.display_name,
admin_level: log.changed_by.admin_level
},
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 6a450b6d..df3d0d9a 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -153,7 +153,7 @@ def user_params
:timezone,
:allow_public_stats_lookup,
:default_timezone_leaderboard,
- :custom_name,
+ :username,
)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 94514547..ab28023e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,23 +2,23 @@ class User < ApplicationRecord
include TimezoneRegions
include PublicActivity::Model
- CUSTOM_NAME_MAX_LENGTH = 21 # going over 21 overflows the navbar
+ USERNAME_MAX_LENGTH = 21 # going over 21 overflows the navbar
has_paper_trail
after_create :create_signup_activity
- before_validation :normalize_custom_name
+ before_validation :normalize_username
encrypts :slack_access_token, :github_access_token
validates :slack_uid, uniqueness: true, allow_nil: true
validates :github_uid, uniqueness: { conditions: -> { where.not(github_access_token: nil) } }, allow_nil: true
validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers }, allow_nil: false
validates :country_code, inclusion: { in: ISO3166::Country.codes }, allow_nil: true
- validates :custom_name,
- length: { maximum: CUSTOM_NAME_MAX_LENGTH },
+ validates :username,
+ length: { maximum: USERNAME_MAX_LENGTH },
format: { with: /\A[A-Za-z0-9_-]+\z/, message: "may only include letters, numbers, '-', and '_'" },
allow_nil: true
- validate :custom_name_must_be_visible
+ validate :username_must_be_visible
attribute :allow_public_stats_lookup, :boolean, default: true
attribute :default_timezone_leaderboard, :boolean, default: true
@@ -468,7 +468,7 @@ def avatar_url
end
def display_name
- name = custom_name || slack_username || github_username
+ name = username || slack_username || github_username
return name if name.present?
# "zach@hackclub.com" -> "zach (email sign-up)"
@@ -517,9 +517,9 @@ def create_signup_activity
create_activity :first_signup, owner: self
end
- def normalize_custom_name
- original = custom_name
- @custom_name_cleared_for_invisible = false
+ def normalize_username
+ original = username
+ @username_cleared_for_invisible = false
return if original.nil?
@@ -527,16 +527,16 @@ def normalize_custom_name
stripped = cleaned.strip
if stripped.empty?
- self.custom_name = nil
- @custom_name_cleared_for_invisible = original.length.positive?
+ self.username = nil
+ @username_cleared_for_invisible = original.length.positive?
else
- self.custom_name = stripped
+ self.username = stripped
end
end
- def custom_name_must_be_visible
- if instance_variable_defined?(:@custom_name_cleared_for_invisible) && @custom_name_cleared_for_invisible
- errors.add(:custom_name, "must include visible characters")
+ def username_must_be_visible
+ if instance_variable_defined?(:@username_cleared_for_invisible) && @username_cleared_for_invisible
+ errors.add(:username, "must include visible characters")
end
end
end
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index 73b0ae44..039d8f19 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -73,24 +73,24 @@
🪪
-
Display Name
+
Display Name
<%= form_with model: @user,
url: @is_own_settings ? my_settings_path : settings_user_path(@user),
method: :patch, local: false,
class: "space-y-4" do |f| %>
- <%= f.label :custom_name, "Custom display name", class: "block text-sm font-medium text-gray-200 mb-2" %>
- <%= f.text_field :custom_name,
+ <%= f.label :username, "Custom display name", class: "block text-sm font-medium text-gray-200 mb-2" %>
+ <%= f.text_field :username,
class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-primary focus:ring-1 focus:ring-primary",
placeholder: "HackClubber",
- maxlength: User::CUSTOM_NAME_MAX_LENGTH %>
- <% if @user.errors[:custom_name].present? %>
-
<%= @user.errors[:custom_name].to_sentence %>
+ maxlength: User::USERNAME_MAX_LENGTH %>
+ <% if @user.errors[:username].present? %>
+
<%= @user.errors[:username].to_sentence %>
<% end %>
- Choose a name to use in Hackatime. This will take priority over Slack or GitHub names when possible. Letters, numbers, "-" and "_" only, max <%= User::CUSTOM_NAME_MAX_LENGTH %> chars.
+ Choose a name to use in Hackatime. This will take priority over Slack or GitHub names when possible. Letters, numbers, "-" and "_" only, max <%= User::USERNAME_MAX_LENGTH %> chars.
<%= f.submit "Save Settings", class: "w-full px-4 py-2 bg-primary text-white font-medium rounded transition-colors duration-200 cursor-pointer" %>
<% end %>
diff --git a/db/migrate/20251021211039_rename_custom_username.rb b/db/migrate/20251021211039_rename_custom_username.rb
new file mode 100644
index 00000000..2ad02cd8
--- /dev/null
+++ b/db/migrate/20251021211039_rename_custom_username.rb
@@ -0,0 +1,5 @@
+class RenameCustomUsername < ActiveRecord::Migration[8.0]
+ def change
+ rename_column :users, :custom_name, :username
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 135c13a0..2a7bd550 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2025_10_21_202329) do
+ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do
create_schema "pganalyze"
# These are extensions that must be enabled in order to support this database
@@ -562,7 +562,7 @@
t.boolean "allow_public_stats_lookup", default: true, null: false
t.boolean "default_timezone_leaderboard", default: true, null: false
t.integer "admin_level", default: 0, null: false
- t.string "custom_name"
+ t.string "username"
t.index ["github_uid", "github_access_token"], name: "index_users_on_github_uid_and_access_token"
t.index ["github_uid"], name: "index_users_on_github_uid"
t.index ["slack_uid"], name: "index_users_on_slack_uid", unique: true
diff --git a/db/seeds.rb b/db/seeds.rb
index a821f48c..5318f9f4 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -14,7 +14,7 @@
if Rails.env.development?
# Creating test user
test_user = User.find_or_create_by(slack_uid: 'TEST123456') do |user|
- user.custom_name = 'testuser'
+ user.username = 'testuser'
user.slack_username = 'testuser'
# 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
From 33ec4c1ce4316a6ddf830e06f41c07eb9059bdcf Mon Sep 17 00:00:00 2001
From: ShyMike <122023566+ImShyMike@users.noreply.github.com>
Date: Tue, 21 Oct 2025 23:22:23 +0100
Subject: [PATCH 7/7] fix timeline
---
app/controllers/admin/timeline_controller.rb | 10 +++++-----
.../admin/trust_level_audit_logs_controller.rb | 4 ++--
app/controllers/api/admin/v1/admin_controller.rb | 4 ++--
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/app/controllers/admin/timeline_controller.rb b/app/controllers/admin/timeline_controller.rb
index 846a2d55..64ae526c 100644
--- a/app/controllers/admin/timeline_controller.rb
+++ b/app/controllers/admin/timeline_controller.rb
@@ -135,7 +135,7 @@ def show
# For Stimulus: provide initial selected users with details
@initial_selected_user_objects = User.where(id: @selected_user_ids)
- .select(:id, :custom_name, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
+ .select(:id, :username, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
.map { |u| { id: u.id, display_name: "#{u.display_name}", avatar_url: u.avatar_url } }
.sort_by { |u_obj| @selected_user_ids.index(u_obj[:id]) || Float::INFINITY } # Preserve order
@@ -185,10 +185,10 @@ def search_users
avatar_url: user_id_match.avatar_url
} ]
else
- users = User.where("LOWER(custom_name) LIKE :query OR LOWER(slack_username) LIKE :query OR CAST(id AS TEXT) LIKE :query OR EXISTS (SELECT 1 FROM email_addresses WHERE email_addresses.user_id = users.id AND LOWER(email_addresses.email) LIKE :query)", query: "%#{query_term}%")
- .order(Arel.sql("CASE WHEN LOWER(custom_name) = #{ActiveRecord::Base.connection.quote(query_term)} THEN 0 ELSE 1 END, username ASC")) # Prioritize exact match
+ users = User.where("LOWER(username) LIKE :query OR LOWER(slack_username) LIKE :query OR CAST(id AS TEXT) LIKE :query OR EXISTS (SELECT 1 FROM email_addresses WHERE email_addresses.user_id = users.id AND LOWER(email_addresses.email) LIKE :query)", query: "%#{query_term}%")
+ .order(Arel.sql("CASE WHEN LOWER(username) = #{ActiveRecord::Base.connection.quote(query_term)} THEN 0 ELSE 1 END, username ASC")) # Prioritize exact match
.limit(20)
- .select(:id, :custom_name, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
+ .select(:id, :username, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
results = users.map do |user|
{
@@ -218,7 +218,7 @@ def leaderboard_users
all_ids_to_fetch.unshift(current_user.id).uniq!
users_data = User.where(id: all_ids_to_fetch)
- .select(:id, :custom_name, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
+ .select(:id, :username, :slack_username, :github_username, :slack_avatar_url, :github_avatar_url)
.index_by(&:id)
final_user_objects = []
diff --git a/app/controllers/admin/trust_level_audit_logs_controller.rb b/app/controllers/admin/trust_level_audit_logs_controller.rb
index 60959f55..a9257983 100644
--- a/app/controllers/admin/trust_level_audit_logs_controller.rb
+++ b/app/controllers/admin/trust_level_audit_logs_controller.rb
@@ -25,7 +25,7 @@ def index
if params[:user_search].present?
search_term = params[:user_search].strip
user_ids = User.joins(:email_addresses)
- .where("LOWER(users.custom_name) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
+ .where("LOWER(users.username) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
"%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term}%")
.pluck(:id)
@audit_logs = @audit_logs.where(user_id: user_ids)
@@ -35,7 +35,7 @@ def index
if params[:admin_search].present?
search_term = params[:admin_search].strip
admin_ids = User.joins(:email_addresses)
- .where("LOWER(users.custom_name) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
+ .where("LOWER(users.username) LIKE ? OR LOWER(users.slack_username) LIKE ? OR LOWER(users.github_username) LIKE ? OR LOWER(email_addresses.email) LIKE ? OR CAST(users.id AS TEXT) LIKE ?",
"%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term.downcase}%", "%#{search_term}%")
.pluck(:id)
@audit_logs = @audit_logs.where(changed_by_id: admin_ids)
diff --git a/app/controllers/api/admin/v1/admin_controller.rb b/app/controllers/api/admin/v1/admin_controller.rb
index 42afaf27..14f9da24 100644
--- a/app/controllers/api/admin/v1/admin_controller.rb
+++ b/app/controllers/api/admin/v1/admin_controller.rb
@@ -15,7 +15,7 @@ def check
},
creator: {
id: creator.id,
- username: creator.custom_name,
+ username: creator.username,
display_name: creator.display_name,
admin_level: creator.admin_level
}
@@ -75,7 +75,7 @@ def user_info
render json: {
user: {
id: user.id,
- username: user.custom_name,
+ username: user.username,
display_name: user.display_name,
slack_uid: user.slack_uid,
slack_username: user.slack_username,