diff --git a/app/controllers/admin/post_reviews_controller.rb b/app/controllers/admin/post_reviews_controller.rb new file mode 100644 index 0000000..5f7e820 --- /dev/null +++ b/app/controllers/admin/post_reviews_controller.rb @@ -0,0 +1,139 @@ +# app/controllers/admin/post_reviews_controller.rb +class Admin::PostReviewsController < Admin::BaseController + include ApplicationHelper # For short_time_simple, short_time_detailed helpers + + before_action :set_post, only: [ :show ] + + def show + # User related to the post + slack_uid = @post.airtable_fields["slackId"]&.first + @user = User.find_by(slack_uid: slack_uid) + ensure_exists @user # Redirects if user not found + + @target_user_timezone = @user.timezone || "UTC" # Target user's timezone + @project_repo_mappings_for_user = @user.project_repo_mappings.index_by(&:project_name) + + begin + post_start_str = @post.airtable_fields["lastPost"] + post_end_str = @post.airtable_fields["createdAt"] + @total_post_hackatime_seconds = @post.airtable_fields["hackatimeTime"]&.to_i || 0 + + + if post_start_str.blank? || post_end_str.blank? + raise ArgumentError, "Post start or end date is missing from Airtable. lastPost: '#{post_start_str}', createdAt: '#{post_end_str}'" + end + + post_start_utc = Time.zone.parse(post_start_str) + post_end_utc = Time.zone.parse(post_end_str) + rescue ArgumentError, TypeError => e + Rails.logger.error "Failed to parse post dates from Airtable for post #{@post.id} (User: #{@user.id}): #{e.message}" + flash.now[:alert] = "Could not parse post dates for post ID #{@post.airtable_id}. Please check Airtable data. Error: #{e.message}" + @review_start_date = Date.current - 1.day + @review_end_date = Date.current + 1.day + @post_start_display = Time.current + @post_end_display = Time.current + @commits = [] + @detailed_spans = [] + render and return + end + + @post_start_display = post_start_utc.in_time_zone(@target_user_timezone) + @post_end_display = post_end_utc.in_time_zone(@target_user_timezone) + + @review_start_date = (@post_start_display.to_date - 1.day) + @review_end_date = (@post_end_display.to_date + 1.day) + + query_start_utc = @review_start_date.in_time_zone(@target_user_timezone).beginning_of_day.utc + query_end_utc = @review_end_date.in_time_zone(@target_user_timezone).end_of_day.utc + + @commits = Commit.where(user: @user, created_at: query_start_utc..query_end_utc).order(created_at: :asc) + + all_heartbeats_for_user_in_review_window = Heartbeat + .where.not(project: nil) + .where(user: @user, time: query_start_utc.to_f..query_end_utc.to_f) + .select(:id, :user_id, :time, :entity, :project, :editor, :language) + .order(:time) + .to_a + + @unique_project_names = all_heartbeats_for_user_in_review_window + .map(&:project) + .compact + .reject(&:blank?) + .uniq + .sort + + @recommended_project_names = @post.app.projects.map(&:airtable_fields).map { |p| p["name"] }.compact.uniq.sort + + if params[:projects].present? + selected_projects = params[:projects].split(",") + all_heartbeats_for_user_in_review_window = all_heartbeats_for_user_in_review_window.select do |hb| + selected_projects.include?(hb.project) + end + end + + @detailed_spans = [] + timeout_duration = 20.minutes.to_i + if all_heartbeats_for_user_in_review_window.any? + current_span_heartbeats = [] + + all_heartbeats_for_user_in_review_window.each_with_index do |heartbeat, index| + current_span_heartbeats << heartbeat + is_last_heartbeat = (index == all_heartbeats_for_user_in_review_window.length - 1) + time_to_next = is_last_heartbeat ? Float::INFINITY : (all_heartbeats_for_user_in_review_window[index + 1].time - heartbeat.time) + + if time_to_next > timeout_duration || is_last_heartbeat + if current_span_heartbeats.any? + start_time_numeric = current_span_heartbeats.first.time + last_hb_time_numeric = current_span_heartbeats.last.time + + actual_coded_duration_seconds = Heartbeat.where(id: current_span_heartbeats.map(&:id)).duration_seconds + + files = current_span_heartbeats.map { |h| h.entity&.split("/")&.last }.compact.uniq.sort + + projects_details_for_span = [] + unique_project_names = current_span_heartbeats.map(&:project).compact.reject(&:blank?).uniq + + unique_project_names.each do |p_name| + repo_mapping = @project_repo_mappings_for_user[p_name] + projects_details_for_span << { + name: p_name, + repo_url: repo_mapping&.repo_url + } + end + + editors = current_span_heartbeats.map(&:editor).compact.uniq.sort + languages = current_span_heartbeats.map(&:language).compact.uniq.sort + + @detailed_spans << { + id: "span_#{SecureRandom.hex(4)}", # Unique ID for checkbox + start_time: start_time_numeric, + end_time: last_hb_time_numeric, + duration: actual_coded_duration_seconds, + files_edited: files, + projects_edited_details: projects_details_for_span.sort_by { |p| p[:name].downcase }, + editors: editors, + languages: languages + } + current_span_heartbeats = [] + end + end + end + end + + @current_user_timezone = current_user.timezone + end + + private + + def set_post + @post = Neighborhood::Post.find_by(airtable_id: params[:post_id]) + ensure_exists @post + end + + def ensure_exists(value) + unless value.present? + object_name = value.nil? ? "Record" : value.class.name.demodulize + redirect_to admin_timeline_path, alert: "#{object_name} not found." + end + end +end diff --git a/app/controllers/admin/ysws_reviews_controller.rb b/app/controllers/admin/ysws_reviews_controller.rb new file mode 100644 index 0000000..b59fab4 --- /dev/null +++ b/app/controllers/admin/ysws_reviews_controller.rb @@ -0,0 +1,25 @@ +class Admin::YswsReviewsController < Admin::BaseController + include ApplicationHelper + + before_action :set_submission, only: [ :show ] + + def show + end + + private + + def set_submission + @submission = Neighborhood::YswsSubmission.find_by(airtable_id: params[:record_id]) + ensure_exists @submission + @app = @submission.app + ensure_exists @app + @posts = @app.posts + end + + def ensure_exists(value) + unless value.present? + object_name = value.nil? ? "Record" : value.class.name.demodulize + redirect_to admin_timeline_path, alert: "#{object_name} not found." + end + end +end diff --git a/app/jobs/neighborhood/sync_from_airtable_job.rb b/app/jobs/neighborhood/sync_from_airtable_job.rb index a4bcec4..cd1e89c 100644 --- a/app/jobs/neighborhood/sync_from_airtable_job.rb +++ b/app/jobs/neighborhood/sync_from_airtable_job.rb @@ -5,5 +5,6 @@ class Neighborhood::SyncFromAirtableJob < ApplicationJob Neighborhood::App.pull_all_from_airtable! Neighborhood::Project.pull_all_from_airtable! Neighborhood::Post.pull_all_from_airtable! + Neighborhood::YswsSubmission.pull_all_from_airtable! end end diff --git a/app/models/neighborhood/app.rb b/app/models/neighborhood/app.rb index a4712eb..9e2ce62 100644 --- a/app/models/neighborhood/app.rb +++ b/app/models/neighborhood/app.rb @@ -6,4 +6,14 @@ class Neighborhood::App < ApplicationRecord has_table_sync base: "appnsN4MzbnfMY0ai", table: "tbls2fHyQYCtCbYbl", pat: ENV["NEIGHBORHOOD_AIRTABLE_PAT"] + + def posts + return [] unless airtable_fields["devlog"]&.any? + Neighborhood::Post.where(airtable_id: airtable_fields["devlog"]) + end + + def projects + return [] unless airtable_fields["hackatimeProjects"]&.any? + Neighborhood::Project.where(airtable_id: airtable_fields["hackatimeProjects"]) + end end diff --git a/app/models/neighborhood/post.rb b/app/models/neighborhood/post.rb index 883a95c..718eae1 100644 --- a/app/models/neighborhood/post.rb +++ b/app/models/neighborhood/post.rb @@ -6,4 +6,9 @@ class Neighborhood::Post < ApplicationRecord has_table_sync base: "appnsN4MzbnfMY0ai", table: "tbl0iKxglbySiEbB4", pat: ENV["NEIGHBORHOOD_AIRTABLE_PAT"] + + def app + return nil unless airtable_fields["app"]&.first + Neighborhood::App.find_by(airtable_id: airtable_fields["app"].first) + end end diff --git a/app/models/neighborhood/ysws_submission.rb b/app/models/neighborhood/ysws_submission.rb new file mode 100644 index 0000000..ca67da2 --- /dev/null +++ b/app/models/neighborhood/ysws_submission.rb @@ -0,0 +1,14 @@ +class Neighborhood::YswsSubmission < ApplicationRecord + self.table_name = "neighborhood_ysws_submissions" + + include HasTableSync + + has_table_sync base: "appnsN4MzbnfMY0ai", + table: "tblbyu0FABZJ0wvaJ", + pat: ENV["NEIGHBORHOOD_AIRTABLE_PAT"] + + def app + return nil unless airtable_fields["app"]&.first + Neighborhood::App.find_by(airtable_id: airtable_fields["app"].first) + end +end diff --git a/app/views/admin/post_reviews/show.html.erb b/app/views/admin/post_reviews/show.html.erb new file mode 100644 index 0000000..620877f --- /dev/null +++ b/app/views/admin/post_reviews/show.html.erb @@ -0,0 +1,456 @@ +<%# app/views/admin/post_reviews/show.html.erb %> +<% content_for :title, "Post Review: #{@post.airtable_fields['description']&.truncate(30) || 'Untitled Post'}" %> + +<% + pixels_per_hour = 70 + pixels_per_minute = pixels_per_hour / 60.0 + timeline_start_hour = 0 + timeline_end_hour = 23 + day_column_width_px = 320 + hour_label_width_rem = 3.5 + header_height_px = 40 + + days_to_display = (@review_start_date..@review_end_date).to_a + num_days_in_review = days_to_display.count +%> + + + +
User: <%= render "shared/user_mention", user: @user %>
<% end %> +Description: <%= simple_format(@post.airtable_fields["description"]) %>
++ Post Period (User TZ: <%= @target_user_timezone %>): + <%= @post_start_display.strftime("%B %d, %Y %l:%M %p") %> + to + <%= @post_end_display.strftime("%B %d, %Y %l:%M %p") %> +
++ Review Window (Dates in User TZ): + <%= @review_start_date.strftime("%B %d, %Y") %> to <%= @review_end_date.strftime("%B %d, %Y") %> +
+ <% if @post.airtable_fields["demoVideo"].present? %> + <%= render "shared/video_on_hover", src: @post.airtable_fields["demoVideo"] %> + <% end %> + <% if @post.airtable_fields["photoboothVideo"].present? %> + <%= render "shared/video_on_hover", src: @post.airtable_fields["photoboothVideo"] %> + <% end %> +Airtable Link: <%= link_to "View on Airtable", @post.airtable_url, target: "_blank", class: "button-style" %>
+API Link: <%= link_to "View on stats endpoint", api_v1_path(username: @user.id, start_date: @post_start_display.iso8601, end_date: @post_end_display.iso8601, features: "projects") %>
++ User: <%= render "shared/user_mention", user: @user %> +
+ <% end %> + <% if @app.airtable_fields["Icon"]&.any? %> ++ App: <%= @app.airtable_fields["Name"] %> +
++ Airtable Link: + <%= link_to "View on Airtable", @submission.airtable_url, target: "_blank", style: "color: #60A5FA; text-decoration: none;" %> +
+No posts found for this submission.
+ <% end %> +