Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c723598
Add basic regex to map roles from ETDA API
jvitorbarros15 Feb 4, 2026
9ab610c
Fix committee_role_normalizer to pass Rspec
jvitorbarros15 Feb 4, 2026
4d4a22c
Fix Dockerfile with updated image version and rubocop corrections
jvitorbarros15 Feb 4, 2026
f2fbf9d
Rubocop
jvitorbarros15 Feb 5, 2026
8eb2396
Add edta api importer
jvitorbarros15 Feb 11, 2026
9ae71a2
Fix name to etda_committee_memberships_xml_builder.rb
jvitorbarros15 Feb 11, 2026
804bc0f
Add rspec tests for the api importer
jvitorbarros15 Feb 11, 2026
f887c48
Refactor ETDA committee memberships XML builder to use Nokogiri
jvitorbarros15 Feb 12, 2026
e898cc7
Setup skeleton for the api client with initialize
usmannsiddiqui Feb 17, 2026
15f502a
Skeleton initialized, didnt save file initially
usmannsiddiqui Feb 17, 2026
f488a2b
Finished setting up the parameters for faculty committee
madhurakhandkar Feb 19, 2026
eb0e15c
Created rspec tests for committee records client
madhurakhandkar Feb 19, 2026
21063fa
Add .envrc.example for environment configuration
usmannsiddiqui Feb 23, 2026
e8df657
Add environment variables to .envrc.example
usmannsiddiqui Feb 23, 2026
2bbed27
Fix API client to use env variable methods
usmannsiddiqui Feb 24, 2026
f76696c
changed explanation in envr.example
usmannsiddiqui Feb 24, 2026
a40040c
Complete RSpec tests for CommitteeRecordsClient and fix API key header
usmannsiddiqui Feb 27, 2026
740d5bc
Add Committee.delete_all to ApplicationJob cleanup
usmannsiddiqui Feb 27, 2026
70bff3c
Finished rspec tests for committee_records_client
madhurakhandkar Mar 2, 2026
54279de
WIP: Add EtdaImporter skeleton for pulling committee data from ETDA API
usmannsiddiqui Mar 2, 2026
8b3fd0a
Merge branch 'api-client-etda' into etda-post-receiver
usmannsiddiqui Mar 2, 2026
be3fc7a
Wire EtdaImporter into ActivityInsightCommitteeJob and fix importer
usmannsiddiqui Mar 5, 2026
8fe4f47
Modified the etda importer and created rspec tests for the etda importer
madhurakhandkar Mar 5, 2026
ebc0229
adding rubocop fixes for now
madhurakhandkar Mar 17, 2026
3a0ef49
Rspec tests passed - Fix integrate method to accept target directly i…
usmannsiddiqui Mar 17, 2026
3011030
Fix etda importer spec to correctly test committee creation and error…
usmannsiddiqui Mar 17, 2026
aa0093a
Fix rspec tests for committee records client
usmannsiddiqui Mar 17, 2026
a832109
deleted 2 redundant files
usmannsiddiqui Mar 19, 2026
74c5441
fixed up regex in committe normalizer and updated envrc.example
usmannsiddiqui Mar 19, 2026
4c30e5a
Rubocop corrections
jvitorbarros15 Mar 19, 2026
d086479
Fix Activity Insight dropdown mappings for committee data
usmannsiddiqui Mar 24, 2026
092e2f4
Fix XML tag COMP -> COMPSTAGE to match Activity Insight schema
usmannsiddiqui Mar 24, 2026
7cfbafb
Remove wihtdrawn from determine_comp method
usmannsiddiqui Mar 26, 2026
1f78a66
Remove degree_type and role_other_explanation from committee data pip…
usmannsiddiqui Mar 26, 2026
1442e91
Add migration to drop degree_type and role_other_explanation from com…
usmannsiddiqui Mar 26, 2026
010744d
Fix LDAP Check CSV parsing for Excel-exported files
usmannsiddiqui Mar 31, 2026
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
17 changes: 17 additions & 0 deletions .envrc.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Example environment variables for FAMS Tools
# Copy this file to .envrc and replace with actual values

