Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,42 @@ jobs:
rustc --version
cargo --version

- restore_cache:
keys:
- v1-cargo-{{ checksum "ext/widget_renderer/Cargo.lock" }}
- v1-cargo-

- run:
name: Build widget renderer (Rust)
command: |
source $HOME/.cargo/env
cargo build --release --manifest-path ext/widget_renderer/Cargo.toml

- run:
name: Verify Rust native library linkage
command: |
set -euo pipefail
LIB=ext/widget_renderer/target/release/libwidget_renderer.so
if [ -f "$LIB" ]; then
echo "Found built rust library; verifying linkage..."
if ldd "$LIB" 2>&1 | grep -q "not found"; then
echo "ERROR: Rust library has unresolved dependencies (ldd shows 'not found')."
ldd "$LIB" || true
exit 1
else
echo "Rust library linkage looks good"
fi
else
echo "No Rust library built - skipping linkage verification"
fi

- save_cache:
paths:
- ext/widget_renderer/target
- ~/.cargo/registry
- ~/.cargo/git
key: v1-cargo-{{ checksum "ext/widget_renderer/Cargo.lock" }}

# Download and cache dependencies
- restore_cache:
keys:
Expand Down
7 changes: 5 additions & 2 deletions app/models/cx_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ def duplicate!(new_user:)
end

def self.to_csv
collections = all.includes(:organization, :service_provider, :service, :user).references(:organization).order(:fiscal_year, :quarter, 'organizations.name')
collections = all
.includes(:organization, :service, :user, :cx_collection_details, service_provider: :organization)
.references(:organization)
.order(:fiscal_year, :quarter, 'organizations.name')

