Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ group :development, :test do
gem "pry-rails"

# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
gem "brakeman", "~> 8.0.2", require: false
gem "brakeman", "~> 8.0.4", require: false

# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false
Expand All @@ -72,4 +72,8 @@ group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"

# RSpec test helpers
gem "shoulda-matchers", "~> 6.0"
gem "factory_bot_rails"
end
13 changes: 11 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ GEM
bindex (0.8.1)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (8.0.2)
brakeman (8.0.4)
racc
builder (3.3.0)
capybara (3.40.0)
Expand Down Expand Up @@ -111,6 +111,11 @@ GEM
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
factory_bot (6.5.6)
activesupport (>= 6.1.0)
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
fugit (1.11.2)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
Expand Down Expand Up @@ -325,6 +330,8 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
shoulda-matchers (6.5.0)
activesupport (>= 5.2.0)
solid_cable (3.0.12)
actioncable (>= 7.2)
activejob (>= 7.2)
Expand Down Expand Up @@ -396,10 +403,11 @@ PLATFORMS

DEPENDENCIES
bootsnap
brakeman (~> 8.0.2)
brakeman (~> 8.0.4)
capybara
csv
debug
factory_bot_rails
hirb
importmap-rails
jbuilder
Expand All @@ -412,6 +420,7 @@ DEPENDENCIES
rspec-rails
rubocop-rails-omakase
selenium-webdriver
shoulda-matchers (~> 6.0)
solid_cable
solid_cache
solid_queue
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/companies_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ def set_company

# Only allow a list of trusted parameters through.
def company_params
params.expect(company: [ :name, :industry, :company_type, :location, :size, :website, :linkedin, :known_tech_stack ])
params.expect(company: [
:name, :industry, :company_type, :location, :size, :website, :linkedin, :known_tech_stack,
:primary_product, :revenue_model, :funding_stage, :estimated_revenue, :estimated_employees,
:growth_signal, :product_maturity, :engineering_maturity, :process_maturity,
:market_position, :competitor_tier, :brand_signal_strength, :market_size_estimate
])
end
end
53 changes: 53 additions & 0 deletions app/controllers/interview_sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class InterviewSessionsController < ApplicationController
before_action :set_interview_session, only: [ :show, :edit, :update, :destroy ]

def index
@interview_sessions = InterviewSession.all.includes(:opportunity).order(date: :desc)
end

def show
end

def new
@interview_session = InterviewSession.new
end

def create
@interview_session = InterviewSession.new(interview_session_params)

if @interview_session.save
redirect_to @interview_session, notice: "Interview session was successfully created."
else
render :new, status: :unprocessable_entity
end
end

def edit
end

def update
if @interview_session.update(interview_session_params)
redirect_to @interview_session, notice: "Interview session was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@interview_session.destroy
redirect_to interview_sessions_url, notice: "Interview session was successfully deleted."
end

private

def set_interview_session
@interview_session = InterviewSession.find(params[:id])
end

def interview_session_params
params.require(:interview_session).permit(
:opportunity_id, :stage, :date, :confidence_score, :clarity_score,
:questions_asked, :weak_areas, :strong_areas, :follow_up, :overall_signal
)
end
end
4 changes: 2 additions & 2 deletions app/controllers/opportunities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def index
sort_direction = params[:direction] == "desc" ? "desc" : "asc"

