From 749356d2a94c14a9a214aa944ca0b57620e1b413 Mon Sep 17 00:00:00 2001 From: Daniel Miretsky Date: Thu, 2 Jul 2026 01:26:56 +0000 Subject: [PATCH 1/3] switch to using event_title from event_name for 25live events --- .../external/twenty_five_live_service.rb | 218 +++++++++++++++++- .../university_calendar_ics_service.rb | 25 +- .../external/twenty_five_live_service_spec.rb | 60 +++++ 3 files changed, 301 insertions(+), 2 deletions(-) diff --git a/app/services/external/twenty_five_live_service.rb b/app/services/external/twenty_five_live_service.rb index 8309f027..fcf00346 100644 --- a/app/services/external/twenty_five_live_service.rb +++ b/app/services/external/twenty_five_live_service.rb @@ -3,8 +3,13 @@ module External class TwentyFiveLiveService < ApplicationService require "net/http" + require "cgi" + require "faraday" + require "nokogiri" BASE_URL = "https://webservices.collegenet.com/r25ws/wrd/wit/run/" + EVENT_BASE_URL = BASE_URL + NS = { "r25" => "http://www.collegenet.com/r25" }.freeze class RequestError < StandardError attr_reader :status @@ -35,6 +40,44 @@ def call! true end + def self.sync_events + new.sync_events + end + + def sync_events + start_dt = Time.zone.today.strftime("%Y%m%d") + end_dt = (Time.zone.today + 1.year).strftime("%Y%m%d") + + result = { created: 0, updated: 0, unchanged: 0, errors: [] } + current_page = 1 + total_pages = nil + + loop do + doc, page_index, total_pages_from_doc = fetch_events_page_xml( + start_dt: start_dt, + end_dt: end_dt, + page: current_page + ) + + total_pages ||= total_pages_from_doc || current_page + + Rails.logger.info("[TwentyFiveLiveService] events.xml page #{page_index}/#{total_pages}") + + doc.xpath("//r25:event", NS).each do |node| + upsert_event(node, result) + rescue => e + result[:errors] << { page: current_page, error: e.message } + Rails.logger.error("[TwentyFiveLiveService] Error parsing event: #{e.message}") + end + + break if current_page >= total_pages + + current_page += 1 + end + + result + end + private def fetch(endpoint) @@ -62,9 +105,95 @@ def fetch(endpoint) end end + def fetch_events_page_xml(start_dt:, end_dt:, page:, page_size: 100) + params = { + scope: "extended", + include: "reservations", + start_dt: start_dt, + end_dt: end_dt, + paginate: nil, + page: page, + page_size: page_size, + node_type: "E" + } + + response = event_connection.get("events.xml", params.compact) + + Rails.logger.info("[TwentyFiveLiveService] events.xml page=#{page} status=#{response.status}") + + raise "HTTP #{response.status} fetching events.xml" unless response.status == 200 + + body = response.body + raise "Empty events.xml body" if body.nil? || body.strip.empty? + + doc = Nokogiri::XML(body) + root = doc.root + + page_index = (root["page_index"] || root["pageIndex"] || page).to_i + total_pages = (root["total_pages"] || root["totalPages"]).to_i + total_pages = 1 if total_pages.zero? + + [ doc, page_index, total_pages ] + end + + def event_connection + username = Rails.application.credentials.dig(:TwentyFiveLive, :username) + password = Rails.application.credentials.dig(:TwentyFiveLive, :password) + + @event_connection ||= Faraday.new(url: EVENT_BASE_URL) do |faraday| + faraday.request :authorization, :basic, username, password + faraday.adapter Faraday.default_adapter + end + end + + def upsert_event(node, result) + attrs = parse_event_attrs(node) + event = TwentyFiveLive::Event.find_or_initialize_by(event_id: attrs[:event_id]) + + event.assign_attributes(attrs) + if event.new_record? + event.save! + result[:created] += 1 + elsif event.changed? + event.save! + result[:updated] += 1 + else + result[:unchanged] += 1 + end + + event.update!(last_synced_at: Time.current) + end + + def parse_event_attrs(node) + description_node = node.at_xpath("r25:event_text[r25:text_type_id[text()='1']]/r25:text", NS) + raw_description = description_node&.text + description = raw_description ? CGI.unescapeHTML(raw_description) : nil + + public_attr = node.at_xpath("r25:custom_attribute[r25:attribute_id[text()='32']]/r25:attribute_value", NS) + + { + event_id: node.at_xpath("r25:event_id", NS)&.text&.to_i, + event_locator: node.at_xpath("r25:event_locator", NS)&.text, + event_name: node.at_xpath("r25:event_name", NS)&.text, + event_title: node.at_xpath("r25:event_title", NS)&.text.presence, + start_date: node.at_xpath("r25:start_date", NS)&.text, + end_date: node.at_xpath("r25:end_date", NS)&.text, + event_type_id: node.at_xpath("r25:event_type_id", NS)&.text&.to_i, + event_type_name: node.at_xpath("r25:event_type_name", NS)&.text, + state: node.at_xpath("r25:state", NS)&.text&.to_i, + state_name: node.at_xpath("r25:state_name", NS)&.text, + cabinet_id: node.at_xpath("r25:cabinet_id", NS)&.text&.to_i, + cabinet_name: node.at_xpath("r25:cabinet_name", NS)&.text, + description: description, + registration_url: node.at_xpath("r25:registration_url", NS)&.text.presence, + public_website: public_attr&.text == "T", + last_mod_dt: node.at_xpath("r25:last_mod_dt", NS)&.text, + creation_dt: node.at_xpath("r25:creation_dt", NS)&.text + } + end + # --------------------------------------------------------------------------- # DB-backed syncs - # --------------------------------------------------------------------------- def sync_organizations data = fetch("organizations") @@ -94,6 +223,93 @@ def sync_event_categories cats = Array.wrap(data.dig("categories", "category") || data.dig("r25:categories", "r25:category")) + def fetch_events_page_xml(start_dt:, end_dt:, page:, page_size: 100) + params = { + scope: "extended", + include: "reservations", + start_dt: start_dt, + end_dt: end_dt, + paginate: nil, + page: page, + page_size: page_size, + node_type: "E" + } + + response = event_connection.get("events.xml", params.compact) + + Rails.logger.info("[TwentyFiveLiveService] events.xml page=#{page} status=#{response.status}") + + raise "HTTP #{response.status} fetching events.xml" unless response.status == 200 + + body = response.body + raise "Empty events.xml body" if body.nil? || body.strip.empty? + + doc = Nokogiri::XML(body) + root = doc.root + + page_index = (root["page_index"] || root["pageIndex"] || page).to_i + total_pages = (root["total_pages"] || root["totalPages"]).to_i + total_pages = 1 if total_pages.zero? + + [ doc, page_index, total_pages ] + end + + def event_connection + username = Rails.application.credentials.dig(:TwentyFiveLive, :username) + password = Rails.application.credentials.dig(:TwentyFiveLive, :password) + + @event_connection ||= Faraday.new(url: EVENT_BASE_URL) do |faraday| + faraday.request :authorization, :basic, username, password + faraday.adapter Faraday.default_adapter + end + end + + def upsert_event(node, result) + attrs = parse_event_attrs(node) + event = TwentyFiveLive::Event.find_or_initialize_by(event_id: attrs[:event_id]) + + event.assign_attributes(attrs) + if event.new_record? + event.save! + result[:created] += 1 + elsif event.changed? + event.save! + result[:updated] += 1 + else + result[:unchanged] += 1 + end + + event.update!(last_synced_at: Time.current) + end + + def parse_event_attrs(node) + description_node = node.at_xpath("r25:event_text[r25:text_type_id[text()='1']]/r25:text", NS) + raw_description = description_node&.text + description = raw_description ? CGI.unescapeHTML(raw_description) : nil + + public_attr = node.at_xpath("r25:custom_attribute[r25:attribute_id[text()='32']]/r25:attribute_value", NS) + + { + event_id: node.at_xpath("r25:event_id", NS)&.text&.to_i, + event_locator: node.at_xpath("r25:event_locator", NS)&.text, + # event_name: node.at_xpath("r25:event_name", NS)&.text, + event_title: node.at_xpath("r25:event_title", NS)&.text.presence, + start_date: node.at_xpath("r25:start_date", NS)&.text, + end_date: node.at_xpath("r25:end_date", NS)&.text, + event_type_id: node.at_xpath("r25:event_type_id", NS)&.text&.to_i, + event_type_name: node.at_xpath("r25:event_type_name", NS)&.text, + state: node.at_xpath("r25:state", NS)&.text&.to_i, + state_name: node.at_xpath("r25:state_name", NS)&.text, + cabinet_id: node.at_xpath("r25:cabinet_id", NS)&.text&.to_i, + cabinet_name: node.at_xpath("r25:cabinet_name", NS)&.text, + description: description, + registration_url: node.at_xpath("r25:registration_url", NS)&.text.presence, + public_website: public_attr&.text == "T", + last_mod_dt: node.at_xpath("r25:last_mod_dt", NS)&.text, + creation_dt: node.at_xpath("r25:creation_dt", NS)&.text + } + end + cats.each do |raw| id = (raw["category_id"] || raw["r25:category_id"])&.to_i name = (raw["category_name"] || raw["r25:category_name"])&.strip diff --git a/app/services/university_calendar_ics_service.rb b/app/services/university_calendar_ics_service.rb index b8ce9322..a0f5682f 100644 --- a/app/services/university_calendar_ics_service.rb +++ b/app/services/university_calendar_ics_service.rb @@ -122,9 +122,10 @@ def build_event_attributes(ics_event) organization = decode_html_entities(raw_custom_fields["Organization"]) academic_term = decode_html_entities(raw_custom_fields["Academic Term"]) event_type = decode_html_entities(raw_custom_fields["Event Type"]) + event_title = decode_html_entities(raw_custom_fields["Event Title"]) event_name = decode_html_entities(raw_custom_fields["Event Name"]) - summary = event_name.presence || decode_html_entities(ics_event.summary.to_s) + summary = event_title.presence || title_from_25live_event_name(event_name).presence || event_name.presence || decode_html_entities(ics_event.summary.to_s) location = decode_html_entities(ics_event.location&.to_s) start_time = parse_ics_time(ics_event.dtstart) end_time = parse_ics_time(ics_event.dtend || ics_event.dtstart) @@ -446,6 +447,28 @@ def html_entities_decoder @html_entities_decoder ||= HTMLEntities.new end + def title_from_25live_event_name(event_name) + return nil if event_name.blank? + + title_by_name = twenty_five_live_title_by_name + title_by_name[event_name] + end + + def twenty_five_live_title_by_name + @twenty_five_live_title_by_name ||= begin + rows = TwentyFiveLive::Event + .where.not(event_name: [ nil, "" ]) + .where.not(event_title: [ nil, "" ]) + .order(updated_at: :desc) + .pluck(:event_name, :event_title) + + rows.each_with_object({}) do |(name, title), map| + # Keep the most recently synced title for duplicate event names. + map[name] ||= title + end + end + end + def find_term_from_academic_term(academic_term_str, event_date, category = nil, summary = nil) if category == "term_dates" return infer_term_from_date_for_boundary_event(academic_term_str, event_date, summary) diff --git a/spec/services/external/twenty_five_live_service_spec.rb b/spec/services/external/twenty_five_live_service_spec.rb index 06f15966..82a1e973 100644 --- a/spec/services/external/twenty_five_live_service_spec.rb +++ b/spec/services/external/twenty_five_live_service_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "rails_helper" +require "webmock/rspec" RSpec.describe External::TwentyFiveLiveService, type: :service do fixtures :buildings, :rooms @@ -27,6 +28,65 @@ def spaces_payload(spaces) { "spaces" => { "space" => spaces } } end + describe "#sync_events" do + let(:events_xml) { Rails.root.join("spec/fixtures/files/twenty_five_live_events.xml").read } + + def stub_events(body: events_xml, status: 200) + stub_request(:get, /webservices\.collegenet\.com.*events\.xml/) + .to_return(status: status, body: body, headers: { "Content-Type" => "text/xml" }) + end + + describe "event syncing" do + subject(:service) { described_class.new } + + before { stub_events } + + it "returns a result hash with created/updated/unchanged/errors keys" do + result = service.sync_events + + expect(result.keys).to match_array(%i[created updated unchanged errors]) + end + + context "event field parsing" do + before { service.sync_events } + + let(:event) { TwentyFiveLive::Event.find_by(event_id: 123_456) } + + it "parses event_title" do + expect(event.event_title).to eq("Taco Tuesday Event") + end + + it "prefers event_title for display_name" do + expect(event.display_name).to eq("Taco Tuesday Event") + end + + it "parses event_name" do + expect(event.event_name).to eq("Time Out and Tacos") + end + end + + context "minimal event (no title)" do + before { service.sync_events } + + let(:event) { TwentyFiveLive::Event.find_by(event_id: 999) } + + it "falls back to event_name for display_name" do + expect(event.display_name).to eq("Minimal Event") + end + end + + context "result counts" do + it "reports created count on first sync" do + result = service.sync_events + + expect(result[:created]).to eq(2) + expect(result[:updated]).to eq(0) + expect(result[:unchanged]).to eq(0) + end + end + end + end + describe "#sync_spaces" do it "associates 25Live IDs and formal names to room and building" do StubbedService.new("spaces" => spaces_payload([ WENTWORTH_SPACE ])).send(:sync_spaces) From 0e3e4565cbdf4149b510d912239ea5f5f7cd71df Mon Sep 17 00:00:00 2001 From: Daniel Miretsky Date: Sat, 4 Jul 2026 17:16:33 +0000 Subject: [PATCH 2/3] revert 25live changes and just use the full event titles from the ics feed --- .../external/twenty_five_live_service.rb | 220 +----------------- .../university_calendar_ics_service.rb | 32 +-- .../external/twenty_five_live_service_spec.rb | 60 ----- 3 files changed, 10 insertions(+), 302 deletions(-) diff --git a/app/services/external/twenty_five_live_service.rb b/app/services/external/twenty_five_live_service.rb index fcf00346..99959dbf 100644 --- a/app/services/external/twenty_five_live_service.rb +++ b/app/services/external/twenty_five_live_service.rb @@ -3,13 +3,8 @@ module External class TwentyFiveLiveService < ApplicationService require "net/http" - require "cgi" - require "faraday" - require "nokogiri" BASE_URL = "https://webservices.collegenet.com/r25ws/wrd/wit/run/" - EVENT_BASE_URL = BASE_URL - NS = { "r25" => "http://www.collegenet.com/r25" }.freeze class RequestError < StandardError attr_reader :status @@ -40,44 +35,6 @@ def call! true end - def self.sync_events - new.sync_events - end - - def sync_events - start_dt = Time.zone.today.strftime("%Y%m%d") - end_dt = (Time.zone.today + 1.year).strftime("%Y%m%d") - - result = { created: 0, updated: 0, unchanged: 0, errors: [] } - current_page = 1 - total_pages = nil - - loop do - doc, page_index, total_pages_from_doc = fetch_events_page_xml( - start_dt: start_dt, - end_dt: end_dt, - page: current_page - ) - - total_pages ||= total_pages_from_doc || current_page - - Rails.logger.info("[TwentyFiveLiveService] events.xml page #{page_index}/#{total_pages}") - - doc.xpath("//r25:event", NS).each do |node| - upsert_event(node, result) - rescue => e - result[:errors] << { page: current_page, error: e.message } - Rails.logger.error("[TwentyFiveLiveService] Error parsing event: #{e.message}") - end - - break if current_page >= total_pages - - current_page += 1 - end - - result - end - private def fetch(endpoint) @@ -105,95 +62,9 @@ def fetch(endpoint) end end - def fetch_events_page_xml(start_dt:, end_dt:, page:, page_size: 100) - params = { - scope: "extended", - include: "reservations", - start_dt: start_dt, - end_dt: end_dt, - paginate: nil, - page: page, - page_size: page_size, - node_type: "E" - } - - response = event_connection.get("events.xml", params.compact) - - Rails.logger.info("[TwentyFiveLiveService] events.xml page=#{page} status=#{response.status}") - - raise "HTTP #{response.status} fetching events.xml" unless response.status == 200 - - body = response.body - raise "Empty events.xml body" if body.nil? || body.strip.empty? - - doc = Nokogiri::XML(body) - root = doc.root - - page_index = (root["page_index"] || root["pageIndex"] || page).to_i - total_pages = (root["total_pages"] || root["totalPages"]).to_i - total_pages = 1 if total_pages.zero? - - [ doc, page_index, total_pages ] - end - - def event_connection - username = Rails.application.credentials.dig(:TwentyFiveLive, :username) - password = Rails.application.credentials.dig(:TwentyFiveLive, :password) - - @event_connection ||= Faraday.new(url: EVENT_BASE_URL) do |faraday| - faraday.request :authorization, :basic, username, password - faraday.adapter Faraday.default_adapter - end - end - - def upsert_event(node, result) - attrs = parse_event_attrs(node) - event = TwentyFiveLive::Event.find_or_initialize_by(event_id: attrs[:event_id]) - - event.assign_attributes(attrs) - if event.new_record? - event.save! - result[:created] += 1 - elsif event.changed? - event.save! - result[:updated] += 1 - else - result[:unchanged] += 1 - end - - event.update!(last_synced_at: Time.current) - end - - def parse_event_attrs(node) - description_node = node.at_xpath("r25:event_text[r25:text_type_id[text()='1']]/r25:text", NS) - raw_description = description_node&.text - description = raw_description ? CGI.unescapeHTML(raw_description) : nil - - public_attr = node.at_xpath("r25:custom_attribute[r25:attribute_id[text()='32']]/r25:attribute_value", NS) - - { - event_id: node.at_xpath("r25:event_id", NS)&.text&.to_i, - event_locator: node.at_xpath("r25:event_locator", NS)&.text, - event_name: node.at_xpath("r25:event_name", NS)&.text, - event_title: node.at_xpath("r25:event_title", NS)&.text.presence, - start_date: node.at_xpath("r25:start_date", NS)&.text, - end_date: node.at_xpath("r25:end_date", NS)&.text, - event_type_id: node.at_xpath("r25:event_type_id", NS)&.text&.to_i, - event_type_name: node.at_xpath("r25:event_type_name", NS)&.text, - state: node.at_xpath("r25:state", NS)&.text&.to_i, - state_name: node.at_xpath("r25:state_name", NS)&.text, - cabinet_id: node.at_xpath("r25:cabinet_id", NS)&.text&.to_i, - cabinet_name: node.at_xpath("r25:cabinet_name", NS)&.text, - description: description, - registration_url: node.at_xpath("r25:registration_url", NS)&.text.presence, - public_website: public_attr&.text == "T", - last_mod_dt: node.at_xpath("r25:last_mod_dt", NS)&.text, - creation_dt: node.at_xpath("r25:creation_dt", NS)&.text - } - end - # --------------------------------------------------------------------------- # DB-backed syncs + # --------------------------------------------------------------------------- def sync_organizations data = fetch("organizations") @@ -223,93 +94,6 @@ def sync_event_categories cats = Array.wrap(data.dig("categories", "category") || data.dig("r25:categories", "r25:category")) - def fetch_events_page_xml(start_dt:, end_dt:, page:, page_size: 100) - params = { - scope: "extended", - include: "reservations", - start_dt: start_dt, - end_dt: end_dt, - paginate: nil, - page: page, - page_size: page_size, - node_type: "E" - } - - response = event_connection.get("events.xml", params.compact) - - Rails.logger.info("[TwentyFiveLiveService] events.xml page=#{page} status=#{response.status}") - - raise "HTTP #{response.status} fetching events.xml" unless response.status == 200 - - body = response.body - raise "Empty events.xml body" if body.nil? || body.strip.empty? - - doc = Nokogiri::XML(body) - root = doc.root - - page_index = (root["page_index"] || root["pageIndex"] || page).to_i - total_pages = (root["total_pages"] || root["totalPages"]).to_i - total_pages = 1 if total_pages.zero? - - [ doc, page_index, total_pages ] - end - - def event_connection - username = Rails.application.credentials.dig(:TwentyFiveLive, :username) - password = Rails.application.credentials.dig(:TwentyFiveLive, :password) - - @event_connection ||= Faraday.new(url: EVENT_BASE_URL) do |faraday| - faraday.request :authorization, :basic, username, password - faraday.adapter Faraday.default_adapter - end - end - - def upsert_event(node, result) - attrs = parse_event_attrs(node) - event = TwentyFiveLive::Event.find_or_initialize_by(event_id: attrs[:event_id]) - - event.assign_attributes(attrs) - if event.new_record? - event.save! - result[:created] += 1 - elsif event.changed? - event.save! - result[:updated] += 1 - else - result[:unchanged] += 1 - end - - event.update!(last_synced_at: Time.current) - end - - def parse_event_attrs(node) - description_node = node.at_xpath("r25:event_text[r25:text_type_id[text()='1']]/r25:text", NS) - raw_description = description_node&.text - description = raw_description ? CGI.unescapeHTML(raw_description) : nil - - public_attr = node.at_xpath("r25:custom_attribute[r25:attribute_id[text()='32']]/r25:attribute_value", NS) - - { - event_id: node.at_xpath("r25:event_id", NS)&.text&.to_i, - event_locator: node.at_xpath("r25:event_locator", NS)&.text, - # event_name: node.at_xpath("r25:event_name", NS)&.text, - event_title: node.at_xpath("r25:event_title", NS)&.text.presence, - start_date: node.at_xpath("r25:start_date", NS)&.text, - end_date: node.at_xpath("r25:end_date", NS)&.text, - event_type_id: node.at_xpath("r25:event_type_id", NS)&.text&.to_i, - event_type_name: node.at_xpath("r25:event_type_name", NS)&.text, - state: node.at_xpath("r25:state", NS)&.text&.to_i, - state_name: node.at_xpath("r25:state_name", NS)&.text, - cabinet_id: node.at_xpath("r25:cabinet_id", NS)&.text&.to_i, - cabinet_name: node.at_xpath("r25:cabinet_name", NS)&.text, - description: description, - registration_url: node.at_xpath("r25:registration_url", NS)&.text.presence, - public_website: public_attr&.text == "T", - last_mod_dt: node.at_xpath("r25:last_mod_dt", NS)&.text, - creation_dt: node.at_xpath("r25:creation_dt", NS)&.text - } - end - cats.each do |raw| id = (raw["category_id"] || raw["r25:category_id"])&.to_i name = (raw["category_name"] || raw["r25:category_name"])&.strip @@ -702,4 +486,4 @@ def build_drift(entity, const_rows, api_rows, compare_keys:) { entity: entity, added: added, removed: removed, changed: changed } end end -end +end \ No newline at end of file diff --git a/app/services/university_calendar_ics_service.rb b/app/services/university_calendar_ics_service.rb index a0f5682f..19dc1d15 100644 --- a/app/services/university_calendar_ics_service.rb +++ b/app/services/university_calendar_ics_service.rb @@ -122,14 +122,20 @@ def build_event_attributes(ics_event) organization = decode_html_entities(raw_custom_fields["Organization"]) academic_term = decode_html_entities(raw_custom_fields["Academic Term"]) event_type = decode_html_entities(raw_custom_fields["Event Type"]) - event_title = decode_html_entities(raw_custom_fields["Event Title"]) event_name = decode_html_entities(raw_custom_fields["Event Name"]) - summary = event_title.presence || title_from_25live_event_name(event_name).presence || event_name.presence || decode_html_entities(ics_event.summary.to_s) + # The ICS SUMMARY carries the full event title; the "Event Name" custom field + # is a truncated SingleLine value, so prefer SUMMARY and fall back to it. + summary = decode_html_entities(ics_event.summary.to_s).presence || event_name.presence location = decode_html_entities(ics_event.location&.to_s) start_time = parse_ics_time(ics_event.dtstart) end_time = parse_ics_time(ics_event.dtend || ics_event.dtstart) description = clean_description(ics_event.description&.to_s) + # The feed builds "Event Name:" from the truncated custom field; rewrite that + # line with the full SUMMARY so the description shows the complete title too. + if description.present? && summary.present? + description = description.sub(/\AEvent Name:.*/, "Event Name: #{summary}") + end category = UniversityCalendarEvent.infer_category(summary, event_type) is_all_day = %w[holiday study_day].include?(category) || all_day_event?(ics_event) @@ -447,28 +453,6 @@ def html_entities_decoder @html_entities_decoder ||= HTMLEntities.new end - def title_from_25live_event_name(event_name) - return nil if event_name.blank? - - title_by_name = twenty_five_live_title_by_name - title_by_name[event_name] - end - - def twenty_five_live_title_by_name - @twenty_five_live_title_by_name ||= begin - rows = TwentyFiveLive::Event - .where.not(event_name: [ nil, "" ]) - .where.not(event_title: [ nil, "" ]) - .order(updated_at: :desc) - .pluck(:event_name, :event_title) - - rows.each_with_object({}) do |(name, title), map| - # Keep the most recently synced title for duplicate event names. - map[name] ||= title - end - end - end - def find_term_from_academic_term(academic_term_str, event_date, category = nil, summary = nil) if category == "term_dates" return infer_term_from_date_for_boundary_event(academic_term_str, event_date, summary) diff --git a/spec/services/external/twenty_five_live_service_spec.rb b/spec/services/external/twenty_five_live_service_spec.rb index 82a1e973..06f15966 100644 --- a/spec/services/external/twenty_five_live_service_spec.rb +++ b/spec/services/external/twenty_five_live_service_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "rails_helper" -require "webmock/rspec" RSpec.describe External::TwentyFiveLiveService, type: :service do fixtures :buildings, :rooms @@ -28,65 +27,6 @@ def spaces_payload(spaces) { "spaces" => { "space" => spaces } } end - describe "#sync_events" do - let(:events_xml) { Rails.root.join("spec/fixtures/files/twenty_five_live_events.xml").read } - - def stub_events(body: events_xml, status: 200) - stub_request(:get, /webservices\.collegenet\.com.*events\.xml/) - .to_return(status: status, body: body, headers: { "Content-Type" => "text/xml" }) - end - - describe "event syncing" do - subject(:service) { described_class.new } - - before { stub_events } - - it "returns a result hash with created/updated/unchanged/errors keys" do - result = service.sync_events - - expect(result.keys).to match_array(%i[created updated unchanged errors]) - end - - context "event field parsing" do - before { service.sync_events } - - let(:event) { TwentyFiveLive::Event.find_by(event_id: 123_456) } - - it "parses event_title" do - expect(event.event_title).to eq("Taco Tuesday Event") - end - - it "prefers event_title for display_name" do - expect(event.display_name).to eq("Taco Tuesday Event") - end - - it "parses event_name" do - expect(event.event_name).to eq("Time Out and Tacos") - end - end - - context "minimal event (no title)" do - before { service.sync_events } - - let(:event) { TwentyFiveLive::Event.find_by(event_id: 999) } - - it "falls back to event_name for display_name" do - expect(event.display_name).to eq("Minimal Event") - end - end - - context "result counts" do - it "reports created count on first sync" do - result = service.sync_events - - expect(result[:created]).to eq(2) - expect(result[:updated]).to eq(0) - expect(result[:unchanged]).to eq(0) - end - end - end - end - describe "#sync_spaces" do it "associates 25Live IDs and formal names to room and building" do StubbedService.new("spaces" => spaces_payload([ WENTWORTH_SPACE ])).send(:sync_spaces) From bdbd9578221a7c939e1d78c1a2d14b709d0a722c Mon Sep 17 00:00:00 2001 From: Daniel Miretsky Date: Sat, 4 Jul 2026 17:20:26 +0000 Subject: [PATCH 3/3] add newline --- app/services/external/twenty_five_live_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/external/twenty_five_live_service.rb b/app/services/external/twenty_five_live_service.rb index 99959dbf..8309f027 100644 --- a/app/services/external/twenty_five_live_service.rb +++ b/app/services/external/twenty_five_live_service.rb @@ -486,4 +486,4 @@ def build_drift(entity, const_rows, api_rows, compare_keys:) { entity: entity, added: added, removed: removed, changed: changed } end end -end \ No newline at end of file +end