From 2264239a71e6434d808e31d219ad8916c0e17745 Mon Sep 17 00:00:00 2001 From: Karthik Sankar Date: Wed, 12 Mar 2025 11:56:49 +0000 Subject: [PATCH 1/8] simplified local development --- README.md | 37 +++++++-- app/controllers/leaderboards_controller.rb | 1 + config/environments/development.rb | 3 + db/seeds.rb | 92 ++++++++++++++++++++-- 4 files changed, 119 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 08a9b2a4..500cfd0c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# README +# Harbor README ## Local development @@ -8,23 +8,44 @@ $ git clone https://github.com/hackclub/harbor && cd harbor # Set your config $ cp .env.example .env -# The only thing you need to set is SEED_USER_API_KEY, which should be your key +``` + +Edit your `.env` file to include the following: -# Build & run the project +``` +# Database configurations - these work with the Docker setup +DATABASE_URL=postgres://postgres:secureorpheus123@db:5432/app_development +WAKATIME_DATABASE_URL=postgres://postgres:secureorpheus123@db:5432/app_development +SAILORS_LOG_DATABASE_URL=postgres://postgres:secureorpheus123@db:5432/app_development + +# Generate these with `rails secret` or use these for development +SECRET_KEY_BASE=alallalalallalalallalalalladlalllalal +ENCRYPTION_PRIMARY_KEY=32characterrandomstring12345678901 +ENCRYPTION_DETERMINISTIC_KEY=32characterrandomstring12345678902 +ENCRYPTION_KEY_DERIVATION_SALT=16charssalt1234 +``` + + +## Build & Run the project +``` $ docker compose run --service-ports web /bin/bash +# Now, setup the database using: +app# bin/rails db:create db:migrate db:seed + # Now you're inside docker & you can do all the fun rails things... app# bin/rails s -b 0.0.0.0 # this hosts the server on your computer w/ default port 3000 app# bin/rails c # start an interactive irb! app# bin/rails db:migrate # migrate the database ``` -Ever need to setup a new database? +You can now access the app at http://localhost:3000/ -```sh -# start a shell inside docker -$ docker compose run web --service-ports /bin/bash +Use email authentication from the homepage with test@example.com (you can view emails at http://localhost:3000/letter_opener)! -# once inside, reset the db +Ever need to setup a new database? + +``` +# inside the docker container, reset the db app# $ bin/rails db:drop db:create db:migrate db:seed ``` diff --git a/app/controllers/leaderboards_controller.rb b/app/controllers/leaderboards_controller.rb index a520b9a3..8a0277a2 100644 --- a/app/controllers/leaderboards_controller.rb +++ b/app/controllers/leaderboards_controller.rb @@ -1,6 +1,7 @@ class LeaderboardsController < ApplicationController def index @leaderboard = Leaderboard.find_by(start_date: Date.current, deleted_at: nil) + @entries = [] if @leaderboard.nil? LeaderboardUpdateJob.perform_later diff --git a/config/environments/development.rb b/config/environments/development.rb index cfe35b16..8994c9c6 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -15,6 +15,9 @@ # Enable server timing. config.server_timing = true + # Disable CSRF protection in development + config.action_controller.forgery_protection_origin_check = false + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. # Run rails dev:cache to toggle Action Controller caching. if Rails.root.join("tmp/caching-dev.txt").exist? diff --git a/db/seeds.rb b/db/seeds.rb index 4fbd6ed9..2a882e16 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,9 +1,89 @@ # This file should ensure the existence of records required to run the application in every environment (production, # development, test). The code here should be idempotent so that it can be executed at any point in every environment. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Example: -# -# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| -# MovieGenre.find_or_create_by!(name: genre_name) -# end + +# Only seed test data in development environment +if Rails.env.development? + # Creating test user + test_user = User.find_or_create_by(slack_uid: 'TEST123456') do |user| + user.username = 'testuser' + user.is_admin = true + end + + # Add email address + email = test_user.email_addresses.find_or_create_by(email: 'test@example.com') + + # Create API key + api_key = test_user.api_keys.find_or_create_by(name: 'Development API Key') do |key| + key.token = 'dev-api-key-12345' + end + + # Create a sign-in token that doesn't expire + token = test_user.sign_in_tokens.find_or_create_by(token: 'testing-token') do |t| + t.expires_at = 1.year.from_now + t.auth_type = :email + end + + puts "Created test user:" + puts " Username: #{test_user.username}" + puts " Email: #{email.email}" + puts " API Key: #{api_key.token}" + + # Create required tables for wakatime and sailors_log + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + api_key TEXT + ); + SQL + + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE IF NOT EXISTS heartbeats ( + id SERIAL PRIMARY KEY, + user_id TEXT, + time float8, + entity TEXT, + type TEXT, + category TEXT, + project TEXT, + branch TEXT, + language TEXT, + is_write BOOLEAN, + lines INTEGER, + project_root_count INTEGER, + line_number INTEGER, + cursor_position INTEGER, + line_additions INTEGER, + line_deletions INTEGER, + created_at TIMESTAMP, + updated_at TIMESTAMP + ); + SQL + + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE IF NOT EXISTS project_labels ( + id SERIAL PRIMARY KEY, + user_id TEXT, + project_key TEXT, + label TEXT + ); + SQL + + # Create sample heartbeats + if test_user.heartbeats.count == 0 + 5.times do |i| + test_user.heartbeats.create!( + time: (Time.current - i.hours).to_f, + entity: "test/file_#{i}.rb", + project: "test-project", + language: "ruby", + source_type: :direct_entry + ) + end + puts "Created 5 sample heartbeats for the test user" + else + puts "Sample heartbeats already exist for the test user" + end +else + puts "Skipping development seed data in #{Rails.env} environment" +end \ No newline at end of file From 870de7ae4497f2452e0f1c08a6a1e181d7aa55a2 Mon Sep 17 00:00:00 2001 From: Karthik Sankar <49610482+emergenitro@users.noreply.github.com> Date: Wed, 12 Mar 2025 12:19:03 +0000 Subject: [PATCH 2/8] fixed linting errors --- db/seeds.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 2a882e16..056d0beb 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -9,26 +9,26 @@ user.username = 'testuser' user.is_admin = true end - + # Add email address email = test_user.email_addresses.find_or_create_by(email: 'test@example.com') - + # Create API key api_key = test_user.api_keys.find_or_create_by(name: 'Development API Key') do |key| key.token = 'dev-api-key-12345' end - + # Create a sign-in token that doesn't expire token = test_user.sign_in_tokens.find_or_create_by(token: 'testing-token') do |t| t.expires_at = 1.year.from_now t.auth_type = :email end - + puts "Created test user:" puts " Username: #{test_user.username}" puts " Email: #{email.email}" puts " API Key: #{api_key.token}" - + # Create required tables for wakatime and sailors_log ActiveRecord::Base.connection.execute(<<~SQL) CREATE TABLE IF NOT EXISTS users ( @@ -36,8 +36,8 @@ api_key TEXT ); SQL - - ActiveRecord::Base.connection.execute(<<~SQL) + + ActiveRecord::Base.connection.execute(<<~SQL) CREATE TABLE IF NOT EXISTS heartbeats ( id SERIAL PRIMARY KEY, user_id TEXT, @@ -59,7 +59,7 @@ updated_at TIMESTAMP ); SQL - + ActiveRecord::Base.connection.execute(<<~SQL) CREATE TABLE IF NOT EXISTS project_labels ( id SERIAL PRIMARY KEY, @@ -68,7 +68,7 @@ label TEXT ); SQL - + # Create sample heartbeats if test_user.heartbeats.count == 0 5.times do |i| @@ -86,4 +86,4 @@ end else puts "Skipping development seed data in #{Rails.env} environment" -end \ No newline at end of file +end From 1d47fe99e511df87ec159de3f715cb8dad6f5bd2 Mon Sep 17 00:00:00 2001 From: Karthik Sankar <49610482+emergenitro@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:34:05 +0000 Subject: [PATCH 3/8] should fix --- README.md | 2 +- db/seeds.rb | 41 +---------------------------------------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 500cfd0c..75986cd3 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ ENCRYPTION_KEY_DERIVATION_SALT=16charssalt1234 $ docker compose run --service-ports web /bin/bash # Now, setup the database using: -app# bin/rails db:create db:migrate db:seed +app# bin/rails db:create db:schema:load db:seed # Now you're inside docker & you can do all the fun rails things... app# bin/rails s -b 0.0.0.0 # this hosts the server on your computer w/ default port 3000 diff --git a/db/seeds.rb b/db/seeds.rb index 056d0beb..ffd7d9f7 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -28,46 +28,7 @@ puts " Username: #{test_user.username}" puts " Email: #{email.email}" puts " API Key: #{api_key.token}" - - # Create required tables for wakatime and sailors_log - ActiveRecord::Base.connection.execute(<<~SQL) - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - api_key TEXT - ); - SQL - - ActiveRecord::Base.connection.execute(<<~SQL) - CREATE TABLE IF NOT EXISTS heartbeats ( - id SERIAL PRIMARY KEY, - user_id TEXT, - time float8, - entity TEXT, - type TEXT, - category TEXT, - project TEXT, - branch TEXT, - language TEXT, - is_write BOOLEAN, - lines INTEGER, - project_root_count INTEGER, - line_number INTEGER, - cursor_position INTEGER, - line_additions INTEGER, - line_deletions INTEGER, - created_at TIMESTAMP, - updated_at TIMESTAMP - ); - SQL - - ActiveRecord::Base.connection.execute(<<~SQL) - CREATE TABLE IF NOT EXISTS project_labels ( - id SERIAL PRIMARY KEY, - user_id TEXT, - project_key TEXT, - label TEXT - ); - SQL + puts " Sign-in Token: #{token.token}" # Create sample heartbeats if test_user.heartbeats.count == 0 From f65d86537a80535717c7098227366a01438f1e3a Mon Sep 17 00:00:00 2001 From: Karthik Sankar <49610482+emergenitro@users.noreply.github.com> Date: Sat, 15 Mar 2025 00:32:11 +0000 Subject: [PATCH 4/8] fix indentation --- db/seeds.rb | 72 ++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index ffd7d9f7..9c609917 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -4,47 +4,47 @@ # Only seed test data in development environment if Rails.env.development? - # Creating test user - test_user = User.find_or_create_by(slack_uid: 'TEST123456') do |user| - user.username = 'testuser' - user.is_admin = true - end + # Creating test user + test_user = User.find_or_create_by(slack_uid: 'TEST123456') do |user| + user.username = 'testuser' + user.is_admin = true + end - # Add email address - email = test_user.email_addresses.find_or_create_by(email: 'test@example.com') + # Add email address + email = test_user.email_addresses.find_or_create_by(email: 'test@example.com') - # Create API key - api_key = test_user.api_keys.find_or_create_by(name: 'Development API Key') do |key| - key.token = 'dev-api-key-12345' - end + # Create API key + api_key = test_user.api_keys.find_or_create_by(name: 'Development API Key') do |key| + key.token = 'dev-api-key-12345' + end - # Create a sign-in token that doesn't expire - token = test_user.sign_in_tokens.find_or_create_by(token: 'testing-token') do |t| - t.expires_at = 1.year.from_now - t.auth_type = :email - end + # Create a sign-in token that doesn't expire + token = test_user.sign_in_tokens.find_or_create_by(token: 'testing-token') do |t| + t.expires_at = 1.year.from_now + t.auth_type = :email + end - puts "Created test user:" - puts " Username: #{test_user.username}" - puts " Email: #{email.email}" - puts " API Key: #{api_key.token}" - puts " Sign-in Token: #{token.token}" + puts "Created test user:" + puts " Username: #{test_user.username}" + puts " Email: #{email.email}" + puts " API Key: #{api_key.token}" + puts " Sign-in Token: #{token.token}" - # Create sample heartbeats - if test_user.heartbeats.count == 0 - 5.times do |i| - test_user.heartbeats.create!( - time: (Time.current - i.hours).to_f, - entity: "test/file_#{i}.rb", - project: "test-project", - language: "ruby", - source_type: :direct_entry - ) - end - puts "Created 5 sample heartbeats for the test user" - else - puts "Sample heartbeats already exist for the test user" + # Create sample heartbeats + if test_user.heartbeats.count == 0 + 5.times do |i| + test_user.heartbeats.create!( + time: (Time.current - i.hours).to_f, + entity: "test/file_#{i}.rb", + project: "test-project", + language: "ruby", + source_type: :direct_entry + ) end + puts "Created 5 sample heartbeats for the test user" + else + puts "Sample heartbeats already exist for the test user" + end else - puts "Skipping development seed data in #{Rails.env} environment" + puts "Skipping development seed data in #{Rails.env} environment" end From 7092f95cf8f4c6e49af01cc27093ff692c88fc7b Mon Sep 17 00:00:00 2001 From: Karthik Sankar Date: Thu, 20 Mar 2025 15:06:40 +0800 Subject: [PATCH 5/8] added kudos giving stuff --- app/assets/stylesheets/activity.css | 110 ++++++++++++++++++ .../project_milestones_controller.rb | 31 +++++ .../controllers/kudos_controller.js | 43 +++++++ app/jobs/project_milestone_check_job.rb | 52 +++++++++ app/models/project_milestone.rb | 44 +++++++ app/models/project_milestone_kudos.rb | 6 + app/views/shared/_nav.html.erb | 40 +++++++ config/initializers/good_job.rb | 4 + config/routes.rb | 8 +- ...0250320052532_create_project_milestones.rb | 17 +++ ...20052612_create_project_milestone_kudos.rb | 12 ++ db/schema.rb | 25 +++- 12 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 app/assets/stylesheets/activity.css create mode 100644 app/controllers/project_milestones_controller.rb create mode 100644 app/javascript/controllers/kudos_controller.js create mode 100644 app/jobs/project_milestone_check_job.rb create mode 100644 app/models/project_milestone.rb create mode 100644 app/models/project_milestone_kudos.rb create mode 100644 db/migrate/20250320052532_create_project_milestones.rb create mode 100644 db/migrate/20250320052612_create_project_milestone_kudos.rb diff --git a/app/assets/stylesheets/activity.css b/app/assets/stylesheets/activity.css new file mode 100644 index 00000000..cd59ef0c --- /dev/null +++ b/app/assets/stylesheets/activity.css @@ -0,0 +1,110 @@ +.recent-activity { + margin-top: 1rem; +} + +.recent-activity h3 { + font-size: 1.2rem; + margin-bottom: 0.75rem; + color: var(--muted-color); +} + +.activity-list { + max-height: 600px; + overflow-y: auto; +} + +.activity-item { + padding: 0.75rem 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + font-size: 0.9rem; +} + +.activity-item:last-child { + border-bottom: none; +} + +.activity-content { + margin: 0.5rem 0; +} + +.activity-content p { + margin: 0; +} + +.activity-time { + color: var(--muted-color); + font-size: 0.75rem; +} + +.activity-actions { + display: flex; + margin-top: 0.5rem; +} + +.give-kudos-btn { + display: inline-flex; + align-items: center; + background-color: rgba(var(--primary-color-rgb), 0.1); + color: var(--primary-color); + padding: 0.25rem 0.5rem; + border-radius: 4px; + text-decoration: none; + font-size: 0.75rem; + transition: background-color 0.2s; +} + +.give-kudos-btn:hover { + background-color: rgba(var(--primary-color-rgb), 0.2); +} + +.kudos-icon { + margin-right: 0.25rem; + font-style: normal; +} + +.kudos-count { + margin-left: 0.25rem; + background: rgba(var(--primary-color-rgb), 0.2); + border-radius: 10px; + padding: 0.1rem 0.4rem; + font-size: 0.7rem; +} + +.kudos-given, +.kudos-received { + display: inline-flex; + align-items: center; + color: var(--primary-color); + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + background-color: rgba(var(--primary-color-rgb), 0.1); +} + +/* Dark mode overrides */ +@media (prefers-color-scheme: dark) { + .activity-item { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + } + + .activity-time { + color: rgba(255, 255, 255, 0.5); + } + + .give-kudos-btn { + background-color: rgba(var(--primary-color-rgb), 0.2); + } + + .give-kudos-btn:hover { + background-color: rgba(var(--primary-color-rgb), 0.3); + } + + .kudos-count { + background: rgba(var(--primary-color-rgb), 0.3); + } + + .kudos-given, + .kudos-received { + background-color: rgba(var(--primary-color-rgb), 0.2); + } +} \ No newline at end of file diff --git a/app/controllers/project_milestones_controller.rb b/app/controllers/project_milestones_controller.rb new file mode 100644 index 00000000..4b50b3a0 --- /dev/null +++ b/app/controllers/project_milestones_controller.rb @@ -0,0 +1,31 @@ +class ProjectMilestonesController < ApplicationController + before_action :authenticate_user! + + def give_kudos + milestone = ProjectMilestone.find(params[:id]) + + # Don't allow users to give kudos to themselves + if milestone.user_id == current_user.id + return render json: { error: "You cannot give kudos to yourself" }, status: :unprocessable_entity + end + + # Check if user already gave kudos + if milestone.kudos_from?(current_user.id) + return render json: { error: "You already gave kudos for this milestone" }, status: :unprocessable_entity + end + + kudos = ProjectMilestoneKudos.new( + project_milestone: milestone, + user_id: current_user.id + ) + + if kudos.save + render json: { + success: true, + kudos_count: milestone.reload.kudos_count + } + else + render json: { error: kudos.errors.full_messages.join(", ") }, status: :unprocessable_entity + end + end +end \ No newline at end of file diff --git a/app/javascript/controllers/kudos_controller.js b/app/javascript/controllers/kudos_controller.js new file mode 100644 index 00000000..bb879e71 --- /dev/null +++ b/app/javascript/controllers/kudos_controller.js @@ -0,0 +1,43 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["button"] + + connect() { + console.log("Kudos controller connected") + } + + giveKudos(event) { + event.preventDefault() + + const button = event.currentTarget + const url = button.getAttribute("href") + + fetch(url, { + method: "POST", + headers: { + "X-CSRF-Token": document.querySelector("meta[name='csrf-token']").content, + "Accept": "application/json" + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Replace the button with the kudos count + const container = button.closest(".activity-actions") + + const kudosEl = document.createElement("span") + kudosEl.classList.add("kudos-given") + kudosEl.innerHTML = `👏 ${data.kudos_count}` + + container.innerHTML = "" + container.appendChild(kudosEl) + } else { + console.error("Error giving kudos:", data.error) + } + }) + .catch(error => { + console.error("Error giving kudos:", error) + }) + } +} \ No newline at end of file diff --git a/app/jobs/project_milestone_check_job.rb b/app/jobs/project_milestone_check_job.rb new file mode 100644 index 00000000..e7c1e9f5 --- /dev/null +++ b/app/jobs/project_milestone_check_job.rb @@ -0,0 +1,52 @@ +class ProjectMilestoneCheckJob < ApplicationJob + queue_as :default + + def perform + Rails.logger.info "Checking for project milestones" + + # Get all users with heartbeats in the last hour + active_users = Heartbeat.where("created_at > ?", 1.hour.ago) + .distinct.pluck(:user_id) + + active_users.each do |user_id| + check_hourly_milestones(user_id) + end + end + + private + + def check_hourly_milestones(user_id) + user = User.find_by(id: user_id) + return unless user + + # Get projects with significant time in the last period + project_durations = user.heartbeats.today.group(:project).duration_seconds + + project_durations.each do |project, duration| + next if project.blank? + + # Convert to hours + hours = (duration / 3600.0).floor + next if hours < 1 + + # Check if we already have a milestone for this hour count + existing = ProjectMilestone.where( + user_id: user_id, + project_name: project, + milestone_type: :hourly, + milestone_value: hours + ).where("created_at > ?", 1.day.ago).exists? + + # If no milestone exists, create one + unless existing + ProjectMilestone.create!( + user_id: user_id, + project_name: project, + milestone_type: :hourly, + milestone_value: hours + ) + Rails.logger.info "Created hourly milestone for user #{user_id} on project #{project}: #{hours} hours" + end + end + end +end \ No newline at end of file diff --git a/app/models/project_milestone.rb b/app/models/project_milestone.rb new file mode 100644 index 00000000..7111e92a --- /dev/null +++ b/app/models/project_milestone.rb @@ -0,0 +1,44 @@ +class ProjectMilestone < ApplicationRecord + belongs_to :user, foreign_key: :user_id + + has_many :project_milestone_kudos, class_name: 'ProjectMilestoneKudos' + + validates :project_name, presence: true + validates :milestone_type, presence: true + validates :milestone_value, presence: true + + enum :milestone_type, { + hourly: 0, + daily: 1, + weekly: 2 + } + + # Get milestones for display in the sidebar + def self.recent_for_display(limit = 20) + order(created_at: :desc) + .includes(:user, :project_milestone_kudos) + .limit(limit) + end + + # Check if the current user has given kudos to this milestone + def kudos_from?(user_id) + project_milestone_kudos.where(user_id: user_id).exists? + end + + # Get the kudos count + def kudos_count + project_milestone_kudos.count + end + + # Format the milestone message + def formatted_message + case milestone_type + when "hourly" + "completed #{milestone_value} hour#{'s' if milestone_value > 1} on #{project_name}" + when "daily" + "worked on #{project_name} for #{ApplicationController.helpers.short_time_simple(milestone_value)} today" + when "weekly" + "spent #{ApplicationController.helpers.short_time_simple(milestone_value)} on #{project_name} this week" + end + end +end \ No newline at end of file diff --git a/app/models/project_milestone_kudos.rb b/app/models/project_milestone_kudos.rb new file mode 100644 index 00000000..e1467d48 --- /dev/null +++ b/app/models/project_milestone_kudos.rb @@ -0,0 +1,6 @@ +class ProjectMilestoneKudos < ApplicationRecord + belongs_to :project_milestone + belongs_to :user + + validates :project_milestone_id, uniqueness: { scope: :user_id } +end \ No newline at end of file diff --git a/app/views/shared/_nav.html.erb b/app/views/shared/_nav.html.erb index 2a29e4ad..1be2c7f7 100644 --- a/app/views/shared/_nav.html.erb +++ b/app/views/shared/_nav.html.erb @@ -64,4 +64,44 @@ <% end %> <% end %> + + <% if current_user %> +
+ +
+