# Validate sort column to prevent SQL injection
allowed_columns = %w[position_title application_date status remote tech_stack created_at salary_range chatgpt_match jobright_match linkedin_match role_type]
allowed_columns = %w[position_title application_date status remote tech_stack created_at salary_range bus_factor chatgpt_match jobright_match linkedin_match role_type]
if allowed_columns.include?(sort_column)
@opportunities = @opportunities.order("#{sort_column} #{sort_direction}")
elsif sort_column == "company"
Expand Down Expand Up @@ -111,7 +111,7 @@ def set_opportunity
def opportunity_params
params.expect(opportunity: [
:company_id, :position_title, :application_date, :status, :notes, :remote,
:tech_stack, :other_tech_stack, :source, :salary_range, :listing_url,
:tech_stack, :other_tech_stack, :source, :salary_range, :bus_factor, :listing_url,
:chatgpt_match, :jobright_match, :linkedin_match, :role_type,
technology_ids: [],
role_metadata: {}
Expand Down
73 changes: 73 additions & 0 deletions app/controllers/star_stories_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
class StarStoriesController < ApplicationController
before_action :set_star_story, only: [ :show, :edit, :update, :destroy ]

def index
@star_stories = StarStory.all.order(created_at: :desc)

respond_to do |format|
format.html
format.csv do
exporter = StarStoriesCsvExporter.new(@star_stories)
send_data exporter.generate,
filename: "star_stories_#{Date.current.strftime('%Y-%m-%d')}.csv",
type: "text/csv"
end
end
end

def new
@star_story = StarStory.new
end

def create
@star_story = StarStory.new(star_story_params)
process_skills_from_form

if @star_story.save
redirect_to @star_story, notice: "STAR story was successfully created."
else
render :new, status: :unprocessable_entity
end
end

def edit
end

def update
process_skills_from_form

if @star_story.update(star_story_params)
redirect_to @star_story, notice: "STAR story was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@star_story.destroy
redirect_to star_stories_url, notice: "STAR story was successfully deleted."
end

def show
end

private

def set_star_story
@star_story = StarStory.find(params[:id])
end

def star_story_params
params.require(:star_story).permit(
:title, :situation, :task, :action, :result, :category, :outcome,
:strength_score, :notes, :skills, opportunity_ids: []
)
end

def process_skills_from_form
if params[:star_story][:skills].is_a?(String)
skills_array = params[:star_story][:skills].split("\n").map(&:strip).reject(&:blank?)
params[:star_story][:skills] = skills_array
end
end
end
2 changes: 2 additions & 0 deletions app/helpers/interview_sessions_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module InterviewSessionsHelper
end
2 changes: 2 additions & 0 deletions app/helpers/star_stories_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module StarStoriesHelper
end
60 changes: 60 additions & 0 deletions app/models/core_narrative.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
class CoreNarrative < ApplicationRecord
# Enums
enum :narrative_type, {
about_me: "about_me",
why_company: "why_company",
why_me: "why_me",
salary: "salary",
remote_positioning: "remote_positioning",
leadership: "leadership",
technical_depth: "technical_depth",
career_trajectory: "career_trajectory"
}, prefix: true

enum :role_target, {
software_engineer: "software_engineer",
sales_engineer: "sales_engineer",
solutions_engineer: "solutions_engineer",
product_manager: "product_manager",
support_engineer: "support_engineer",
success_engineer: "success_engineer",
devops: "devops",
hybrid: "hybrid",
universal: "universal"
}, prefix: true

# Validations
validates :narrative_type, presence: true
validates :content, presence: true

# Callbacks
before_save :set_last_updated, if: :content_changed?

# Scopes
scope :for_role, ->(role) { where(role_target: [ role, :universal ]) }
scope :by_type, ->(type) { where(narrative_type: type) }
scope :recently_updated, -> { where("last_updated >= ?", 30.days.ago).order(last_updated: :desc) }
scope :current_version, -> { where(version: "current").or(where(version: nil)) }

# Methods
def word_count
content.to_s.split.size
end

def estimated_reading_time
# Average reading speed: 200 words per minute
(word_count / 200.0).ceil
end

def needs_update?
return true if last_updated.nil?

last_updated < 60.days.ago
end

private

def set_last_updated
self.last_updated = Date.today
end
end
47 changes: 47 additions & 0 deletions app/models/interview_session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class InterviewSession < ApplicationRecord
belongs_to :opportunity

# Enums
enum :stage, {
recruiter: "recruiter",
tech_screen: "tech_screen",
panel: "panel",
final: "final",
hiring_manager: "hiring_manager",
peer: "peer",
behavioral: "behavioral"
}, prefix: true

enum :overall_signal, {
strong: "strong",
neutral: "neutral",
weak: "weak"
}, prefix: true

# Validations
validates :stage, presence: true
validates :date, presence: true
validates :confidence_score, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 10 }, allow_nil: true
validates :clarity_score, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 10 }, allow_nil: true

# Scopes
scope :recent, -> { order(date: :desc) }
scope :by_stage, ->(stage) { where(stage: stage) }
scope :strong_signals, -> { where(overall_signal: :strong) }
scope :this_month, -> { where(date: Date.today.beginning_of_month..Date.today.end_of_month) }

# Methods
def performance_score
return nil unless confidence_score.present? && clarity_score.present?

(confidence_score + clarity_score) / 2.0
end

def needs_follow_up?
follow_up.present? && follow_up.strip.length > 0
end

def has_preparation_gaps?
weak_areas.present? && weak_areas.strip.length > 0
end
end
26 changes: 26 additions & 0 deletions app/models/opportunity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Opportunity < ApplicationRecord
belongs_to :company
has_many :opportunity_technologies, dependent: :destroy
has_many :technologies, through: :opportunity_technologies
has_many :interview_sessions, dependent: :destroy
has_many :star_story_opportunities, dependent: :destroy
has_many :star_stories, through: :star_story_opportunities

accepts_nested_attributes_for :opportunity_technologies, allow_destroy: true

Expand All @@ -28,7 +31,30 @@ class Opportunity < ApplicationRecord
"other" => "Other"
}.freeze

# Career Intelligence Enums
enum :retirement_plan_type, {
four_oh_one_k: "401k",
simple_ira: "simple_ira",
none: "none",
unknown: "unknown"
}, prefix: :retirement

enum :remote_type, {
remote: "remote",
hybrid: "hybrid",
onsite: "onsite"
}, prefix: :work

enum :risk_level, {
low: "low",
medium: "medium",
high: "high"
}, prefix: :risk

validates :role_type, presence: true, inclusion: { in: ROLE_TYPES.keys }
validates :fit_score, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 10 }, allow_nil: true
validates :trajectory_score, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 10 }, allow_nil: true
validates :strategic_value, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 10 }, allow_nil: true

before_save :shorten_urls
before_save :standardize_salary_range
Expand Down
Loading