export ETDA_API_URL="http://localhost:3000"
export ETDA_API_TOKEN="generate_a_token_in_ETDA_console"
export FAMS_WEBSERVICES_USERNAME
export FAMS_WEBSERVICES_PASSWORD
export FAMS_MAIN_USERNAME
export FAMS_MAIN_PASSWORD
export FAMS_BACKUPS_SERVICE_USERNAME
export FAMS_BACKUPS_SERVICE_PASSWORD
export FAMS_METADATA_DB_KEY
export FAMS_S3_BUCKET_API_KEY
export FAMS_LP_SFTP_USERNAME
export FAMS_LP_SFTP_HOST
export CENTRAL_LDAP_HOST
export CENTRAL_LDAP_PORT
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ spec/examples.txt
.cache
.bash_history
.envrc
.irb_history
.irb_historyx
.DS_Store
CLAUDE.md
11 changes: 11 additions & 0 deletions .irb_history
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
importer = CommitteeData::EtdaImporter.new
Committee.last.attributes
ENV['ETDA_API_URL']
quit
importer = CommitteeData::EtdaImporter.new
Committee.last.attributes
importer.import_all
quit
importer = CommitteeData::EtdaImporter.new
importer.import_all
quit
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM harbor.k8s.libraries.psu.edu/library/ruby-3.4.1-node-22:20260123 as base
FROM harbor.k8s.libraries.psu.edu/library/ruby-3.4.1-node-22:20260202 as base
ARG UID=1001
WORKDIR /app

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This app parses data from faculty CVs, and integrates data into Activity Insight

Add the following untracked files/folders:

* `config/database.yml` This will be similar to `config/database.yml.sample`
* `'config/database.yml'` This will be similar to `config/database.yml.sample`
* `config/activity_insight.yml`
* `config/integration_passcode.yml`
* `public/log`
Expand Down
6 changes: 5 additions & 1 deletion app/importers/committee_data/committee_xml_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ def build_xml(batch)
committees.each do |committee|
xml.DSL do
xml.ROLE committee.role
xml.TYPE committee.type_of_work
xml.TYPE_OTHER committee.type_other_explanation if committee.type_other_explanation.present?
xml.COMPSTAGE committee.stage_of_completion
xml.DTY_START committee.start_year if committee.start_year
xml.DTY_END committee.completion_year if committee.completion_year

xml.DSL_STUDENT do
xml.FNAME committee.student_fname
xml.LNAME committee.student_lname
xml.DEG committee.degree_type
xml.TITLE committee.thesis_title
end
end
Expand Down
74 changes: 74 additions & 0 deletions app/importers/committee_data/etda_importer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require 'etda/committee_records_client'

module CommitteeData
class EtdaImporter
class DegreeTypeError < RuntimeError; end

def import_all
Faculty.find_each do |faculty|
import_for_faculty(faculty)
rescue Etda::CommitteeRecordsClient::CommitteeRecordsClientError => e
Rails.logger.error("Failed to import committees for #{faculty.access_id}: #{e.message}")
end
end

private

def import_for_faculty(faculty)
result = Etda::CommitteeRecordsClient.new.faculty_committees(faculty.access_id)
committees_data = result[:data]['committees']

committees_data.each do |committee|
normalized_role = CommitteeRoleNormalizer.normalize(committee['role'])

faculty.committees.create!(
student_fname: committee['student_fname'],
student_lname: committee['student_lname'],
role: normalized_role,
thesis_title: committee['title'],
type_of_work: map_type_of_work(committee['degree_type']),
stage_of_completion: determine_completion_stage(
committee['final_submission_approved_at'],
committee['submission_status']
),
start_year: extract_year(committee['approval_started_at']),
completion_year: extract_year(committee['final_submission_approved_at'])
)
end

Rails.logger.info("Imported #{committees_data.length} committees for #{faculty.access_id}")
end

def map_type_of_work(degree_type)
return nil if degree_type.blank?

case degree_type.strip
when 'Master Thesis'
"Master's Committee"
when 'Dissertation'
'Dissertation Committee'
when 'Thesis'
'Undergraduate Honors Thesis'
when 'Final Paper'
"Master's Paper Committee"

else
raise DegreeTypeError, "Unexpected Degree Type: #{degree_type.strip}"
end
end

def extract_year(date_string)
return nil if date_string.blank?

Date.parse(date_string).year
rescue ArgumentError
nil
end

def determine_completion_stage(final_submission_approved_at, _submission_status)
return 'Completed' if final_submission_approved_at.present?

'In Process'
end
end
end
6 changes: 3 additions & 3 deletions app/jobs/activity_insight_committee_job.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class ActivityInsightCommitteeJob < ApplicationJob
def integrate(params)
target = params[:target]
def integrate(target)
CommitteeData::EtdaImporter.new.import_all

builder = CommitteeData::CommitteeXmlBuilder.new
xml_enum = builder.xmls_enumerator
Expand All @@ -12,6 +12,6 @@ def integrate(params)
private

