Add airtable url to table sync concern (#236)

This commit is contained in:
Max Wofford
2025-05-16 13:02:27 -04:00
committed by GitHub
parent 0985cb0f38
commit f9a4ce9cfa
6 changed files with 246 additions and 8 deletions

View File

@@ -5,8 +5,32 @@ class Admin::TimelineController < Admin::BaseController
# for span calculations (visual blocks in the timeline)
timeout_duration = 10.minutes.to_i # This remains for span display
@review_item = Neighborhood::Post.find_by(airtable_id: params["post_id"]) if params["post_id"].present?
# all posts from user
@posts_from_user = Neighborhood::Post.where(airtable_fields: { slackId: @review_item.airtable_fields["slackId"] }) if @review_item.present?
if @review_item.present?
puts "review item found"
puts @review_item.airtable_fields
@date = @review_item.airtable_fields["createdAt"].to_date
@slack_uid = @review_item.airtable_fields["slackId"]&.first
@review_item_user = User.find_by(slack_uid: @slack_uid) if @slack_uid.present?
if @review_item.present? && @review_item_user.present?
@review_item_marker = {
user_id: @review_item_user.id,
timestamp: Time.parse(@review_item.airtable_fields["createdAt"]).to_f,
description: @review_item.airtable_fields["description"],
demo_video: @review_item.airtable_fields["demoVideo"],
photobooth_video: @review_item.airtable_fields["photoboothVideo"],
hackatime_time: @review_item.airtable_fields["hackatimeTime"],
hackatime_time_hours: @review_item.airtable_fields["hackatimeTimeHours"]
}
end
end
# Determine the date to display (default to today)
@date = params[:date] ? Date.parse(params[:date]) : Time.current.to_date
@date ||= params[:date] ? Date.parse(params[:date]) : Time.current.to_date
# Calculate next and previous dates
@next_date = @date + 1.day
@@ -17,6 +41,7 @@ class Admin::TimelineController < Admin::BaseController
# Always include current_user (admin)
@selected_user_ids = [ current_user.id ] + raw_user_ids
@selected_user_ids << @review_item_user.id if @review_item_user.present?
@selected_user_ids.uniq!
user_ids_to_fetch = @selected_user_ids
@@ -122,6 +147,51 @@ class Admin::TimelineController < Admin::BaseController
@primary_user = @users_with_timeline_data.first&.[](:user) || current_user
# Get slack_uids for all selected users
slack_uids = User.where(id: @selected_user_ids).pluck(:slack_uid).compact
uids_sql_array = slack_uids.map { |uid| ActiveRecord::Base.connection.quote(uid) }.join(", ")
date_str = @date.to_s
posts_for_timeline = Neighborhood::Post.where(
"airtable_fields -> 'slackId' ?| array[#{uids_sql_array}]"
).where("DATE(airtable_fields ->> 'createdAt') = ?", date_str)
@timeline_post_markers = posts_for_timeline.map do |post|
{
user_id: User.find_by(slack_uid: post.airtable_fields["slackId"]&.first)&.id,
timestamp: Time.parse(post.airtable_fields["createdAt"]).to_f,
description: post.airtable_fields["description"],
demo_video: post.airtable_fields["demoVideo"],
photobooth_video: post.airtable_fields["photoboothVideo"],
hackatime_time: post.airtable_fields["hackatimeTime"],
hackatime_time_hours: post.airtable_fields["hackatimeTimeHours"],
highlighted: (@review_item.present? && post.id == @review_item.id),
airtable_url: post.airtable_url
}
end.compact
# Get commit markers for timeline
commits_for_timeline = Commit.where(
user_id: @selected_user_ids,
created_at: @date.beginning_of_day..@date.end_of_day
)
@timeline_commit_markers = commits_for_timeline.map do |commit|
raw = commit.github_raw || {}
# Use committer date if available, else fallback to created_at
commit_time = if raw.dig("commit", "committer", "date")
Time.parse(raw["commit"]["committer"]["date"])
else
commit.created_at
end
{
user_id: commit.user_id,
timestamp: commit_time.to_f,
additions: (raw["stats"] && raw["stats"]["additions"]) || raw.dig("files", 0, "additions"),
deletions: (raw["stats"] && raw["stats"]["deletions"]) || raw.dig("files", 0, "deletions"),
github_url: raw["html_url"]
}
end
render :show # Renders app/views/admin/timeline/show.html.erb
end

View File

@@ -4,6 +4,10 @@ module HasTableSync
included do
validates :airtable_fields, presence: true
validates :airtable_id, presence: true, uniqueness: true
def airtable_url
"https://airtable.com/#{self.class.table_sync_base}/#{self.class.table_sync_table}/#{airtable_id}"
end
end
class_methods do
@@ -20,5 +24,15 @@ module HasTableSync
upsert_all(records, unique_by: :airtable_id)
end
end
def table_sync_pat
@table_sync_pat
end
def table_sync_base
@table_sync_base
end
def table_sync_table
@table_sync_table
end
end
end

View File

@@ -211,7 +211,7 @@
duration_seconds_in_view = effective_end_time - effective_start_time
height_px = (duration_seconds_in_view / 60.0) * pixels_per_minute
return nil if height_px <= 0.5
final_top_px = (minutes_from_view_start * pixels_per_minute)
final_top_px = (minutes_from_view_start * pixels_per_minute).round
title_parts = []
title_parts << "Languages: #{span_data[:languages].join(', ')}" if span_data[:languages]&.any?
project_title_segments = (span_data[:projects_edited_details] || []).map do |proj_detail|
@@ -290,11 +290,145 @@
</div>
<% end %>
<% end %>
<% # Group markers by user and sort by timestamp %>
<% cap_width = 16 %>
<% cap_height = 4 %>
<% vertical_padding = 6 %>
<% grouped_markers = @timeline_post_markers.group_by { |m| m[:user_id] } %>
<% grouped_markers.each do |user_id, markers| %>
<% user_index = users_data_array.find_index { |data| data[:user].id == user_id } %>
<% next unless user_index %>
<% user = users_data_array[user_index][:user] %>
<% user_tz = user&.timezone || primary_user_tz %>
<% today_start_of_day_for_user = @date.in_time_zone(user_tz).beginning_of_day %>
<% view_start_datetime = today_start_of_day_for_user.advance(hours: timeline_start_hour) %>
<% base_offset_px = user_index * min_column_width_px + (user_index > 0 ? user_index * gutter_px : 0) %>
<% marker_left_px = (line_left_rem * 16) + base_offset_px + min_column_width_px - 4 %>
<% cap_left_px = marker_left_px - (cap_width / 2) + 2 %> <!-- Center the cap on the vertical line -->
<% sorted_markers = markers.sort_by { |m| m[:timestamp] } %>
<% sorted_markers.each_with_index do |marker, idx| %>
<% marker_time = Time.at(marker[:timestamp]).in_time_zone(user_tz) %>
<% marker_minutes_from_view_start = ((marker_time - view_start_datetime) / 60.0).to_f %>
<% marker_top_px = (marker_minutes_from_view_start * pixels_per_minute).round %>
<% if idx == 0 %>
<% prev_time = view_start_datetime %>
<% else %>
<% prev_time = Time.at(sorted_markers[idx-1][:timestamp]).in_time_zone(user_tz) %>
<% end %>
<% prev_minutes_from_view_start = ((prev_time - view_start_datetime) / 60.0).to_f %>
<% prev_top_px = (prev_minutes_from_view_start * pixels_per_minute).round %>
<% line_start_px = prev_top_px + vertical_padding %>
<% line_height = marker_top_px - line_start_px %>
<!-- Vertical line -->
<div class="timeline-post-marker-vertical"
style="position: absolute;
left: <%= marker_left_px %>px;
top: <%= line_start_px %>px;
width: 4px;
height: <%= line_height %>px;
background: <%= marker[:highlighted] ? '#FFD600' : '#00BFFF' %>;
border-radius: 2px;
z-index: 10;
cursor: pointer;"
data-description="<%= marker[:description] %>"
data-demo-video="<%= marker[:demo_video] %>"
data-photobooth-video="<%= marker[:photobooth_video] %>"
data-hackatime-time="<%= marker[:hackatime_time] %>"
data-hackatime-time-hours="<%= marker[:hackatime_time_hours] %>"
data-airtable-url="<%= marker[:airtable_url] %>"
onclick="showReviewItemPopup(this)">
</div>
<!-- Circle at the end -->
<% circle_diameter = 14 %>
<% circle_left_px = marker_left_px - (circle_diameter / 2) + 2 %>
<div class="timeline-post-marker-circle"
style="position: absolute;
left: <%= circle_left_px %>px;
top: <%= marker_top_px - (circle_diameter / 2) %>px;
width: <%= circle_diameter %>px;
height: <%= circle_diameter %>px;
background: <%= marker[:highlighted] ? '#FFD600' : '#00BFFF' %>;
border-radius: 50%;
border: 2px solid #222;
z-index: 12;
pointer-events: none;">
</div>
<% end %>
<% end %>
<% @timeline_commit_markers.each do |commit| %>
<% user_index = users_data_array.find_index { |data| data[:user].id == commit[:user_id] } %>
<% next unless user_index %>
<% user = users_data_array[user_index][:user] %>
<% user_tz = user&.timezone || primary_user_tz %>
<% today_start_of_day_for_user = @date.in_time_zone(user_tz).beginning_of_day %>
<% view_start_datetime = today_start_of_day_for_user.advance(hours: timeline_start_hour) %>
<% base_offset_px = user_index * min_column_width_px + (user_index > 0 ? user_index * gutter_px : 0) %>
<% pill_left_px = (line_left_rem * 16) + base_offset_px + min_column_width_px - 60 %> <!-- right align, adjust as needed -->
<% commit_minutes_from_view_start = ((Time.at(commit[:timestamp]).in_time_zone(user_tz) - view_start_datetime) / 60.0).to_f %>
<% commit_top_px = (commit_minutes_from_view_start * pixels_per_minute).round %>
<a href="<%= commit[:github_url] %>" target="_blank" rel="noopener noreferrer"
title="<%= commit[:message] %>"
class="timeline-commit-pill"
style="position: absolute;
left: <%= pill_left_px %>px;
top: <%= commit_top_px %>px;
z-index: 20;
background: #222;
color: #fff;
border-radius: 12px;
padding: 0px 7px;
font-size: 0.72em;
border: 1px solid rgba(255,255,255,0.1);
text-decoration: none;
box-shadow: 0 1px 4px rgba(0,0,0,0.10);">
<span style="<%= commit[:additions].to_i > 0 ? 'color: #22c55e; font-weight: 600;' : '' %>">+<%= commit[:additions] %></span>
/
<span style="<%= commit[:deletions].to_i > 0 ? 'color: #ef4444; font-weight: 600;' : '' %>">-<%= commit[:deletions] %></span>
</a>
<% end %>
</div> <!-- End #timeline-grid-scroll-container -->
</div> <!-- End .admin-timeline-content-sizer -->
</div> <!-- End .admin-timeline-view-wrapper -->
</div> <!-- End Main Page Container -->
<div id="review-item-popup" style="display:none; position:fixed; z-index:1000; background:#222; color:#fff; padding:1rem; border-radius:8px; max-width:320px;"></div>
<script>
window.showReviewItemPopup = function(el) {
const popup = document.getElementById('review-item-popup');
// Format time using short_time_simple via a data attribute
let hackatimeTimeHours = el.dataset.hackatimeTimeHours;
let formattedTime = hackatimeTimeHours && !isNaN(hackatimeTimeHours) ? shortTimeSimple(parseFloat(hackatimeTimeHours) * 3600) : 'N/A';
let airtableUrl = el.dataset.airtableUrl;
popup.innerHTML = `
<strong>Description:</strong> ${el.dataset.description || 'N/A'}<br>
<strong>Demo Video:</strong> ${el.dataset.demoVideo ? `<a href='${el.dataset.demoVideo}' target='_blank'>View</a>` : 'N/A'}<br>
<strong>Photobooth Video:</strong> ${el.dataset.photoboothVideo ? `<a href='${el.dataset.photoboothVideo}' target='_blank'>View</a>` : 'N/A'}<br>
<strong>Time:</strong> ${formattedTime}<br>
${airtableUrl ? `<a href='${airtableUrl}' target='_blank' style='color:#FFD600;'>See on Airtable</a><br>` : ''}
<button onclick='this.parentElement.style.display="none"' style='margin-top:0.5rem;'>Close</button>
`;
popup.style.display = 'block';
popup.style.left = (window.innerWidth/2 - 160) + 'px';
popup.style.top = '100px';
}
// Helper for formatting seconds as short_time_simple
function shortTimeSimple(seconds) {
seconds = Math.round(seconds);
if (seconds < 60) return `${seconds}s`;
const m = Math.floor(seconds / 60);
const s = seconds % 60;
if (m < 60) return s ? `${m}m ${s}s` : `${m}m`;
const h = Math.floor(m / 60);
const min = m % 60;
return min ? `${h}h ${min}m` : `${h}h`;
}
</script>
<% content_for :head do %>
<style>
.user-selector-compact .form-label-sm { font-size: 0.75rem; margin-bottom: 0.25rem; }

View File

@@ -23,6 +23,29 @@
],
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "ab7733a24f039adc6c400b3ec502206f9a158413249958f8a042f2204950e293",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/admin/timeline_controller.rb",
"line": 155,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Neighborhood::Post.where(\"airtable_fields -> 'slackId' ?| array[#{User.where(:id => (([current_user.id] + (params[:user_ids].split(\",\").map(&:to_i).uniq or [])) << User.find_by(:slack_uid => Neighborhood::Post.find_by(:airtable_id => params[\"post_id\"]).airtable_fields[\"slackId\"].first).id)).pluck(:slack_uid).compact.map do\n ActiveRecord::Base.connection.quote(uid)\n end.join(\", \")}]\")",
"render_path": null,
"location": {
"type": "method",
"class": "Admin::TimelineController",
"method": "show"
},
"user_input": "User.where(:id => (([current_user.id] + (params[:user_ids].split(\",\").map(&:to_i).uniq or [])) << User.find_by(:slack_uid => Neighborhood::Post.find_by(:airtable_id => params[\"post_id\"]).airtable_fields[\"slackId\"].first).id)).pluck(:slack_uid)",
"confidence": "High",
"cwe_id": [
89
],
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,

View File

@@ -17,9 +17,9 @@ Rails.application.routes.draw do
get "/stop_impersonating", to: "sessions#stop_impersonating", as: :stop_impersonating
namespace :admin do
get 'timeline', to: 'timeline#show', as: :timeline
get 'timeline/search_users', to: 'timeline#search_users'
get 'timeline/leaderboard_users', to: 'timeline#leaderboard_users'
get "timeline", to: "timeline#show", as: :timeline
get "timeline/search_users", to: "timeline#search_users"
get "timeline/leaderboard_users", to: "timeline#leaderboard_users"
end
if Rails.env.development?

3
db/schema.rb generated
View File

@@ -10,12 +10,9 @@
#
# It's strongly recommended that you check this file into your version control system.
create_schema "pganalyze"
ActiveRecord::Schema[8.0].define(version: 2025_05_16_142043) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "pg_stat_statements"
create_table "ahoy_events", force: :cascade do |t|
t.bigint "visit_id"