Recent activity

+ +
+ <% ProjectMilestone.recent_for_display(10).each do |milestone| %> +
+ <%= render "shared/user_mention", user: milestone.user %> +
+

<%= milestone.formatted_message %>

+
+ <% if current_user.id != milestone.user_id %> + <% if milestone.kudos_from?(current_user.id) %> + + 👏 <%= milestone.kudos_count %> + + <% else %> + <%= link_to give_kudos_project_milestone_path(milestone), + data: { turbo_method: :post, controller: "kudos", action: "click->kudos#giveKudos" }, + class: "give-kudos-btn" do %> + 👏 Give kudos + <% if milestone.kudos_count > 0 %><%= milestone.kudos_count %><% end %> + <% end %> + <% end %> + <% elsif milestone.kudos_count > 0 %> + + 👏 <%= milestone.kudos_count %> + + <% end %> +
+
+ <%= time_ago_in_words(milestone.created_at) %> ago +
+ <% end %> +
+
+ <% end %> \ No newline at end of file diff --git a/config/initializers/good_job.rb b/config/initializers/good_job.rb index f36065a2..37a56fb3 100644 --- a/config/initializers/good_job.rb +++ b/config/initializers/good_job.rb @@ -37,6 +37,10 @@ scan_github_repos: { cron: "0 10 * * *", class: "ScanGithubReposJob" + }, + project_milestone_check: { + cron: "*/5 * * * *", + class: "ProjectMilestoneCheckJob" } } end diff --git a/config/routes.rb b/config/routes.rb index 2f1abfbd..abe01557 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -90,4 +90,10 @@ def self.matches?(request) end resources :scrapyard_leaderboards, only: [ :index, :show ] -end + + resources :project_milestones, only: [] do + member do + post :give_kudos + end + end +end \ No newline at end of file diff --git a/db/migrate/20250320052532_create_project_milestones.rb b/db/migrate/20250320052532_create_project_milestones.rb new file mode 100644 index 00000000..7d715eea --- /dev/null +++ b/db/migrate/20250320052532_create_project_milestones.rb @@ -0,0 +1,17 @@ +class CreateProjectMilestones < ActiveRecord::Migration[8.0] + def change + create_table :project_milestones do |t| + t.bigint :user_id, null: false + t.string :project_name, null: false + t.integer :milestone_type, null: false, default: 0 + t.integer :milestone_value, null: false + t.boolean :notified, default: false + + t.timestamps + end + + add_index :project_milestones, :user_id + add_index :project_milestones, [:user_id, :project_name, :milestone_type] + add_index :project_milestones, :created_at + end +end diff --git a/db/migrate/20250320052612_create_project_milestone_kudos.rb b/db/migrate/20250320052612_create_project_milestone_kudos.rb new file mode 100644 index 00000000..948594a0 --- /dev/null +++ b/db/migrate/20250320052612_create_project_milestone_kudos.rb @@ -0,0 +1,12 @@ +class CreateProjectMilestoneKudos < ActiveRecord::Migration[8.0] + def change + create_table :project_milestone_kudos do |t| + t.references :project_milestone, null: false, foreign_key: true + t.bigint :user_id, null: false + + t.timestamps + end + + add_index :project_milestone_kudos, [:project_milestone_id, :user_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index db64e5cc..1e839ab8 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_03_19_193636) do +ActiveRecord::Schema[8.0].define(version: 2025_03_20_052612) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -175,6 +175,28 @@ t.integer "period_type", default: 0, null: false end + create_table "project_milestone_kudos", force: :cascade do |t| + t.bigint "project_milestone_id", null: false + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["project_milestone_id", "user_id"], name: "idx_on_project_milestone_id_user_id_218c1b857a", unique: true + t.index ["project_milestone_id"], name: "index_project_milestone_kudos_on_project_milestone_id" + end + + create_table "project_milestones", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "project_name", null: false + t.integer "milestone_type", default: 0, null: false + t.integer "milestone_value", null: false + t.boolean "notified", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["created_at"], name: "index_project_milestones_on_created_at" + t.index ["user_id", "project_name", "milestone_type"], name: "idx_on_user_id_project_name_milestone_type_06e1e9487d" + t.index ["user_id"], name: "index_project_milestones_on_user_id" + end + create_table "project_repo_mappings", force: :cascade do |t| t.bigint "user_id", null: false t.string "project_name", null: false @@ -269,6 +291,7 @@ add_foreign_key "heartbeats", "users" add_foreign_key "leaderboard_entries", "leaderboards" add_foreign_key "leaderboard_entries", "users" + add_foreign_key "project_milestone_kudos", "project_milestones" add_foreign_key "project_repo_mappings", "users" add_foreign_key "sign_in_tokens", "users" end From 2a81d56eb14a94bdc0813013b4dbaea8ff2a6de3 Mon Sep 17 00:00:00 2001 From: Karthik Sankar Date: Thu, 20 Mar 2025 15:17:51 +0800 Subject: [PATCH 6/8] fix linting errors --- .../project_milestones_controller.rb | 18 +++++++-------- app/jobs/project_milestone_check_job.rb | 22 +++++++++---------- app/models/project_milestone.rb | 18 +++++++-------- app/models/project_milestone_kudos.rb | 4 ++-- config/routes.rb | 4 ++-- ...0250320052532_create_project_milestones.rb | 6 ++--- ...20052612_create_project_milestone_kudos.rb | 6 ++--- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/app/controllers/project_milestones_controller.rb b/app/controllers/project_milestones_controller.rb index 4b50b3a0..996aee23 100644 --- a/app/controllers/project_milestones_controller.rb +++ b/app/controllers/project_milestones_controller.rb @@ -1,31 +1,31 @@ class ProjectMilestonesController < ApplicationController before_action :authenticate_user! - + def give_kudos milestone = ProjectMilestone.find(params[:id]) - + # Don't allow users to give kudos to themselves if milestone.user_id == current_user.id return render json: { error: "You cannot give kudos to yourself" }, status: :unprocessable_entity end - + # Check if user already gave kudos if milestone.kudos_from?(current_user.id) return render json: { error: "You already gave kudos for this milestone" }, status: :unprocessable_entity end - + kudos = ProjectMilestoneKudos.new( project_milestone: milestone, user_id: current_user.id ) - + if kudos.save - render json: { - success: true, - kudos_count: milestone.reload.kudos_count + render json: { + success: true, + kudos_count: milestone.reload.kudos_count } else render json: { error: kudos.errors.full_messages.join(", ") }, status: :unprocessable_entity end end -end \ No newline at end of file +end diff --git a/app/jobs/project_milestone_check_job.rb b/app/jobs/project_milestone_check_job.rb index e7c1e9f5..4f1e09e7 100644 --- a/app/jobs/project_milestone_check_job.rb +++ b/app/jobs/project_milestone_check_job.rb @@ -1,34 +1,34 @@ class ProjectMilestoneCheckJob < ApplicationJob queue_as :default - + def perform Rails.logger.info "Checking for project milestones" - + # Get all users with heartbeats in the last hour active_users = Heartbeat.where("created_at > ?", 1.hour.ago) .distinct.pluck(:user_id) - + active_users.each do |user_id| check_hourly_milestones(user_id) end end - + private - + def check_hourly_milestones(user_id) user = User.find_by(id: user_id) return unless user - + # Get projects with significant time in the last period project_durations = user.heartbeats.today.group(:project).duration_seconds - + project_durations.each do |project, duration| next if project.blank? - + # Convert to hours hours = (duration / 3600.0).floor next if hours < 1 - + # Check if we already have a milestone for this hour count existing = ProjectMilestone.where( user_id: user_id, @@ -36,7 +36,7 @@ def check_hourly_milestones(user_id) milestone_type: :hourly, milestone_value: hours ).where("created_at > ?", 1.day.ago).exists? - + # If no milestone exists, create one unless existing ProjectMilestone.create!( @@ -49,4 +49,4 @@ def check_hourly_milestones(user_id) end end end -end \ No newline at end of file +end diff --git a/app/models/project_milestone.rb b/app/models/project_milestone.rb index 7111e92a..e500d251 100644 --- a/app/models/project_milestone.rb +++ b/app/models/project_milestone.rb @@ -1,35 +1,35 @@ class ProjectMilestone < ApplicationRecord belongs_to :user, foreign_key: :user_id - - has_many :project_milestone_kudos, class_name: 'ProjectMilestoneKudos' - + + has_many :project_milestone_kudos, class_name: "ProjectMilestoneKudos" + validates :project_name, presence: true validates :milestone_type, presence: true validates :milestone_value, presence: true - + enum :milestone_type, { hourly: 0, daily: 1, weekly: 2 } - + # Get milestones for display in the sidebar def self.recent_for_display(limit = 20) order(created_at: :desc) .includes(:user, :project_milestone_kudos) .limit(limit) end - + # Check if the current user has given kudos to this milestone def kudos_from?(user_id) project_milestone_kudos.where(user_id: user_id).exists? end - + # Get the kudos count def kudos_count project_milestone_kudos.count end - + # Format the milestone message def formatted_message case milestone_type @@ -41,4 +41,4 @@ def formatted_message "spent #{ApplicationController.helpers.short_time_simple(milestone_value)} on #{project_name} this week" end end -end \ No newline at end of file +end diff --git a/app/models/project_milestone_kudos.rb b/app/models/project_milestone_kudos.rb index e1467d48..7f652fbd 100644 --- a/app/models/project_milestone_kudos.rb +++ b/app/models/project_milestone_kudos.rb @@ -1,6 +1,6 @@ class ProjectMilestoneKudos < ApplicationRecord belongs_to :project_milestone belongs_to :user - + validates :project_milestone_id, uniqueness: { scope: :user_id } -end \ No newline at end of file +end diff --git a/config/routes.rb b/config/routes.rb index abe01557..9f1fabce 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -90,10 +90,10 @@ def self.matches?(request) end resources :scrapyard_leaderboards, only: [ :index, :show ] - + resources :project_milestones, only: [] do member do post :give_kudos end end -end \ No newline at end of file +end diff --git a/db/migrate/20250320052532_create_project_milestones.rb b/db/migrate/20250320052532_create_project_milestones.rb index 7d715eea..4850db9f 100644 --- a/db/migrate/20250320052532_create_project_milestones.rb +++ b/db/migrate/20250320052532_create_project_milestones.rb @@ -6,12 +6,12 @@ def change t.integer :milestone_type, null: false, default: 0 t.integer :milestone_value, null: false t.boolean :notified, default: false - + t.timestamps end - + add_index :project_milestones, :user_id - add_index :project_milestones, [:user_id, :project_name, :milestone_type] + add_index :project_milestones, [ :user_id, :project_name, :milestone_type ] add_index :project_milestones, :created_at end end diff --git a/db/migrate/20250320052612_create_project_milestone_kudos.rb b/db/migrate/20250320052612_create_project_milestone_kudos.rb index 948594a0..08560f19 100644 --- a/db/migrate/20250320052612_create_project_milestone_kudos.rb +++ b/db/migrate/20250320052612_create_project_milestone_kudos.rb @@ -3,10 +3,10 @@ def change create_table :project_milestone_kudos do |t| t.references :project_milestone, null: false, foreign_key: true t.bigint :user_id, null: false - + t.timestamps end - - add_index :project_milestone_kudos, [:project_milestone_id, :user_id], unique: true + + add_index :project_milestone_kudos, [ :project_milestone_id, :user_id ], unique: true end end From 458e5b00363b370d91a41d7aed3b549c1c8c4931 Mon Sep 17 00:00:00 2001 From: Karthik Sankar Date: Sat, 22 Mar 2025 22:06:50 +0800 Subject: [PATCH 7/8] fixed things after testing --- app/jobs/project_milestone_check_job.rb | 2 -- app/models/project_milestone.rb | 16 +++++----------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app/jobs/project_milestone_check_job.rb b/app/jobs/project_milestone_check_job.rb index 4f1e09e7..6457830c 100644 --- a/app/jobs/project_milestone_check_job.rb +++ b/app/jobs/project_milestone_check_job.rb @@ -2,8 +2,6 @@ class ProjectMilestoneCheckJob < ApplicationJob queue_as :default def perform - Rails.logger.info "Checking for project milestones" - # Get all users with heartbeats in the last hour active_users = Heartbeat.where("created_at > ?", 1.hour.ago) .distinct.pluck(:user_id) diff --git a/app/models/project_milestone.rb b/app/models/project_milestone.rb index e500d251..791135c7 100644 --- a/app/models/project_milestone.rb +++ b/app/models/project_milestone.rb @@ -7,6 +7,7 @@ class ProjectMilestone < ApplicationRecord validates :milestone_type, presence: true validates :milestone_value, presence: true + # We keep this because I don't want to change the database schema enum :milestone_type, { hourly: 0, daily: 1, @@ -15,7 +16,8 @@ class ProjectMilestone < ApplicationRecord # Get milestones for display in the sidebar def self.recent_for_display(limit = 20) - order(created_at: :desc) + where(milestone_type: :hourly) + .order(created_at: :desc) .includes(:user, :project_milestone_kudos) .limit(limit) end @@ -30,15 +32,7 @@ def kudos_count project_milestone_kudos.count end - # Format the milestone message def formatted_message - case milestone_type - when "hourly" - "completed #{milestone_value} hour#{'s' if milestone_value > 1} on #{project_name}" - when "daily" - "worked on #{project_name} for #{ApplicationController.helpers.short_time_simple(milestone_value)} today" - when "weekly" - "spent #{ApplicationController.helpers.short_time_simple(milestone_value)} on #{project_name} this week" - end + "completed #{milestone_value} hour#{'s' if milestone_value > 1} on #{project_name}" end -end +end \ No newline at end of file From c783b00e8c5987969e594a8077c459d1eae8a459 Mon Sep 17 00:00:00 2001 From: Karthik Sankar Date: Sat, 22 Mar 2025 22:08:37 +0800 Subject: [PATCH 8/8] fix linting errors --- app/models/project_milestone.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project_milestone.rb b/app/models/project_milestone.rb index 791135c7..c33faec3 100644 --- a/app/models/project_milestone.rb +++ b/app/models/project_milestone.rb @@ -35,4 +35,4 @@ def kudos_count def formatted_message "completed #{milestone_value} hour#{'s' if milestone_value > 1} on #{project_name}" end -end \ No newline at end of file +end