diff --git a/app/controllers/addresses_controller.rb b/app/controllers/addresses_controller.rb new file mode 100644 index 000000000..9600752dd --- /dev/null +++ b/app/controllers/addresses_controller.rb @@ -0,0 +1,2 @@ +class AddressesController < ApplicationController +end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb new file mode 100644 index 000000000..83e326d1e --- /dev/null +++ b/app/controllers/organizations_controller.rb @@ -0,0 +1,2 @@ +class OrganizationsController < ApplicationController +end diff --git a/app/models/address.rb b/app/models/address.rb new file mode 100644 index 000000000..9743f67d2 --- /dev/null +++ b/app/models/address.rb @@ -0,0 +1,8 @@ +class Address < ApplicationRecord + belongs_to :organization + + validates :street, presence: true + validates :city, presence: true + validates :state, presence: true + validates :zip, presence: true +end diff --git a/app/models/facilitator.rb b/app/models/facilitator.rb index d003cdf9c..bbf73e78d 100644 --- a/app/models/facilitator.rb +++ b/app/models/facilitator.rb @@ -1,5 +1,7 @@ class Facilitator < ApplicationRecord has_one :user + has_many :facilitator_organizations, dependent: :restrict_with_exception + has_many :organizations, through: :facilitator_organizations CONTACT_TYPES = ["Work", "Personal"].freeze PERMITTED_PARAMS = [ diff --git a/app/models/facilitator_organization.rb b/app/models/facilitator_organization.rb new file mode 100644 index 000000000..094ad0d6c --- /dev/null +++ b/app/models/facilitator_organization.rb @@ -0,0 +1,4 @@ +class FacilitatorOrganization < ApplicationRecord + belongs_to :facilitator + belongs_to :organization +end \ No newline at end of file diff --git a/app/models/organization.rb b/app/models/organization.rb new file mode 100644 index 000000000..a9008ed8c --- /dev/null +++ b/app/models/organization.rb @@ -0,0 +1,9 @@ +class Organization < ApplicationRecord + has_many :addresses, dependent: :destroy + has_many :facilitator_organizations, dependent: :restrict_with_exception + has_many :facilitators, through: :facilitator_organizations + + validates :name, presence: true + validates :agency_type, presence: true + validates :phone, presence: true +end diff --git a/config/routes.rb b/config/routes.rb index 72d0fe396..4bf3fe221 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,6 +51,7 @@ resources :users resources :user_forms resources :facilitators + resources :organizations get 'reports/:id/edit_story', to: 'reports#edit_story', as: 'reports_edit_story' put 'reports/update_story/:id', to: 'reports#update_story', as: 'reports_update_story' diff --git a/db/migrate/20250912174408_create_organizations.rb b/db/migrate/20250912174408_create_organizations.rb new file mode 100644 index 000000000..0efa60a15 --- /dev/null +++ b/db/migrate/20250912174408_create_organizations.rb @@ -0,0 +1,18 @@ +class CreateOrganizations < ActiveRecord::Migration[6.1] + def change + create_table :organizations do |t| + t.string :name, null: false + t.boolean :is_active, default: true + t.date :start_date + t.date :close_date + t.string :website_url + t.string :agency_type, null: false + t.string :agency_type_other + t.string :phone, null: false + t.text :mission + t.string :project_id + + t.timestamps + end + end +end diff --git a/db/migrate/20250912184522_create_addresses.rb b/db/migrate/20250912184522_create_addresses.rb new file mode 100644 index 000000000..aeb9791cf --- /dev/null +++ b/db/migrate/20250912184522_create_addresses.rb @@ -0,0 +1,19 @@ +class CreateAddresses < ActiveRecord::Migration[6.1] + def change + create_table :addresses do |t| + t.references :organization, null: false, foreign_key: true + t.string :street, null: false + t.string :city, null: false + t.string :state, null: false + t.string :zip, null: false + t.string :country + t.string :locality + t.string :county + t.integer :la_city_council_district + t.integer :la_supervisorial_district + t.integer :la_service_planning_area + + t.timestamps + end + end +end diff --git a/db/migrate/20250913000000_create_facilitator_organizations.rb b/db/migrate/20250913000000_create_facilitator_organizations.rb new file mode 100644 index 000000000..d86d6bcce --- /dev/null +++ b/db/migrate/20250913000000_create_facilitator_organizations.rb @@ -0,0 +1,13 @@ +class CreateFacilitatorOrganizations < ActiveRecord::Migration[6.1] + def change + create_table :facilitator_organizations do |t| + t.references :facilitator, null: false, foreign_key: true + t.references :organization, null: false, foreign_key: true + + t.timestamps + end + + add_index :facilitator_organizations, [:facilitator_id, :organization_id], + unique: true, name: 'index_facilitator_organizations_on_ids' + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 3b219061b..223820a45 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,25 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_09_13_171135) do +ActiveRecord::Schema.define(version: 2025_09_13_171135) do + + create_table "addresses", charset: "utf8mb3", force: :cascade do |t| + t.bigint "organization_id", null: false + t.string "street", null: false + t.string "city", null: false + t.string "state", null: false + t.string "zip", null: false + t.string "country" + t.string "locality" + t.string "county" + t.integer "la_city_council_district" + t.integer "la_supervisorial_district" + t.integer "la_service_planning_area" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["organization_id"], name: "index_addresses_on_organization_id" + end + create_table "admins", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.datetime "created_at", precision: nil t.datetime "current_sign_in_at", precision: nil @@ -138,6 +156,16 @@ t.datetime "updated_at", null: false end + create_table "facilitator_organizations", charset: "utf8mb3", force: :cascade do |t| + t.bigint "facilitator_id", null: false + t.bigint "organization_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["facilitator_id", "organization_id"], name: "index_facilitator_organizations_on_ids", unique: true + t.index ["facilitator_id"], name: "index_facilitator_organizations_on_facilitator_id" + t.index ["organization_id"], name: "index_facilitator_organizations_on_organization_id" + end + create_table "facilitators", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "city", null: false t.string "country", null: false @@ -285,6 +313,21 @@ t.datetime "updated_at", precision: nil, null: false end + create_table "organizations", charset: "utf8mb3", force: :cascade do |t| + t.string "name", null: false + t.boolean "is_active", default: true + t.date "start_date" + t.date "close_date" + t.string "website_url" + t.string "agency_type", null: false + t.string "agency_type_other" + t.string "phone", null: false + t.text "mission" + t.string "project_id" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + create_table "permissions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.integer "legacy_id" @@ -672,11 +715,14 @@ t.index ["windows_type_id"], name: "index_workshops_on_windows_type_id" end + add_foreign_key "addresses", "organizations" add_foreign_key "age_ranges", "windows_types" add_foreign_key "bookmark_annotations", "bookmarks" add_foreign_key "bookmarks", "users" add_foreign_key "categories", "metadata" add_foreign_key "event_registrations", "events" + add_foreign_key "facilitator_organizations", "facilitators" + add_foreign_key "facilitator_organizations", "organizations" add_foreign_key "form_builders", "windows_types" add_foreign_key "form_field_answer_options", "answer_options" add_foreign_key "form_field_answer_options", "form_fields" diff --git a/spec/factories/addresses.rb b/spec/factories/addresses.rb new file mode 100644 index 000000000..29f227832 --- /dev/null +++ b/spec/factories/addresses.rb @@ -0,0 +1,17 @@ +FactoryBot.define do + factory :address do + association :organization + + street { Faker::Address.street_address } + city { Faker::Address.city } + state { Faker::Address.state_abbr } + zip { Faker::Address.zip_code } + + country { Faker::Address.country} + locality { ["LA City", "LA County", "Southern CA", "Northern CA", "Central CA", "Orange County", "Outside CA", "Outside USA"].sample } + county { Faker::Address.state } + la_city_council_district { Faker::Number.between(from: 1, to: 15) } + la_supervisorial_district { Faker::Number.between(from: 1, to: 5) } + la_service_planning_area { Faker::Number.between(from: 1, to: 8) } + end +end diff --git a/spec/factories/facilitators.rb b/spec/factories/facilitators.rb index 1c4241ff3..4424333ad 100644 --- a/spec/factories/facilitators.rb +++ b/spec/factories/facilitators.rb @@ -12,5 +12,11 @@ mailing_address_type { 'Personal' } phone_number { Faker::PhoneNumber.phone_number } phone_number_type { 'Personal' } + + trait :with_organization do + after(:create) do |facilitator| + facilitator.organizations << create(:organization) + end + end end end diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb new file mode 100644 index 000000000..962c71e77 --- /dev/null +++ b/spec/factories/organizations.rb @@ -0,0 +1,25 @@ +FactoryBot.define do + factory :organization do + name { Faker::Company.name } + start_date { Faker::Date.between(from: 2.years.ago, to: 1.year.ago) } + close_date { nil } + website_url { Faker::Internet.url } + agency_type { ["Non-Profit", "Government", "For-Profit", "Other"].sample } + agency_type_other { nil } + phone { Faker::PhoneNumber.phone_number } + mission { Faker::Company.catch_phrase } + project_id { Faker::Number.number(digits: 4).to_s } + + trait :with_facilitator do + after(:create) do |organization| + organization.facilitators << create(:facilitator) + end + end + + trait :with_workshop do + after(:create) do |organization| + organization.workshops << create(:workshop) + end + end + end +end diff --git a/spec/models/address_spec.rb b/spec/models/address_spec.rb new file mode 100644 index 000000000..a7a5cbb9e --- /dev/null +++ b/spec/models/address_spec.rb @@ -0,0 +1,71 @@ +require 'rails_helper' + +RSpec.describe Address, type: :model do + describe 'associations' do + it { should belong_to(:organization) } + end + + describe 'validations' do + let(:address) { build(:address) } + + it 'is valid with valid attributes' do + expect(address).to be_valid + end + + it 'requires a street' do + address.street = nil + expect(address).not_to be_valid + expect(address.errors[:street]).to include("can't be blank") + end + + it 'requires a city' do + address.city = nil + expect(address).not_to be_valid + expect(address.errors[:city]).to include("can't be blank") + end + + it 'requires a state' do + address.state = nil + expect(address).not_to be_valid + expect(address.errors[:state]).to include("can't be blank") + end + + it 'requires a zip' do + address.zip = nil + expect(address).not_to be_valid + expect(address.errors[:zip]).to include("can't be blank") + end + + it 'requires an organization' do + address.organization = nil + expect(address).not_to be_valid + expect(address.errors[:organization]).to include("must exist") + end + end + + describe 'optional fields' do + let(:address) { build(:address) } + + it 'allows country to be nil' do + address.country = nil + expect(address).to be_valid + end + + it 'allows locality to be nil' do + address.locality = nil + expect(address).to be_valid + end + + it 'allows county to be nil' do + address.county = nil + expect(address).to be_valid + end + + it 'allows LA-specific fields to be nil' do + address.la_city_council_district = nil + address.la_supervisorial_district = nil + address.la_service_planning_area = nil + expect(address).to be_valid + end + end +end diff --git a/spec/models/facilitator_organization_spec.rb b/spec/models/facilitator_organization_spec.rb new file mode 100644 index 000000000..2bb081c56 --- /dev/null +++ b/spec/models/facilitator_organization_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' + +RSpec.describe FacilitatorOrganization, type: :model do + describe 'associations' do + it { should belong_to(:facilitator) } + it { should belong_to(:organization) } + end +end \ No newline at end of file diff --git a/spec/models/facilitator_spec.rb b/spec/models/facilitator_spec.rb index 7ac26c372..14d99277c 100644 --- a/spec/models/facilitator_spec.rb +++ b/spec/models/facilitator_spec.rb @@ -3,6 +3,8 @@ RSpec.describe Facilitator, type: :model do describe 'associations' do it { should have_one(:user) } + it { should have_many(:facilitator_organizations).dependent(:restrict_with_exception) } + it { should have_many(:organizations).through(:facilitator_organizations) } end describe 'validations' do diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb new file mode 100644 index 000000000..4f8432ee4 --- /dev/null +++ b/spec/models/organization_spec.rb @@ -0,0 +1,85 @@ +require 'rails_helper' + +RSpec.describe Organization, type: :model do + describe 'associations' do + it { should have_many(:addresses).dependent(:destroy) } + it { should have_many(:facilitator_organizations).dependent(:restrict_with_exception) } + it { should have_many(:facilitators).through(:facilitator_organizations) } + end + + describe 'validations' do + let(:organization) { build(:organization) } + + it 'is valid with valid attributes' do + expect(organization).to be_valid + end + + it 'requires a name' do + organization.name = nil + expect(organization).not_to be_valid + expect(organization.errors[:name]).to include("can't be blank") + end + + it 'requires an agency_type' do + organization.agency_type = nil + expect(organization).not_to be_valid + expect(organization.errors[:agency_type]).to include("can't be blank") + end + + it 'requires a phone' do + organization.phone = nil + expect(organization).not_to be_valid + expect(organization.errors[:phone]).to include("can't be blank") + end + end + + describe 'default values' do + it 'defaults is_active to true' do + organization = create(:organization) + expect(organization.is_active).to be true + end + end + + describe 'optional fields' do + let(:organization) { build(:organization) } + + it 'allows start_date to be nil' do + organization.start_date = nil + expect(organization).to be_valid + end + + it 'allows close_date to be nil' do + organization.close_date = nil + expect(organization).to be_valid + end + + it 'allows website_url to be nil' do + organization.website_url = nil + expect(organization).to be_valid + end + + it 'allows agency_type_other to be nil' do + organization.agency_type_other = nil + expect(organization).to be_valid + end + + it 'allows mission to be nil' do + organization.mission = nil + expect(organization).to be_valid + end + + it 'allows project_id to be nil' do + organization.project_id = nil + expect(organization).to be_valid + end + end + + describe 'string representations' do + it 'has a valid website URL when present' do + organization = create(:organization) + if organization.website_url.present? + expect(organization.website_url).to start_with("http") + end + end + end +end