From 7a9faf9512c5c06ec0e3ae9d7006b69e0c526388 Mon Sep 17 00:00:00 2001 From: jazairi <16103405+jazairi@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:26:13 -0700 Subject: [PATCH 1/6] Add JSON metadata export feature Why these changes are being introduced: The metadata team has requested a JSON metadata export, as it would be easier for them to parse than the MARC file. Relevant ticket(s): - [ETD-695](https://mitlibraries.atlassian.net/browse/ETD-695) How this addresses that need: This adds JsonExporter and JsonBatch classes, which serialize thesis metadata and writes the metadata to a JSON file. This workflow is invoked in the MarcExportJob, and the output is included alongside the MARC file in the report email. It also creates two rake tasks: one to export the JSON metadata of a single thesis, and one to export all theses in a given term. This is primarily for testing purposes. Side effects of this change: The export job and batch emails are not clearly named for the time being. This feels like an acceptable short-term risk. We plan to retire the MARC export altogether, at which point we can rename those files accordingly. --- app/jobs/marc_export_job.rb | 10 ++- app/mailers/batch_mailer.rb | 5 +- app/models/json_batch.rb | 27 +++++++ app/models/json_exporter.rb | 60 +++++++++++++++ .../batch_mailer/marc_batch_email.html.erb | 2 + lib/tasks/metadata.rake | 49 +++++++++++++ test/jobs/marc_export_job_test.rb | 25 +++++++ test/mailers/batch_mailer_test.rb | 19 +++-- test/models/json_batch_test.rb | 73 +++++++++++++++++++ test/models/json_exporter_test.rb | 69 ++++++++++++++++++ 10 files changed, 327 insertions(+), 12 deletions(-) create mode 100644 app/models/json_batch.rb create mode 100644 app/models/json_exporter.rb create mode 100644 lib/tasks/metadata.rake create mode 100644 test/models/json_batch_test.rb create mode 100644 test/models/json_exporter_test.rb diff --git a/app/jobs/marc_export_job.rb b/app/jobs/marc_export_job.rb index 3d24cda7..a3af7677 100644 --- a/app/jobs/marc_export_job.rb +++ b/app/jobs/marc_export_job.rb @@ -4,11 +4,15 @@ class MarcExportJob < ActiveJob::Base def perform(theses) marc_filename = "#{filename}.mrc" zip_filename = "#{filename}.zip" + json_filename = "#{filename}.json" + begin - zip_file = MarcBatch.new(theses, marc_filename, zip_filename).build - BatchMailer.marc_batch_email(zip_filename, zip_file, theses).deliver_now + marc_zip_file = MarcBatch.new(theses, marc_filename, zip_filename).build + json_file = JsonBatch.new(theses, json_filename).build + BatchMailer.marc_batch_email(zip_filename, marc_zip_file, json_filename, json_file, theses).deliver_now ensure - zip_file&.close + marc_zip_file&.close + json_file&.close end end diff --git a/app/mailers/batch_mailer.rb b/app/mailers/batch_mailer.rb index b4c7d206..3d6c5e97 100644 --- a/app/mailers/batch_mailer.rb +++ b/app/mailers/batch_mailer.rb @@ -1,13 +1,14 @@ class BatchMailer < ApplicationMailer - def marc_batch_email(marc_zip_filename, marc_zip_file, theses) + def marc_batch_email(marc_zip_filename, marc_zip_file, json_filename, json_file, theses) return unless ENV.fetch('DISABLE_ALL_EMAIL', 'true') == 'false' # allows PR builds to disable emails @theses = theses attachments[marc_zip_filename.to_s] = File.binread(marc_zip_file) + attachments[json_filename.to_s] = File.read(json_file) mail(from: "MIT Libraries <#{ENV['ETD_APP_EMAIL']}>", to: ENV['METADATA_ADMIN_EMAIL'], cc: ENV['MAINTAINER_EMAIL'], - subject: 'ETD MARC batch export') + subject: 'ETD metadata batch export') end def proquest_export_email(json_blob, csv_blob, thesis_count, budget_report_count) diff --git a/app/models/json_batch.rb b/app/models/json_batch.rb new file mode 100644 index 00000000..2592c2f1 --- /dev/null +++ b/app/models/json_batch.rb @@ -0,0 +1,27 @@ +class JsonBatch + def initialize(theses, json_filename) + @theses = theses + @json_filename = json_filename + end + + def build + json_file = Tempfile.new(@json_filename) + write_json_file(json_file) + json_file + end + + private + + def write_json_file(json_file) + theses_data = @theses.map do |thesis| + JsonExporter.new(thesis).to_hash + end + + json_output = { + theses: theses_data + }.to_json + + json_file.write(JSON.pretty_generate(JSON.parse(json_output))) + json_file.rewind + end +end diff --git a/app/models/json_exporter.rb b/app/models/json_exporter.rb new file mode 100644 index 00000000..a78f94ba --- /dev/null +++ b/app/models/json_exporter.rb @@ -0,0 +1,60 @@ +class JsonExporter + def initialize(thesis) + @thesis = thesis + end + + def to_hash + { + abstract:, + advisors:, + authors:, + degrees:, + departments:, + dspace_url:, + graduation_year:, + title: + } + end + + private + + def abstract + @thesis.abstract + end + + def advisors + @thesis.advisors.map do |advisor| + { name: advisor.name } + end + end + + def authors + @thesis.authors.map do |author| + { name: author.user.preferred_name } + end + end + + def degrees + @thesis.degrees.map do |degree| + { abbreviation: degree.abbreviation } + end + end + + def departments + @thesis.departments.map do |department| + { name: department.name_dspace } + end + end + + def dspace_url + "https://dspace.mit.edu/handle/#{@thesis.dspace_handle}" + end + + def graduation_year + @thesis.graduation_year + end + + def title + @thesis.title&.squish + end +end diff --git a/app/views/batch_mailer/marc_batch_email.html.erb b/app/views/batch_mailer/marc_batch_email.html.erb index 0c5ae7b2..d95818d6 100644 --- a/app/views/batch_mailer/marc_batch_email.html.erb +++ b/app/views/batch_mailer/marc_batch_email.html.erb @@ -3,4 +3,6 @@
Attached is a metadata export of <%= @theses.count %> theses generated on <%= Date.current.strftime('%A, %B %d, %Y') %> at <%= Time.now.strftime('%r %Z') %>. +
This export includes both MARC (in zip) and JSON.
+Please contact the ETD team at <%= ENV['THESIS_ADMIN_EMAIL'] %> with any questions.
diff --git a/lib/tasks/metadata.rake b/lib/tasks/metadata.rake new file mode 100644 index 00000000..641895ec --- /dev/null +++ b/lib/tasks/metadata.rake @@ -0,0 +1,49 @@ +namespace :metadata do + desc 'Generate a JSON export of a single published thesis for testing' + task :json_export_thesis, [:thesis_id] => :environment do |_t, args| + if args.thesis_id.blank? + puts 'No thesis ID provided.' + next + end + + thesis = Thesis.find(args.thesis_id) + + if thesis.publication_status == 'Published' + json_exporter = JsonExporter.new(thesis) + json_data = json_exporter.to_hash + + puts "JSON Export for Thesis #{args.thesis_id}:" + puts JSON.pretty_generate(json_data) + else + puts "Thesis status of #{thesis.publication_status} cannot be exported. Only published theses can be exported." + end + end + + desc 'Generate a JSON export batch for a specific term (e.g., "2024-June") and save to temp file' + task :json_export_batch, %i[term output_file] => :environment do |_t, args| + if args.term.blank? + puts 'Usage: rake metadata:json_export_batch["2024-June","output.json"]' + puts 'Term format: YYYY-Month (e.g., 2024-June, 2024-September)' + next + end + + year, month_name = args.term.split('-') + query_date = Date.parse("1 #{month_name} #{year}") + + output_file = args.output_file || Rails.root.join("tmp/json_export_#{args.term}_#{DateTime.now.utc.strftime('%H_%M')}.json").to_s + + theses = Thesis.where(publication_status: 'Published').select do |t| + t.grad_date.year == query_date.year && t.grad_date.month == query_date.month + end + + if theses.any? + json_batch = JsonBatch.new(theses, File.basename(output_file)) + json_file = json_batch.build + FileUtils.cp(json_file.path, output_file) + json_file.close + puts "Exported #{theses.count} theses to: #{output_file}" + else + puts "No published theses found for #{args.term}" + end + end +end diff --git a/test/jobs/marc_export_job_test.rb b/test/jobs/marc_export_job_test.rb index a9b68dba..b62ae06e 100644 --- a/test/jobs/marc_export_job_test.rb +++ b/test/jobs/marc_export_job_test.rb @@ -21,4 +21,29 @@ class MarcExportJobTest < ActiveJob::TestCase end end end + + test 'sent email includes both MARC and JSON attachments' do + ClimateControl.modify DISABLE_ALL_EMAIL: 'false' do + theses = [theses(:one)] + Timecop.freeze(Time.utc(2022, 2, 14, 17, 10, 0)) do + email = MarcExportJob.perform_now(theses) + assert_equal 2, email.attachments.count + filenames = email.attachments.map(&:filename) + assert(filenames.include?('marc_220214_17_10.zip')) + assert(filenames.include?('marc_220214_17_10.json')) + end + end + end + + test 'JSON attachment is valid JSON' do + ClimateControl.modify DISABLE_ALL_EMAIL: 'false' do + theses = [theses(:one)] + email = MarcExportJob.perform_now(theses) + json_attachment = email.attachments.find { |a| a.filename.ends_with?('.json') } + assert_not_nil(json_attachment) + + json_data = JSON.parse(json_attachment.body.to_s) + assert(json_data.key?('theses')) + end + end end diff --git a/test/mailers/batch_mailer_test.rb b/test/mailers/batch_mailer_test.rb index d99d8dc3..9427d796 100644 --- a/test/mailers/batch_mailer_test.rb +++ b/test/mailers/batch_mailer_test.rb @@ -4,19 +4,23 @@ class BatchMailerTest < ActionMailer::TestCase test 'sends emails for MARC batch exports' do ClimateControl.modify DISABLE_ALL_EMAIL: 'false' do theses = [theses(:one), theses(:two)] - zip_file = MarcBatch.new(theses, 'marc.xml', 'marc.zip').build - email = BatchMailer.marc_batch_email('marc.zip', zip_file, theses) + marc_zip_file = MarcBatch.new(theses, 'marc.xml', 'marc.zip').build + json_file = JsonBatch.new(theses, 'marc.json').build + email = BatchMailer.marc_batch_email('marc.zip', marc_zip_file, 'marc.json', json_file, theses) # Send the email, then test that it got queued assert_emails 1 do email.deliver_now end - # Make sure it was sent to the right person with the expected attachment. + # Make sure it was sent to the right person with the expected attachments. assert_equal ['app@example.com'], email.from assert_equal ['test-metadata@example.com'], email.to - assert_equal 'ETD MARC batch export', email.subject - assert_equal 'marc.zip', email.attachments.first.filename + assert_equal 'ETD metadata batch export', email.subject + assert_equal 2, email.attachments.count + filenames = email.attachments.map(&:filename) + assert_includes filenames, 'marc.zip' + assert_includes filenames, 'marc.json' assert_includes '2 theses', email.body.to_s end end @@ -24,8 +28,9 @@ class BatchMailerTest < ActionMailer::TestCase test 'zip file is attached with correct mimetype' do ClimateControl.modify DISABLE_ALL_EMAIL: 'false' do theses = [theses(:one), theses(:two)] - zip_file = MarcBatch.new(theses, 'marc.xml', 'marc.zip').build - email = BatchMailer.marc_batch_email('marc.zip', zip_file, theses) + marc_zip_file = MarcBatch.new(theses, 'marc.xml', 'marc.zip').build + json_file = JsonBatch.new(theses, 'marc.json').build + email = BatchMailer.marc_batch_email('marc.zip', marc_zip_file, 'marc.json', json_file, theses) attachment = email.attachments['marc.zip'] assert_equal 'application/zip; filename=marc.zip', attachment.content_type end diff --git a/test/models/json_batch_test.rb b/test/models/json_batch_test.rb new file mode 100644 index 00000000..2e5b12a5 --- /dev/null +++ b/test/models/json_batch_test.rb @@ -0,0 +1,73 @@ +require 'test_helper' + +class JsonBatchTest < ActiveSupport::TestCase + test 'builds a valid JSON file' do + theses = [theses(:published)] + batch = JsonBatch.new(theses, 'test.json') + json_file = batch.build + + json_content = File.read(json_file.path) + json_data = JSON.parse(json_content) + + assert_not_nil(json_data) + json_file.close + end + + test 'wraps theses in a wrapper object with theses key' do + theses = [theses(:published)] + batch = JsonBatch.new(theses, 'test.json') + json_file = batch.build + + json_content = File.read(json_file.path) + json_data = JSON.parse(json_content) + + assert(json_data.key?('theses')) + assert(json_data['theses'].is_a?(Array)) + json_file.close + end + + test 'includes all theses in the batch' do + theses = [theses(:published), theses(:one)] + batch = JsonBatch.new(theses, 'test.json') + json_file = batch.build + + json_content = File.read(json_file.path) + json_data = JSON.parse(json_content) + + assert_equal(2, json_data['theses'].count) + json_file.close + end + + test 'includes all required fields' do + theses = [theses(:published)] + batch = JsonBatch.new(theses, 'test.json') + json_file = batch.build + + json_content = File.read(json_file.path) + json_data = JSON.parse(json_content) + + thesis_data = json_data['theses'].first + + assert(thesis_data.key?('abstract')) + assert(thesis_data.key?('advisors')) + assert(thesis_data.key?('authors')) + assert(thesis_data.key?('degrees')) + assert(thesis_data.key?('departments')) + assert(thesis_data.key?('dspace_url')) + assert(thesis_data.key?('graduation_year')) + assert(thesis_data.key?('title')) + + json_file.close + end + + test 'empty theses array produces valid JSON' do + batch = JsonBatch.new([], 'test.json') + json_file = batch.build + + json_content = File.read(json_file.path) + json_data = JSON.parse(json_content) + + assert_equal(0, json_data['theses'].count) + json_file.close + end +end diff --git a/test/models/json_exporter_test.rb b/test/models/json_exporter_test.rb new file mode 100644 index 00000000..38687f99 --- /dev/null +++ b/test/models/json_exporter_test.rb @@ -0,0 +1,69 @@ +require 'test_helper' + +class JsonExporterTest < ActiveSupport::TestCase + test 'includes correctly formatted title' do + thesis = theses(:published) + exporter = JsonExporter.new(thesis) + json_hash = exporter.to_hash + assert_equal thesis.title&.squish, json_hash[:title] + end + + test 'includes abstract' do + thesis = theses(:published) + exporter = JsonExporter.new(thesis) + json_hash = exporter.to_hash + assert_equal thesis.abstract, json_hash[:abstract] + end + + test 'includes grad year' do + thesis = theses(:published) + exporter = JsonExporter.new(thesis) + json_hash = exporter.to_hash + assert_equal thesis.graduation_year, json_hash[:graduation_year] + end + + test 'includes correctly formatted dspace_url' do + thesis = theses(:published) + exporter = JsonExporter.new(thesis) + json_hash = exporter.to_hash + expected_url = "https://dspace.mit.edu/handle/#{thesis.dspace_handle}" + assert_equal expected_url, json_hash[:dspace_url] + end + + test 'includes authors with nested structure' do + thesis = theses(:published) + exporter = JsonExporter.new(thesis) + json_hash = exporter.to_hash + assert json_hash[:authors].is_a?(Array) + assert json_hash[:authors].first.is_a?(Hash) + assert json_hash[:authors].first[:name] + end + + test 'includes degrees with nested structure' do + thesis = theses(:published) + exporter = JsonExporter.new(thesis) + json_hash = exporter.to_hash + assert json_hash[:degrees].is_a?(Array) + assert json_hash[:degrees].first.is_a?(Hash) + assert json_hash[:degrees].first[:abbreviation] + end + + test 'includes advisors with nested structure' do + thesis = theses(:published) + thesis.advisors << advisors(:first) + exporter = JsonExporter.new(thesis) + json_hash = exporter.to_hash + assert json_hash[:advisors].is_a?(Array) + assert json_hash[:advisors].first.is_a?(Hash) + assert json_hash[:advisors].first[:name] + end + + test 'includes departments with nested structure' do + thesis = theses(:published) + exporter = JsonExporter.new(thesis) + json_hash = exporter.to_hash + assert json_hash[:departments].is_a?(Array) + assert json_hash[:departments].first.is_a?(Hash) + assert json_hash[:departments].first[:name] + end +end From ff7c8c3c97126058eb8db389de4ad78ea721dd2c Mon Sep 17 00:00:00 2001 From: Isra JazairiHello,
Attached is a metadata export of <%= @theses.count %> theses generated on -<%= Date.current.strftime('%A, %B %d, %Y') %> at <%= Time.now.strftime('%r %Z') %>. +<%= Date.current.strftime('%A, %B %d, %Y') %> at <%= Time.now.strftime('%r %Z') %>.
This export includes both MARC (in zip) and JSON.
diff --git a/lib/tasks/metadata.rake b/lib/tasks/metadata.rake index 641895ec..7d08b99c 100644 --- a/lib/tasks/metadata.rake +++ b/lib/tasks/metadata.rake @@ -32,9 +32,7 @@ namespace :metadata do output_file = args.output_file || Rails.root.join("tmp/json_export_#{args.term}_#{DateTime.now.utc.strftime('%H_%M')}.json").to_s - theses = Thesis.where(publication_status: 'Published').select do |t| - t.grad_date.year == query_date.year && t.grad_date.month == query_date.month - end + theses = Thesis.published.where(grad_date: query_date.all_month) if theses.any? json_batch = JsonBatch.new(theses, File.basename(output_file)) From 0fc146c2df4a3c5ab9c4d1b51c7fb2360a3d56ae Mon Sep 17 00:00:00 2001 From: jazairi <16103405+jazairi@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:46:52 -0700 Subject: [PATCH 3/6] Address code review feedback - Rename and document new batch/export classes - Remove unnecessary safe navigation - Call `close!` on tempfiles to ensure immediate cleanup --- app/jobs/marc_export_job.rb | 6 +-- app/models/catalog_batch.rb | 37 +++++++++++++++++++ .../{json_exporter.rb => catalog_exporter.rb} | 16 +++++++- app/models/json_batch.rb | 25 ------------- lib/tasks/metadata.rake | 8 ++-- test/mailers/batch_mailer_test.rb | 4 +- ...on_batch_test.rb => catalog_batch_test.rb} | 12 +++--- ...orter_test.rb => catalog_exporter_test.rb} | 18 ++++----- 8 files changed, 75 insertions(+), 51 deletions(-) create mode 100644 app/models/catalog_batch.rb rename app/models/{json_exporter.rb => catalog_exporter.rb} (56%) delete mode 100644 app/models/json_batch.rb rename test/models/{json_batch_test.rb => catalog_batch_test.rb} (85%) rename test/models/{json_exporter_test.rb => catalog_exporter_test.rb} (82%) diff --git a/app/jobs/marc_export_job.rb b/app/jobs/marc_export_job.rb index a3af7677..cb0ef025 100644 --- a/app/jobs/marc_export_job.rb +++ b/app/jobs/marc_export_job.rb @@ -8,11 +8,11 @@ def perform(theses) begin marc_zip_file = MarcBatch.new(theses, marc_filename, zip_filename).build - json_file = JsonBatch.new(theses, json_filename).build + json_file = CatalogBatch.new(theses, json_filename).build BatchMailer.marc_batch_email(zip_filename, marc_zip_file, json_filename, json_file, theses).deliver_now ensure - marc_zip_file&.close - json_file&.close + marc_zip_file&.close! + json_file&.close! end end diff --git a/app/models/catalog_batch.rb b/app/models/catalog_batch.rb new file mode 100644 index 00000000..e219d810 --- /dev/null +++ b/app/models/catalog_batch.rb @@ -0,0 +1,37 @@ +# Generates a JSON metadata file from a collection of theses to add to the Libraries catalog. +# +# Produces a tempfile containing a JSON object with a 'theses' array, where each thesis is +# exported via CatalogExporter. +# +# Example: +# batch = CatalogBatch.new(theses_array, 'export.json') +# json_file = batch.build +# File.write('export.json', File.read(json_file.path)) +# json_file.close! # Clean up tempfile +class CatalogBatch + def initialize(theses, json_filename) + @theses = theses + @json_filename = json_filename + end + + # Builds and returns a Tempfile containing the JSON metadata export. The file is ready to read + # (file pointer rewound after writing). Caller is responsible for closing the file. + def build + json_file = Tempfile.new(@json_filename) + write_json_file(json_file) + json_file + end + + private + + def write_json_file(json_file) + theses_data = @theses.map do |thesis| + CatalogExporter.new(thesis).to_hash + end + + json_output = { theses: theses_data } + + json_file.write(JSON.pretty_generate(json_output)) + json_file.rewind + end +end diff --git a/app/models/json_exporter.rb b/app/models/catalog_exporter.rb similarity index 56% rename from app/models/json_exporter.rb rename to app/models/catalog_exporter.rb index a78f94ba..d77fd4fa 100644 --- a/app/models/json_exporter.rb +++ b/app/models/catalog_exporter.rb @@ -1,8 +1,20 @@ -class JsonExporter +# Exports a single thesis as a hash for JSON serialization. +# +# Transforms a thesis record into a flat-ish structure with nested arrays for repeating fields +# (authors, advisors, degrees, departments). +# +# Example: +# exporter = CatalogExporter.new(thesis) +# hash = exporter.to_hash +# # => { title: "...", abstract: "...", authors: [{name: "..."}, ...], ... } +class CatalogExporter def initialize(thesis) @thesis = thesis end + # Returns a hash representation of the thesis with all fields required by the metadata team. + # Includes: title, abstract, graduation_year, dspace_url, advisors, authors, degrees, and + # departments. Array fields are normalized to hashes with relevant metadata. def to_hash { abstract:, @@ -55,6 +67,6 @@ def graduation_year end def title - @thesis.title&.squish + @thesis.title.squish end end diff --git a/app/models/json_batch.rb b/app/models/json_batch.rb deleted file mode 100644 index 15f14cd6..00000000 --- a/app/models/json_batch.rb +++ /dev/null @@ -1,25 +0,0 @@ -class JsonBatch - def initialize(theses, json_filename) - @theses = theses - @json_filename = json_filename - end - - def build - json_file = Tempfile.new(@json_filename) - write_json_file(json_file) - json_file - end - - private - - def write_json_file(json_file) - theses_data = @theses.map do |thesis| - JsonExporter.new(thesis).to_hash - end - - json_output = { theses: theses_data } - - json_file.write(JSON.pretty_generate(json_output)) - json_file.rewind - end -end diff --git a/lib/tasks/metadata.rake b/lib/tasks/metadata.rake index 7d08b99c..e352563e 100644 --- a/lib/tasks/metadata.rake +++ b/lib/tasks/metadata.rake @@ -1,5 +1,5 @@ namespace :metadata do - desc 'Generate a JSON export of a single published thesis for testing' + desc 'Generate a JSON export of a single published thesis for debugging' task :json_export_thesis, [:thesis_id] => :environment do |_t, args| if args.thesis_id.blank? puts 'No thesis ID provided.' @@ -9,7 +9,7 @@ namespace :metadata do thesis = Thesis.find(args.thesis_id) if thesis.publication_status == 'Published' - json_exporter = JsonExporter.new(thesis) + json_exporter = CatalogExporter.new(thesis) json_data = json_exporter.to_hash puts "JSON Export for Thesis #{args.thesis_id}:" @@ -35,10 +35,10 @@ namespace :metadata do theses = Thesis.published.where(grad_date: query_date.all_month) if theses.any? - json_batch = JsonBatch.new(theses, File.basename(output_file)) + json_batch = CatalogBatch.new(theses, File.basename(output_file)) json_file = json_batch.build FileUtils.cp(json_file.path, output_file) - json_file.close + json_file.close! puts "Exported #{theses.count} theses to: #{output_file}" else puts "No published theses found for #{args.term}" diff --git a/test/mailers/batch_mailer_test.rb b/test/mailers/batch_mailer_test.rb index 9427d796..5f0cbf05 100644 --- a/test/mailers/batch_mailer_test.rb +++ b/test/mailers/batch_mailer_test.rb @@ -5,7 +5,7 @@ class BatchMailerTest < ActionMailer::TestCase ClimateControl.modify DISABLE_ALL_EMAIL: 'false' do theses = [theses(:one), theses(:two)] marc_zip_file = MarcBatch.new(theses, 'marc.xml', 'marc.zip').build - json_file = JsonBatch.new(theses, 'marc.json').build + json_file = CatalogBatch.new(theses, 'marc.json').build email = BatchMailer.marc_batch_email('marc.zip', marc_zip_file, 'marc.json', json_file, theses) # Send the email, then test that it got queued @@ -29,7 +29,7 @@ class BatchMailerTest < ActionMailer::TestCase ClimateControl.modify DISABLE_ALL_EMAIL: 'false' do theses = [theses(:one), theses(:two)] marc_zip_file = MarcBatch.new(theses, 'marc.xml', 'marc.zip').build - json_file = JsonBatch.new(theses, 'marc.json').build + json_file = CatalogBatch.new(theses, 'marc.json').build email = BatchMailer.marc_batch_email('marc.zip', marc_zip_file, 'marc.json', json_file, theses) attachment = email.attachments['marc.zip'] assert_equal 'application/zip; filename=marc.zip', attachment.content_type diff --git a/test/models/json_batch_test.rb b/test/models/catalog_batch_test.rb similarity index 85% rename from test/models/json_batch_test.rb rename to test/models/catalog_batch_test.rb index 2e5b12a5..5ad8587c 100644 --- a/test/models/json_batch_test.rb +++ b/test/models/catalog_batch_test.rb @@ -1,9 +1,9 @@ require 'test_helper' -class JsonBatchTest < ActiveSupport::TestCase +class CatalogBatchTest < ActiveSupport::TestCase test 'builds a valid JSON file' do theses = [theses(:published)] - batch = JsonBatch.new(theses, 'test.json') + batch = CatalogBatch.new(theses, 'test.json') json_file = batch.build json_content = File.read(json_file.path) @@ -15,7 +15,7 @@ class JsonBatchTest < ActiveSupport::TestCase test 'wraps theses in a wrapper object with theses key' do theses = [theses(:published)] - batch = JsonBatch.new(theses, 'test.json') + batch = CatalogBatch.new(theses, 'test.json') json_file = batch.build json_content = File.read(json_file.path) @@ -28,7 +28,7 @@ class JsonBatchTest < ActiveSupport::TestCase test 'includes all theses in the batch' do theses = [theses(:published), theses(:one)] - batch = JsonBatch.new(theses, 'test.json') + batch = CatalogBatch.new(theses, 'test.json') json_file = batch.build json_content = File.read(json_file.path) @@ -40,7 +40,7 @@ class JsonBatchTest < ActiveSupport::TestCase test 'includes all required fields' do theses = [theses(:published)] - batch = JsonBatch.new(theses, 'test.json') + batch = CatalogBatch.new(theses, 'test.json') json_file = batch.build json_content = File.read(json_file.path) @@ -61,7 +61,7 @@ class JsonBatchTest < ActiveSupport::TestCase end test 'empty theses array produces valid JSON' do - batch = JsonBatch.new([], 'test.json') + batch = CatalogBatch.new([], 'test.json') json_file = batch.build json_content = File.read(json_file.path) diff --git a/test/models/json_exporter_test.rb b/test/models/catalog_exporter_test.rb similarity index 82% rename from test/models/json_exporter_test.rb rename to test/models/catalog_exporter_test.rb index 38687f99..5b631f46 100644 --- a/test/models/json_exporter_test.rb +++ b/test/models/catalog_exporter_test.rb @@ -1,30 +1,30 @@ require 'test_helper' -class JsonExporterTest < ActiveSupport::TestCase +class CatalogExporterTest < ActiveSupport::TestCase test 'includes correctly formatted title' do thesis = theses(:published) - exporter = JsonExporter.new(thesis) + exporter = CatalogExporter.new(thesis) json_hash = exporter.to_hash assert_equal thesis.title&.squish, json_hash[:title] end test 'includes abstract' do thesis = theses(:published) - exporter = JsonExporter.new(thesis) + exporter = CatalogExporter.new(thesis) json_hash = exporter.to_hash assert_equal thesis.abstract, json_hash[:abstract] end test 'includes grad year' do thesis = theses(:published) - exporter = JsonExporter.new(thesis) + exporter = CatalogExporter.new(thesis) json_hash = exporter.to_hash assert_equal thesis.graduation_year, json_hash[:graduation_year] end test 'includes correctly formatted dspace_url' do thesis = theses(:published) - exporter = JsonExporter.new(thesis) + exporter = CatalogExporter.new(thesis) json_hash = exporter.to_hash expected_url = "https://dspace.mit.edu/handle/#{thesis.dspace_handle}" assert_equal expected_url, json_hash[:dspace_url] @@ -32,7 +32,7 @@ class JsonExporterTest < ActiveSupport::TestCase test 'includes authors with nested structure' do thesis = theses(:published) - exporter = JsonExporter.new(thesis) + exporter = CatalogExporter.new(thesis) json_hash = exporter.to_hash assert json_hash[:authors].is_a?(Array) assert json_hash[:authors].first.is_a?(Hash) @@ -41,7 +41,7 @@ class JsonExporterTest < ActiveSupport::TestCase test 'includes degrees with nested structure' do thesis = theses(:published) - exporter = JsonExporter.new(thesis) + exporter = CatalogExporter.new(thesis) json_hash = exporter.to_hash assert json_hash[:degrees].is_a?(Array) assert json_hash[:degrees].first.is_a?(Hash) @@ -51,7 +51,7 @@ class JsonExporterTest < ActiveSupport::TestCase test 'includes advisors with nested structure' do thesis = theses(:published) thesis.advisors << advisors(:first) - exporter = JsonExporter.new(thesis) + exporter = CatalogExporter.new(thesis) json_hash = exporter.to_hash assert json_hash[:advisors].is_a?(Array) assert json_hash[:advisors].first.is_a?(Hash) @@ -60,7 +60,7 @@ class JsonExporterTest < ActiveSupport::TestCase test 'includes departments with nested structure' do thesis = theses(:published) - exporter = JsonExporter.new(thesis) + exporter = CatalogExporter.new(thesis) json_hash = exporter.to_hash assert json_hash[:departments].is_a?(Array) assert json_hash[:departments].first.is_a?(Hash) From d13b1d81e38016b351272e11a7c7c09f512ad86f Mon Sep 17 00:00:00 2001 From: jazairi <16103405+jazairi@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:11:20 -0700 Subject: [PATCH 4/6] Rename instances of json_file to catalog_file for clarity --- app/jobs/marc_export_job.rb | 8 ++++---- app/models/catalog_batch.rb | 22 +++++++++++----------- lib/tasks/metadata.rake | 8 ++++---- test/mailers/batch_mailer_test.rb | 8 ++++---- test/models/catalog_batch_test.rb | 30 +++++++++++++++--------------- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/app/jobs/marc_export_job.rb b/app/jobs/marc_export_job.rb index cb0ef025..86e9b942 100644 --- a/app/jobs/marc_export_job.rb +++ b/app/jobs/marc_export_job.rb @@ -4,15 +4,15 @@ class MarcExportJob < ActiveJob::Base def perform(theses) marc_filename = "#{filename}.mrc" zip_filename = "#{filename}.zip" - json_filename = "#{filename}.json" + catalog_filename = "#{filename}.json" begin marc_zip_file = MarcBatch.new(theses, marc_filename, zip_filename).build - json_file = CatalogBatch.new(theses, json_filename).build - BatchMailer.marc_batch_email(zip_filename, marc_zip_file, json_filename, json_file, theses).deliver_now + catalog_file = CatalogBatch.new(theses, catalog_filename).build + BatchMailer.marc_batch_email(zip_filename, marc_zip_file, catalog_filename, catalog_file, theses).deliver_now ensure marc_zip_file&.close! - json_file&.close! + catalog_file&.close! end end diff --git a/app/models/catalog_batch.rb b/app/models/catalog_batch.rb index e219d810..8d74cf2e 100644 --- a/app/models/catalog_batch.rb +++ b/app/models/catalog_batch.rb @@ -5,33 +5,33 @@ # # Example: # batch = CatalogBatch.new(theses_array, 'export.json') -# json_file = batch.build -# File.write('export.json', File.read(json_file.path)) -# json_file.close! # Clean up tempfile +# catalog_file = batch.build +# File.write('export.json', File.read(catalog_file.path)) +# catalog_file.close! # Clean up tempfile class CatalogBatch - def initialize(theses, json_filename) + def initialize(theses, filename) @theses = theses - @json_filename = json_filename + @filename = filename end # Builds and returns a Tempfile containing the JSON metadata export. The file is ready to read # (file pointer rewound after writing). Caller is responsible for closing the file. def build - json_file = Tempfile.new(@json_filename) - write_json_file(json_file) - json_file + catalog_file = Tempfile.new(@filename) + write_catalog_file(catalog_file) + catalog_file end private - def write_json_file(json_file) + def write_catalog_file(catalog_file) theses_data = @theses.map do |thesis| CatalogExporter.new(thesis).to_hash end json_output = { theses: theses_data } - json_file.write(JSON.pretty_generate(json_output)) - json_file.rewind + catalog_file.write(JSON.pretty_generate(json_output)) + catalog_file.rewind end end diff --git a/lib/tasks/metadata.rake b/lib/tasks/metadata.rake index e352563e..4c35e69e 100644 --- a/lib/tasks/metadata.rake +++ b/lib/tasks/metadata.rake @@ -35,10 +35,10 @@ namespace :metadata do theses = Thesis.published.where(grad_date: query_date.all_month) if theses.any? - json_batch = CatalogBatch.new(theses, File.basename(output_file)) - json_file = json_batch.build - FileUtils.cp(json_file.path, output_file) - json_file.close! + catalog_batch = CatalogBatch.new(theses, File.basename(output_file)) + catalog_file = catalog_batch.build + FileUtils.cp(catalog_file.path, output_file) + catalog_file.close! puts "Exported #{theses.count} theses to: #{output_file}" else puts "No published theses found for #{args.term}" diff --git a/test/mailers/batch_mailer_test.rb b/test/mailers/batch_mailer_test.rb index 5f0cbf05..6d2d019f 100644 --- a/test/mailers/batch_mailer_test.rb +++ b/test/mailers/batch_mailer_test.rb @@ -5,8 +5,8 @@ class BatchMailerTest < ActionMailer::TestCase ClimateControl.modify DISABLE_ALL_EMAIL: 'false' do theses = [theses(:one), theses(:two)] marc_zip_file = MarcBatch.new(theses, 'marc.xml', 'marc.zip').build - json_file = CatalogBatch.new(theses, 'marc.json').build - email = BatchMailer.marc_batch_email('marc.zip', marc_zip_file, 'marc.json', json_file, theses) + catalog_file = CatalogBatch.new(theses, 'marc.json').build + email = BatchMailer.marc_batch_email('marc.zip', marc_zip_file, 'marc.json', catalog_file, theses) # Send the email, then test that it got queued assert_emails 1 do @@ -29,8 +29,8 @@ class BatchMailerTest < ActionMailer::TestCase ClimateControl.modify DISABLE_ALL_EMAIL: 'false' do theses = [theses(:one), theses(:two)] marc_zip_file = MarcBatch.new(theses, 'marc.xml', 'marc.zip').build - json_file = CatalogBatch.new(theses, 'marc.json').build - email = BatchMailer.marc_batch_email('marc.zip', marc_zip_file, 'marc.json', json_file, theses) + catalog_file = CatalogBatch.new(theses, 'marc.json').build + email = BatchMailer.marc_batch_email('marc.zip', marc_zip_file, 'marc.json', catalog_file, theses) attachment = email.attachments['marc.zip'] assert_equal 'application/zip; filename=marc.zip', attachment.content_type end diff --git a/test/models/catalog_batch_test.rb b/test/models/catalog_batch_test.rb index 5ad8587c..4c6917cb 100644 --- a/test/models/catalog_batch_test.rb +++ b/test/models/catalog_batch_test.rb @@ -4,46 +4,46 @@ class CatalogBatchTest < ActiveSupport::TestCase test 'builds a valid JSON file' do theses = [theses(:published)] batch = CatalogBatch.new(theses, 'test.json') - json_file = batch.build + catalog_file = batch.build - json_content = File.read(json_file.path) + json_content = File.read(catalog_file.path) json_data = JSON.parse(json_content) assert_not_nil(json_data) - json_file.close + catalog_file.close end test 'wraps theses in a wrapper object with theses key' do theses = [theses(:published)] batch = CatalogBatch.new(theses, 'test.json') - json_file = batch.build + catalog_file = batch.build - json_content = File.read(json_file.path) + json_content = File.read(catalog_file.path) json_data = JSON.parse(json_content) assert(json_data.key?('theses')) assert(json_data['theses'].is_a?(Array)) - json_file.close + catalog_file.close end test 'includes all theses in the batch' do theses = [theses(:published), theses(:one)] batch = CatalogBatch.new(theses, 'test.json') - json_file = batch.build + catalog_file = batch.build - json_content = File.read(json_file.path) + json_content = File.read(catalog_file.path) json_data = JSON.parse(json_content) assert_equal(2, json_data['theses'].count) - json_file.close + catalog_file.close end test 'includes all required fields' do theses = [theses(:published)] batch = CatalogBatch.new(theses, 'test.json') - json_file = batch.build + catalog_file = batch.build - json_content = File.read(json_file.path) + json_content = File.read(catalog_file.path) json_data = JSON.parse(json_content) thesis_data = json_data['theses'].first @@ -57,17 +57,17 @@ class CatalogBatchTest < ActiveSupport::TestCase assert(thesis_data.key?('graduation_year')) assert(thesis_data.key?('title')) - json_file.close + catalog_file.close end test 'empty theses array produces valid JSON' do batch = CatalogBatch.new([], 'test.json') - json_file = batch.build + catalog_file = batch.build - json_content = File.read(json_file.path) + json_content = File.read(catalog_file.path) json_data = JSON.parse(json_content) assert_equal(0, json_data['theses'].count) - json_file.close + catalog_file.close end end From e6bf2ceae7275c5ef95d4e876f84c44ba40848e1 Mon Sep 17 00:00:00 2001 From: jazairi <16103405+jazairi@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:29:49 -0700 Subject: [PATCH 5/6] Clean up metadata rake tasks Update task/variable names and add docstring to recommend batch task for local development only --- lib/tasks/metadata.rake | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/tasks/metadata.rake b/lib/tasks/metadata.rake index 4c35e69e..64f385cd 100644 --- a/lib/tasks/metadata.rake +++ b/lib/tasks/metadata.rake @@ -1,6 +1,6 @@ namespace :metadata do - desc 'Generate a JSON export of a single published thesis for debugging' - task :json_export_thesis, [:thesis_id] => :environment do |_t, args| + desc 'Generate a catalog export of a single published thesis for debugging' + task :catalog_export_thesis, [:thesis_id] => :environment do |_t, args| if args.thesis_id.blank? puts 'No thesis ID provided.' next @@ -9,20 +9,22 @@ namespace :metadata do thesis = Thesis.find(args.thesis_id) if thesis.publication_status == 'Published' - json_exporter = CatalogExporter.new(thesis) - json_data = json_exporter.to_hash + catalog_exporter = CatalogExporter.new(thesis) + json_data = catalog_exporter.to_hash - puts "JSON Export for Thesis #{args.thesis_id}:" + puts "Catalog Export for Thesis #{args.thesis_id}:" puts JSON.pretty_generate(json_data) else puts "Thesis status of #{thesis.publication_status} cannot be exported. Only published theses can be exported." end end - desc 'Generate a JSON export batch for a specific term (e.g., "2024-June") and save to temp file' - task :json_export_batch, %i[term output_file] => :environment do |_t, args| + # This task is recommended for local development only. On Heroku (or other ephemeral filesystems), + # files saved to disk will be deleted when the dyno restarts, making them inaccessible. + desc 'Generate a catalog export batch for a specific term (e.g., "2024-June") and save to temp file' + task :catalog_export_batch, %i[term output_file] => :environment do |_t, args| if args.term.blank? - puts 'Usage: rake metadata:json_export_batch["2024-June","output.json"]' + puts 'Usage: rake metadata:catalog_export_batch["2024-June","output.json"]' puts 'Term format: YYYY-Month (e.g., 2024-June, 2024-September)' next end @@ -30,7 +32,7 @@ namespace :metadata do year, month_name = args.term.split('-') query_date = Date.parse("1 #{month_name} #{year}") - output_file = args.output_file || Rails.root.join("tmp/json_export_#{args.term}_#{DateTime.now.utc.strftime('%H_%M')}.json").to_s + output_file = args.output_file || Rails.root.join("tmp/catalog_export_#{args.term}_#{DateTime.now.utc.strftime('%H_%M')}.json").to_s theses = Thesis.published.where(grad_date: query_date.all_month) From 43ad3a46e709bcdcef7a4ea1d4496a31da7a4993 Mon Sep 17 00:00:00 2001 From: jazairi <16103405+jazairi@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:50:05 -0700 Subject: [PATCH 6/6] Confirm correct format of titles in catalog exporter test --- test/models/catalog_exporter_test.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/models/catalog_exporter_test.rb b/test/models/catalog_exporter_test.rb index 5b631f46..2903d910 100644 --- a/test/models/catalog_exporter_test.rb +++ b/test/models/catalog_exporter_test.rb @@ -3,9 +3,11 @@ class CatalogExporterTest < ActiveSupport::TestCase test 'includes correctly formatted title' do thesis = theses(:published) + thesis.title = " A weirdly \n spaced title " + thesis.save exporter = CatalogExporter.new(thesis) json_hash = exporter.to_hash - assert_equal thesis.title&.squish, json_hash[:title] + assert_equal "A weirdly spaced title", json_hash[:title] end test 'includes abstract' do