mirror of
https://github.com/SrIzan10/hc-harbor.git
synced 2026-05-01 10:45:21 +00:00
Add airtable url to table sync concern (#236)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
3
db/schema.rb
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user