attributes = %i[
id
Expand Down Expand Up @@ -118,7 +121,7 @@ def self.to_csv
csv << attributes

collections.each do |collection|
csv << attributes = [
csv << [
collection.id,
collection.name,
collection.organization_id,
Expand Down
4 changes: 3 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ class User < ApplicationRecord

def cx_collections
user_org = organization
user_parent_org = user_org&.parent
return CxCollection.none if user_org.nil?

user_parent_org = user_org.parent

CxCollection.where(cx_collections: { organization_id: [user_org.id, user_parent_org&.id].compact })
end
Expand Down
5 changes: 4 additions & 1 deletion config/initializers/widget_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
begin
# Try loading the precompiled Rutie extension.
require_relative '../../ext/widget_renderer/lib/widget_renderer'

# Verify the class was properly defined
if defined?(WidgetRenderer) && WidgetRenderer.respond_to?(:generate_js)
Rails.logger.info "WidgetRenderer: Rust extension loaded successfully! generate_js method available."
Expand All @@ -14,6 +14,9 @@
rescue LoadError => e
Rails.logger.warn "Widget renderer native library not available: #{e.message}"
Rails.logger.warn 'Rust extension must be built during staging; falling back to ERB template rendering.'
rescue SystemExit => e
Rails.logger.error "Widget renderer exited during load: #{e.message}"
Rails.logger.warn 'Falling back to ERB template rendering'
rescue StandardError => e
Rails.logger.error "Widget renderer failed to load: #{e.class}: #{e.message}"
Rails.logger.error e.backtrace.join("\n") if e.backtrace
Expand Down
16 changes: 16 additions & 0 deletions db/migrate/20251210192727_add_indexes_to_cx_collections.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class AddIndexesToCxCollections < ActiveRecord::Migration[8.0]
def change
# cx_collections table - missing all FK indexes
add_index :cx_collections, :organization_id
add_index :cx_collections, :user_id
add_index :cx_collections, :service_provider_id
add_index :cx_collections, :service_id

# cx_collection_details table - missing FK index
add_index :cx_collection_details, :cx_collection_id

# cx_responses table - missing FK indexes
add_index :cx_responses, :cx_collection_detail_id
add_index :cx_responses, :cx_collection_detail_upload_id
end
end
9 changes: 8 additions & 1 deletion 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[8.0].define(version: 2025_07_17_034402) do
ActiveRecord::Schema[8.0].define(version: 2025_12_10_192727) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"

Expand Down Expand Up @@ -97,6 +97,7 @@
t.text "trust_question_text"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["cx_collection_id"], name: "index_cx_collection_details_on_cx_collection_id"
end

create_table "cx_collections", force: :cascade do |t|
Expand Down Expand Up @@ -124,6 +125,10 @@
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "submitted_at"
t.index ["organization_id"], name: "index_cx_collections_on_organization_id"
t.index ["service_id"], name: "index_cx_collections_on_service_id"
t.index ["service_provider_id"], name: "index_cx_collections_on_service_provider_id"
t.index ["user_id"], name: "index_cx_collections_on_user_id"
end

create_table "cx_responses", force: :cascade do |t|
Expand All @@ -149,6 +154,8 @@
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "external_id"
t.index ["cx_collection_detail_id"], name: "index_cx_responses_on_cx_collection_detail_id"
t.index ["cx_collection_detail_upload_id"], name: "index_cx_responses_on_cx_collection_detail_upload_id"
end

create_table "digital_product_versions", force: :cascade do |t|
Expand Down
23 changes: 15 additions & 8 deletions ext/widget_renderer/lib/widget_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@
puts "WidgetRenderer: root=#{root}"
puts "WidgetRenderer: __dir__=#{__dir__}"

# If a stale module exists, remove it so Rutie can define or reopen the class.
if defined?(WidgetRenderer) && WidgetRenderer.is_a?(Module) && !WidgetRenderer.is_a?(Class)
Object.send(:remove_const, :WidgetRenderer)
end
# Ensure the constant exists as a Class so rb_define_class will reopen it instead of erroring on Module.
WidgetRenderer = Class.new unless defined?(WidgetRenderer) && WidgetRenderer.is_a?(Class)

# Check for library file extensions based on platform
lib_extensions = %w[.so .bundle .dylib]
lib_names = lib_extensions.map { |ext| "libwidget_renderer#{ext}" }
Expand Down Expand Up @@ -84,6 +77,13 @@
end
end
end

# If a stale module exists, remove it so Rutie can define or reopen the class.
if defined?(WidgetRenderer) && WidgetRenderer.is_a?(Module) && !WidgetRenderer.is_a?(Class)
Object.send(:remove_const, :WidgetRenderer)
end
# Ensure the constant exists as a Class so rb_define_class will reopen it instead of erroring on Module.
WidgetRenderer = Class.new unless defined?(WidgetRenderer) && WidgetRenderer.is_a?(Class)
else
puts 'WidgetRenderer: Library not found in any checked path. Listing root contents:'
# List files in root to help debug
Expand All @@ -104,6 +104,9 @@
else
puts "WidgetRenderer: target/release directory does not exist at #{release_dir}"
end

# No native library available; let caller handle fallback.
raise LoadError, 'WidgetRenderer native library not found'
end

# Rutie expects the project root and appends /target/release/lib<name>.so
Expand All @@ -112,4 +115,8 @@

puts "WidgetRenderer: Initializing Rutie with path: #{path}"

Rutie.new(:widget_renderer).init 'Init_widget_renderer', path
begin
Rutie.new(:widget_renderer).init 'Init_widget_renderer', path
rescue SystemExit => e
raise LoadError, "WidgetRenderer native init exited: #{e.message}"
end
38 changes: 38 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,44 @@
end
end

describe "#cx_collections" do
let(:user_with_org) { FactoryBot.create(:user, organization: organization) }
let(:service_provider) { FactoryBot.create(:service_provider, organization: organization) }
let(:service) { FactoryBot.create(:service, organization: organization, service_provider: service_provider) }

context "when user has no organization" do
it "returns an empty collection" do
user_without_org = User.new(email: "test@example.gov")
user_without_org.organization = nil
result = user_without_org.cx_collections
expect(result).to eq(CxCollection.none)
expect(result.count).to eq(0)
end
end

context "when user has an organization" do
it "returns cx_collections for the user's organization" do
cx_collection = FactoryBot.create(:cx_collection, organization: organization, service: service, service_provider: service_provider, user: user_with_org)
result = user_with_org.cx_collections
expect(result).to include(cx_collection)
end
end

context "when user's organization has a parent organization" do
let(:parent_org) { FactoryBot.create(:organization, name: "Parent Org", domain: "parent.gov", abbreviation: "PARENT") }
let(:child_org) { FactoryBot.create(:organization, name: "Child Org", domain: "child.gov", abbreviation: "CHILD", parent: parent_org) }
let(:child_user) { FactoryBot.create(:user, organization: child_org) }
let(:parent_service_provider) { FactoryBot.create(:service_provider, organization: parent_org) }
let(:parent_service) { FactoryBot.create(:service, organization: parent_org, service_provider: parent_service_provider) }

it "includes cx_collections from the parent organization" do
parent_cx_collection = FactoryBot.create(:cx_collection, organization: parent_org, service: parent_service, service_provider: parent_service_provider, user: child_user)
result = child_user.cx_collections
expect(result).to include(parent_cx_collection)
end
end
end

describe "#ensure_organization" do
before do
@org2 = Organization.create(name: "Subdomain Example", domain: "sub.example.gov", abbreviation: "SUB")
Expand Down
Loading