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