def name
'Committee Membership Integration'
'Committee Integration'
end
end
1 change: 1 addition & 0 deletions app/jobs/application_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,6 @@ def delete_all_data
Yearly.delete_all
ComEffort.delete_all
ComQuality.delete_all
Committee.delete_all
end
end
25 changes: 25 additions & 0 deletions app/services/committee_role_normalizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class CommitteeRoleNormalizer
PRIORITY_REGEX = [
['Co-Chairperson', %r{(co[-\s]?chair|co[-\s]?chairperson|committee chair/co-chair)}i],
['Chairperson', /(chairperson|chair of committee|committee chair|chair.)/i],
['Co-Advisor', /(co[-\s]?dissertation\s*advis(or|er)|co[-\s]?advisor)/i],
['Advisor', /(dissertation\s*advis(?:o?r|er)|advis(?:o?r|er))/i],
['Supervisor', /supervisor/i],
['Mentor', /mentor/i],
['Second Reader', /second\s+reader/i],
['Reader', /reader/i],
['Member', /(member|rep|represent|representative|substitute)/i]

].freeze

def self.normalize(raw_name)
text = raw_name.to_s.strip
return 'Other' if text.empty?

PRIORITY_REGEX.each do |label, regex|
return label if text.match?(regex)
end

'Other'
end
end
4 changes: 2 additions & 2 deletions app/services/ldap_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ def initialize(disable_client = AiDisableClient.new)
end

def check(data, should_disable)
uids = CSV.parse(data.read, headers: true)
uids = CSV.parse(data.read.force_encoding('UTF-8').sub(/\A\xEF\xBB\xBF/, ''), headers: true)
.filter_map { |row| row['Username'] }

return { error: 'No usernames were found in the uploaded CSV. Make sure there is a "Usernames" column.' } if uids.empty?
return { error: 'No usernames were found in the uploaded CSV. Make sure there is a "Username" column.' } if uids.empty?

entries = pull_ldap_data(uids)
disabled_uids = find_disabled_users(entries)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class AddActivityInsightFieldsToCommittees < ActiveRecord::Migration[7.2]
def change
change_table :committees, bulk: true do |t|
t.string :type_of_work
t.string :stage_of_completion
t.integer :start_year
t.integer :completion_year
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AddOtherExplanationFieldsToCommittees < ActiveRecord::Migration[7.2]
def change
change_table :committees, bulk: true do |t|
t.text :role_other_explanation, limit: 20_000
t.text :type_other_explanation, limit: 20_000
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class RemoveDegreeTypeAndRoleOtherFromCommittees < ActiveRecord::Migration[7.0]
def change
change_table :committees, bulk: true do |t|
t.remove :degree_type, type: :string
t.remove :role_other_explanation, type: :text
end
end
end
8 changes: 6 additions & 2 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.2].define(version: 2025_12_05_172036) do
ActiveRecord::Schema[7.2].define(version: 2026_03_26_000000) do
create_table "authors", charset: "utf8mb4", force: :cascade do |t|
t.string "f_name"
t.string "m_name"
Expand Down Expand Up @@ -58,9 +58,13 @@
t.string "student_lname"
t.string "role"
t.string "thesis_title"
t.string "degree_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "type_of_work"
t.string "stage_of_completion"
t.integer "start_year"
t.integer "completion_year"
t.text "type_other_explanation"
t.index ["faculty_id"], name: "index_committees_on_faculty_id"
end

Expand Down
53 changes: 53 additions & 0 deletions lib/etda/committee_records_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require './lib/etda/committee_records_client'
require 'httparty'
module Etda
class CommitteeRecordsClient
class CommitteeRecordsClientError < StandardError; end

def faculty_committees(access_id)
response = HTTParty.post(
"#{base_url}/api/v1/committee_records/faculty_committees",
headers: headers,
body: body(access_id)
)

handle_response(response)

# Tells us the error with committe records client with the error itself
rescue StandardError => e
raise CommitteeRecordsClientError, e.message
end

private

def base_url
@base_url ||= ENV.fetch('ETDA_API_URL', 'http://localhost:3000')
end

def api_token
@api_token ||= ENV.fetch('ETDA_API_TOKEN', 'abc123')
end

def headers
{
'X-API-KEY' => api_token,
'Content-Type' => 'application/json'
}
end

def body(access_id)
{
access_id: access_id
}.to_json
end

def handle_response(response)
raise CommitteeRecordsClientError, response.parsed_response['error'] || 'Unknown error' unless response.success?

{
success: true,
data: response.parsed_response
}
end
end
end
Loading