Website/lib/cv.rb

921 lines
27 KiB
Ruby

require "text.rb"
require "latex.rb"
class NilClass
def lookup_or_nil(x)
nil;
end
end
class Array
def lookup_or_nil(x)
self[x]
end
def oxford_comma
if length <= 1 then self.join(", ")
elsif length == 2 then "#{self[0]} and #{self[1]}"
else
"#{self[0...-1].join(", ")}, and #{self[-1]}"
end
end
end
class Hash
def lookup_or_nil(x)
self[x]
end
end
class CV < Latex::Builder
include Text
def initialize(who, out, options = {})
super(out)
@data = $db["cv"][who]
@who = who
@include_rejected_grants = options.fetch(:include_rejected_grants, false)
@include_pending_papers = options.fetch(:include_pending_papers, false)
@include_pending_grants = options.fetch(:include_pending_grants, false)
@include_blurb = options.fetch(:include_blurb, false)
@title = options.fetch(:title, "Curriculum Vitae")
@since = Hash.new { |h,k| if h.has_key? :rest then h[:rest] else 0 end }
options[:since].each { |k, v| @since[k] = v } if options.has_key?(:since)
@skip = options.fetch(:skip, [:blurb])
@abbreviate = options.fetch(:abbreviate, [])
gen
end
def CV.make(who, out, options = {})
CV.new(who, out, options)
end
def make_section(title)
section!(title)
print_direct "~"
endl("-0.4in")
hrule
nopagebreak
print_direct "~"
endl("-.2in")
nopagebreak
puts_direct "\n\n"
end
def gen
documentclass("11pt", "article")
usepackage "fullpage"
usepackage "bibentry"
usepackage "enumitem"
document do
pagestyle "empty"
render_header
vspace "0.15in"
unless skip?(:contact)
render_contact_details
vspace! "-0.1in"
end
render_blurb if @include_blurb
render_education unless skip?(:education)
render_employment_history unless skip?(:employment)
render_honors unless skip?(:honors)
render_students unless skip?(:students)
render_professional_activities unless skip?(:service)
render_invited_talks unless skip?(:invited_talks)
render_courses_taught unless skip?(:courses)
render_grant_support unless skip?(:grants)
render_short_grant_support if @abbreviate.include? :grants
render_pubs_and_artifacts unless skip?(:publications)
end
end
def filter_records(category, records)
records.select { |record|
duration_is_since?(record, @since[category])
}
end
def build_filtered_itemize(category, records, *args)
args.unshift(filter_records(category, records))
build_itemize(*args) { |record| yield record }
end
def skip?(category)
@skip.include? category or @abbreviate.include? category
end
def since_title_subscript(category)
if @since.has_key? category
" (since #{@since[category]})"
else "" end
end
############### HEADER ################
def render_header
noindent
textbf { huge; puts @title }
hfill
textbf { huge; puts @data["name"] }
endl "0.05in"
hrule
end
############### CONTACT DETAILS ################
def render_contact_details
noindent
puts @data["work"]["dept"]
hfill
puts address_line_1(@data["home"])
endl
puts @data["work"]["employer"]
hfill
puts address_line_2(@data["home"])
endl
puts address_line_1(@data["work"]["address"])
endl
puts address_line_2(@data["work"]["address"])
puts ""
puts ""
smallskip
noindent
even_odd = false
@data["contact"].each do |k,v|
textit { puts "#{k}:" }
puts v
if even_odd then endl
else hfill end
even_odd = !even_odd
end
end
############### BLURB ################
def render_blurb
make_section("Overview")
puts @data["blurb"]
end
############### EDUCATION ################
def render_education
make_section("Education")
build_filtered_itemize(:education, @data["education"], "label=,leftmargin=0mm") do |r|
textit { puts "#{r["degree"]}," }
puts "#{r["university"]}, #{r["city"]}"
hfill
puts r["year"]
endl
puts r["dept"]
case r["degree"].split(/ +/)[0]
when "PhD"
endl
puts "Thesis: "
textit { puts "``#{r["Thesis"]}''" }
endl
puts "Advisor: #{r["Advisor"]}"
end
end
end
############### EMPLOYMENT HISTORY ################
def render_employment_history(elems = @data["employment"])
make_section("Employment History")
build_filtered_itemize(:employment, elems) do |r|
puts "#{r["title"]}, #{r["employer"]}"
hfill
puts duration(r)
end
end
############### HONORS ################
def render_honors
make_section("Honors")
build_filtered_itemize(:honors, @data["honors"], "label=,leftmargin=0mm") do |r|
puts r["description"]
end
end
############### PROFESSIONAL ACTIVITIES ################
def render_one_activity_class(activity_class, data, role_title)
role_title =
case role_title
when Proc then role_title
else
orig_role_title = role_title
lambda { |r| r[orig_role_title] }
end
build_filtered_itemize(activity_class, data, "noitemsep,leftmargin=5mm,label=--") do |r|
puts(role_title.call(r))
hfill
puts duration(r)
unless r["roles"].nil?
build_itemize(r["roles"], "noitemsep") do |role|
puts role["title"]
hfill
puts duration(role)
end
end
end
end
def render_professional_activities(args = {})
make_section("Professional Activities")
unless skip? :chair
subsection!("Chair Positions")
render_one_activity_class(:reviewer, @data["chairs"],
lambda { |r|
position = r["position"]
raise "Position missing from #{r}" unless position
venue = LabMetadata.complete_venue(r)["type"]
raise "Unknown Venue: #{r}" unless venue;
"#{position.capitalize}: #{LabMetadata.venue_name(r, size: :full)}"
})
end
unless skip? :reviewer
subsection!("Program Comittee Member")
render_one_activity_class(:reviewer, @data["reviewer"].select { |r| r.fetch("pc", false) },
lambda { |r|
venue = LabMetadata.complete_venue(r)["type"]
raise "Unknown Venue: #{r}" unless venue;
"#{venue.capitalize}: #{LabMetadata.venue_name(r)}"
})
subsection!("Reviewer")
render_one_activity_class(:reviewer, @data["reviewer"].select { |r| not r.fetch("pc", false) },
lambda { |r|
venue = LabMetadata.complete_venue(r)["type"]
raise "Unknown Venue: #{r}" unless venue;
"#{venue.capitalize}: #{LabMetadata.venue_name(r)}"
})
end
unless skip? :general_service
subsection!("Service")
render_one_activity_class(:general_service, @data["service"]["general"], "description")
end
unless skip? :memberships
subsection!("Professional Memberships")
render_one_activity_class(:memberships, @data["memberships"], "org")
end
unless skip? :volunteering
subsection!("Volunteer Work")
render_one_activity_class(:volunteering, @data["volunteering"], "org")
end
unless skip? :dept_service
subsection!("Departmental Service")
build_itemize(@data["service"]["dept"]) do |dept|
puts dept["org"]
render_one_activity_class(:dept_service, dept["service"], "org")
end
end
end
############### INVITED TALKS ################
def render_invited_talks
make_section("Invited Talks"+since_title_subscript(:talks))
build_filtered_itemize(:talks, @data["talks"], "noitemsep,leftmargin=5mm", enumerate: true) do |r|
textit { puts r["venue"] }
wrap {
small
em_dash
puts " #{r["talk"]}"
puts "(with #{r["with"].join(', ')})" unless r["with"].nil?
}
hfill
puts r["date"]
end
end
############### COURSES ################
def render_courses_taught
make_section("Courses Taught"+since_title_subscript(:courses))
courses = @data["courses"]
since = @since[:courses]
courses = courses.map do |dept|
dept = dept.clone
dept["courses"] = dept["courses"].select do |c|
unless /([0-9]{4})/ =~ c["semester"]
raise "Invalid Semester: '#{c["semester"]}'"
end
$1.to_i >= since
end
dept unless dept["courses"].empty?
end.compact
build_itemize(courses, "leftmargin=1mm,label=") do |dept|
puts dept["venue"]
build_itemize(dept["courses"], "noitemsep,leftmargin=7mm") do |r|
texttt { puts r["code"] }
puts ":"
wrap {
small
puts "#{r["title"]}"
puts " (as #{r["role"]})" unless r["role"].nil?
puts "--- #{r["enrollment"]} students" unless r["enrollment"].nil?
}
hfill
puts r["semester"]
end
end
end
############### GRANT SUPPORT ################
def render_grant_support(args = {})
make_section("Grant Support"+since_title_subscript(:grants))
test_for_since = proc { |r| duration_is_since?(r, @since[:grants]) }
grant_categories = [
["Planned Applications", "planned", ["grant", "gift"]],
["Pending Applications", "submitted", ["grant", "gift"]],
["Active Grants", "accepted", ["grant"]],
["Gifts", "accepted", ["gift"]],
]
grant_categories += [
["Completed", "completed", ["grant", "gift"]]
] unless skip?(:completed_grants)
grant_categories += [
["Declined", "rejected", ["grant", "gift"]]
] if args.fetch(:include_rejected, @include_rejected_grants)
fields_to_display = args.fetch(:fields, [
"title",
"agency",
"role",
"effective dates",
"start",
"end",
"amount",
"effort",
"copis",
"collaborative",
"feedback",
"supplements"
])
grant_categories.each do |sec, status, type|
## Find the set of grants that apply to the current categories
grants_in_category = @data["grants"].select { |r|
(r["status"] == status) and
(type.include?(r["type"])) and
test_for_since.call(r)
}.sort { |a,b| to_date(b["start"]) <=> to_date(a["start"])}
unless grants_in_category.empty?
awarded_grants =
grants_in_category
.select { |r| r["status"] == "accepted" or r["status"] == "completed" }
total_amount =
awarded_grants.map { |r| amt = r["amount"]; if amt.is_a? Numeric then amt else 0 end }.sum
by_effort =
awarded_grants.map { |r| amt = r["amount"]; if amt.is_a? Numeric then amt * (r["effort"].sub(/%/, "").to_f/100.0) else 0 end }.sum
dollar_summary =
if total_amount > 0
"--- #{format_money total_amount} Total; #{format_money by_effort} By Effort"
else
""
end
## Header for the category
subsection!("#{sec} (#{grants_in_category.length}) #{dollar_summary}")
## For each grant in the category
grants_in_category.each do |r|
collaborative_total = 0;
supplements_total = 0
our_total = 0;
total_awarded = 0;
award_value_is_string = true
# Derive grant totals for collaborative grants and
# grants with supplements, as well as fractional
# amounts for us.
if r["amount"].is_a? Numeric then
collaborative_total =
if r["collaborative"].nil? then 0 else
r["collaborative"].map { |x| x["amount"].to_f }.sum
end
supplements_total =
if r["supplements"].nil? then 0 else
r["supplements"].map { |x| x["amount"].to_f }.sum
end
our_total = r["amount"].to_f + supplements_total
total_awarded = our_total + collaborative_total;
award_value_is_string = false
end
noindent
tabular("p{0.15\\textwidth}p{0.8\\textwidth}") do
# For each field being displayed
fields_to_display.each do |k|
# Only display fields if they're available
unless r[k].nil?
# Field title
textit { puts(
case k
when "copis" then "Co-PI".pluralize(r["copis"].length)
when "collaborative" then
"Peer Org.".pluralize(r["collaborative"].length)
else k.capitalize
end
)}
# Break
print_direct ":&"
# Post-processing on field value
case k
when "title", "agency"
parbox("5in") do
if k == "title" then textsf { puts r[k] } else puts r[k] end
end
when "effective dates"
puts duration(r)
when "copis"
puts r["copis"].join(", ")
when "amount"
unless award_value_is_string
puts format_money our_total
unless r["collaborative"].nil?
puts " (Total Across Collaboration: #{format_money total_awarded})"
end
else
puts r["amount"]
end
when "collaborative"
r["collaborative"].each do |i|
puts i["institution"]
puts "(#{format_money i["amount"]})"
end
when "supplements"
unless award_value_is_string then
puts(r["supplements"].map do |supp|
"#{supp["type"]}"
end.join(", "))
puts "(#{"amount".pluralize(r["supplements"].length)} included in total above)"
else
puts(r["supplements"].map do |supp|
"#{supp["type"]} (#{format_money supp["amount"].to_f})"
end.join(", "))
end
else # case k
puts r[k]
end # case k
endl("0.02in")
end # unless r[k].nil?
end # fields_to_display.each
end # block("tabular") (field table)
endl("0.2in")
end # grants_in_category.each
end # unless grants_in_category.empty?
end # grant_categories.each
end
def render_short_grant_support
make_section("Grant Support"+since_title_subscript(:grants))
test_for_since = proc { |r| duration_is_since?(r, @since[:grants]) }
grants = @data["grants"].select { |r|
r["status"] == "accepted" and test_for_since.call(r)
}
build_itemize(grants, "noitemsep,leftmargin=5mm,label=--") do |r|
copis = r.fetch("copis", [])
copi_string = if copis.empty? then "" else "; with "+copis.oxford_comma end
puts "#{r["title"]} (#{format_money r["amount"]} from #{r["agency"].split(/:/)[0]}#{copi_string})"
end
end
############### PUBLICATIONS AND ARTEFACTS ################
def render_author_list(record)
advisors = $db["cv/#{@who}/education"].map { |r| r["Advisor"] }.compact
print_direct "#{
record["authors"].map do |name|
abbreviated_name = LabMetadata.person_name(name, :size => :short)
if LabMetadata.is_member? name
"\\textbf{#{abbreviated_name}}"
elsif advisors.include? name
"\\underline{#{abbreviated_name}}"
else
safe_text(abbreviated_name)
end
end.join(", ")
}#{
case record["authornote"]
when nil then ""
when "alpha" then " (\\textit{Authors listed in alphabetical order})"
else " (\\textit{#{safe_text(record["authornote"])}}"
end
}; "
end
def render_editor_list(record)
v =
eds = LabMetadata.complete_venue(record).
lookup_or_nil("editors").
lookup_or_nil(record["year"])
if eds
puts "Ed#{eds.length != 1 ? "s" : ""}. #{eds.join(", ")}"
end
end
def render_selectivity(record)
year = record["year"].to_s
v = LabMetadata.complete_venue(record)
sel_data = v["selectivity"]
rate = "acceptance rate"
if sel_data.nil?
"#{rate} not available"
elsif sel_data[year].is_a? Numeric
"#{rate} #{(sel_data[year]*100).round(2)}%"
elsif not sel_data[year].nil?
"#{rate} #{sel_data[year]}"
elsif sel_data["average"].is_a? Numeric
"#{rate} unknown; average rate for past conferences #{
get_avg_selectivity(record)
}"
elsif not sel_data["average"].nil?
"#{rate} #{sel_data["average"]}"
else
"#{rate} unknown; average rate for past conferences #{
get_avg_selectivity(record)
}"
end
end
def get_venue_type(record)
LabMetadata.complete_venue(record).
lookup_or_nil("type")
end
def get_publisher(record)
LabMetadata.complete_venue(record).
lookup_or_nil("publisher")
end
def get_pub_length_string(record)
case record["length"]
when String
record["length"]
else
"#{record["length"]} #{"page".pluralize(record["length"])}";
end
end
def get_avg_selectivity(record, rounding = 2)
LabMetadata.venue_selectivity(record, rounding: rounding)
end
def render_paper_title(title)
print_direct "\\textsc{#{title}}"
end
def render_one_pub_or_artifact(title,list,fmt,filter = proc { true })
list = list.select { |r| filter.call(r) }
unless list.empty?
subsection!("#{title} (#{list.length})")
build_itemize(list, "leftmargin=0mm,label=") { |r| fmt.call(r) }
end
end
def render_pubs_and_artifacts(args = {})
global_filter =
proc { |r| duration_is_since?(r, @since[:publications])}
all_my_pubs = LabMetadata.publications_for(@who)
make_section("Publications and Artifacts"+since_title_subscript(:publications))
center {
print_direct "\\noindent Students I advise or co-advise and I are in \\textbf{bold}, my former advisor is \\underline{underlined}"
}
# block("center") do
# puts "(Author lists marked with a * are not correlated with level of contribution)"
# end
unless skip?(:journal_pubs)
render_one_pub_or_artifact(
"Journal Publications",
all_my_pubs.where do |r|
case get_venue_type(r)
when "journal" then true
else false
end
end,
proc do |r|
render_author_list(r)
textit {
render_paper_title(r["title"])
}
puts "---"
extra_fields =
if r["volume"].nil? then []
else ["Volume #{r["volume"]}, Number #{r["number"]}"] end +
if r["issuetitle"].nil? then []
else [r["issuetitle"]] end
textit {
puts "#{LabMetadata.venue_name(r)} #{r["year"]}#{if extra_fields.length > 0 then " (#{extra_fields.join("; ")}), " else ", " end}"
}
puts "#{get_pub_length_string(r)}, "
puts render_selectivity(r)
puts " / "+r["notes"].join(" / ") if r.has_key? "notes"
end,
global_filter
)
end
unless skip?(:conference_pubs)
render_one_pub_or_artifact(
"Conference Publications",
all_my_pubs.where do |r|
case get_venue_type(r)
when "conference" then true
else false
end
end,
proc do |r|
render_author_list(r)
textit {
render_paper_title(r["title"])
}
puts "---"
textit{ puts LabMetadata.venue_name(r) }
puts "#{r["year"]}; "
venue = LabMetadata.complete_venue(r)
raise "Unknown location for venue: #{venue["acronym"] || venue["venue"]} #{r["year"]}" unless venue.has_key? "location"
puts "#{venue["location"]}; "
puts "#{get_pub_length_string(r)}, "
puts render_selectivity(r)
puts " / "+r["notes"].join(" / ") if r.has_key? "notes"
end,
global_filter
)
end
unless skip?(:workshop_pubs)
render_one_pub_or_artifact(
"Workshop Publications",
all_my_pubs.where do |r|
case get_venue_type(r)
when "workshop" then true
else false
end
end,
proc do |r|
render_author_list(r)
textit {
render_paper_title(r["title"])
}
puts "---"
textit { puts LabMetadata.venue_name(r) }
puts "#{r["year"]}; "
venue = LabMetadata.complete_venue(r)
raise "Unknown location for venue: #{venue["acronym"] || venue["venue"]} #{r["year"]}" unless venue.has_key? "location"
puts "#{venue["location"]}; "
puts "#{get_pub_length_string(r)}, "
puts render_selectivity(r)
end,
global_filter
)
end
unless skip?(:book_chapters)
render_one_pub_or_artifact(
"Book Chapters",
all_my_pubs.where do |r|
case get_venue_type(r)
when "chapter" then true
else false
end
end,
proc do |r|
render_author_list(r)
textit {
render_paper_title(r["title"])
}
puts "---"
textit { puts r["booktitle"] || LabMetadata.venue_name(r) }
render_editor_list(r)
publisher = get_publisher(r)
venue = LabMetadata.complete_venue(r)
series = r["series"] || venue["series"] || venue["fullname"]
puts "---" if publisher || series
puts "#{publisher}#{if series then ";" else "" end}" if publisher
puts series if series
textit { puts r["year"] }
puts "(#{get_pub_length_string(r)}, #{render_selectivity(r)})"
end,
global_filter
)
end
if @include_pending_papers
render_one_pub_or_artifact(
"Submitted and Planned Publications",
@data["publications_submitted"],
proc do |r|
render_author_list(r)
textit {
render_paper_title(r["title"])
}
puts "---"
textit {
puts "#{LabMetadata.venue_name(r)} #{r["year"]}"
case r.fetch("status", "unknown")
when "submitted"
puts " (under submission)"
when "planned"
puts " (planned for submission)"
end
puts " --- "
}
# puts "#{get_pub_length_string(r)}; "
puts "Historical average acceptance rate #{get_avg_selectivity(r, 0)}"
end,
global_filter
)
end
unless skip?(:tech_reports)
render_one_pub_or_artifact(
"Technical Reports",
all_my_pubs.where do |r|
case get_venue_type(r)
when "techreport" then true
else false
end
end,
proc do |r|
render_author_list(r)
textit {
render_paper_title(r["title"])
}
puts "---"
textit {
puts "#{LabMetadata.venue_name(r)}#{if r["id"].nil? then "" else " ##{r["id"]}" end}, "
}
puts get_pub_length_string(r)
end,
global_filter
)
end
unless skip?(:patents)
render_one_pub_or_artifact(
"Patents",
all_my_pubs.where do |r|
case get_venue_type(r)
when "patent" then true
else false
end
end,
proc do |r|
render_author_list(r)
textit {
render_paper_title(r["title"])
}
puts "--- #{r["country"]} Patent#{if r["status"] == "granted" then "" else " Application" end} ##{r["id"]}"
end,
global_filter
)
end
unless skip?(:artifacts)
render_one_pub_or_artifact(
"Artifacts",
@data["artifacts"],
proc do |r|
print_direct "\\textsc{#{safe_text r["name"]}} (#{safe_text r["class"]})"
unless r["released"].nil?
d = to_date(r["released"])
hfill
puts "First released #{d.strftime("%B %Y")}"
end
endl
print_direct "\\textit{#{safe_text r["description"]}}"
unless r["url"].nil?
endl
url(r["url"])
end
unless r["metrics"].nil?
unless r["metricsasof"].nil?
endl
puts "As of #{r["metricsasof"]}, #{r["name"]} had "
else
puts "#{r["name"]} has "
end
metrics = r["metrics"].to_a.map do |m,v|
case m
when "sitevisits" then "#{safe_text v} unique website visitors"
when "downloads" then "#{safe_text v} unique downloads"
when "github" then v.to_a.map { |gh_m, gh_v| "#{gh_v.to_i} GitHub #{gh_m}" }
else puts "#{safe_text v} #{safe_text m}"
end
end.flatten
case metrics.length
when 0 then raise "Empty Metrics Field for #{r["name"]}"
when 1 then puts metrics[0]
when 2 then puts metrics.join(" and ")
else
metrics.push("and #{metrics.pop}")
puts metrics.join(", ")
end
end
end
) if args.fetch(:show_artifacts, true)
end
end
def render_students
make_section("Students Advised")
priority = ["PhD", "MS", "BS"]
students =
($db["lab/members"].values + $db["lab/alumni"].values).
where { |r| r["advisor"].nil? or r["joint_advisor"] }.
map { |r| [ (r["degree"] or r["status"]), r ] }.
map { |cat, r|
cat = cat.split("-")[0]
cat = cat.split(/, */).sort_by { |c| priority.index(c) }[0]
[cat, r]
}.
where { |cat, r| priority.include? cat }.
my_reduce { |cat, records| records.sort_by { |r| [(r["year"] or 100000), r["name"]] }}
render_student_list = proc { |cat, students|
current = students.where { |s| s["year"].nil? }.size
graduated = students.where { |s| s["year"] }.size
subsection!("#{cat} Students Advised (#{graduated} graduated, #{current} current)")
puts(
students.
map { |r|
comments = []
if r["year"]
comments.push("Graduated in #{r["year"]}")
else
comments.push("Current Student")
end
comments.push("Jointly advised with #{r["advisor"].oxford_comma}") if r["advisor"]
comments.push("Expected Graduation in #{r["expected"]}") if r["expected"]
"#{r["name"]} (#{comments.join("; ")})"
}.
join(", ")
)
}
render_student_list.call("PhD", students["PhD"])
render_student_list.call("MS", students["MS"])
render_student_list.call("BS", students["BS"])
theses = @data["thesis_committees"]
# Thesis Committees
subsection!("Thesis Committees (Excluding joint advisees; #{theses.size} graduated)")
puts(
theses.map { |r| "#{r["name"]} (#{r["date"]})" }.join(", ")
)
end
end