diff --git a/.rubocop.yml b/.rubocop.yml index 5583f32a7..e556f2ac8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,7 +2,7 @@ AllCops: Exclude: - 'bin/*' - 'node_modules/**/*' - - 'spec/dummy/db/schema.rb' + # - 'spec/dummy/db/schema.rb' - 'vendor/**/*' NewCops: enable TargetRubyVersion: 3.4 diff --git a/Gemfile.lock b/Gemfile.lock index 3ffb09ed8..db65ed955 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,6 +24,7 @@ PATH active_storage_validations activerecord-import activerecord-postgis-adapter + acts_as_tenant bootstrap (~> 5.3.2) dartsass-sprockets (~> 3.1) devise @@ -46,6 +47,10 @@ PATH mobility (>= 1.0.1, < 2.0) mobility-actiontext (~> 1.1) noticed + octokit + omniauth + omniauth-github (~> 2.0.0) + omniauth-rails_csrf_protection premailer-rails public_activity pundit (>= 2.1, < 2.6) @@ -158,6 +163,8 @@ GEM mutex_m securerandom (>= 0.3) tzinfo (~> 2.0) + acts_as_tenant (1.0.1) + rails (>= 6.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) asset_sync (2.19.2) @@ -468,6 +475,8 @@ GEM mobility (~> 1.2) msgpack (1.8.0) multi_json (1.17.0) + multi_xml (0.7.1) + bigdecimal (~> 3.1) multipart-post (2.4.1) mutex_m (0.3.0) net-http (0.6.0) @@ -490,6 +499,29 @@ GEM racc (~> 1.4) noticed (2.8.0) rails (>= 6.1.0) + oauth2 (2.0.9) + faraday (>= 0.17.3, < 3.0) + jwt (>= 1.0, < 3.0) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0) + version_gem (~> 1.1) + octokit (9.1.0) + faraday (>= 1, < 3) + sawyer (~> 0.9) + omniauth (2.1.2) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-github (2.0.1) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.8) + omniauth-oauth2 (1.8.0) + oauth2 (>= 1.4, < 3) + omniauth (~> 2.0) + omniauth-rails_csrf_protection (1.0.2) + actionpack (>= 4.2) + omniauth (~> 2.0) optimist (3.2.1) orm_adapter (0.5.0) parallel (1.27.0) @@ -710,6 +742,9 @@ GEM ffi (~> 1.9) sassc-embedded (1.80.4) sass-embedded (~> 1.80) + sawyer (0.9.2) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) securerandom (0.4.1) selenium-webdriver (4.35.0) base64 (~> 0.2) @@ -741,6 +776,9 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) spring (4.4.0) spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) @@ -785,6 +823,7 @@ GEM unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uri (1.0.3) + version_gem (1.1.4) virtus (2.0.0) axiom-types (~> 0.1) coercible (~> 1.0) diff --git a/app/assets/stylesheets/better_together/theme.scss b/app/assets/stylesheets/better_together/theme.scss index 5e2c6e6a6..df5604151 100644 --- a/app/assets/stylesheets/better_together/theme.scss +++ b/app/assets/stylesheets/better_together/theme.scss @@ -1,3 +1,24 @@ +// New to NL colour palette +$blue-1: #004BA8; +$blue-2: #3E78B2; +$beige: #EAF0CE; +$teal: #5a8f9b; +$beige-light: lighten($beige, 10%); +$green-2: #70A288; +$green-1: #42b983; // variant + +// Override the color variables in the host application +$primary: $green-1; /* Change to a different blue */ +$secondary: $green-2; /* Change to a different green */ +$success: $green-2; +$info: $green-1; +$warning: #C9B947; +$danger: #BF4A47; +$text-opposite-theme-color: #222; /* Darker text */ +$background-opposite-theme-color: #f0f0f0; /* Lighter background */ +$light-background-text-color: #222; +$dark-background-text-color: #f0f0f0; + @import "bootstrap/functions"; @import "bootstrap/mixins"; @import "bootstrap/variables"; diff --git a/app/controllers/better_together/omniauth_callbacks_controller.rb b/app/controllers/better_together/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..55c965cb3 --- /dev/null +++ b/app/controllers/better_together/omniauth_callbacks_controller.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module BetterTogether + class OmniauthCallbacksController < Devise::OmniauthCallbacksController # rubocop:todo Style/Documentation + # See https://github.com/omniauth/omniauth/wiki/FAQ#rails-session-is-clobbered-after-callback-on-developer-strategy + before_action :verify_oauth_state, only: %i[github] + + before_action :set_person_platform_integration, except: [:failure] + before_action :set_user, except: [:failure] + before_action :generate_oauth_state, only: %i[github] + + attr_reader :person_platform_integration, :user + + def github + handle_auth 'Github' + end + + private + + def verify_oauth_state + return unless params[:state] != session[:oauth_state] + + flash[:alert] = 'Invalid OAuth state parameter' + redirect_to new_user_registration_path + end + + def handle_auth(kind) # rubocop:todo Metrics/AbcSize + if user.present? + flash[:success] = t 'devise_omniauth_callbacks.success', kind: kind if is_navigational_format? + sign_in_and_redirect user, event: :authentication + redirect_to edit_user_registration_path + else + flash[:alert] = + t 'devise_omniauth_callbacks.failure', kind:, reason: "#{auth.info.email} is not authorized" + redirect_to new_user_registration_path + end + end + + def auth + request.env['omniauth.auth'] + end + + def set_person_platform_integration + @person_platform_integration = PersonPlatformIntegration.find_by(provider: auth.provider, uid: auth.uid) + end + + def set_user + @user = ::BetterTogether.user_class.from_omniauth( + person_platform_integration:, + auth:, + current_user: + ) + end + + def generate_oauth_state + session[:oauth_state] = SecureRandom.hex(24) + end + + def failure + flash[:error] = 'There was a problem signing you in. Please register or try signing in later.' + redirect_to helpers.base_url + end + end +end diff --git a/app/controllers/better_together/pages_controller.rb b/app/controllers/better_together/pages_controller.rb index 6be098889..7a3133493 100644 --- a/app/controllers/better_together/pages_controller.rb +++ b/app/controllers/better_together/pages_controller.rb @@ -4,6 +4,8 @@ module BetterTogether # Responds to requests for pages class PagesController < FriendlyResourceController # rubocop:todo Metrics/ClassLength before_action :set_page, only: %i[show edit update destroy] + skip_before_action :check_platform_setup, unless: -> { ::BetterTogether::Platform.where(host: true).any? } + before_action only: %i[new edit], if: -> { Rails.env.development? } do # Make sure that all BLock subclasses are loaded in dev to generate new block buttons BetterTogether::Content::Block.load_all_subclasses diff --git a/app/controllers/better_together/person_platform_integrations_controller.rb b/app/controllers/better_together/person_platform_integrations_controller.rb new file mode 100644 index 000000000..94a4b6c75 --- /dev/null +++ b/app/controllers/better_together/person_platform_integrations_controller.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module BetterTogether + # Allows for the management of PersonPlatformIntegrations + class PersonPlatformIntegrationsController < ApplicationController + before_action :set_person_platform_integration, only: %i[show edit update destroy] + + # GET /better_together/person_platform_integrations + def index + @person_platform_integrations = BetterTogether::PersonPlatformIntegration.all + end + + # GET /better_together/person_platform_integrations/1 + def show; end + + # GET /better_together/person_platform_integrations/new + def new + @person_platform_integration = BetterTogether::PersonPlatformIntegration.new + end + + # GET /better_together/person_platform_integrations/1/edit + def edit; end + + # POST /better_together/person_platform_integrations + def create + @better_together_person_platform_integration = BetterTogether::PersonPlatformIntegration.new(person_platform_integration_params) + if @person_platform_integration.save + redirect_to @person_platform_integration, notice: 'PersonPlatformIntegration was successfully created.' + else + render :new, status: :unprocessable_entity + end + end + + # PATCH/PUT /better_together/person_platform_integrations/1 + def update + if @person_platform_integration.update(person_platform_integration_params) + redirect_to @person_platform_integration, notice: 'PersonPlatformIntegration was successfully updated.', + status: :see_other + else + render :edit, status: :unprocessable_entity + end + end + + # DELETE /better_together/person_platform_integrations/1 + def destroy + @person_platform_integration.destroy! + redirect_to person_platform_integrations_url, notice: 'PersonPlatformIntegration was successfully destroyed.', + status: :see_other + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_person_platform_integration + @person_platform_integration = BetterTogether::PersonPlatformIntegration.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def person_platform_integration_params + params.require(:person_platform_integration).permit(:provider, :uid, :token, :secret, :profile_url, :user_id) + end + end +end diff --git a/app/controllers/better_together/seeds_controller.rb b/app/controllers/better_together/seeds_controller.rb new file mode 100644 index 000000000..34b2e0a18 --- /dev/null +++ b/app/controllers/better_together/seeds_controller.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module BetterTogether + # CRUD for Seed records + class SeedsController < ApplicationController + before_action :set_seed, only: %i[show edit update destroy] + + # GET /seeds + def index + @seeds = Seed.all + end + + # GET /seeds/1 + def show; end + + # GET /seeds/new + def new + @seed = Seed.new + end + + # GET /seeds/1/edit + def edit; end + + # POST /seeds + def create + @seed = Seed.new(seed_params) + + if @seed.save + redirect_to @seed, notice: 'Seed was successfully created.' + else + render :new, status: :unprocessable_entity + end + end + + # PATCH/PUT /seeds/1 + def update + if @seed.update(seed_params) + redirect_to @seed, notice: 'Seed was successfully updated.', status: :see_other + else + render :edit, status: :unprocessable_entity + end + end + + # DELETE /seeds/1 + def destroy + @seed.destroy! + redirect_to seeds_url, notice: 'Seed was successfully destroyed.', status: :see_other + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_seed + @seed = Seed.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def seed_params + params.fetch(:seed, {}) + end + end +end diff --git a/app/helpers/better_together/form_helper.rb b/app/helpers/better_together/form_helper.rb index d0508e7f8..94c6e5894 100644 --- a/app/helpers/better_together/form_helper.rb +++ b/app/helpers/better_together/form_helper.rb @@ -69,7 +69,7 @@ def language_select_field(form: nil, field_name: :locale, selected_locale: I18n. def locale_options_for_select(selected_locale = I18n.locale) options_for_select( - I18n.available_locales.map { |locale| [I18n.t("locales.#{locale}", locale:), locale] }, + I18n.available_locales.map { |locale| [I18n.t("better_together.languages.#{locale}", locale:), locale] }, selected_locale ) end diff --git a/app/helpers/better_together/person_platform_integrations_helper.rb b/app/helpers/better_together/person_platform_integrations_helper.rb new file mode 100644 index 000000000..d91962639 --- /dev/null +++ b/app/helpers/better_together/person_platform_integrations_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module BetterTogether + # This module conains helper methods for PersonPLatformIntegrations + module PersonPlatformIntegrationsHelper + end +end diff --git a/app/helpers/better_together/seeds_helper.rb b/app/helpers/better_together/seeds_helper.rb new file mode 100644 index 000000000..f1a440aa5 --- /dev/null +++ b/app/helpers/better_together/seeds_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module BetterTogether + module SeedsHelper # rubocop:todo Style/Documentation + end +end diff --git a/app/helpers/better_together/translatable_fields_helper.rb b/app/helpers/better_together/translatable_fields_helper.rb index cc45b8358..073c0c12a 100644 --- a/app/helpers/better_together/translatable_fields_helper.rb +++ b/app/helpers/better_together/translatable_fields_helper.rb @@ -40,7 +40,7 @@ def tab_button(locale, unique_locale_attribute, translation_present) # rubocop:t type: 'button', aria: { controls: "#{unique_locale_attribute}-field", selected: locale.to_s == I18n.locale.to_s }) do - (t("locales.#{locale}") + translation_indicator(translation_present)).html_safe + (t("better_together.languages.#{locale}") + translation_indicator(translation_present)).html_safe end end @@ -73,7 +73,7 @@ def dropdown_menu(_attribute, locale, unique_locale_attribute, base_url) # ruboc content_tag(:ul, class: 'dropdown-menu') do I18n.available_locales.reject { |available_locale| available_locale == locale }.map do |available_locale| content_tag(:li) do - link_to "AI Translate from #{I18n.t("locales.#{available_locale}")}", '#ai-translate', + link_to "AI Translate from #{I18n.t("better_together.languages.#{available_locale}")}", '#ai-translate', class: 'dropdown-item', data: { 'better_together--translation-target' => 'aiTranslate', diff --git a/app/integrations/better_together/github.rb b/app/integrations/better_together/github.rb new file mode 100644 index 000000000..09480977b --- /dev/null +++ b/app/integrations/better_together/github.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'octokit' + +module BetterTogether + # This class allows integration with the GitHub API + class Github + def access_token + Octokit::Client.new(bearer_token: jwt).create_app_installation_access_token(Rails.application.credentials.dig( + :github, :installation_id + ))[:token] + end + + def jwt + payload = { + iat: Time.now.to_i - 60, # issued at time + exp: Time.now.to_i + (10 * 60), + iss: Rails.application.credentials.dig(:github, :app_id) + } + JWT.encode(payload, private_key, 'RS256') + end + + def private_key + @private_key ||= OpenSSL::PKey::RSA.new(private_pem) + end + + def private_pem + @private_pem ||= Rails.application.credentials.dig(:github, :private_pem) + end + end +end diff --git a/app/javascript/controllers/better_together/time_zone_controller.js b/app/javascript/controllers/better_together/time_zone_controller.js new file mode 100644 index 000000000..ed094a206 --- /dev/null +++ b/app/javascript/controllers/better_together/time_zone_controller.js @@ -0,0 +1,20 @@ +import { Controller } from "stimulus" + +export default class extends Controller { + static targets = [ "select" ] + + connect() { + // Called when the controller is initialized and the element is in the DOM + const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + if (this.hasSelectTarget) { + const options = this.selectTarget.options; + for (let i = 0; i < options.length; i++) { + if (options[i].value === userTimeZone) { + this.selectTarget.selectedIndex = i; + break; + } + } + } + } +} diff --git a/app/models/better_together/application_record.rb b/app/models/better_together/application_record.rb index e9c40b930..064bd16ad 100644 --- a/app/models/better_together/application_record.rb +++ b/app/models/better_together/application_record.rb @@ -5,6 +5,7 @@ module BetterTogether class ApplicationRecord < ActiveRecord::Base self.abstract_class = true include BetterTogetherId + include Seedable def self.extra_permitted_attributes [] diff --git a/app/models/better_together/person.rb b/app/models/better_together/person.rb index 4812b46f7..06484fd3d 100644 --- a/app/models/better_together/person.rb +++ b/app/models/better_together/person.rb @@ -18,6 +18,7 @@ def self.primary_community_delegation_attrs include Member include PrimaryCommunity include Privacy + include Seedable include Viewable include Metrics::Viewable include ::Storext.model @@ -68,6 +69,8 @@ def self.primary_community_delegation_attrs slugged :identifier, dependent: :delete_all + has_many :person_platform_integrations, dependent: :destroy + store_attributes :preferences do locale String, default: I18n.default_locale.to_s time_zone String, default: ENV.fetch('APP_TIME_ZONE', 'Newfoundland') diff --git a/app/models/better_together/person_platform_integration.rb b/app/models/better_together/person_platform_integration.rb new file mode 100644 index 000000000..47879fb85 --- /dev/null +++ b/app/models/better_together/person_platform_integration.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +module BetterTogether + # This model represents a bridge between a person and an external oauth platform + class PersonPlatformIntegration < ApplicationRecord + PROVIDERS = { + facebook: 'Facebook', + github: 'Github', + google_oauth2: 'Google', + linkedin: 'Linkedin' + }.freeze + + belongs_to :person + belongs_to :platform + belongs_to :user + + Devise.omniauth_configs.each_key do |provider| + scope provider, -> { where(provider:) } + end + + def expired? + expires_at? && expires_at <= Time.zone.now + end + + def token + renew_token! if expired? + access_token + end + + def renew_token! + new_token = current_token.refresh! + update( + access_token: new_token.token, + refresh_token: new_token.refresh_token, + expires_at: Time.at(new_token.expires_at) + ) + end + + def refresh_auth_hash + renew_token! if expired? + + omniauth = strategy + omniauth.access_token = current_token + + update(self.class.attributes_from_omniauth(omniauth.auth_hash)) + end + + def current_token + OAuth2::AccessToken.new( + strategy.client, + access_token, + refresh_token: + ) + end + + # return an OmniAuth::Strategies instance for the provider + def strategy + OmniAuth::Strategies.const_get(OmniAuth::Utils.camelize(provider)).new( + nil, + ENV.fetch("#{provider.downcase}_client_id", nil), + ENV.fetch("#{provider.downcase}_client_secret", nil) + ) + end + + # rubocop:todo Metrics/MethodLength + def self.attributes_from_omniauth(auth) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength + expires_at = auth.credentials.expires_at.present? ? Time.at(auth.credentials.expires_at) : nil + + attributes = { + provider: auth.provider, + uid: auth.uid, + expires_at:, + access_token: auth.credentials.token, + access_token_secret: auth.credentials.secret, + auth: auth.to_hash + } + + attributes[:handle] = auth.info.nickname if auth.info.nickname.present? + attributes[:name] = auth.info.name if auth.info.name.present? + attributes[:image_url] = URI.parse(auth.info.image) if auth.info.image.present? + + attributes[:profile_url] = auth.info.urls.first.last unless person_platform_integration.persisted? + + attributes + end + # rubocop:enable Metrics/MethodLength + + def self.update_or_initialize(person_platform_integration, auth) + if person_platform_integration.present? + person_platform_integration.update(attributes_from_omniauth(auth)) + else + person_platform_integration = new( + attributes_from_omniauth(auth) + ) + end + + person_platform_integration + end + end +end diff --git a/app/models/better_together/seed.rb b/app/models/better_together/seed.rb new file mode 100644 index 000000000..417a8268a --- /dev/null +++ b/app/models/better_together/seed.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +module BetterTogether + # Allows for import and export of data in a structured and standardized way + class Seed < ApplicationRecord # rubocop:todo Metrics/ClassLength + self.table_name = 'better_together_seeds' + self.inheritance_column = :type # Defensive for STI safety + + include Creatable + include Identifier + include Privacy + + DEFAULT_ROOT_KEY = 'better_together' + + # 1) Make sure you have Active Storage set up in your app + # This attaches a single YAML file to each seed record + has_one_attached :yaml_file + + # 2) Polymorphic association: optional + belongs_to :seedable, polymorphic: true, optional: true + + validates :type, :identifier, :version, :created_by, :seeded_at, + :description, :origin, :payload, presence: true + + after_create_commit :attach_yaml_file + after_update_commit :attach_yaml_file + + # ------------------------------------------------------------- + # Scopes + # ------------------------------------------------------------- + scope :by_type, ->(type) { where(type: type) } + scope :by_identifier, ->(identifier) { where(identifier: identifier) } + scope :latest_first, -> { order(created_at: :desc) } + scope :latest_version, ->(type, identifier) { by_type(type).by_identifier(identifier).latest_first.limit(1) } + scope :latest, -> { latest_first.limit(1) } + + # ------------------------------------------------------------- + # Accessor overrides for origin/payload => Indifferent Access + # ------------------------------------------------------------- + def origin + super&.with_indifferent_access || {} + end + + def payload + super&.with_indifferent_access || {} + end + + # Helpers for nested origin data + def contributors + origin[:contributors] || [] + end + + def platforms + origin[:platforms] || [] + end + + # ------------------------------------------------------------- + # plant = internal DB creation (used by import) + # ------------------------------------------------------------- + def self.plant(type:, identifier:, version:, metadata:, content:) # rubocop:todo Metrics/MethodLength + create!( + type: type, + identifier: identifier, + version: version, + created_by: metadata[:created_by], + seeded_at: metadata[:created_at], + description: metadata[:description], + origin: metadata[:origin], + payload: content, + seedable_type: metadata[:seedable_type], + seedable_id: metadata[:seedable_id] + ) + end + + # ------------------------------------------------------------- + # import = read a seed and store in DB + # ------------------------------------------------------------- + def self.import(seed_data, root_key: DEFAULT_ROOT_KEY) # rubocop:todo Metrics/MethodLength + data = seed_data.deep_symbolize_keys.fetch(root_key.to_sym) + metadata = data.fetch(:seed) + content = data.except(:version, :seed) + + plant( + type: metadata.fetch(:type), + identifier: metadata.fetch(:identifier), + version: data.fetch(:version), + metadata: { + created_by: metadata.fetch(:created_by), + created_at: Time.iso8601(metadata.fetch(:created_at)), + description: metadata.fetch(:description), + origin: metadata.fetch(:origin), + seedable_type: metadata[:seedable_type], + seedable_id: metadata[:seedable_id] + }, + content: content + ) + end + + # ------------------------------------------------------------- + # export = produce a structured hash including seedable info + # ------------------------------------------------------------- + # rubocop:todo Metrics/MethodLength + def export(root_key: DEFAULT_ROOT_KEY) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength + seed_obj = { + type: type, + identifier: identifier, + created_by: created_by, + created_at: seeded_at.iso8601, + description: description, + origin: origin.deep_symbolize_keys + } + + # If seedable_type or seedable_id is present, include them + seed_obj[:seedable_type] = seedable_type if seedable_type.present? + seed_obj[:seedable_id] = seedable_id if seedable_id.present? + + { + root_key => { + version: version, + seed: seed_obj, + **payload.deep_symbolize_keys + } + } + end + # rubocop:enable Metrics/MethodLength + + # Export as YAML + def export_yaml(root_key: DEFAULT_ROOT_KEY) + export(root_key: root_key).deep_stringify_keys.to_yaml + end + + # A recommended file name for the exported seed + def versioned_file_name + timestamp = seeded_at.utc.strftime('%Y%m%d%H%M%S') + "#{type.demodulize.underscore}_#{identifier}_v#{version}_#{timestamp}.yml" + end + + # ------------------------------------------------------------- + # load_seed for file or named namespace + # ------------------------------------------------------------- + def self.load_seed(source, root_key: DEFAULT_ROOT_KEY) # rubocop:todo Metrics/MethodLength + # 1) Direct file path + if File.exist?(source) + begin + seed_data = YAML.load_file(source) + return import(seed_data, root_key: root_key) + rescue StandardError => e + raise "Error loading seed from file '#{source}': #{e.message}" + end + end + + # 2) 'namespace' approach => config/seeds/#{source}.yml + path = Rails.root.join('config', 'seeds', "#{source}.yml").to_s + raise "Seed file not found for '#{source}' at path '#{path}'" unless File.exist?(path) + + begin + seed_data = YAML.load_file(path) + import(seed_data, root_key: root_key) + rescue StandardError => e + raise "Error loading seed from namespace '#{source}' at path '#{path}': #{e.message}" + end + end + + # ------------------------------------------------------------- + # Attach the exported YAML as an Active Storage file + # ------------------------------------------------------------- + def attach_yaml_file + yml_data = export_yaml + yaml_file.attach( + io: StringIO.new(yml_data), + filename: versioned_file_name, + content_type: 'text/yaml' + ) + end + end +end diff --git a/app/models/better_together/user.rb b/app/models/better_together/user.rb index ba815960c..0b3ebcf73 100644 --- a/app/models/better_together/user.rb +++ b/app/models/better_together/user.rb @@ -7,9 +7,12 @@ class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - devise :database_authenticatable, :registerable, + + devise :database_authenticatable, :omniauthable, :registerable, :recoverable, :rememberable, :validatable, :confirmable, - :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist + :jwt_authenticatable, + jwt_revocation_strategy: JwtDenylist, + omniauth_providers: %i[github] has_one :person_identification, lambda { diff --git a/app/models/better_together/wizard.rb b/app/models/better_together/wizard.rb index 55b539d71..2464e5e0a 100644 --- a/app/models/better_together/wizard.rb +++ b/app/models/better_together/wizard.rb @@ -2,7 +2,7 @@ # app/models/better_together/wizard.rb module BetterTogether - # Ordered step defintions that the user must complete + # Ordered step definitions that the user must complete class Wizard < ApplicationRecord include Identifier include Protected @@ -19,13 +19,9 @@ class Wizard < ApplicationRecord validates :max_completions, numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :current_completions, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - # Additional logic and methods as needed - def completed? - # TODO: Adjust for wizards with multiple possible completions completed = wizard_steps.size == wizard_step_definitions.size && wizard_steps.ordered.all?(&:completed) - mark_completed if completed current_completions.positive? end @@ -39,9 +35,26 @@ def mark_completed self.current_completions += 1 self.last_completed_at = DateTime.now - self.first_completed_at = DateTime.now if first_completed_at.nil? - + self.first_completed_at ||= DateTime.now save end + + # ------------------------------------- + # Overriding #plant for the Seedable concern + # ------------------------------------- + def plant + # Pull in the default fields from the base Seedable (model_class, record_id, etc.) + super.merge( + name: name, + identifier: identifier, + description: description, + max_completions: max_completions, + current_completions: current_completions, + last_completed_at: last_completed_at, + first_completed_at: first_completed_at, + # Optionally embed your wizard_step_definitions so they're all in one seed + step_definitions: wizard_step_definitions.map(&:plant) + ) + end end end diff --git a/app/models/concerns/better_together/devise_user.rb b/app/models/concerns/better_together/devise_user.rb index 78afcf977..e2364225a 100644 --- a/app/models/concerns/better_together/devise_user.rb +++ b/app/models/concerns/better_together/devise_user.rb @@ -5,13 +5,55 @@ module BetterTogether module DeviseUser extend ActiveSupport::Concern - included do + included do # rubocop:todo Metrics/BlockLength include FriendlySlug slugged :email + has_many :person_platform_integrations, dependent: :destroy + validates :email, presence: true, uniqueness: { case_sensitive: false } + # rubocop:todo Metrics/CyclomaticComplexity + # rubocop:todo Metrics/MethodLength + def self.from_omniauth(person_platform_integration:, auth:, current_user:) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength + person_platform_integration = PersonPlatformIntegration.update_or_initialize(person_platform_integration, auth) + + return person_platform_integration.user if person_platform_integration.user.present? + + unless person_platform_integration.persisted? + user = current_user.present? ? current_user : find_by(email: auth['info']['email']) + + if user.blank? + user = new + user.skip_confirmation! + user.password = ::Devise.friendly_token[0, 20] + user.attributes_from_auth(auth) + + person_attributes = { + name: person_platform_integration.name || user.email.split('@').first || 'Unidentified Person', + handle: person_platform_integration.handle || user.email.split('@').first + } + user.build_person(person_attributes) + + user.save + end + + person_platform_integration.user = user + person_platform_integration.person = user.person + + person_platform_integration.save + end + + person_platform_integration.user + end + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity + + def attributes_from_auth(auth) + self.email = auth.info.email + end + def send_devise_notification(notification, *) devise_mailer.send(notification, self, *).deliver_later end @@ -31,6 +73,10 @@ def send_confirmation_instructions(opts = {}) send_devise_notification(:confirmation_instructions, @raw_confirmation_token, opts) end + def send_devise_notification(notification, *) + devise_mailer.send(notification, self, *).deliver_later + end + # # override devise method to include additional info as opts hash def send_reset_password_instructions(opts = {}) token = set_reset_password_token diff --git a/app/models/concerns/better_together/seedable.rb b/app/models/concerns/better_together/seedable.rb new file mode 100644 index 000000000..9fbba109f --- /dev/null +++ b/app/models/concerns/better_together/seedable.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +# app/models/concerns/seedable.rb +require_dependency 'better_together/seed' + +module BetterTogether + # Defines interface allowing models to implement import/export as seed feature + module Seedable + extend ActiveSupport::Concern + + # ---------------------------------------- + # This submodule holds methods that we want on the ActiveRecord::Relation + # e.g., Wizard.where(...).export_collection_as_seed(...) + # ---------------------------------------- + module RelationMethods + def export_collection_as_seed(root_key: BetterTogether::Seed::DEFAULT_ROOT_KEY, version: '1.0') + # `self` is the AR relation. We call the model’s class method with this scope’s records. + klass = self.klass + klass.export_collection_as_seed(to_a, root_key: root_key, version: version) + end + + def export_collection_as_seed_yaml(root_key: BetterTogether::Seed::DEFAULT_ROOT_KEY, version: '1.0') + klass = self.klass + klass.export_collection_as_seed_yaml(to_a, root_key: root_key, version: version) + end + end + + included do + has_many :seeds, as: :seedable, class_name: 'BetterTogether::Seed', dependent: :nullify + end + + # ---------------------------------------- + # Overridable method: convert this record into a hash for the seed's payload + # ---------------------------------------- + def plant + { + model_class: self.class.name, + record_id: id + # Add more fields if needed, e.g., name:, etc. + } + end + + # ---------------------------------------- + # Export single record and create a seed + # ---------------------------------------- + # rubocop:todo Metrics/MethodLength + def export_as_seed( # rubocop:todo Metrics/AbcSize, Metrics/MethodLength + root_key: BetterTogether::Seed::DEFAULT_ROOT_KEY, + version: '1.0', + seed_description: "Seed data for #{self.class.name} record" + ) + seed_hash = { + root_key => { + version: version, + seed: { + created_at: Time.now.utc.iso8601, + description: seed_description, + origin: { + contributors: [], + platforms: [], + license: 'LGPLv3', + usage_notes: 'Generated by BetterTogether::Seedable' + } + }, + record: plant + } + } + + # Must be persisted to create child records + unless persisted? + raise ActiveRecord::RecordNotSaved, "Can't export seed from unsaved record (#{self.class.name}). Save it first." + end + + seeds.create!( + type: 'BetterTogether::Seed', + identifier: "#{self.class.name.demodulize.underscore}-#{id}-#{SecureRandom.hex(4)}", + version: version, + created_by: 'SystemExport', + seeded_at: Time.now, + seedable_type: self.class.name, + seedable_id: id, + description: seed_description, + origin: { 'export_root_key' => root_key }, + payload: seed_hash + ) + + seed_hash + end + # rubocop:enable Metrics/MethodLength + + def export_as_seed_yaml(**) + export_as_seed(**).deep_stringify_keys.to_yaml + end + + # ---------------------------------------- + # Class Methods - Exporting Collections + # ---------------------------------------- + class_methods do # rubocop:todo Metrics/BlockLength + # Overriding `.relation` ensures that *every* AR query for this model + # is extended with `RelationMethods`. + def relation + super.extending(RelationMethods) + end + + # Overload with array of records + def export_collection_as_seed( # rubocop:todo Metrics/MethodLength + records, + root_key: BetterTogether::Seed::DEFAULT_ROOT_KEY, + version: '1.0' + ) + seed_hash = { + root_key => { + version: version, + seed: { + created_at: Time.now.utc.iso8601, + description: "Seed data for a collection of #{name} records", + origin: { + contributors: [], + platforms: [], + license: 'LGPLv3', + usage_notes: 'Generated by BetterTogether::Seedable' + } + }, + records: records.map(&:plant) + } + } + + BetterTogether::Seed.create!( + type: 'BetterTogether::Seed', + identifier: "#{name.demodulize.underscore}-collection-#{SecureRandom.hex(4)}", + version: version, + created_by: 'SystemExport', + seeded_at: Time.now, + seedable_type: name, # e.g. "BetterTogether::Wizard" + seedable_id: nil, # no single record + description: "Collection export of #{name} (size: #{records.size})", + origin: { 'export_root_key' => root_key }, + payload: seed_hash + ) + + seed_hash + end + + def export_collection_as_seed_yaml(records, **) + export_collection_as_seed(records, **).deep_stringify_keys.to_yaml + end + end + end +end diff --git a/app/policies/better_together/person_block_policy.rb b/app/policies/better_together/person_block_policy.rb index 385557487..c1b5b6d49 100644 --- a/app/policies/better_together/person_block_policy.rb +++ b/app/policies/better_together/person_block_policy.rb @@ -6,14 +6,32 @@ def index? user.present? end + def new? + user.present? + end + def create? - user.present? && record.blocker == agent && !record.blocked.permitted_to?('manage_platform') + # Must be logged in and be the blocker + return false unless user.present? && record.blocker == agent + + # Must have a valid blocked person + return false unless record.blocked.present? + + # Cannot block platform managers + !blocked_user_is_platform_manager? end def destroy? user.present? && record.blocker == agent end + def blocked_user_is_platform_manager? + return false unless record.blocked&.user + + # Check if the blocked person's user has platform management permissions + record.blocked.user.permitted_to?('manage_platform') + end + class Scope < Scope # rubocop:todo Style/Documentation def resolve scope.where(blocker: agent) diff --git a/app/views/better_together/person_platform_integrations/_authorization.html.erb b/app/views/better_together/person_platform_integrations/_authorization.html.erb new file mode 100644 index 000000000..6f3343382 --- /dev/null +++ b/app/views/better_together/person_platform_integrations/_authorization.html.erb @@ -0,0 +1,32 @@ +
+

+ Provider: + <%= authorization.provider %> +

+ +

+ Uid: + <%= authorization.uid %> +

+ +

+ Token: + <%= authorization.token %> +

+ +

+ Secret: + <%= authorization.secret %> +

+ +

+ Profile url: + <%= authorization.profile_url %> +

+ +

+ User: + <%= authorization.user_id %> +

+ +
diff --git a/app/views/better_together/person_platform_integrations/_form.html.erb b/app/views/better_together/person_platform_integrations/_form.html.erb new file mode 100644 index 000000000..0668f3275 --- /dev/null +++ b/app/views/better_together/person_platform_integrations/_form.html.erb @@ -0,0 +1,47 @@ +<%= form_with(model: person_platform_integration) do |form| %> + <% if person_platform_integration.errors.any? %> +
+

<%= pluralize(person_platform_integration.errors.count, "error") %> prohibited this person_platform_integration from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :provider, style: "display: block" %> + <%= form.text_field :provider %> +
+ +
+ <%= form.label :uid, style: "display: block" %> + <%= form.text_field :uid %> +
+ +
+ <%= form.label :token, style: "display: block" %> + <%= form.text_field :token %> +
+ +
+ <%= form.label :secret, style: "display: block" %> + <%= form.text_field :secret %> +
+ +
+ <%= form.label :profile_url, style: "display: block" %> + <%= form.text_field :profile_url %> +
+ +
+ <%= form.label :user_id, style: "display: block" %> + <%= form.text_field :user_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/better_together/person_platform_integrations/edit.html.erb b/app/views/better_together/person_platform_integrations/edit.html.erb new file mode 100644 index 000000000..defb278c7 --- /dev/null +++ b/app/views/better_together/person_platform_integrations/edit.html.erb @@ -0,0 +1,10 @@ +

Editing authorization

+ +<%= render "form", person_platform_integration: @person_platform_integration %> + +
+ +
+ <%= link_to "Show this authorization", @person_platform_integration %> | + <%= link_to "Back to authorizations", person_platform_integrations_path %> +
diff --git a/app/views/better_together/person_platform_integrations/index.html.erb b/app/views/better_together/person_platform_integrations/index.html.erb new file mode 100644 index 000000000..b3a18fb79 --- /dev/null +++ b/app/views/better_together/person_platform_integrations/index.html.erb @@ -0,0 +1,14 @@ +

<%= notice %>

+ +

Authorizations

+ +
+ <% @person_platform_integrations.each do |person_platform_integration| %> + <%= render person_platform_integration %> +

+ <%= link_to "Show this authorization", person_platform_integration %> +

+ <% end %> +
+ +<%= link_to "New authorization", new_person_platform_integration_path %> diff --git a/app/views/better_together/person_platform_integrations/new.html.erb b/app/views/better_together/person_platform_integrations/new.html.erb new file mode 100644 index 000000000..541e805dc --- /dev/null +++ b/app/views/better_together/person_platform_integrations/new.html.erb @@ -0,0 +1,9 @@ +

New authorization

+ +<%= render "form", person_platform_integration: @person_platform_integration %> + +
+ +
+ <%= link_to "Back to authorizations", person_platform_integrations_path %> +
diff --git a/app/views/better_together/person_platform_integrations/show.html.erb b/app/views/better_together/person_platform_integrations/show.html.erb new file mode 100644 index 000000000..377133dc6 --- /dev/null +++ b/app/views/better_together/person_platform_integrations/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @person_platform_integration %> + +
+ <%= link_to "Edit this authorization", edit_person_platform_integration_path(@person_platform_integration) %> | + <%= link_to "Back to authorizations", person_platform_integrations_path %> + + <%= button_to "Destroy this authorization", @person_platform_integration, method: :delete %> +
diff --git a/app/views/better_together/seeds/_form.html.erb b/app/views/better_together/seeds/_form.html.erb new file mode 100644 index 000000000..7d2714f00 --- /dev/null +++ b/app/views/better_together/seeds/_form.html.erb @@ -0,0 +1,17 @@ +<%= form_with(model: seed) do |form| %> + <% if seed.errors.any? %> +
+

<%= pluralize(seed.errors.count, "error") %> prohibited this seed from being saved:

+ + +
+ <% end %> + +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/better_together/seeds/_seed.html.erb b/app/views/better_together/seeds/_seed.html.erb new file mode 100644 index 000000000..cf313c194 --- /dev/null +++ b/app/views/better_together/seeds/_seed.html.erb @@ -0,0 +1,2 @@ +
+
diff --git a/app/views/better_together/seeds/edit.html.erb b/app/views/better_together/seeds/edit.html.erb new file mode 100644 index 000000000..a0d543cef --- /dev/null +++ b/app/views/better_together/seeds/edit.html.erb @@ -0,0 +1,10 @@ +

Editing seed

+ +<%= render "form", seed: @seed %> + +
+ +
+ <%= link_to "Show this seed", @seed %> | + <%= link_to "Back to seeds", seeds_path %> +
diff --git a/app/views/better_together/seeds/index.html.erb b/app/views/better_together/seeds/index.html.erb new file mode 100644 index 000000000..81d709adc --- /dev/null +++ b/app/views/better_together/seeds/index.html.erb @@ -0,0 +1,14 @@ +

<%= notice %>

+ +

Seeds

+ +
+ <% @seeds.each do |seed| %> + <%= render seed %> +

+ <%= link_to "Show this seed", seed %> +

+ <% end %> +
+ +<%= link_to "New seed", new_seed_path %> diff --git a/app/views/better_together/seeds/new.html.erb b/app/views/better_together/seeds/new.html.erb new file mode 100644 index 000000000..a69a84528 --- /dev/null +++ b/app/views/better_together/seeds/new.html.erb @@ -0,0 +1,9 @@ +

New seed

+ +<%= render "form", seed: @seed %> + +
+ +
+ <%= link_to "Back to seeds", seeds_path %> +
diff --git a/app/views/better_together/seeds/show.html.erb b/app/views/better_together/seeds/show.html.erb new file mode 100644 index 000000000..642385aa0 --- /dev/null +++ b/app/views/better_together/seeds/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @seed %> + +
+ <%= link_to "Edit this seed", edit_seed_path(@seed) %> | + <%= link_to "Back to seeds", seeds_path %> + + <%= button_to "Destroy this seed", @seed, method: :delete %> +
diff --git a/app/views/better_together/wizard_step_definitions/host_setup/admin_creation.html.erb b/app/views/better_together/wizard_step_definitions/host_setup/admin_creation.html.erb index a6ed4ed70..77fe7be31 100644 --- a/app/views/better_together/wizard_step_definitions/host_setup/admin_creation.html.erb +++ b/app/views/better_together/wizard_step_definitions/host_setup/admin_creation.html.erb @@ -1,102 +1,141 @@ -<%= form_for @form, url: setup_wizard_step_create_admin_path(wizard_id: 'host_setup', wizard_step_definition_id: :admin_creation), method: :post, html: { class: 'form-group' } do |f| %> -
+<% min_password_length = Devise.password_length.min %> - <% if @form.errors.any? %> -
-

Please correct the following errors:

- +<%= form_for @form, url: setup_wizard_step_create_admin_path(wizard_id: 'host_setup', wizard_step_definition_id: :admin_creation), method: :post, class: 'needs-validation form', id: 'admin-creation-form', data: { turbo: false, controller: 'better_together--form-validation', action: 'submit->better_together--form-validation#validateBeforeSubmit' } do |f| %> + + +
+
+
+
+
+

Create Your Platform Manager Account

+

+ This is the first account on your platform and gives you full control to manage settings, content, and members. It’s important to choose secure credentials and create a clear, welcoming profile. +

+
+
- <% end %> +
+
+ +
-
-

Create Admin Account

+
+ +
+
+
+ Step 2 of 2 +
+
+
+ +

+ Let’s set up your Platform Manager account. This will be your personal login and public profile as the community’s first manager. +

+ + <% if @form.errors.any? %> +
+

<%= pluralize(@form.errors.count, "error") %> prevented this account from being created:

+
    + <% @form.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + + +
+ <%= f.label :email, 'Email Address', class: 'form-label' %> + <%= f.email_field :email, autofocus: true, required: true, + class: "form-control#{' is-invalid' if @form.errors[:email].any?}", + data: { action: 'blur->better_together--form-validation#validate' } %> + This will be your primary login and how the platform contacts you. + <% if @form.errors[:email].any? %> +
<%= @form.errors[:email].join(', ') %>
+ <% end %> +
+ + +
+ <%= f.label :password, 'Password', class: 'form-label' %> + <%= f.password_field :password, required: true, minlength: min_password_length, + class: "form-control#{' is-invalid' if @form.errors[:password].any?}", + data: { action: 'blur->better_together--form-validation#validate', 'better_together--form-validation-min-length-value': min_password_length } %> + + Your password must be at least <%= min_password_length %> characters long. Use a mix of upper and lowercase letters, numbers, and symbols for extra security. + + <% if @form.errors[:password].any? %> +
<%= @form.errors[:password].join(', ') %>
+ <% end %> +
+ + +
+ <%= f.label :password_confirmation, 'Confirm Password', class: 'form-label' %> + <%= f.password_field :password_confirmation, required: true, minlength: min_password_length, + class: "form-control#{' is-invalid' if @form.errors[:password_confirmation].any?}", + data: { action: 'blur->better_together--form-validation#validate', 'better_together--form-validation-min-length-value': min_password_length } %> + <% if @form.errors[:password_confirmation].any? %> +
<%= @form.errors[:password_confirmation].join(', ') %>
+ <% end %> +
-
-

Login Details

- + +

Your Public Profile

+

+ These details will be visible to other members of the platform. You can edit them later. +

+ + <%= f.fields_for :person do |person_form| %> +
- <%= f.label :email, class: 'form-label' %> - <%= f.email_field :email, autofocus: true, class: "form-control#{' is-invalid' if @form.errors[:email].any?}" %> - <% if @form.errors[:email].any? %> -
- <%= @form.errors[:email].join(", ") %> -
+ <%= person_form.label :name, 'Full Name', class: 'form-label' %> + <%= person_form.text_field :name, required: true, + class: "form-control#{' is-invalid' if @form.errors[:name].any?}", + data: { action: 'blur->better_together--form-validation#validate' } %> + + Use your real name or the name you want your community to recognize you by. + + <% if @form.errors[:name].any? %> +
<%= @form.errors[:name].join(', ') %>
<% end %>
- +
- <%= f.label :password, class: 'form-label' %> - <%= f.password_field :password, class: "form-control#{' is-invalid' if @form.errors[:password].any?}" %> - <% if @form.errors[:password].any? %> -
- <%= @form.errors[:password].join(", ") %> -
+ <%= person_form.label :identifier, 'Username', class: 'form-label' %> + <%= person_form.text_field :identifier, required: true, + class: "form-control#{' is-invalid' if @form.errors[:identifier].any?}", + data: { action: 'blur->better_together--form-validation#validate' } %> + + Your unique handle (no spaces). This will appear in your profile URL and mentions. + + <% if @form.errors[:identifier].any? %> +
<%= @form.errors[:identifier].join(', ') %>
<% end %>
- +
- <%= f.label :password_confirmation, class: 'form-label' %> - <%= f.password_field :password_confirmation, class: "form-control#{' is-invalid' if @form.errors[:password_confirmation].any?}" %> - <% if @form.errors[:password_confirmation].any? %> -
- <%= @form.errors[:password_confirmation].join(", ") %> -
+ <%= person_form.label :description, 'Short Bio', class: 'form-label' %> + <%= person_form.text_area :description, required: true, rows: 3, + class: "form-control#{' is-invalid' if @form.errors[:description].any?}", + data: { action: 'blur->better_together--form-validation#validate' } %> + + Introduce yourself to the community. Why are you starting this platform? What’s your vision? + + <% if @form.errors[:description].any? %> +
<%= @form.errors[:description].join(', ') %>
<% end %>
-
- -
-

Profile Details

- - <%= f.fields_for :person do |person_form| %> - -
- <%= person_form.label :name, class: 'form-label' %> - <%= person_form.text_field :name, class: "form-control#{' is-invalid' if @form.errors[:name].any?}" %> - <% if @form.errors[:name].any? %> -
- <%= @form.errors[:name].join(", ") %> -
- <% end %> -
- - -
- <%= person_form.label :identifier, class: 'form-label' %> - <%= person_form.text_field :identifier, class: "form-control#{' is-invalid' if @form.errors[:identifier].any?}" %> - - Your identifier is a unique username that identifies your profile on the site. - <% if @form.errors[:identifier].any? %> -
- <%= @form.errors[:identifier].join(", ") %> -
- <% end %> -
- - -
- <%= person_form.label :description, class: 'form-label' %> - <%= person_form.text_area :description, class: "form-control#{' is-invalid' if @form.errors[:description].any?}" %> - <% if @form.errors[:description].any? %> -
- <%= @form.errors[:description].join(", ") %> -
- <% end %> -
- - <% end %> -
+ <% end %> -
- <%= f.submit 'Finish Setup', class: 'btn btn-primary' %> +
+ <%= f.submit 'Finish Setup', class: 'btn btn-primary w-100' %>
diff --git a/app/views/better_together/wizard_step_definitions/host_setup/platform_details.html.erb b/app/views/better_together/wizard_step_definitions/host_setup/platform_details.html.erb index 32f2fef03..e53f4fa0b 100644 --- a/app/views/better_together/wizard_step_definitions/host_setup/platform_details.html.erb +++ b/app/views/better_together/wizard_step_definitions/host_setup/platform_details.html.erb @@ -1,10 +1,44 @@ - + +
+
+
+
+
+

Welcome to Your Community Space

+

+ Let’s create a welcoming home for your community to connect, share, and thrive. +

+
+
+
+
+
+
-
-

Setup Your Better Together Community Platform

-

Fill out the platform details below to set up your platform.

+
+ +
+
+
+ Step 1 of 2 +
+
+
+ +

+ A strong community starts with a solid foundation. Let’s begin by filling in + a few details about your platform. Before you know it, you’ll have a home base + where everyone can gather and collaborate. +

<% if @form.errors.any? %>
@@ -19,48 +53,63 @@ <%= form_for @form, url: setup_wizard_step_create_host_platform_path, method: :post, class: 'needs-validation', novalidate: true do |f| %>
- <%= f.label :name, class: 'form-label' %> - <%= f.text_field :name, autofocus: true, class: "form-control#{' is-invalid' if @form.errors[:name].any?}", required: true %> + <%= f.label :name, 'What should we call your platform?', class: 'form-label' %> + <%= f.text_field :name, autofocus: true, placeholder: 'Example: The Community Hall', class: "form-control#{' is-invalid' if @form.errors[:name].any?}", required: true %> + + This name will appear at the top of your platform and welcome everyone who arrives. +
- <%= f.label :description, class: 'form-label' %> - <%= f.text_area :description, class: "form-control#{' is-invalid' if @form.errors[:description].any?}", rows: 3, required: true %> + <%= f.label :description, 'How would you describe your community space?', class: 'form-label' %> + <%= f.text_area :description, placeholder: 'A place where neighbors and friends support each other.', class: "form-control#{' is-invalid' if @form.errors[:description].any?}", rows: 3, required: true %> + + This helps visitors understand what your community is all about. +
- <%= f.label :url, class: 'form-label' %> - <%= f.text_field :url, class: "form-control#{' is-invalid' if @form.errors[:url].any?}", required: true %> + <%= f.label :url, 'Where can people find you online?', class: 'form-label' %> + <%= f.text_field :url, placeholder: 'https://yourplatform.com', class: "form-control#{' is-invalid' if @form.errors[:url].any?}", required: true %> + + This will be your platform’s web address — its front door on the internet. +
- <%= f.label :privacy, class: 'form-label' %> + <%= f.label :privacy, 'Who can visit your space?', class: 'form-label' %> <%= f.select :privacy, BetterTogether::Platform.privacies.keys.map { |privacy| [privacy.humanize, privacy] }, {}, { class: 'form-select', required: true } %> + + Choose whether your space is open to the public or invitation-only. +
-
- <%= f.label :time_zone, class: 'form-label' %> - <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.all, {}, { class: 'form-select', id: 'time_zone_select', required: true } %> + +
+ <%= f.label :time_zone, 'Which time zone should we use?', class: 'form-label' %> + <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.all, {}, + { + class: 'form-select', + data: { 'better-together--time-zone-target': 'select' }, + id: 'time_zone_select', + required: true + } + %> + + This helps keep events and notifications in sync for everyone. +
- <%= f.submit 'Next Step', class: 'btn btn-primary' %> +
+ <%= f.submit 'Next Step', class: 'btn btn-primary w-100' %> +
<% end %> + +
- - diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb index ec56bb2a3..67b1d1eea 100644 --- a/app/views/devise/shared/_links.html.erb +++ b/app/views/devise/shared/_links.html.erb @@ -16,16 +16,10 @@ <%= link_to t('.didn_t_receive_confirmation_instructions'), new_confirmation_path(resource_name), class: 'devise-link' %>
<% end %> - <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> - <%# this should be moved to omniautho block. unlock translation key incorrect. %> - <%= - link_to t('.sign_in_with_provider', provider: OmniAuth::Utils.camelize(provider)), new_unlock_path(resource_name), class: 'devise-link' %>
- <% end %> - - <%- if devise_mapping.omniauthable? %> - <%- resource_class.omniauth_providers.each do |provider| %> - <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false }, class: 'devise-link' %>
- <% end %> +<%- if devise_mapping.omniauthable? && params[:oauth_login].present? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %>
<% end %> +<% end %>
diff --git a/app/views/layouts/better_together/_locale_switcher.html.erb b/app/views/layouts/better_together/_locale_switcher.html.erb index 505762279..765744bd6 100644 --- a/app/views/layouts/better_together/_locale_switcher.html.erb +++ b/app/views/layouts/better_together/_locale_switcher.html.erb @@ -6,8 +6,8 @@ diff --git a/better_together.gemspec b/better_together.gemspec index 1311fa52d..94e4050ab 100644 --- a/better_together.gemspec +++ b/better_together.gemspec @@ -32,6 +32,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'activerecord-postgis-adapter' spec.add_dependency 'active_storage_svg_sanitizer' spec.add_dependency 'active_storage_validations' + spec.add_dependency 'acts_as_tenant' spec.add_dependency 'bootstrap', '~> 5.3.2' spec.add_dependency 'dartsass-sprockets', '~> 3.1' spec.add_dependency 'devise' @@ -54,6 +55,10 @@ Gem::Specification.new do |spec| spec.add_dependency 'mobility', '>= 1.0.1', '< 2.0' spec.add_dependency 'mobility-actiontext', '~> 1.1' spec.add_dependency 'noticed' + spec.add_dependency 'octokit' + spec.add_dependency 'omniauth' + spec.add_dependency 'omniauth-github', '~> 2.0.0' + spec.add_dependency 'omniauth-rails_csrf_protection' spec.add_dependency 'premailer-rails' spec.add_dependency 'public_activity' spec.add_dependency 'pundit', '>= 2.1', '< 2.6' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 4153c5126..cf772aff6 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -273,7 +273,8 @@ # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. - # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + config.omniauth :github, ENV.fetch('GITHUB_CLIENT_ID', nil), ENV.fetch('GITHUB_CLIENT_SECRET', nil), + scope: 'user,public_repo' # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/routes.rb b/config/routes.rb index e21f52788..f921f064d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,11 @@ require 'sidekiq/web' BetterTogether::Engine.routes.draw do # rubocop:todo Metrics/BlockLength + devise_for :users, + class_name: BetterTogether.user_class.to_s, + only: :omniauth_callbacks, + controllers: { omniauth_callbacks: 'better_together/users/omniauth_callbacks' } + scope ':locale', # rubocop:todo Metrics/BlockLength locale: /#{I18n.available_locales.join('|')}/ do # bt base path @@ -14,7 +19,6 @@ class_name: BetterTogether.user_class.to_s, controllers: { confirmations: 'better_together/users/confirmations', - # omniauth_callbacks: 'better_together/users/omniauth_callbacks', passwords: 'better_together/users/passwords', registrations: 'better_together/users/registrations', sessions: 'better_together/users/sessions' @@ -31,6 +35,7 @@ defaults: { format: :html, locale: I18n.locale } get 'search', to: 'search#search' + # Avoid clobbering admin users_path helper; keep redirect but rename helper get 'users', to: redirect('users/sign-in'), as: :redirect_users # redirect for user after_sign_up @@ -77,6 +82,8 @@ end end + resources :person_platform_integrations + resources :person_blocks, path: :blocks, only: %i[index create destroy] resources :reports, only: [:create] diff --git a/config/seeds/better_together/wizards/host_setup_wizard.yml b/config/seeds/better_together/wizards/host_setup_wizard.yml new file mode 100644 index 000000000..edc29971c --- /dev/null +++ b/config/seeds/better_together/wizards/host_setup_wizard.yml @@ -0,0 +1,156 @@ +better_together: + version: "1.0" + seed: + type: "wizard" + identifier: "host_setup" + created_by: "Better Together Solutions" + created_at: "2025-03-04T12:00:00Z" + description: > + This is The Seed file for the Host Setup Wizard. It guides the creation + of a new community platform using the Community Engine. + + origin: + platforms: + - name: "Community Engine" + version: "1.0" + url: "https://bebettertogether.ca" + contributors: + - name: "Robert Smith" + role: "Creator" + contact: "robert@bebettertogether.ca" + organization: "Better Together Solutions" + license: "LGPLv3" + usage_notes: > + Created as part of the foundational work on Better Together's platform onboarding process. + This seed may be reused, adapted, and redistributed with appropriate attribution under the terms of LGPLv3. + + wizard: + name: "Host Setup Wizard" + identifier: "host_setup" + description: "Initial setup wizard for configuring the host platform." + max_completions: 1 + success_message: > + Thank you! You have finished setting up your Better Together platform! + Your platform manager account has been created successfully. Please check your + email to confirm your address before signing in. + success_path: "/" + + steps: + - identifier: "welcome" + name: "Language, Welcome, Land & Data Sovereignty" + description: > + Set your language, understand data sovereignty, and read the land acknowledgment. + form_class: "::BetterTogether::HostSetup::WelcomeForm" + step_number: 1 + message: "Welcome! Let’s begin your journey." + fields: + - identifier: "locale" + type: "locale_select" + required: true + label: "Select Your Language" + + - identifier: "community_identity" + name: "Community Identity" + description: "Name your community and describe its purpose." + form_class: "::BetterTogether::HostSetup::CommunityIdentityForm" + step_number: 2 + message: "Let’s name your community and describe its purpose." + fields: + - identifier: "name" + type: "string" + required: true + label: "Community Name" + - identifier: "description" + type: "text" + required: true + label: "Short Description" + - identifier: "logo" + type: "file" + required: false + label: "Upload a Logo" + + - identifier: "privacy_settings" + name: "Platform Access & Privacy" + description: "Choose the platform URL and privacy settings." + form_class: "::BetterTogether::HostSetup::PrivacySettingsForm" + step_number: 3 + message: "Set your platform’s web address and decide who can visit." + fields: + - identifier: "url" + type: "string" + required: true + label: "Platform URL" + - identifier: "privacy" + type: "select" + required: true + label: "Privacy Level" + options: ["public", "private"] + + - identifier: "admin_creation" + name: "Platform Host Account" + description: "Create the first administrator account." + form_class: "::BetterTogether::HostSetup::AdministratorForm" + step_number: 4 + message: "Create your first platform administrator account." + fields: + - identifier: "admin_name" + type: "string" + required: true + label: "Administrator Name" + - identifier: "email" + type: "email" + required: true + label: "Administrator Email" + - identifier: "password" + type: "password" + required: true + label: "Password" + - identifier: "password_confirmation" + type: "password" + required: true + label: "Confirm Password" + + - identifier: "time_zone" + name: "Time Zone" + description: "Set your platform’s time zone." + form_class: "::BetterTogether::HostSetup::TimeZoneForm" + step_number: 5 + message: "Set your platform’s time zone for accurate scheduling." + fields: + - identifier: "time_zone" + type: "timezone_select" + required: true + label: "Select Your Time Zone" + + - identifier: "purpose_and_features" + name: "Purpose & Features" + description: "Choose the initial purpose and features for your platform." + form_class: "::BetterTogether::HostSetup::PurposeAndFeaturesForm" + step_number: 6 + message: "What will your platform be used for? Choose features to match your needs." + fields: + - identifier: "purpose" + type: "multi_select" + required: true + label: "Primary Purpose(s)" + options: ["storytelling", "organizing", "resource_sharing", "mutual_aid", "other"] + + - identifier: "first_welcome_page" + name: "First Welcome Page" + description: "Draft your first welcome message for visitors." + form_class: "::BetterTogether::HostSetup::WelcomePageForm" + step_number: 7 + message: "Write a welcoming message for your community’s front page." + fields: + - identifier: "welcome_message" + type: "rich_text" + required: true + label: "Welcome Message" + + - identifier: "review_and_launch" + name: "Review & Launch" + description: "Review your choices and launch your platform." + form_class: "::BetterTogether::HostSetup::ReviewForm" + step_number: 8 + message: "Review your choices and launch your platform when ready." + fields: [] diff --git a/config/seeds/seed_example.yml b/config/seeds/seed_example.yml new file mode 100644 index 000000000..2afbcd9ab --- /dev/null +++ b/config/seeds/seed_example.yml @@ -0,0 +1,30 @@ +better_together: + version: "1.0" + seed: + type: "wizard" + identifier: "" + created_by: "" + created_at: "" + description: "" + + origin: + platforms: [] + contributors: [] + license: "" + usage_notes: "" + + wizard: + name: "" + identifier: "" + description: "" + max_completions: 1 + success_message: "" + success_path: "" + + steps: [] + translatable_attributes: [] # New list of attributes that expect translations (names, messages, etc.) + + translations: + en: {} + fr: {} + es: {} diff --git a/db/migrate/20190301040948_create_better_together_invitations.rb b/db/migrate/20190301040948_create_better_together_invitations.rb index 2fd2a73c8..4a08bcd2e 100644 --- a/db/migrate/20190301040948_create_better_together_invitations.rb +++ b/db/migrate/20190301040948_create_better_together_invitations.rb @@ -4,7 +4,7 @@ class CreateBetterTogetherInvitations < ActiveRecord::Migration[7.0] def change # rubocop:todo Metrics/MethodLength, Metrics/AbcSize create_bt_table :invitations do |t| # rubocop:todo Metrics/BlockLength - t.string "type", default: "BetterTogether::Invitation", null: false + t.string 'type', default: 'BetterTogether::Invitation', null: false t.string :status, limit: 20, null: false, @@ -65,11 +65,11 @@ def change # rubocop:todo Metrics/MethodLength, Metrics/AbcSize add_index :better_together_invitations, %i[invitee_email invitable_id], unique: true, # rubocop:todo Layout/LineLength - name: "invitations_on_invitee_email_and_invitable_id" + name: 'invitations_on_invitee_email_and_invitable_id' # rubocop:enable Layout/LineLength add_index :better_together_invitations, %i[invitable_id status], - name: "invitations_on_invitable_id_and_status" + name: 'invitations_on_invitable_id_and_status' add_index :better_together_invitations, :invitee_email, where: "status = 'pending'", - name: "pending_invites_on_invitee_email" + name: 'pending_invites_on_invitee_email' end end diff --git a/db/migrate/20190425130144_create_better_together_posts.rb b/db/migrate/20190425130144_create_better_together_posts.rb index 0fe0bd9db..aaac200a1 100644 --- a/db/migrate/20190425130144_create_better_together_posts.rb +++ b/db/migrate/20190425130144_create_better_together_posts.rb @@ -4,7 +4,7 @@ class CreateBetterTogetherPosts < ActiveRecord::Migration[7.0] def change # rubocop:todo Metrics/MethodLength create_bt_table :posts do |t| - t.string "type", default: "BetterTogether::Post", null: false + t.string 'type', default: 'BetterTogether::Post', null: false t.bt_identifier t.bt_protected t.bt_privacy diff --git a/spec/dummy/db/migrate/20231125163430_create_active_storage_tables.active_storage.rb b/db/migrate/20231125163430_create_active_storage_tables.active_storage.rb similarity index 100% rename from spec/dummy/db/migrate/20231125163430_create_active_storage_tables.active_storage.rb rename to db/migrate/20231125163430_create_active_storage_tables.active_storage.rb diff --git a/spec/dummy/db/migrate/20231125164028_create_action_text_tables.action_text.rb b/db/migrate/20231125164028_create_action_text_tables.action_text.rb similarity index 100% rename from spec/dummy/db/migrate/20231125164028_create_action_text_tables.action_text.rb rename to db/migrate/20231125164028_create_action_text_tables.action_text.rb diff --git a/db/migrate/20240522200922_add_primary_community_to_people.rb b/db/migrate/20240522200922_add_primary_community_to_people.rb index e7c3a2ec3..1975f407e 100644 --- a/db/migrate/20240522200922_add_primary_community_to_people.rb +++ b/db/migrate/20240522200922_add_primary_community_to_people.rb @@ -6,7 +6,7 @@ def change unless column_exists?(:better_together_people, :community_id, :uuid) # Custom community reference here to allow for null references for existing records t.bt_references :community, target_table: :better_together_communities, null: true, - index: { name: "by_person_community" } + index: { name: 'by_person_community' } end end end diff --git a/db/migrate/20240612113954_create_better_together_person_platform_integrations.rb b/db/migrate/20240612113954_create_better_together_person_platform_integrations.rb new file mode 100644 index 000000000..c02615bbc --- /dev/null +++ b/db/migrate/20240612113954_create_better_together_person_platform_integrations.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# This table is used to store the relationship between a person and an external platform +class CreateBetterTogetherPersonPlatformIntegrations < ActiveRecord::Migration[7.1] + def change # rubocop:todo Metrics/MethodLength + create_bt_table :person_platform_integrations do |t| + t.string :provider, limit: 50, null: false, default: '' + t.string :uid, limit: 50, null: false, default: '' + + t.string :name + t.string :handle + t.string :profile_url + t.string :image_url + + t.string :access_token + t.string :access_token_secret + t.string :refresh_token + t.datetime :expires_at + t.jsonb :auth + + t.bt_references :person, index: { name: 'bt_person_platform_conections_by_person' } + t.bt_references :platform, index: { name: 'bt_person_platform_conections_by_platform' } + t.bt_references :user, index: { name: 'bt_person_platform_conections_by_user' } + end + end +end diff --git a/db/migrate/20240826143510_create_better_together_platform_invitations.rb b/db/migrate/20240826143510_create_better_together_platform_invitations.rb index 784e42fa2..af558a407 100644 --- a/db/migrate/20240826143510_create_better_together_platform_invitations.rb +++ b/db/migrate/20240826143510_create_better_together_platform_invitations.rb @@ -74,8 +74,8 @@ def change # rubocop:todo Metrics/AbcSize, Metrics/MethodLength add_index :better_together_platform_invitations, %i[invitee_email invitable_id], unique: true add_index :better_together_platform_invitations, %i[invitable_id status], - name: "index_platform_invitations_on_invitable_id_and_status" + name: 'index_platform_invitations_on_invitable_id_and_status' add_index :better_together_platform_invitations, :invitee_email, where: "status = 'pending'", - name: "index_pending_invitations_on_invitee_email" + name: 'index_pending_invitations_on_invitee_email' end end diff --git a/db/migrate/20250227163308_create_better_together_metrics_page_view_reports.rb b/db/migrate/20250227163308_create_better_together_metrics_page_view_reports.rb index aed76379d..d0be23d24 100644 --- a/db/migrate/20250227163308_create_better_together_metrics_page_view_reports.rb +++ b/db/migrate/20250227163308_create_better_together_metrics_page_view_reports.rb @@ -6,7 +6,7 @@ def change create_bt_table :page_view_reports, prefix: :better_together_metrics do |t| t.jsonb :filters, null: false, default: {} t.boolean :sort_by_total_views, null: false, default: false - t.string :file_format, null: false, default: "csv" + t.string :file_format, null: false, default: 'csv' t.jsonb :report_data, null: false, default: {} end diff --git a/db/migrate/20250228154526_create_better_together_metrics_link_click_reports.rb b/db/migrate/20250228154526_create_better_together_metrics_link_click_reports.rb index 841dc4f94..ef2876ed8 100644 --- a/db/migrate/20250228154526_create_better_together_metrics_link_click_reports.rb +++ b/db/migrate/20250228154526_create_better_together_metrics_link_click_reports.rb @@ -6,7 +6,7 @@ def change create_bt_table :link_click_reports, prefix: :better_together_metrics do |t| t.jsonb :filters, null: false, default: {} t.boolean :sort_by_total_clicks, null: false, default: false - t.string :file_format, null: false, default: "csv" + t.string :file_format, null: false, default: 'csv' t.jsonb :report_data, null: false, default: {} end diff --git a/db/migrate/20250304142407_set_privacy_default_private.rb b/db/migrate/20250304142407_set_privacy_default_private.rb new file mode 100644 index 000000000..68085891f --- /dev/null +++ b/db/migrate/20250304142407_set_privacy_default_private.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Ensures that all tables with the privacy column default to private +# Replaces existing 'unlisted' values with 'private' +class SetPrivacyDefaultPrivate < ActiveRecord::Migration[7.1] + def up + ActiveRecord::Base.connection.tables.each do |table| + next unless column_exists?(table, :privacy) + + # Replace existing 'unlisted' values with 'private' + execute "UPDATE #{table} SET privacy = 'private' WHERE privacy = 'unlisted'" + + privacy_column = ActiveRecord::Base.connection.columns(table).find { |col| col.name == 'privacy' } + next unless privacy_column + next if privacy_column.default == 'private' + + say "Changing default privacy for table #{table} from #{privacy_column.default.inspect} to 'private'" + change_column_default table, :privacy, 'private' + end + end + + def down + # No reversal defined as reverting the default value change and data update is not supported. + end +end diff --git a/db/migrate/20250304173431_create_better_together_seeds.rb b/db/migrate/20250304173431_create_better_together_seeds.rb new file mode 100644 index 000000000..bb89399f9 --- /dev/null +++ b/db/migrate/20250304173431_create_better_together_seeds.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Creates table to track and store Better Together Seed records +class CreateBetterTogetherSeeds < ActiveRecord::Migration[7.1] + def change # rubocop:todo Metrics/MethodLength + create_bt_table :seeds, id: :uuid do |t| + t.string :type, null: false, default: 'BetterTogether::Seed' + + t.bt_references :seedable, polymorphic: true, null: true, index: 'by_seed_seedable' + + t.bt_creator + t.bt_identifier + t.bt_privacy + + t.string :version, null: false + t.string :created_by, null: false + t.datetime :seeded_at, null: false + t.text :description, null: false + + t.jsonb :origin, null: false # Full origin block (platforms, contributors, license, usage_notes) + t.jsonb :payload, null: false # Full wizard/page_template/content_block data + end + + add_index :better_together_seeds, %i[type identifier], unique: true + # JSONB indexes - GIN index for fast key lookups inside origin and payload + add_index :better_together_seeds, :origin, using: :gin + add_index :better_together_seeds, :payload, using: :gin + end +end diff --git a/lib/better_together/engine.rb b/lib/better_together/engine.rb index 0344ade23..cfa327dca 100644 --- a/lib/better_together/engine.rb +++ b/lib/better_together/engine.rb @@ -24,6 +24,8 @@ require 'importmap-rails' require 'kaminari' require 'noticed' +require 'omniauth/rails_csrf_protection' +require 'omniauth-github' require 'premailer/rails' require 'rack/attack' require 'reform/rails' @@ -137,7 +139,7 @@ class Engine < ::Rails::Engine # Exclude postgis tables from database dumper initializer 'better_together.spatial_tables' do - ::ActiveRecord::SchemaDumper.ignore_tables = %w[spatial_ref_sys] + ::ActiveRecord::SchemaDumper.ignore_tables + ::ActiveRecord::SchemaDumper.ignore_tables = ::ActiveRecord::SchemaDumper.ignore_tables + %w[spatial_ref_sys] end initializer 'better_together.turbo' do |app| diff --git a/spec/concerns/better_together/seedable_spec.rb b/spec/concerns/better_together/seedable_spec.rb new file mode 100644 index 000000000..2a0b3652b --- /dev/null +++ b/spec/concerns/better_together/seedable_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +module BetterTogether + describe Seedable, type: :model do + # Define a test ActiveRecord model inline for this spec + # rubocop:todo RSpec/LeakyConstantDeclaration + class TestSeedableClass < ApplicationRecord # rubocop:todo Lint/ConstantDefinitionInBlock + include Seedable + end + # rubocop:enable RSpec/LeakyConstantDeclaration + + before(:all) do # rubocop:todo RSpec/BeforeAfterAll + create_table(:better_together_test_seedable_classes) do |t| + t.string :name + end + end + + after(:all) do # rubocop:todo RSpec/BeforeAfterAll + drop_table(:better_together_test_seedable_classes) + end + + describe TestSeedableClass, type: :model do + FactoryBot.define do + factory 'better_together/test_seedable_class', class: '::BetterTogether::TestSeedableClass' do + sequence(:name) { |n| "Test seedable #{n}" } + end + end + it_behaves_like 'a seedable model' + end + end +end diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index 1d04cb2cb..421b3ec7f 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -24,9 +24,6 @@ class Application < Rails::Application # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' - # Use the latest cache format and remove deprecated Active Storage setting - config.active_support.cache_format_version = 7.1 - config.generators do |g| g.orm :active_record, primary_key_type: :uuid g.fixture_replacement :factory_bot, dir: 'spec/factories' diff --git a/spec/dummy/config/i18n-tasks.yml b/spec/dummy/config/i18n-tasks.yml index 75d4669b8..741024e7b 100644 --- a/spec/dummy/config/i18n-tasks.yml +++ b/spec/dummy/config/i18n-tasks.yml @@ -1,6 +1,6 @@ # config/i18n-tasks.yml base_locale: en -locales: +locales: - en - es - fr diff --git a/spec/dummy/config/initializers/assets.rb b/spec/dummy/config/initializers/assets.rb index bcafccdd3..019d0bbb4 100644 --- a/spec/dummy/config/initializers/assets.rb +++ b/spec/dummy/config/initializers/assets.rb @@ -7,8 +7,3 @@ # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path - -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in the app/assets -# folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index afb01a2b9..57300570e 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -10,1396 +12,1532 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_08_22_143049) do +ActiveRecord::Schema[7.1].define(version: 20_250_822_143_049) do # These are extensions that must be enabled in order to support this database - enable_extension "pgcrypto" - enable_extension "plpgsql" - enable_extension "postgis" - - create_table "action_text_rich_texts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.string "name", null: false - t.text "body" - t.string "record_type", null: false - t.uuid "record_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "locale" - t.index ["record_type", "record_id", "name", "locale"], name: "index_action_text_rich_texts_uniqueness", unique: true - end - - create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.string "name", null: false - t.string "record_type", null: false - t.uuid "record_id", null: false - t.uuid "blob_id", null: false - t.datetime "created_at", null: false - t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" - t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true - end - - create_table "active_storage_blobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.string "key", null: false - t.string "filename", null: false - t.string "content_type" - t.text "metadata" - t.string "service_name", null: false - t.bigint "byte_size", null: false - t.string "checksum" - t.datetime "created_at", null: false - t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true - end - - create_table "active_storage_variant_records", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.uuid "blob_id", null: false - t.string "variation_digest", null: false - t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true - end - - create_table "better_together_activities", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "trackable_type" - t.uuid "trackable_id" - t.string "owner_type" - t.uuid "owner_id" - t.string "key" - t.jsonb "parameters", default: "{}" - t.string "recipient_type" - t.uuid "recipient_id" - t.string "privacy", limit: 50, default: "private", null: false - t.index ["owner_type", "owner_id"], name: "bt_activities_by_owner" - t.index ["privacy"], name: "by_better_together_activities_privacy" - t.index ["recipient_type", "recipient_id"], name: "bt_activities_by_recipient" - t.index ["trackable_type", "trackable_id"], name: "bt_activities_by_trackable" - end - - create_table "better_together_addresses", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "label", default: "main", null: false - t.boolean "physical", default: true, null: false - t.boolean "postal", default: false, null: false - t.string "line1" - t.string "line2" - t.string "city_name" - t.string "state_province_name" - t.string "postal_code" - t.string "country_name" - t.string "privacy", limit: 50, default: "private", null: false - t.uuid "contact_detail_id" - t.boolean "primary_flag", default: false, null: false - t.index ["contact_detail_id", "primary_flag"], name: "index_bt_addresses_on_contact_detail_id_and_primary", unique: true, where: "(primary_flag IS TRUE)" - t.index ["contact_detail_id"], name: "index_better_together_addresses_on_contact_detail_id" - t.index ["privacy"], name: "by_better_together_addresses_privacy" - end - - create_table "better_together_agreement_participants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "agreement_id", null: false - t.uuid "person_id", null: false - t.string "group_identifier" - t.datetime "accepted_at" - t.index ["agreement_id", "person_id"], name: "index_bt_agreement_participants_on_agreement_and_person", unique: true - t.index ["agreement_id"], name: "index_better_together_agreement_participants_on_agreement_id" - t.index ["group_identifier"], name: "idx_on_group_identifier_06b6e57c0b" - t.index ["person_id"], name: "index_better_together_agreement_participants_on_person_id" - end - - create_table "better_together_agreement_terms", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.integer "position", null: false - t.boolean "protected", default: false, null: false - t.uuid "agreement_id", null: false - t.index ["agreement_id"], name: "index_better_together_agreement_terms_on_agreement_id" - t.index ["identifier"], name: "index_better_together_agreement_terms_on_identifier", unique: true - end - - create_table "better_together_agreements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.boolean "collective", default: false, null: false - t.uuid "page_id" - t.index ["creator_id"], name: "by_better_together_agreements_creator" - t.index ["identifier"], name: "index_better_together_agreements_on_identifier", unique: true - t.index ["page_id"], name: "index_better_together_agreements_on_page_id" - t.index ["privacy"], name: "by_better_together_agreements_privacy" - end - - create_table "better_together_ai_log_translations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "request", null: false - t.text "response" - t.string "model", null: false - t.integer "prompt_tokens", default: 0, null: false - t.integer "completion_tokens", default: 0, null: false - t.integer "tokens_used", default: 0, null: false - t.decimal "estimated_cost", precision: 10, scale: 5, default: "0.0", null: false - t.datetime "start_time" - t.datetime "end_time" - t.string "status", default: "pending", null: false - t.uuid "initiator_id" - t.string "source_locale", null: false - t.string "target_locale", null: false - t.index ["initiator_id"], name: "index_better_together_ai_log_translations_on_initiator_id" - t.index ["model"], name: "index_better_together_ai_log_translations_on_model" - t.index ["source_locale"], name: "index_better_together_ai_log_translations_on_source_locale" - t.index ["status"], name: "index_better_together_ai_log_translations_on_status" - t.index ["target_locale"], name: "index_better_together_ai_log_translations_on_target_locale" - end - - create_table "better_together_authorships", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "position", null: false - t.string "authorable_type", null: false - t.uuid "authorable_id", null: false - t.uuid "author_id", null: false - t.uuid "creator_id" - t.index ["author_id"], name: "by_authorship_author" - t.index ["authorable_type", "authorable_id"], name: "by_authorship_authorable" - t.index ["creator_id"], name: "by_better_together_authorships_creator" - end - - create_table "better_together_calendar_entries", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "calendar_id" - t.string "schedulable_type" - t.uuid "schedulable_id" - t.datetime "starts_at", null: false - t.datetime "ends_at" - t.decimal "duration_minutes" - t.uuid "event_id", null: false - t.index ["calendar_id", "event_id"], name: "by_calendar_and_event", unique: true - t.index ["calendar_id"], name: "index_better_together_calendar_entries_on_calendar_id" - t.index ["ends_at"], name: "bt_calendar_events_by_ends_at" - t.index ["event_id"], name: "bt_calendar_entries_by_event" - t.index ["schedulable_type", "schedulable_id"], name: "index_better_together_calendar_entries_on_schedulable" - t.index ["starts_at"], name: "bt_calendar_events_by_starts_at" - end - - create_table "better_together_calendars", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "community_id", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.string "locale", limit: 5, default: "en", null: false - t.string "privacy", limit: 50, default: "private", null: false - t.boolean "protected", default: false, null: false - t.index ["community_id"], name: "by_better_together_calendars_community" - t.index ["creator_id"], name: "by_better_together_calendars_creator" - t.index ["identifier"], name: "index_better_together_calendars_on_identifier", unique: true - t.index ["locale"], name: "by_better_together_calendars_locale" - t.index ["privacy"], name: "by_better_together_calendars_privacy" - end - - create_table "better_together_calls_for_interest", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "type", default: "BetterTogether::CallForInterest", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.string "interestable_type" - t.uuid "interestable_id" - t.datetime "starts_at" - t.datetime "ends_at" - t.index ["creator_id"], name: "by_better_together_calls_for_interest_creator" - t.index ["ends_at"], name: "bt_calls_for_interest_by_ends_at" - t.index ["identifier"], name: "index_better_together_calls_for_interest_on_identifier", unique: true - t.index ["interestable_type", "interestable_id"], name: "index_better_together_calls_for_interest_on_interestable" - t.index ["privacy"], name: "by_better_together_calls_for_interest_privacy" - t.index ["starts_at"], name: "bt_calls_for_interest_by_starts_at" - end - - create_table "better_together_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.integer "position", null: false - t.boolean "protected", default: false, null: false - t.string "type", default: "BetterTogether::Category", null: false - t.string "icon", default: "fas fa-icons", null: false - t.index ["identifier", "type"], name: "index_better_together_categories_on_identifier_and_type", unique: true - end - - create_table "better_together_categorizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "category_type", null: false - t.uuid "category_id", null: false - t.string "categorizable_type", null: false - t.uuid "categorizable_id", null: false - t.index ["categorizable_type", "categorizable_id"], name: "index_better_together_categorizations_on_categorizable" - t.index ["category_type", "category_id"], name: "index_better_together_categorizations_on_category" - end - - create_table "better_together_comments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "commentable_type", null: false - t.uuid "commentable_id", null: false - t.uuid "creator_id" - t.text "content", default: "", null: false - t.index ["commentable_type", "commentable_id"], name: "bt_comments_on_commentable" - t.index ["creator_id"], name: "by_better_together_comments_creator" - end - - create_table "better_together_communities", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "host", default: false, null: false - t.boolean "protected", default: false, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.uuid "creator_id" - t.string "type", default: "BetterTogether::Community", null: false - t.index ["creator_id"], name: "by_creator" - t.index ["host"], name: "index_better_together_communities_on_host", unique: true, where: "(host IS TRUE)" - t.index ["identifier"], name: "index_better_together_communities_on_identifier", unique: true - t.index ["privacy"], name: "by_community_privacy" - end - - create_table "better_together_contact_details", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "contactable_type", null: false - t.uuid "contactable_id", null: false - t.string "type", default: "BetterTogether::ContactDetail", null: false - t.string "name" - t.string "role" - t.index ["contactable_type", "contactable_id"], name: "index_better_together_contact_details_on_contactable" - end - - create_table "better_together_content_blocks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "type", null: false - t.string "identifier", limit: 100 - t.jsonb "accessibility_attributes", default: {}, null: false - t.jsonb "content_settings", default: {}, null: false - t.jsonb "css_settings", default: {}, null: false - t.jsonb "data_attributes", default: {}, null: false - t.jsonb "html_attributes", default: {}, null: false - t.jsonb "layout_settings", default: {}, null: false - t.jsonb "media_settings", default: {}, null: false - t.jsonb "content_data", default: {} - t.uuid "creator_id" - t.string "privacy", limit: 50, default: "private", null: false - t.boolean "visible", default: true, null: false - t.jsonb "content_area_settings", default: {}, null: false - t.index ["creator_id"], name: "by_better_together_content_blocks_creator" - t.index ["privacy"], name: "by_better_together_content_blocks_privacy" - end - - create_table "better_together_content_page_blocks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "page_id", null: false - t.uuid "block_id", null: false - t.integer "position", null: false - t.index ["block_id"], name: "index_better_together_content_page_blocks_on_block_id" - t.index ["page_id", "block_id", "position"], name: "content_page_blocks_on_page_block_and_position" - t.index ["page_id", "block_id"], name: "content_page_blocks_on_page_and_block", unique: true - t.index ["page_id"], name: "index_better_together_content_page_blocks_on_page_id" - end - - create_table "better_together_content_platform_blocks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "platform_id", null: false - t.uuid "block_id", null: false - t.index ["block_id"], name: "index_better_together_content_platform_blocks_on_block_id" - t.index ["platform_id"], name: "index_better_together_content_platform_blocks_on_platform_id" - end - - create_table "better_together_conversation_participants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "conversation_id", null: false - t.uuid "person_id", null: false - t.index ["conversation_id"], name: "idx_on_conversation_id_30b3b70bad" - t.index ["person_id"], name: "index_better_together_conversation_participants_on_person_id" - end - - create_table "better_together_conversations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "title", null: false - t.uuid "creator_id", null: false - t.index ["creator_id"], name: "index_better_together_conversations_on_creator_id" - end - - create_table "better_together_email_addresses", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "email", null: false - t.string "label", null: false - t.string "privacy", limit: 50, default: "private", null: false - t.uuid "contact_detail_id", null: false - t.boolean "primary_flag", default: false, null: false - t.index ["contact_detail_id", "primary_flag"], name: "index_bt_email_addresses_on_contact_detail_id_and_primary", unique: true, where: "(primary_flag IS TRUE)" - t.index ["contact_detail_id"], name: "index_better_together_email_addresses_on_contact_detail_id" - t.index ["privacy"], name: "by_better_together_email_addresses_privacy" - end - - create_table "better_together_event_attendances", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "event_id", null: false - t.uuid "person_id", null: false - t.string "status", default: "interested", null: false - t.index ["event_id", "person_id"], name: "by_event_and_person", unique: true - t.index ["event_id"], name: "bt_event_attendance_by_event" - t.index ["person_id"], name: "bt_event_attendance_by_person" - end - - create_table "better_together_event_hosts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "event_id" - t.string "host_type" - t.uuid "host_id" - t.index ["event_id"], name: "index_better_together_event_hosts_on_event_id" - t.index ["host_type", "host_id"], name: "index_better_together_event_hosts_on_host" - end - - create_table "better_together_events", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "type", default: "BetterTogether::Event", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.datetime "starts_at" - t.datetime "ends_at" - t.decimal "duration_minutes" - t.string "registration_url" - t.index ["creator_id"], name: "by_better_together_events_creator" - t.index ["ends_at"], name: "bt_events_by_ends_at" - t.index ["identifier"], name: "index_better_together_events_on_identifier", unique: true - t.index ["privacy"], name: "by_better_together_events_privacy" - t.index ["starts_at"], name: "bt_events_by_starts_at" - end - - create_table "better_together_geography_continents", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.uuid "community_id", null: false - t.index ["community_id"], name: "by_geography_continent_community" - t.index ["identifier"], name: "index_better_together_geography_continents_on_identifier", unique: true - end - - create_table "better_together_geography_countries", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.string "iso_code", limit: 2, null: false - t.boolean "protected", default: false, null: false - t.uuid "community_id", null: false - t.index ["community_id"], name: "by_geography_country_community" - t.index ["identifier"], name: "index_better_together_geography_countries_on_identifier", unique: true - t.index ["iso_code"], name: "index_better_together_geography_countries_on_iso_code", unique: true - end - - create_table "better_together_geography_country_continents", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "country_id" - t.uuid "continent_id" - t.index ["continent_id"], name: "country_continent_by_continent" - t.index ["country_id", "continent_id"], name: "index_country_continents_on_country_and_continent", unique: true - t.index ["country_id"], name: "country_continent_by_country" - end - - create_table "better_together_geography_geospatial_spaces", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "geospatial_type" - t.uuid "geospatial_id" - t.integer "position", null: false - t.boolean "primary_flag", default: false, null: false - t.uuid "space_id" - t.index ["geospatial_id", "primary_flag"], name: "index_geospatial_spaces_on_geospatial_id_and_primary", unique: true, where: "(primary_flag IS TRUE)" - t.index ["geospatial_type", "geospatial_id"], name: "index_better_together_geography_geospatial_spaces_on_geospatial" - t.index ["space_id"], name: "index_better_together_geography_geospatial_spaces_on_space_id" - end - - create_table "better_together_geography_locatable_locations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "creator_id" - t.string "location_type" - t.uuid "location_id" - t.string "locatable_type", null: false - t.uuid "locatable_id", null: false - t.string "name" - t.index ["creator_id"], name: "by_better_together_geography_locatable_locations_creator" - t.index ["locatable_id", "locatable_type", "location_id", "location_type"], name: "locatable_locations" - t.index ["locatable_type", "locatable_id"], name: "locatable_location_by_locatable" - t.index ["location_type", "location_id"], name: "locatable_location_by_location" - t.index ["name"], name: "locatable_location_by_name" - end - - create_table "better_together_geography_maps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.string "locale", limit: 5, default: "en", null: false - t.string "privacy", limit: 50, default: "private", null: false - t.boolean "protected", default: false, null: false - t.geography "center", limit: {srid: 4326, type: "st_point", geographic: true} - t.integer "zoom", default: 13, null: false - t.geography "viewport", limit: {srid: 4326, type: "st_polygon", geographic: true} - t.jsonb "metadata", default: {}, null: false - t.string "mappable_type" - t.uuid "mappable_id" - t.string "type", default: "BetterTogether::Geography::Map", null: false - t.index ["creator_id"], name: "by_better_together_geography_maps_creator" - t.index ["identifier"], name: "index_better_together_geography_maps_on_identifier", unique: true - t.index ["locale"], name: "by_better_together_geography_maps_locale" - t.index ["mappable_type", "mappable_id"], name: "index_better_together_geography_maps_on_mappable" - t.index ["privacy"], name: "by_better_together_geography_maps_privacy" - end - - create_table "better_together_geography_region_settlements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "protected", default: false, null: false - t.uuid "region_id" - t.uuid "settlement_id" - t.index ["region_id"], name: "bt_region_settlement_by_region" - t.index ["settlement_id"], name: "bt_region_settlement_by_settlement" - end - - create_table "better_together_geography_regions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.uuid "community_id", null: false - t.uuid "country_id" - t.uuid "state_id" - t.string "type", default: "BetterTogether::Geography::Region", null: false - t.index ["community_id"], name: "by_geography_region_community" - t.index ["country_id"], name: "index_better_together_geography_regions_on_country_id" - t.index ["identifier"], name: "index_better_together_geography_regions_on_identifier", unique: true - t.index ["state_id"], name: "index_better_together_geography_regions_on_state_id" - end - - create_table "better_together_geography_settlements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.uuid "community_id", null: false - t.uuid "country_id" - t.uuid "state_id" - t.index ["community_id"], name: "by_geography_settlement_community" - t.index ["country_id"], name: "index_better_together_geography_settlements_on_country_id" - t.index ["identifier"], name: "index_better_together_geography_settlements_on_identifier", unique: true - t.index ["state_id"], name: "index_better_together_geography_settlements_on_state_id" - end - - create_table "better_together_geography_spaces", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.float "elevation" - t.float "latitude" - t.float "longitude" - t.jsonb "properties", default: {} - t.jsonb "metadata", default: {} - t.index ["creator_id"], name: "by_better_together_geography_spaces_creator" - t.index ["identifier"], name: "index_better_together_geography_spaces_on_identifier", unique: true - end - - create_table "better_together_geography_states", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.string "iso_code", limit: 5, null: false - t.boolean "protected", default: false, null: false - t.uuid "community_id", null: false - t.uuid "country_id" - t.index ["community_id"], name: "by_geography_state_community" - t.index ["country_id"], name: "index_better_together_geography_states_on_country_id" - t.index ["identifier"], name: "index_better_together_geography_states_on_identifier", unique: true - t.index ["iso_code"], name: "index_better_together_geography_states_on_iso_code", unique: true - end - - create_table "better_together_identifications", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "active", null: false - t.string "identity_type", null: false - t.uuid "identity_id", null: false - t.string "agent_type", null: false - t.uuid "agent_id", null: false - t.index ["active", "agent_type", "agent_id"], name: "active_identification", unique: true - t.index ["active"], name: "by_active_state" - t.index ["agent_type", "agent_id"], name: "by_agent" - t.index ["identity_type", "identity_id", "agent_type", "agent_id"], name: "unique_identification", unique: true - t.index ["identity_type", "identity_id"], name: "by_identity" - end - - create_table "better_together_infrastructure_building_connections", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "building_id", null: false - t.string "connection_type", null: false - t.uuid "connection_id", null: false - t.integer "position", null: false - t.boolean "primary_flag", default: false, null: false - t.index ["building_id"], name: "bt_building_connections_building" - t.index ["connection_id", "primary_flag"], name: "index_bt_building_connections_on_connection_id_and_primary", unique: true, where: "(primary_flag IS TRUE)" - t.index ["connection_type", "connection_id"], name: "bt_building_connections_connection" - end - - create_table "better_together_infrastructure_buildings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "type", default: "BetterTogether::Infrastructure::Building", null: false - t.uuid "community_id", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.integer "floors_count", default: 0, null: false - t.integer "rooms_count", default: 0, null: false - t.uuid "address_id" - t.index ["address_id"], name: "index_better_together_infrastructure_buildings_on_address_id" - t.index ["community_id"], name: "by_better_together_infrastructure_buildings_community" - t.index ["creator_id"], name: "by_better_together_infrastructure_buildings_creator" - t.index ["identifier"], name: "index_better_together_infrastructure_buildings_on_identifier", unique: true - t.index ["privacy"], name: "by_better_together_infrastructure_buildings_privacy" - end - - create_table "better_together_infrastructure_floors", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "building_id" - t.uuid "community_id", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.integer "position", null: false - t.integer "level", default: 0, null: false - t.integer "rooms_count", default: 0, null: false - t.index ["building_id"], name: "index_better_together_infrastructure_floors_on_building_id" - t.index ["community_id"], name: "by_better_together_infrastructure_floors_community" - t.index ["creator_id"], name: "by_better_together_infrastructure_floors_creator" - t.index ["identifier"], name: "index_better_together_infrastructure_floors_on_identifier", unique: true - t.index ["privacy"], name: "by_better_together_infrastructure_floors_privacy" - end - - create_table "better_together_infrastructure_rooms", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "floor_id" - t.uuid "community_id", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.index ["community_id"], name: "by_better_together_infrastructure_rooms_community" - t.index ["creator_id"], name: "by_better_together_infrastructure_rooms_creator" - t.index ["floor_id"], name: "index_better_together_infrastructure_rooms_on_floor_id" - t.index ["identifier"], name: "index_better_together_infrastructure_rooms_on_identifier", unique: true - t.index ["privacy"], name: "by_better_together_infrastructure_rooms_privacy" - end - - create_table "better_together_invitations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "type", default: "BetterTogether::Invitation", null: false - t.string "status", limit: 20, null: false - t.datetime "valid_from", null: false - t.datetime "valid_until" - t.datetime "last_sent" - t.datetime "accepted_at" - t.string "locale", limit: 5, default: "en", null: false - t.string "token", limit: 24, null: false - t.string "invitable_type", null: false - t.uuid "invitable_id", null: false - t.string "inviter_type", null: false - t.uuid "inviter_id", null: false - t.string "invitee_type", null: false - t.uuid "invitee_id", null: false - t.string "invitee_email", null: false - t.uuid "role_id" - t.index ["invitable_id", "status"], name: "invitations_on_invitable_id_and_status" - t.index ["invitable_type", "invitable_id"], name: "by_invitable" - t.index ["invitee_email", "invitable_id"], name: "invitations_on_invitee_email_and_invitable_id", unique: true - t.index ["invitee_email"], name: "invitations_by_invitee_email" - t.index ["invitee_email"], name: "pending_invites_on_invitee_email", where: "((status)::text = 'pending'::text)" - t.index ["invitee_type", "invitee_id"], name: "by_invitee" - t.index ["inviter_type", "inviter_id"], name: "by_inviter" - t.index ["locale"], name: "by_better_together_invitations_locale" - t.index ["role_id"], name: "by_role" - t.index ["status"], name: "by_status" - t.index ["token"], name: "invitations_by_token", unique: true - t.index ["valid_from"], name: "by_valid_from" - t.index ["valid_until"], name: "by_valid_until" - end - - create_table "better_together_joatu_agreements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "offer_id", null: false - t.uuid "request_id", null: false - t.text "terms" - t.string "value" - t.string "status", default: "pending", null: false - t.index ["offer_id", "request_id"], name: "bt_joatu_agreements_unique_offer_request", unique: true - t.index ["offer_id"], name: "bt_joatu_agreements_by_offer" - t.index ["offer_id"], name: "bt_joatu_agreements_one_accepted_per_offer", unique: true, where: "((status)::text = 'accepted'::text)" - t.index ["request_id"], name: "bt_joatu_agreements_by_request" - t.index ["request_id"], name: "bt_joatu_agreements_one_accepted_per_request", unique: true, where: "((status)::text = 'accepted'::text)" - end - - create_table "better_together_joatu_offers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "creator_id" - t.string "status", default: "open", null: false - t.string "target_type" - t.uuid "target_id" - t.string "urgency", default: "normal", null: false - t.uuid "address_id" - t.index ["address_id"], name: "index_better_together_joatu_offers_on_address_id" - t.index ["creator_id"], name: "by_better_together_joatu_offers_creator" - t.index ["target_type", "target_id"], name: "bt_joatu_offers_on_target" - end - - create_table "better_together_joatu_requests", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "creator_id" - t.string "status", default: "open", null: false - t.string "target_type" - t.uuid "target_id" - t.string "urgency", default: "normal", null: false - t.uuid "address_id" - t.index ["address_id"], name: "index_better_together_joatu_requests_on_address_id" - t.index ["creator_id"], name: "by_better_together_joatu_requests_creator" - t.index ["target_type", "target_id"], name: "bt_joatu_requests_on_target" - end - - create_table "better_together_joatu_response_links", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "source_type", null: false - t.uuid "source_id", null: false - t.string "response_type", null: false - t.uuid "response_id", null: false - t.uuid "creator_id" - t.index ["creator_id"], name: "by_better_together_joatu_response_links_creator" - t.index ["response_type", "response_id"], name: "bt_joatu_response_links_by_response" - t.index ["source_type", "source_id", "response_type", "response_id"], name: "bt_joatu_response_links_unique_pair", unique: true - t.index ["source_type", "source_id"], name: "bt_joatu_response_links_by_source" - end - - create_table "better_together_jwt_denylists", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "jti" - t.datetime "exp" - t.index ["jti"], name: "index_better_together_jwt_denylists_on_jti" - end - - create_table "better_together_messages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "content" - t.uuid "sender_id", null: false - t.uuid "conversation_id", null: false - t.index ["conversation_id"], name: "index_better_together_messages_on_conversation_id" - t.index ["sender_id"], name: "index_better_together_messages_on_sender_id" - end - - create_table "better_together_metrics_downloads", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "locale", limit: 5, default: "en", null: false - t.string "downloadable_type" - t.uuid "downloadable_id" - t.string "file_name", null: false - t.string "file_type", null: false - t.bigint "file_size", null: false - t.datetime "downloaded_at", null: false - t.index ["downloadable_type", "downloadable_id"], name: "index_better_together_metrics_downloads_on_downloadable" - t.index ["locale"], name: "by_better_together_metrics_downloads_locale" - end - - create_table "better_together_metrics_link_click_reports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.jsonb "filters", default: {}, null: false - t.boolean "sort_by_total_clicks", default: false, null: false - t.string "file_format", default: "csv", null: false - t.jsonb "report_data", default: {}, null: false - t.index ["filters"], name: "index_better_together_metrics_link_click_reports_on_filters", using: :gin - end - - create_table "better_together_metrics_link_clicks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "url", null: false - t.string "page_url", null: false - t.string "locale", null: false - t.boolean "internal", default: true - t.datetime "clicked_at", null: false - end - - create_table "better_together_metrics_page_view_reports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.jsonb "filters", default: {}, null: false - t.boolean "sort_by_total_views", default: false, null: false - t.string "file_format", default: "csv", null: false - t.jsonb "report_data", default: {}, null: false - t.index ["filters"], name: "index_better_together_metrics_page_view_reports_on_filters", using: :gin - end - - create_table "better_together_metrics_page_views", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "locale", limit: 5, default: "en", null: false - t.string "pageable_type" - t.uuid "pageable_id" - t.datetime "viewed_at", null: false - t.string "page_url" - t.index ["locale"], name: "by_better_together_metrics_page_views_locale" - t.index ["pageable_type", "pageable_id"], name: "index_better_together_metrics_page_views_on_pageable" - end - - create_table "better_together_metrics_search_queries", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "locale", limit: 5, default: "en", null: false - t.string "query", null: false - t.integer "results_count", null: false - t.datetime "searched_at", null: false - t.index ["locale"], name: "by_better_together_metrics_search_queries_locale" - end - - create_table "better_together_metrics_shares", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "locale", limit: 5, default: "en", null: false - t.string "platform", null: false - t.string "url", null: false - t.datetime "shared_at", null: false - t.string "shareable_type" - t.uuid "shareable_id" - t.index ["locale"], name: "by_better_together_metrics_shares_locale" - t.index ["platform", "url"], name: "index_better_together_metrics_shares_on_platform_and_url" - t.index ["shareable_type", "shareable_id"], name: "index_better_together_metrics_shares_on_shareable" - end - - create_table "better_together_navigation_areas", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.boolean "visible", default: true, null: false - t.string "name" - t.string "style" - t.string "navigable_type" - t.bigint "navigable_id" - t.index ["identifier"], name: "index_better_together_navigation_areas_on_identifier", unique: true - t.index ["navigable_type", "navigable_id"], name: "by_navigable" - end - - create_table "better_together_navigation_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.integer "position", null: false - t.boolean "protected", default: false, null: false - t.boolean "visible", default: true, null: false - t.uuid "navigation_area_id", null: false - t.uuid "parent_id" - t.string "url" - t.string "icon" - t.string "item_type", null: false - t.string "linkable_type" - t.uuid "linkable_id" - t.string "route_name" - t.integer "children_count", default: 0, null: false - t.index ["identifier"], name: "index_better_together_navigation_items_on_identifier", unique: true - t.index ["linkable_type", "linkable_id"], name: "by_linkable" - t.index ["navigation_area_id", "parent_id", "position"], name: "navigation_items_area_position", unique: true - t.index ["navigation_area_id"], name: "index_better_together_navigation_items_on_navigation_area_id" - t.index ["parent_id"], name: "by_nav_item_parent" - end - - create_table "better_together_pages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.text "meta_description" - t.string "keywords" - t.datetime "published_at" - t.string "layout" - t.string "template" - t.uuid "sidebar_nav_id" - t.index ["identifier"], name: "index_better_together_pages_on_identifier", unique: true - t.index ["privacy"], name: "by_page_privacy" - t.index ["published_at"], name: "by_page_publication_date" - t.index ["sidebar_nav_id"], name: "by_page_sidebar_nav" - end - - create_table "better_together_people", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.uuid "community_id", null: false - t.jsonb "preferences", default: {}, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.jsonb "notification_preferences", default: {}, null: false - t.index ["community_id"], name: "by_person_community" - t.index ["identifier"], name: "index_better_together_people_on_identifier", unique: true - t.index ["privacy"], name: "by_better_together_people_privacy" - end - - create_table "better_together_person_blocks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "blocker_id", null: false - t.uuid "blocked_id", null: false - t.index ["blocked_id"], name: "index_better_together_person_blocks_on_blocked_id" - t.index ["blocker_id", "blocked_id"], name: "unique_person_blocks", unique: true - t.index ["blocker_id"], name: "index_better_together_person_blocks_on_blocker_id" - end - - create_table "better_together_person_community_memberships", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "member_id", null: false - t.uuid "joinable_id", null: false - t.uuid "role_id", null: false - t.index ["joinable_id", "member_id", "role_id"], name: "unique_person_community_membership_member_role", unique: true - t.index ["joinable_id"], name: "person_community_membership_by_joinable" - t.index ["member_id"], name: "person_community_membership_by_member" - t.index ["role_id"], name: "person_community_membership_by_role" - end - - create_table "better_together_person_platform_memberships", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "member_id", null: false - t.uuid "joinable_id", null: false - t.uuid "role_id", null: false - t.index ["joinable_id", "member_id", "role_id"], name: "unique_person_platform_membership_member_role", unique: true - t.index ["joinable_id"], name: "person_platform_membership_by_joinable" - t.index ["member_id"], name: "person_platform_membership_by_member" - t.index ["role_id"], name: "person_platform_membership_by_role" - end - - create_table "better_together_phone_numbers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "number", null: false - t.string "label", null: false - t.string "privacy", limit: 50, default: "private", null: false - t.uuid "contact_detail_id", null: false - t.boolean "primary_flag", default: false, null: false - t.index ["contact_detail_id", "primary_flag"], name: "index_bt_phone_numbers_on_contact_detail_id_and_primary", unique: true, where: "(primary_flag IS TRUE)" - t.index ["contact_detail_id"], name: "index_better_together_phone_numbers_on_contact_detail_id" - t.index ["privacy"], name: "by_better_together_phone_numbers_privacy" - end - - create_table "better_together_places", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "community_id", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.uuid "space_id", null: false - t.string "privacy", limit: 50, default: "private", null: false - t.index ["community_id"], name: "by_better_together_places_community" - t.index ["creator_id"], name: "by_better_together_places_creator" - t.index ["identifier"], name: "index_better_together_places_on_identifier", unique: true - t.index ["privacy"], name: "by_better_together_places_privacy" - t.index ["space_id"], name: "index_better_together_places_on_space_id" - end - - create_table "better_together_platform_invitations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "community_role_id", null: false - t.string "invitee_email" - t.uuid "invitable_id", null: false - t.uuid "invitee_id" - t.uuid "inviter_id", null: false - t.uuid "platform_role_id" - t.string "status", limit: 20, null: false - t.string "locale", limit: 5, default: "en", null: false - t.string "token", limit: 24, null: false - t.datetime "valid_from", null: false - t.datetime "valid_until" - t.datetime "last_sent" - t.datetime "accepted_at" - t.string "type", default: "BetterTogether::PlatformInvitation", null: false - t.integer "session_duration_mins", default: 30, null: false - t.index ["community_role_id"], name: "platform_invitations_by_community_role" - t.index ["invitable_id", "status"], name: "index_platform_invitations_on_invitable_id_and_status" - t.index ["invitable_id"], name: "platform_invitations_by_invitable" - t.index ["invitee_email", "invitable_id"], name: "idx_on_invitee_email_invitable_id_5a7d642388", unique: true - t.index ["invitee_email"], name: "index_pending_invitations_on_invitee_email", where: "((status)::text = 'pending'::text)" - t.index ["invitee_email"], name: "platform_invitations_by_invitee_email" - t.index ["invitee_id"], name: "platform_invitations_by_invitee" - t.index ["inviter_id"], name: "platform_invitations_by_inviter" - t.index ["locale"], name: "by_better_together_platform_invitations_locale" - t.index ["platform_role_id"], name: "platform_invitations_by_platform_role" - t.index ["status"], name: "platform_invitations_by_status" - t.index ["token"], name: "platform_invitations_by_token", unique: true - t.index ["valid_from"], name: "platform_invitations_by_valid_from" - t.index ["valid_until"], name: "platform_invitations_by_valid_until" - end - - create_table "better_together_platforms", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "host", default: false, null: false - t.boolean "protected", default: false, null: false - t.uuid "community_id", null: false - t.string "privacy", limit: 50, default: "private", null: false - t.string "url", null: false - t.string "time_zone", null: false - t.jsonb "settings", default: {}, null: false - t.index ["community_id"], name: "by_platform_community" - t.index ["host"], name: "index_better_together_platforms_on_host", unique: true, where: "(host IS TRUE)" - t.index ["identifier"], name: "index_better_together_platforms_on_identifier", unique: true - t.index ["privacy"], name: "by_platform_privacy" - t.index ["url"], name: "index_better_together_platforms_on_url", unique: true - end - - create_table "better_together_posts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "type", default: "BetterTogether::Post", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.datetime "published_at" - t.uuid "creator_id" - t.index ["creator_id"], name: "by_better_together_posts_creator" - t.index ["identifier"], name: "index_better_together_posts_on_identifier", unique: true - t.index ["privacy"], name: "by_better_together_posts_privacy" - t.index ["published_at"], name: "by_post_publication_date" - end - - create_table "better_together_reports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "reporter_id", null: false - t.uuid "reportable_id", null: false - t.string "reportable_type", null: false - t.text "reason" - t.index ["reporter_id"], name: "index_better_together_reports_on_reporter_id" - end - - create_table "better_together_resource_permissions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.integer "position", null: false - t.string "resource_type", null: false - t.string "action", null: false - t.string "target", null: false - t.index ["identifier"], name: "index_better_together_resource_permissions_on_identifier", unique: true - t.index ["resource_type", "position"], name: "index_resource_permissions_on_resource_type_and_position", unique: true - end - - create_table "better_together_role_resource_permissions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "role_id", null: false - t.uuid "resource_permission_id", null: false - t.index ["resource_permission_id"], name: "role_resource_permissions_resource_permission" - t.index ["role_id", "resource_permission_id"], name: "unique_role_resource_permission_index", unique: true - t.index ["role_id"], name: "role_resource_permissions_role" - end - - create_table "better_together_roles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.integer "position", null: false - t.string "resource_type", null: false - t.string "type", default: "BetterTogether::Role", null: false - t.index ["identifier"], name: "index_better_together_roles_on_identifier", unique: true - t.index ["resource_type", "position"], name: "index_roles_on_resource_type_and_position", unique: true - end - - create_table "better_together_social_media_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "platform", null: false - t.string "handle", null: false - t.string "url" - t.string "privacy", limit: 50, default: "private", null: false - t.uuid "contact_detail_id", null: false - t.index ["contact_detail_id", "platform"], name: "index_bt_sma_on_contact_detail_and_platform", unique: true - t.index ["contact_detail_id"], name: "idx_on_contact_detail_id_6380b64b3b" - t.index ["privacy"], name: "by_better_together_social_media_accounts_privacy" - end - - create_table "better_together_uploads", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.string "privacy", limit: 50, default: "private", null: false - t.string "type", default: "BetterTogether::Upload", null: false - t.index ["creator_id"], name: "by_better_together_files_creator" - t.index ["identifier"], name: "index_better_together_uploads_on_identifier", unique: true - t.index ["privacy"], name: "by_better_together_files_privacy" - end - - create_table "better_together_users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0, null: false - t.datetime "current_sign_in_at" - t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.string "confirmation_token" - t.datetime "confirmed_at" - t.datetime "confirmation_sent_at" - t.string "unconfirmed_email" - t.integer "failed_attempts", default: 0, null: false - t.string "unlock_token" - t.datetime "locked_at" - t.index ["confirmation_token"], name: "index_better_together_users_on_confirmation_token", unique: true - t.index ["email"], name: "index_better_together_users_on_email", unique: true - t.index ["reset_password_token"], name: "index_better_together_users_on_reset_password_token", unique: true - t.index ["unlock_token"], name: "index_better_together_users_on_unlock_token", unique: true - end - - create_table "better_together_website_links", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "url", null: false - t.string "label", null: false - t.string "privacy", limit: 50, default: "private", null: false - t.uuid "contact_detail_id", null: false - t.index ["contact_detail_id"], name: "index_better_together_website_links_on_contact_detail_id" - t.index ["privacy"], name: "by_better_together_website_links_privacy" - end - - create_table "better_together_wizard_step_definitions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.uuid "wizard_id", null: false - t.string "template" - t.string "form_class" - t.string "message", default: "Please complete this next step.", null: false - t.integer "step_number", null: false - t.index ["identifier"], name: "index_better_together_wizard_step_definitions_on_identifier", unique: true - t.index ["wizard_id", "step_number"], name: "index_wizard_step_definitions_on_wizard_id_and_step_number", unique: true - t.index ["wizard_id"], name: "by_step_definition_wizard" - end - - create_table "better_together_wizard_steps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.uuid "wizard_id", null: false - t.uuid "wizard_step_definition_id", null: false - t.uuid "creator_id" - t.string "identifier", limit: 100, null: false - t.boolean "completed", default: false - t.integer "step_number", null: false - t.index ["creator_id"], name: "by_step_creator" - t.index ["identifier"], name: "by_step_identifier" - t.index ["wizard_id", "identifier", "creator_id"], name: "index_unique_wizard_steps", unique: true, where: "(completed IS FALSE)" - t.index ["wizard_id", "step_number"], name: "index_wizard_steps_on_wizard_id_and_step_number" - t.index ["wizard_id"], name: "by_step_wizard" - t.index ["wizard_step_definition_id"], name: "by_step_wizard_step_definition" - end - - create_table "better_together_wizards", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier", limit: 100, null: false - t.boolean "protected", default: false, null: false - t.integer "max_completions", default: 0, null: false - t.integer "current_completions", default: 0, null: false - t.datetime "first_completed_at" - t.datetime "last_completed_at" - t.text "success_message", default: "Thank you. You have successfully completed the wizard", null: false - t.string "success_path", default: "/", null: false - t.index ["identifier"], name: "index_better_together_wizards_on_identifier", unique: true - end - - create_table "friendly_id_slugs", force: :cascade do |t| - t.string "slug", null: false - t.uuid "sluggable_id", null: false - t.string "sluggable_type", null: false - t.string "scope" - t.integer "lock_version", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "locale", null: false - t.index ["locale"], name: "index_friendly_id_slugs_on_locale" - t.index ["slug", "sluggable_type", "locale"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_locale" - t.index ["slug", "sluggable_type", "scope", "locale"], name: "index_friendly_id_slugs_unique", unique: true - t.index ["sluggable_type", "sluggable_id"], name: "by_sluggable" - end - - create_table "mobility_string_translations", force: :cascade do |t| - t.string "locale", null: false - t.string "key", null: false - t.string "value" - t.string "translatable_type" - t.uuid "translatable_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_string_translations_on_translatable_attribute" - t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_string_translations_on_keys", unique: true - t.index ["translatable_type", "key", "value", "locale"], name: "index_mobility_string_translations_on_query_keys" - end - - create_table "mobility_text_translations", force: :cascade do |t| - t.string "locale", null: false - t.string "key", null: false - t.text "value" - t.string "translatable_type" - t.uuid "translatable_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_text_translations_on_translatable_attribute" - t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_text_translations_on_keys", unique: true - end - - create_table "noticed_events", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.string "type" - t.string "record_type" - t.uuid "record_id" - t.jsonb "params" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "notifications_count" - t.index ["record_type", "record_id"], name: "index_noticed_events_on_record" - end - - create_table "noticed_notifications", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.string "type" - t.uuid "event_id", null: false - t.string "recipient_type", null: false - t.uuid "recipient_id", null: false - t.datetime "read_at", precision: nil - t.datetime "seen_at", precision: nil - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["event_id"], name: "index_noticed_notifications_on_event_id" - t.index ["recipient_type", "recipient_id"], name: "index_noticed_notifications_on_recipient" - end - - add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" - add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" - add_foreign_key "better_together_addresses", "better_together_contact_details", column: "contact_detail_id" - add_foreign_key "better_together_agreement_participants", "better_together_agreements", column: "agreement_id" - add_foreign_key "better_together_agreement_participants", "better_together_people", column: "person_id" - add_foreign_key "better_together_agreement_terms", "better_together_agreements", column: "agreement_id" - add_foreign_key "better_together_agreements", "better_together_pages", column: "page_id" - add_foreign_key "better_together_agreements", "better_together_people", column: "creator_id" - add_foreign_key "better_together_ai_log_translations", "better_together_people", column: "initiator_id" - add_foreign_key "better_together_authorships", "better_together_people", column: "author_id" - add_foreign_key "better_together_calendar_entries", "better_together_calendars", column: "calendar_id" - add_foreign_key "better_together_calendar_entries", "better_together_events", column: "event_id" - add_foreign_key "better_together_calendars", "better_together_communities", column: "community_id" - add_foreign_key "better_together_calendars", "better_together_people", column: "creator_id" - add_foreign_key "better_together_calls_for_interest", "better_together_people", column: "creator_id" - add_foreign_key "better_together_categorizations", "better_together_categories", column: "category_id" - add_foreign_key "better_together_comments", "better_together_people", column: "creator_id" - add_foreign_key "better_together_communities", "better_together_people", column: "creator_id" - add_foreign_key "better_together_content_blocks", "better_together_people", column: "creator_id" - add_foreign_key "better_together_content_page_blocks", "better_together_content_blocks", column: "block_id" - add_foreign_key "better_together_content_page_blocks", "better_together_pages", column: "page_id" - add_foreign_key "better_together_content_platform_blocks", "better_together_content_blocks", column: "block_id" - add_foreign_key "better_together_content_platform_blocks", "better_together_platforms", column: "platform_id" - add_foreign_key "better_together_conversation_participants", "better_together_conversations", column: "conversation_id" - add_foreign_key "better_together_conversation_participants", "better_together_people", column: "person_id" - add_foreign_key "better_together_conversations", "better_together_people", column: "creator_id" - add_foreign_key "better_together_email_addresses", "better_together_contact_details", column: "contact_detail_id" - add_foreign_key "better_together_event_attendances", "better_together_events", column: "event_id" - add_foreign_key "better_together_event_attendances", "better_together_people", column: "person_id" - add_foreign_key "better_together_event_hosts", "better_together_events", column: "event_id" - add_foreign_key "better_together_events", "better_together_people", column: "creator_id" - add_foreign_key "better_together_geography_continents", "better_together_communities", column: "community_id" - add_foreign_key "better_together_geography_countries", "better_together_communities", column: "community_id" - add_foreign_key "better_together_geography_country_continents", "better_together_geography_continents", column: "continent_id" - add_foreign_key "better_together_geography_country_continents", "better_together_geography_countries", column: "country_id" - add_foreign_key "better_together_geography_geospatial_spaces", "better_together_geography_spaces", column: "space_id" - add_foreign_key "better_together_geography_locatable_locations", "better_together_people", column: "creator_id" - add_foreign_key "better_together_geography_maps", "better_together_people", column: "creator_id" - add_foreign_key "better_together_geography_region_settlements", "better_together_geography_regions", column: "region_id" - add_foreign_key "better_together_geography_region_settlements", "better_together_geography_settlements", column: "settlement_id" - add_foreign_key "better_together_geography_regions", "better_together_communities", column: "community_id" - add_foreign_key "better_together_geography_regions", "better_together_geography_countries", column: "country_id" - add_foreign_key "better_together_geography_regions", "better_together_geography_states", column: "state_id" - add_foreign_key "better_together_geography_settlements", "better_together_communities", column: "community_id" - add_foreign_key "better_together_geography_settlements", "better_together_geography_countries", column: "country_id" - add_foreign_key "better_together_geography_settlements", "better_together_geography_states", column: "state_id" - add_foreign_key "better_together_geography_spaces", "better_together_people", column: "creator_id" - add_foreign_key "better_together_geography_states", "better_together_communities", column: "community_id" - add_foreign_key "better_together_geography_states", "better_together_geography_countries", column: "country_id" - add_foreign_key "better_together_infrastructure_building_connections", "better_together_infrastructure_buildings", column: "building_id" - add_foreign_key "better_together_infrastructure_buildings", "better_together_addresses", column: "address_id" - add_foreign_key "better_together_infrastructure_buildings", "better_together_communities", column: "community_id" - add_foreign_key "better_together_infrastructure_buildings", "better_together_people", column: "creator_id" - add_foreign_key "better_together_infrastructure_floors", "better_together_communities", column: "community_id" - add_foreign_key "better_together_infrastructure_floors", "better_together_infrastructure_buildings", column: "building_id" - add_foreign_key "better_together_infrastructure_floors", "better_together_people", column: "creator_id" - add_foreign_key "better_together_infrastructure_rooms", "better_together_communities", column: "community_id" - add_foreign_key "better_together_infrastructure_rooms", "better_together_infrastructure_floors", column: "floor_id" - add_foreign_key "better_together_infrastructure_rooms", "better_together_people", column: "creator_id" - add_foreign_key "better_together_invitations", "better_together_roles", column: "role_id" - add_foreign_key "better_together_joatu_agreements", "better_together_joatu_offers", column: "offer_id" - add_foreign_key "better_together_joatu_agreements", "better_together_joatu_requests", column: "request_id" - add_foreign_key "better_together_joatu_offers", "better_together_addresses", column: "address_id" - add_foreign_key "better_together_joatu_offers", "better_together_people", column: "creator_id" - add_foreign_key "better_together_joatu_requests", "better_together_addresses", column: "address_id" - add_foreign_key "better_together_joatu_requests", "better_together_people", column: "creator_id" - add_foreign_key "better_together_joatu_response_links", "better_together_people", column: "creator_id" - add_foreign_key "better_together_messages", "better_together_conversations", column: "conversation_id" - add_foreign_key "better_together_messages", "better_together_people", column: "sender_id" - add_foreign_key "better_together_navigation_items", "better_together_navigation_areas", column: "navigation_area_id" - add_foreign_key "better_together_navigation_items", "better_together_navigation_items", column: "parent_id" - add_foreign_key "better_together_pages", "better_together_navigation_areas", column: "sidebar_nav_id" - add_foreign_key "better_together_people", "better_together_communities", column: "community_id" - add_foreign_key "better_together_person_blocks", "better_together_people", column: "blocked_id" - add_foreign_key "better_together_person_blocks", "better_together_people", column: "blocker_id" - add_foreign_key "better_together_person_community_memberships", "better_together_communities", column: "joinable_id" - add_foreign_key "better_together_person_community_memberships", "better_together_people", column: "member_id" - add_foreign_key "better_together_person_community_memberships", "better_together_roles", column: "role_id" - add_foreign_key "better_together_person_platform_memberships", "better_together_people", column: "member_id" - add_foreign_key "better_together_person_platform_memberships", "better_together_platforms", column: "joinable_id" - add_foreign_key "better_together_person_platform_memberships", "better_together_roles", column: "role_id" - add_foreign_key "better_together_phone_numbers", "better_together_contact_details", column: "contact_detail_id" - add_foreign_key "better_together_places", "better_together_communities", column: "community_id" - add_foreign_key "better_together_places", "better_together_geography_spaces", column: "space_id" - add_foreign_key "better_together_places", "better_together_people", column: "creator_id" - add_foreign_key "better_together_platform_invitations", "better_together_people", column: "invitee_id" - add_foreign_key "better_together_platform_invitations", "better_together_people", column: "inviter_id" - add_foreign_key "better_together_platform_invitations", "better_together_platforms", column: "invitable_id" - add_foreign_key "better_together_platform_invitations", "better_together_roles", column: "community_role_id" - add_foreign_key "better_together_platform_invitations", "better_together_roles", column: "platform_role_id" - add_foreign_key "better_together_platforms", "better_together_communities", column: "community_id" - add_foreign_key "better_together_posts", "better_together_people", column: "creator_id" - add_foreign_key "better_together_reports", "better_together_people", column: "reporter_id" - add_foreign_key "better_together_role_resource_permissions", "better_together_resource_permissions", column: "resource_permission_id" - add_foreign_key "better_together_role_resource_permissions", "better_together_roles", column: "role_id" - add_foreign_key "better_together_social_media_accounts", "better_together_contact_details", column: "contact_detail_id" - add_foreign_key "better_together_uploads", "better_together_people", column: "creator_id" - add_foreign_key "better_together_website_links", "better_together_contact_details", column: "contact_detail_id" - add_foreign_key "better_together_wizard_step_definitions", "better_together_wizards", column: "wizard_id" - add_foreign_key "better_together_wizard_steps", "better_together_people", column: "creator_id" - add_foreign_key "better_together_wizard_steps", "better_together_wizard_step_definitions", column: "wizard_step_definition_id" - add_foreign_key "better_together_wizard_steps", "better_together_wizards", column: "wizard_id" + enable_extension 'pgcrypto' + enable_extension 'plpgsql' + enable_extension 'postgis' + + create_table 'action_text_rich_texts', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.string 'name', null: false + t.text 'body' + t.string 'record_type', null: false + t.uuid 'record_id', null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'locale' + t.index %w[record_type record_id name locale], name: 'index_action_text_rich_texts_uniqueness', + unique: true + end + + create_table 'active_storage_attachments', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.string 'name', null: false + t.string 'record_type', null: false + t.uuid 'record_id', null: false + t.uuid 'blob_id', null: false + t.datetime 'created_at', null: false + t.index ['blob_id'], name: 'index_active_storage_attachments_on_blob_id' + t.index %w[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', + unique: true + end + + create_table 'active_storage_blobs', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.string 'key', null: false + t.string 'filename', null: false + t.string 'content_type' + t.text 'metadata' + t.string 'service_name', null: false + t.bigint 'byte_size', null: false + t.string 'checksum' + t.datetime 'created_at', null: false + t.index ['key'], name: 'index_active_storage_blobs_on_key', unique: true + end + + create_table 'active_storage_variant_records', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.uuid 'blob_id', null: false + t.string 'variation_digest', null: false + t.index %w[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true + end + + create_table 'better_together_activities', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'trackable_type' + t.uuid 'trackable_id' + t.string 'owner_type' + t.uuid 'owner_id' + t.string 'key' + t.jsonb 'parameters', default: '{}' + t.string 'recipient_type' + t.uuid 'recipient_id' + t.string 'privacy', limit: 50, default: 'private', null: false + t.index %w[owner_type owner_id], name: 'bt_activities_by_owner' + t.index ['privacy'], name: 'by_better_together_activities_privacy' + t.index %w[recipient_type recipient_id], name: 'bt_activities_by_recipient' + t.index %w[trackable_type trackable_id], name: 'bt_activities_by_trackable' + end + + create_table 'better_together_addresses', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'label', default: 'main', null: false + t.boolean 'physical', default: true, null: false + t.boolean 'postal', default: false, null: false + t.string 'line1' + t.string 'line2' + t.string 'city_name' + t.string 'state_province_name' + t.string 'postal_code' + t.string 'country_name' + t.string 'privacy', limit: 50, default: 'private', null: false + t.uuid 'contact_detail_id' + t.boolean 'primary_flag', default: false, null: false + t.index %w[contact_detail_id primary_flag], name: 'index_bt_addresses_on_contact_detail_id_and_primary', + unique: true, where: '(primary_flag IS TRUE)' + t.index ['contact_detail_id'], name: 'index_better_together_addresses_on_contact_detail_id' + t.index ['privacy'], name: 'by_better_together_addresses_privacy' + end + + create_table 'better_together_agreement_participants', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'agreement_id', null: false + t.uuid 'person_id', null: false + t.string 'group_identifier' + t.datetime 'accepted_at' + t.index %w[agreement_id person_id], name: 'index_bt_agreement_participants_on_agreement_and_person', unique: true + t.index ['agreement_id'], name: 'index_better_together_agreement_participants_on_agreement_id' + t.index ['group_identifier'], name: 'idx_on_group_identifier_06b6e57c0b' + t.index ['person_id'], name: 'index_better_together_agreement_participants_on_person_id' + end + + create_table 'better_together_agreement_terms', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.integer 'position', null: false + t.boolean 'protected', default: false, null: false + t.uuid 'agreement_id', null: false + t.index ['agreement_id'], name: 'index_better_together_agreement_terms_on_agreement_id' + t.index ['identifier'], name: 'index_better_together_agreement_terms_on_identifier', unique: true + end + + create_table 'better_together_agreements', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.boolean 'collective', default: false, null: false + t.uuid 'page_id' + t.index ['creator_id'], name: 'by_better_together_agreements_creator' + t.index ['identifier'], name: 'index_better_together_agreements_on_identifier', unique: true + t.index ['page_id'], name: 'index_better_together_agreements_on_page_id' + t.index ['privacy'], name: 'by_better_together_agreements_privacy' + end + + create_table 'better_together_ai_log_translations', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.text 'request', null: false + t.text 'response' + t.string 'model', null: false + t.integer 'prompt_tokens', default: 0, null: false + t.integer 'completion_tokens', default: 0, null: false + t.integer 'tokens_used', default: 0, null: false + t.decimal 'estimated_cost', precision: 10, scale: 5, default: '0.0', null: false + t.datetime 'start_time' + t.datetime 'end_time' + t.string 'status', default: 'pending', null: false + t.uuid 'initiator_id' + t.string 'source_locale', null: false + t.string 'target_locale', null: false + t.index ['initiator_id'], name: 'index_better_together_ai_log_translations_on_initiator_id' + t.index ['model'], name: 'index_better_together_ai_log_translations_on_model' + t.index ['source_locale'], name: 'index_better_together_ai_log_translations_on_source_locale' + t.index ['status'], name: 'index_better_together_ai_log_translations_on_status' + t.index ['target_locale'], name: 'index_better_together_ai_log_translations_on_target_locale' + end + + create_table 'better_together_authorships', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'position', null: false + t.string 'authorable_type', null: false + t.uuid 'authorable_id', null: false + t.uuid 'author_id', null: false + t.uuid 'creator_id' + t.index ['author_id'], name: 'by_authorship_author' + t.index %w[authorable_type authorable_id], name: 'by_authorship_authorable' + t.index ['creator_id'], name: 'by_better_together_authorships_creator' + end + + create_table 'better_together_calendar_entries', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'calendar_id' + t.string 'schedulable_type' + t.uuid 'schedulable_id' + t.datetime 'starts_at', null: false + t.datetime 'ends_at' + t.decimal 'duration_minutes' + t.uuid 'event_id', null: false + t.index %w[calendar_id event_id], name: 'by_calendar_and_event', unique: true + t.index ['calendar_id'], name: 'index_better_together_calendar_entries_on_calendar_id' + t.index ['ends_at'], name: 'bt_calendar_events_by_ends_at' + t.index ['event_id'], name: 'bt_calendar_entries_by_event' + t.index %w[schedulable_type schedulable_id], name: 'index_better_together_calendar_entries_on_schedulable' + t.index ['starts_at'], name: 'bt_calendar_events_by_starts_at' + end + + create_table 'better_together_calendars', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'community_id', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.string 'locale', limit: 5, default: 'en', null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.boolean 'protected', default: false, null: false + t.index ['community_id'], name: 'by_better_together_calendars_community' + t.index ['creator_id'], name: 'by_better_together_calendars_creator' + t.index ['identifier'], name: 'index_better_together_calendars_on_identifier', unique: true + t.index ['locale'], name: 'by_better_together_calendars_locale' + t.index ['privacy'], name: 'by_better_together_calendars_privacy' + end + + create_table 'better_together_calls_for_interest', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'type', default: 'BetterTogether::CallForInterest', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.string 'interestable_type' + t.uuid 'interestable_id' + t.datetime 'starts_at' + t.datetime 'ends_at' + t.index ['creator_id'], name: 'by_better_together_calls_for_interest_creator' + t.index ['ends_at'], name: 'bt_calls_for_interest_by_ends_at' + t.index ['identifier'], name: 'index_better_together_calls_for_interest_on_identifier', unique: true + t.index %w[interestable_type interestable_id], name: 'index_better_together_calls_for_interest_on_interestable' + t.index ['privacy'], name: 'by_better_together_calls_for_interest_privacy' + t.index ['starts_at'], name: 'bt_calls_for_interest_by_starts_at' + end + + create_table 'better_together_categories', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.integer 'position', null: false + t.boolean 'protected', default: false, null: false + t.string 'type', default: 'BetterTogether::Category', null: false + t.string 'icon', default: 'fas fa-icons', null: false + t.index %w[identifier type], name: 'index_better_together_categories_on_identifier_and_type', unique: true + end + + create_table 'better_together_categorizations', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'category_type', null: false + t.uuid 'category_id', null: false + t.string 'categorizable_type', null: false + t.uuid 'categorizable_id', null: false + t.index %w[categorizable_type categorizable_id], name: 'index_better_together_categorizations_on_categorizable' + t.index %w[category_type category_id], name: 'index_better_together_categorizations_on_category' + end + + create_table 'better_together_comments', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'commentable_type', null: false + t.uuid 'commentable_id', null: false + t.uuid 'creator_id' + t.text 'content', default: '', null: false + t.index %w[commentable_type commentable_id], name: 'bt_comments_on_commentable' + t.index ['creator_id'], name: 'by_better_together_comments_creator' + end + + create_table 'better_together_communities', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'host', default: false, null: false + t.boolean 'protected', default: false, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.uuid 'creator_id' + t.string 'type', default: 'BetterTogether::Community', null: false + t.index ['creator_id'], name: 'by_creator' + t.index ['host'], name: 'index_better_together_communities_on_host', unique: true, where: '(host IS TRUE)' + t.index ['identifier'], name: 'index_better_together_communities_on_identifier', unique: true + t.index ['privacy'], name: 'by_community_privacy' + end + + create_table 'better_together_contact_details', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'contactable_type', null: false + t.uuid 'contactable_id', null: false + t.string 'type', default: 'BetterTogether::ContactDetail', null: false + t.string 'name' + t.string 'role' + t.index %w[contactable_type contactable_id], name: 'index_better_together_contact_details_on_contactable' + end + + create_table 'better_together_content_blocks', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'type', null: false + t.string 'identifier', limit: 100 + t.jsonb 'accessibility_attributes', default: {}, null: false + t.jsonb 'content_settings', default: {}, null: false + t.jsonb 'css_settings', default: {}, null: false + t.jsonb 'data_attributes', default: {}, null: false + t.jsonb 'html_attributes', default: {}, null: false + t.jsonb 'layout_settings', default: {}, null: false + t.jsonb 'media_settings', default: {}, null: false + t.jsonb 'content_data', default: {} + t.uuid 'creator_id' + t.string 'privacy', limit: 50, default: 'private', null: false + t.boolean 'visible', default: true, null: false + t.jsonb 'content_area_settings', default: {}, null: false + t.index ['creator_id'], name: 'by_better_together_content_blocks_creator' + t.index ['privacy'], name: 'by_better_together_content_blocks_privacy' + end + + create_table 'better_together_content_page_blocks', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'page_id', null: false + t.uuid 'block_id', null: false + t.integer 'position', null: false + t.index ['block_id'], name: 'index_better_together_content_page_blocks_on_block_id' + t.index %w[page_id block_id position], name: 'content_page_blocks_on_page_block_and_position' + t.index %w[page_id block_id], name: 'content_page_blocks_on_page_and_block', unique: true + t.index ['page_id'], name: 'index_better_together_content_page_blocks_on_page_id' + end + + create_table 'better_together_content_platform_blocks', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'platform_id', null: false + t.uuid 'block_id', null: false + t.index ['block_id'], name: 'index_better_together_content_platform_blocks_on_block_id' + t.index ['platform_id'], name: 'index_better_together_content_platform_blocks_on_platform_id' + end + + create_table 'better_together_conversation_participants', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'conversation_id', null: false + t.uuid 'person_id', null: false + t.index ['conversation_id'], name: 'idx_on_conversation_id_30b3b70bad' + t.index ['person_id'], name: 'index_better_together_conversation_participants_on_person_id' + end + + create_table 'better_together_conversations', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'title', null: false + t.uuid 'creator_id', null: false + t.index ['creator_id'], name: 'index_better_together_conversations_on_creator_id' + end + + create_table 'better_together_email_addresses', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'email', null: false + t.string 'label', null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.uuid 'contact_detail_id', null: false + t.boolean 'primary_flag', default: false, null: false + t.index %w[contact_detail_id primary_flag], name: 'index_bt_email_addresses_on_contact_detail_id_and_primary', + unique: true, where: '(primary_flag IS TRUE)' + t.index ['contact_detail_id'], name: 'index_better_together_email_addresses_on_contact_detail_id' + t.index ['privacy'], name: 'by_better_together_email_addresses_privacy' + end + + create_table 'better_together_event_attendances', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'event_id', null: false + t.uuid 'person_id', null: false + t.string 'status', default: 'interested', null: false + t.index %w[event_id person_id], name: 'by_event_and_person', unique: true + t.index ['event_id'], name: 'bt_event_attendance_by_event' + t.index ['person_id'], name: 'bt_event_attendance_by_person' + end + + create_table 'better_together_event_hosts', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'event_id' + t.string 'host_type' + t.uuid 'host_id' + t.index ['event_id'], name: 'index_better_together_event_hosts_on_event_id' + t.index %w[host_type host_id], name: 'index_better_together_event_hosts_on_host' + end + + create_table 'better_together_events', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'type', default: 'BetterTogether::Event', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.datetime 'starts_at' + t.datetime 'ends_at' + t.decimal 'duration_minutes' + t.string 'registration_url' + t.index ['creator_id'], name: 'by_better_together_events_creator' + t.index ['ends_at'], name: 'bt_events_by_ends_at' + t.index ['identifier'], name: 'index_better_together_events_on_identifier', unique: true + t.index ['privacy'], name: 'by_better_together_events_privacy' + t.index ['starts_at'], name: 'bt_events_by_starts_at' + end + + create_table 'better_together_geography_continents', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.uuid 'community_id', null: false + t.index ['community_id'], name: 'by_geography_continent_community' + t.index ['identifier'], name: 'index_better_together_geography_continents_on_identifier', unique: true + end + + create_table 'better_together_geography_countries', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.string 'iso_code', limit: 2, null: false + t.boolean 'protected', default: false, null: false + t.uuid 'community_id', null: false + t.index ['community_id'], name: 'by_geography_country_community' + t.index ['identifier'], name: 'index_better_together_geography_countries_on_identifier', unique: true + t.index ['iso_code'], name: 'index_better_together_geography_countries_on_iso_code', unique: true + end + + create_table 'better_together_geography_country_continents', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'country_id' + t.uuid 'continent_id' + t.index ['continent_id'], name: 'country_continent_by_continent' + t.index %w[country_id continent_id], name: 'index_country_continents_on_country_and_continent', unique: true + t.index ['country_id'], name: 'country_continent_by_country' + end + + create_table 'better_together_geography_geospatial_spaces', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'geospatial_type' + t.uuid 'geospatial_id' + t.integer 'position', null: false + t.boolean 'primary_flag', default: false, null: false + t.uuid 'space_id' + t.index %w[geospatial_id primary_flag], name: 'index_geospatial_spaces_on_geospatial_id_and_primary', + unique: true, where: '(primary_flag IS TRUE)' + t.index %w[geospatial_type geospatial_id], + name: 'index_better_together_geography_geospatial_spaces_on_geospatial' + t.index ['space_id'], name: 'index_better_together_geography_geospatial_spaces_on_space_id' + end + + create_table 'better_together_geography_locatable_locations', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'creator_id' + t.string 'location_type' + t.uuid 'location_id' + t.string 'locatable_type', null: false + t.uuid 'locatable_id', null: false + t.string 'name' + t.index ['creator_id'], name: 'by_better_together_geography_locatable_locations_creator' + t.index %w[locatable_id locatable_type location_id location_type], name: 'locatable_locations' + t.index %w[locatable_type locatable_id], name: 'locatable_location_by_locatable' + t.index %w[location_type location_id], name: 'locatable_location_by_location' + t.index ['name'], name: 'locatable_location_by_name' + end + + create_table 'better_together_geography_maps', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.string 'locale', limit: 5, default: 'en', null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.boolean 'protected', default: false, null: false + t.geography 'center', limit: { srid: 4326, type: 'st_point', geographic: true } + t.integer 'zoom', default: 13, null: false + t.geography 'viewport', limit: { srid: 4326, type: 'st_polygon', geographic: true } + t.jsonb 'metadata', default: {}, null: false + t.string 'mappable_type' + t.uuid 'mappable_id' + t.string 'type', default: 'BetterTogether::Geography::Map', null: false + t.index ['creator_id'], name: 'by_better_together_geography_maps_creator' + t.index ['identifier'], name: 'index_better_together_geography_maps_on_identifier', unique: true + t.index ['locale'], name: 'by_better_together_geography_maps_locale' + t.index %w[mappable_type mappable_id], name: 'index_better_together_geography_maps_on_mappable' + t.index ['privacy'], name: 'by_better_together_geography_maps_privacy' + end + + create_table 'better_together_geography_region_settlements', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.boolean 'protected', default: false, null: false + t.uuid 'region_id' + t.uuid 'settlement_id' + t.index ['region_id'], name: 'bt_region_settlement_by_region' + t.index ['settlement_id'], name: 'bt_region_settlement_by_settlement' + end + + create_table 'better_together_geography_regions', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.uuid 'community_id', null: false + t.uuid 'country_id' + t.uuid 'state_id' + t.string 'type', default: 'BetterTogether::Geography::Region', null: false + t.index ['community_id'], name: 'by_geography_region_community' + t.index ['country_id'], name: 'index_better_together_geography_regions_on_country_id' + t.index ['identifier'], name: 'index_better_together_geography_regions_on_identifier', unique: true + t.index ['state_id'], name: 'index_better_together_geography_regions_on_state_id' + end + + create_table 'better_together_geography_settlements', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.uuid 'community_id', null: false + t.uuid 'country_id' + t.uuid 'state_id' + t.index ['community_id'], name: 'by_geography_settlement_community' + t.index ['country_id'], name: 'index_better_together_geography_settlements_on_country_id' + t.index ['identifier'], name: 'index_better_together_geography_settlements_on_identifier', unique: true + t.index ['state_id'], name: 'index_better_together_geography_settlements_on_state_id' + end + + create_table 'better_together_geography_spaces', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.float 'elevation' + t.float 'latitude' + t.float 'longitude' + t.jsonb 'properties', default: {} + t.jsonb 'metadata', default: {} + t.index ['creator_id'], name: 'by_better_together_geography_spaces_creator' + t.index ['identifier'], name: 'index_better_together_geography_spaces_on_identifier', unique: true + end + + create_table 'better_together_geography_states', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.string 'iso_code', limit: 5, null: false + t.boolean 'protected', default: false, null: false + t.uuid 'community_id', null: false + t.uuid 'country_id' + t.index ['community_id'], name: 'by_geography_state_community' + t.index ['country_id'], name: 'index_better_together_geography_states_on_country_id' + t.index ['identifier'], name: 'index_better_together_geography_states_on_identifier', unique: true + t.index ['iso_code'], name: 'index_better_together_geography_states_on_iso_code', unique: true + end + + create_table 'better_together_identifications', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.boolean 'active', null: false + t.string 'identity_type', null: false + t.uuid 'identity_id', null: false + t.string 'agent_type', null: false + t.uuid 'agent_id', null: false + t.index %w[active agent_type agent_id], name: 'active_identification', unique: true + t.index ['active'], name: 'by_active_state' + t.index %w[agent_type agent_id], name: 'by_agent' + t.index %w[identity_type identity_id agent_type agent_id], name: 'unique_identification', unique: true + t.index %w[identity_type identity_id], name: 'by_identity' + end + + create_table 'better_together_infrastructure_building_connections', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'building_id', null: false + t.string 'connection_type', null: false + t.uuid 'connection_id', null: false + t.integer 'position', null: false + t.boolean 'primary_flag', default: false, null: false + t.index ['building_id'], name: 'bt_building_connections_building' + t.index %w[connection_id primary_flag], name: 'index_bt_building_connections_on_connection_id_and_primary', + unique: true, where: '(primary_flag IS TRUE)' + t.index %w[connection_type connection_id], name: 'bt_building_connections_connection' + end + + create_table 'better_together_infrastructure_buildings', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'type', default: 'BetterTogether::Infrastructure::Building', null: false + t.uuid 'community_id', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.integer 'floors_count', default: 0, null: false + t.integer 'rooms_count', default: 0, null: false + t.uuid 'address_id' + t.index ['address_id'], name: 'index_better_together_infrastructure_buildings_on_address_id' + t.index ['community_id'], name: 'by_better_together_infrastructure_buildings_community' + t.index ['creator_id'], name: 'by_better_together_infrastructure_buildings_creator' + t.index ['identifier'], name: 'index_better_together_infrastructure_buildings_on_identifier', unique: true + t.index ['privacy'], name: 'by_better_together_infrastructure_buildings_privacy' + end + + create_table 'better_together_infrastructure_floors', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'building_id' + t.uuid 'community_id', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.integer 'position', null: false + t.integer 'level', default: 0, null: false + t.integer 'rooms_count', default: 0, null: false + t.index ['building_id'], name: 'index_better_together_infrastructure_floors_on_building_id' + t.index ['community_id'], name: 'by_better_together_infrastructure_floors_community' + t.index ['creator_id'], name: 'by_better_together_infrastructure_floors_creator' + t.index ['identifier'], name: 'index_better_together_infrastructure_floors_on_identifier', unique: true + t.index ['privacy'], name: 'by_better_together_infrastructure_floors_privacy' + end + + create_table 'better_together_infrastructure_rooms', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'floor_id' + t.uuid 'community_id', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.index ['community_id'], name: 'by_better_together_infrastructure_rooms_community' + t.index ['creator_id'], name: 'by_better_together_infrastructure_rooms_creator' + t.index ['floor_id'], name: 'index_better_together_infrastructure_rooms_on_floor_id' + t.index ['identifier'], name: 'index_better_together_infrastructure_rooms_on_identifier', unique: true + t.index ['privacy'], name: 'by_better_together_infrastructure_rooms_privacy' + end + + create_table 'better_together_invitations', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'type', default: 'BetterTogether::Invitation', null: false + t.string 'status', limit: 20, null: false + t.datetime 'valid_from', null: false + t.datetime 'valid_until' + t.datetime 'last_sent' + t.datetime 'accepted_at' + t.string 'locale', limit: 5, default: 'en', null: false + t.string 'token', limit: 24, null: false + t.string 'invitable_type', null: false + t.uuid 'invitable_id', null: false + t.string 'inviter_type', null: false + t.uuid 'inviter_id', null: false + t.string 'invitee_type', null: false + t.uuid 'invitee_id', null: false + t.string 'invitee_email', null: false + t.uuid 'role_id' + t.index %w[invitable_id status], name: 'invitations_on_invitable_id_and_status' + t.index %w[invitable_type invitable_id], name: 'by_invitable' + t.index %w[invitee_email invitable_id], name: 'invitations_on_invitee_email_and_invitable_id', unique: true + t.index ['invitee_email'], name: 'invitations_by_invitee_email' + t.index ['invitee_email'], name: 'pending_invites_on_invitee_email', where: "((status)::text = 'pending'::text)" + t.index %w[invitee_type invitee_id], name: 'by_invitee' + t.index %w[inviter_type inviter_id], name: 'by_inviter' + t.index ['locale'], name: 'by_better_together_invitations_locale' + t.index ['role_id'], name: 'by_role' + t.index ['status'], name: 'by_status' + t.index ['token'], name: 'invitations_by_token', unique: true + t.index ['valid_from'], name: 'by_valid_from' + t.index ['valid_until'], name: 'by_valid_until' + end + + create_table 'better_together_joatu_agreements', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'offer_id', null: false + t.uuid 'request_id', null: false + t.text 'terms' + t.string 'value' + t.string 'status', default: 'pending', null: false + t.index %w[offer_id request_id], name: 'bt_joatu_agreements_unique_offer_request', unique: true + t.index ['offer_id'], name: 'bt_joatu_agreements_by_offer' + t.index ['offer_id'], name: 'bt_joatu_agreements_one_accepted_per_offer', unique: true, + where: "((status)::text = 'accepted'::text)" + t.index ['request_id'], name: 'bt_joatu_agreements_by_request' + t.index ['request_id'], name: 'bt_joatu_agreements_one_accepted_per_request', unique: true, + where: "((status)::text = 'accepted'::text)" + end + + create_table 'better_together_joatu_offers', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'creator_id' + t.string 'status', default: 'open', null: false + t.string 'target_type' + t.uuid 'target_id' + t.string 'urgency', default: 'normal', null: false + t.uuid 'address_id' + t.index ['address_id'], name: 'index_better_together_joatu_offers_on_address_id' + t.index ['creator_id'], name: 'by_better_together_joatu_offers_creator' + t.index %w[target_type target_id], name: 'bt_joatu_offers_on_target' + end + + create_table 'better_together_joatu_requests', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'creator_id' + t.string 'status', default: 'open', null: false + t.string 'target_type' + t.uuid 'target_id' + t.string 'urgency', default: 'normal', null: false + t.uuid 'address_id' + t.index ['address_id'], name: 'index_better_together_joatu_requests_on_address_id' + t.index ['creator_id'], name: 'by_better_together_joatu_requests_creator' + t.index %w[target_type target_id], name: 'bt_joatu_requests_on_target' + end + + create_table 'better_together_joatu_response_links', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'source_type', null: false + t.uuid 'source_id', null: false + t.string 'response_type', null: false + t.uuid 'response_id', null: false + t.uuid 'creator_id' + t.index ['creator_id'], name: 'by_better_together_joatu_response_links_creator' + t.index %w[response_type response_id], name: 'bt_joatu_response_links_by_response' + t.index %w[source_type source_id response_type response_id], name: 'bt_joatu_response_links_unique_pair', + unique: true + t.index %w[source_type source_id], name: 'bt_joatu_response_links_by_source' + end + + create_table 'better_together_jwt_denylists', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'jti' + t.datetime 'exp' + t.index ['jti'], name: 'index_better_together_jwt_denylists_on_jti' + end + + create_table 'better_together_messages', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.text 'content' + t.uuid 'sender_id', null: false + t.uuid 'conversation_id', null: false + t.index ['conversation_id'], name: 'index_better_together_messages_on_conversation_id' + t.index ['sender_id'], name: 'index_better_together_messages_on_sender_id' + end + + create_table 'better_together_metrics_downloads', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'locale', limit: 5, default: 'en', null: false + t.string 'downloadable_type' + t.uuid 'downloadable_id' + t.string 'file_name', null: false + t.string 'file_type', null: false + t.bigint 'file_size', null: false + t.datetime 'downloaded_at', null: false + t.index %w[downloadable_type downloadable_id], name: 'index_better_together_metrics_downloads_on_downloadable' + t.index ['locale'], name: 'by_better_together_metrics_downloads_locale' + end + + create_table 'better_together_metrics_link_click_reports', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.jsonb 'filters', default: {}, null: false + t.boolean 'sort_by_total_clicks', default: false, null: false + t.string 'file_format', default: 'csv', null: false + t.jsonb 'report_data', default: {}, null: false + t.index ['filters'], name: 'index_better_together_metrics_link_click_reports_on_filters', using: :gin + end + + create_table 'better_together_metrics_link_clicks', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'url', null: false + t.string 'page_url', null: false + t.string 'locale', null: false + t.boolean 'internal', default: true + t.datetime 'clicked_at', null: false + end + + create_table 'better_together_metrics_page_view_reports', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.jsonb 'filters', default: {}, null: false + t.boolean 'sort_by_total_views', default: false, null: false + t.string 'file_format', default: 'csv', null: false + t.jsonb 'report_data', default: {}, null: false + t.index ['filters'], name: 'index_better_together_metrics_page_view_reports_on_filters', using: :gin + end + + create_table 'better_together_metrics_page_views', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'locale', limit: 5, default: 'en', null: false + t.string 'pageable_type' + t.uuid 'pageable_id' + t.datetime 'viewed_at', null: false + t.string 'page_url' + t.index ['locale'], name: 'by_better_together_metrics_page_views_locale' + t.index %w[pageable_type pageable_id], name: 'index_better_together_metrics_page_views_on_pageable' + end + + create_table 'better_together_metrics_search_queries', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'locale', limit: 5, default: 'en', null: false + t.string 'query', null: false + t.integer 'results_count', null: false + t.datetime 'searched_at', null: false + t.index ['locale'], name: 'by_better_together_metrics_search_queries_locale' + end + + create_table 'better_together_metrics_shares', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'locale', limit: 5, default: 'en', null: false + t.string 'platform', null: false + t.string 'url', null: false + t.datetime 'shared_at', null: false + t.string 'shareable_type' + t.uuid 'shareable_id' + t.index ['locale'], name: 'by_better_together_metrics_shares_locale' + t.index %w[platform url], name: 'index_better_together_metrics_shares_on_platform_and_url' + t.index %w[shareable_type shareable_id], name: 'index_better_together_metrics_shares_on_shareable' + end + + create_table 'better_together_navigation_areas', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.boolean 'visible', default: true, null: false + t.string 'name' + t.string 'style' + t.string 'navigable_type' + t.bigint 'navigable_id' + t.index ['identifier'], name: 'index_better_together_navigation_areas_on_identifier', unique: true + t.index %w[navigable_type navigable_id], name: 'by_navigable' + end + + create_table 'better_together_navigation_items', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.integer 'position', null: false + t.boolean 'protected', default: false, null: false + t.boolean 'visible', default: true, null: false + t.uuid 'navigation_area_id', null: false + t.uuid 'parent_id' + t.string 'url' + t.string 'icon' + t.string 'item_type', null: false + t.string 'linkable_type' + t.uuid 'linkable_id' + t.string 'route_name' + t.integer 'children_count', default: 0, null: false + t.index ['identifier'], name: 'index_better_together_navigation_items_on_identifier', unique: true + t.index %w[linkable_type linkable_id], name: 'by_linkable' + t.index %w[navigation_area_id parent_id position], name: 'navigation_items_area_position', unique: true + t.index ['navigation_area_id'], name: 'index_better_together_navigation_items_on_navigation_area_id' + t.index ['parent_id'], name: 'by_nav_item_parent' + end + + create_table 'better_together_pages', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.text 'meta_description' + t.string 'keywords' + t.datetime 'published_at' + t.string 'layout' + t.string 'template' + t.uuid 'sidebar_nav_id' + t.index ['identifier'], name: 'index_better_together_pages_on_identifier', unique: true + t.index ['privacy'], name: 'by_page_privacy' + t.index ['published_at'], name: 'by_page_publication_date' + t.index ['sidebar_nav_id'], name: 'by_page_sidebar_nav' + end + + create_table 'better_together_people', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.uuid 'community_id', null: false + t.jsonb 'preferences', default: {}, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.jsonb 'notification_preferences', default: {}, null: false + t.index ['community_id'], name: 'by_person_community' + t.index ['identifier'], name: 'index_better_together_people_on_identifier', unique: true + t.index ['privacy'], name: 'by_better_together_people_privacy' + end + + create_table 'better_together_person_blocks', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'blocker_id', null: false + t.uuid 'blocked_id', null: false + t.index ['blocked_id'], name: 'index_better_together_person_blocks_on_blocked_id' + t.index %w[blocker_id blocked_id], name: 'unique_person_blocks', unique: true + t.index ['blocker_id'], name: 'index_better_together_person_blocks_on_blocker_id' + end + + create_table 'better_together_person_community_memberships', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'member_id', null: false + t.uuid 'joinable_id', null: false + t.uuid 'role_id', null: false + t.index %w[joinable_id member_id role_id], name: 'unique_person_community_membership_member_role', + unique: true + t.index ['joinable_id'], name: 'person_community_membership_by_joinable' + t.index ['member_id'], name: 'person_community_membership_by_member' + t.index ['role_id'], name: 'person_community_membership_by_role' + end + + create_table 'better_together_person_platform_integrations', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'provider', limit: 50, default: '', null: false + t.string 'uid', limit: 50, default: '', null: false + t.string 'name' + t.string 'handle' + t.string 'profile_url' + t.string 'image_url' + t.string 'access_token' + t.string 'access_token_secret' + t.string 'refresh_token' + t.datetime 'expires_at' + t.jsonb 'auth' + t.uuid 'person_id' + t.uuid 'platform_id' + t.uuid 'user_id' + t.index ['person_id'], name: 'bt_person_platform_conections_by_person' + t.index ['platform_id'], name: 'bt_person_platform_conections_by_platform' + t.index ['user_id'], name: 'bt_person_platform_conections_by_user' + end + + create_table 'better_together_person_platform_memberships', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'member_id', null: false + t.uuid 'joinable_id', null: false + t.uuid 'role_id', null: false + t.index %w[joinable_id member_id role_id], name: 'unique_person_platform_membership_member_role', unique: true + t.index ['joinable_id'], name: 'person_platform_membership_by_joinable' + t.index ['member_id'], name: 'person_platform_membership_by_member' + t.index ['role_id'], name: 'person_platform_membership_by_role' + end + + create_table 'better_together_phone_numbers', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'number', null: false + t.string 'label', null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.uuid 'contact_detail_id', null: false + t.boolean 'primary_flag', default: false, null: false + t.index %w[contact_detail_id primary_flag], name: 'index_bt_phone_numbers_on_contact_detail_id_and_primary', + unique: true, where: '(primary_flag IS TRUE)' + t.index ['contact_detail_id'], name: 'index_better_together_phone_numbers_on_contact_detail_id' + t.index ['privacy'], name: 'by_better_together_phone_numbers_privacy' + end + + create_table 'better_together_places', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'community_id', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.uuid 'space_id', null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.index ['community_id'], name: 'by_better_together_places_community' + t.index ['creator_id'], name: 'by_better_together_places_creator' + t.index ['identifier'], name: 'index_better_together_places_on_identifier', unique: true + t.index ['privacy'], name: 'by_better_together_places_privacy' + t.index ['space_id'], name: 'index_better_together_places_on_space_id' + end + + create_table 'better_together_platform_invitations', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'community_role_id', null: false + t.string 'invitee_email' + t.uuid 'invitable_id', null: false + t.uuid 'invitee_id' + t.uuid 'inviter_id', null: false + t.uuid 'platform_role_id' + t.string 'status', limit: 20, null: false + t.string 'locale', limit: 5, default: 'en', null: false + t.string 'token', limit: 24, null: false + t.datetime 'valid_from', null: false + t.datetime 'valid_until' + t.datetime 'last_sent' + t.datetime 'accepted_at' + t.string 'type', default: 'BetterTogether::PlatformInvitation', null: false + t.integer 'session_duration_mins', default: 30, null: false + t.index ['community_role_id'], name: 'platform_invitations_by_community_role' + t.index %w[invitable_id status], name: 'index_platform_invitations_on_invitable_id_and_status' + t.index ['invitable_id'], name: 'platform_invitations_by_invitable' + t.index %w[invitee_email invitable_id], name: 'idx_on_invitee_email_invitable_id_5a7d642388', unique: true + t.index ['invitee_email'], name: 'index_pending_invitations_on_invitee_email', + where: "((status)::text = 'pending'::text)" + t.index ['invitee_email'], name: 'platform_invitations_by_invitee_email' + t.index ['invitee_id'], name: 'platform_invitations_by_invitee' + t.index ['inviter_id'], name: 'platform_invitations_by_inviter' + t.index ['locale'], name: 'by_better_together_platform_invitations_locale' + t.index ['platform_role_id'], name: 'platform_invitations_by_platform_role' + t.index ['status'], name: 'platform_invitations_by_status' + t.index ['token'], name: 'platform_invitations_by_token', unique: true + t.index ['valid_from'], name: 'platform_invitations_by_valid_from' + t.index ['valid_until'], name: 'platform_invitations_by_valid_until' + end + + create_table 'better_together_platforms', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'host', default: false, null: false + t.boolean 'protected', default: false, null: false + t.uuid 'community_id', null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.string 'url', null: false + t.string 'time_zone', null: false + t.jsonb 'settings', default: {}, null: false + t.index ['community_id'], name: 'by_platform_community' + t.index ['host'], name: 'index_better_together_platforms_on_host', unique: true, where: '(host IS TRUE)' + t.index ['identifier'], name: 'index_better_together_platforms_on_identifier', unique: true + t.index ['privacy'], name: 'by_platform_privacy' + t.index ['url'], name: 'index_better_together_platforms_on_url', unique: true + end + + create_table 'better_together_posts', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'type', default: 'BetterTogether::Post', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.datetime 'published_at' + t.uuid 'creator_id' + t.index ['creator_id'], name: 'by_better_together_posts_creator' + t.index ['identifier'], name: 'index_better_together_posts_on_identifier', unique: true + t.index ['privacy'], name: 'by_better_together_posts_privacy' + t.index ['published_at'], name: 'by_post_publication_date' + end + + create_table 'better_together_reports', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'reporter_id', null: false + t.uuid 'reportable_id', null: false + t.string 'reportable_type', null: false + t.text 'reason' + t.index ['reporter_id'], name: 'index_better_together_reports_on_reporter_id' + end + + create_table 'better_together_resource_permissions', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.integer 'position', null: false + t.string 'resource_type', null: false + t.string 'action', null: false + t.string 'target', null: false + t.index ['identifier'], name: 'index_better_together_resource_permissions_on_identifier', unique: true + t.index %w[resource_type position], name: 'index_resource_permissions_on_resource_type_and_position', + unique: true + end + + create_table 'better_together_role_resource_permissions', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'role_id', null: false + t.uuid 'resource_permission_id', null: false + t.index ['resource_permission_id'], name: 'role_resource_permissions_resource_permission' + t.index %w[role_id resource_permission_id], name: 'unique_role_resource_permission_index', unique: true + t.index ['role_id'], name: 'role_resource_permissions_role' + end + + create_table 'better_together_roles', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.integer 'position', null: false + t.string 'resource_type', null: false + t.string 'type', default: 'BetterTogether::Role', null: false + t.index ['identifier'], name: 'index_better_together_roles_on_identifier', unique: true + t.index %w[resource_type position], name: 'index_roles_on_resource_type_and_position', unique: true + end + + create_table 'better_together_social_media_accounts', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'platform', null: false + t.string 'handle', null: false + t.string 'url' + t.string 'privacy', limit: 50, default: 'private', null: false + t.uuid 'contact_detail_id', null: false + t.index %w[contact_detail_id platform], name: 'index_bt_sma_on_contact_detail_and_platform', unique: true + t.index ['contact_detail_id'], name: 'idx_on_contact_detail_id_6380b64b3b' + t.index ['privacy'], name: 'by_better_together_social_media_accounts_privacy' + end + + create_table 'better_together_uploads', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.string 'type', default: 'BetterTogether::Upload', null: false + t.index ['creator_id'], name: 'by_better_together_files_creator' + t.index ['identifier'], name: 'index_better_together_uploads_on_identifier', unique: true + t.index ['privacy'], name: 'by_better_together_files_privacy' + end + + create_table 'better_together_users', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'email', default: '', null: false + t.string 'encrypted_password', default: '', null: false + t.string 'reset_password_token' + t.datetime 'reset_password_sent_at' + t.datetime 'remember_created_at' + t.integer 'sign_in_count', default: 0, null: false + t.datetime 'current_sign_in_at' + t.datetime 'last_sign_in_at' + t.string 'current_sign_in_ip' + t.string 'last_sign_in_ip' + t.string 'confirmation_token' + t.datetime 'confirmed_at' + t.datetime 'confirmation_sent_at' + t.string 'unconfirmed_email' + t.integer 'failed_attempts', default: 0, null: false + t.string 'unlock_token' + t.datetime 'locked_at' + t.index ['confirmation_token'], name: 'index_better_together_users_on_confirmation_token', unique: true + t.index ['email'], name: 'index_better_together_users_on_email', unique: true + t.index ['reset_password_token'], name: 'index_better_together_users_on_reset_password_token', unique: true + t.index ['unlock_token'], name: 'index_better_together_users_on_unlock_token', unique: true + end + + create_table 'better_together_website_links', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'url', null: false + t.string 'label', null: false + t.string 'privacy', limit: 50, default: 'private', null: false + t.uuid 'contact_detail_id', null: false + t.index ['contact_detail_id'], name: 'index_better_together_website_links_on_contact_detail_id' + t.index ['privacy'], name: 'by_better_together_website_links_privacy' + end + + create_table 'better_together_wizard_step_definitions', id: :uuid, default: lambda { + 'gen_random_uuid()' + }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.uuid 'wizard_id', null: false + t.string 'template' + t.string 'form_class' + t.string 'message', default: 'Please complete this next step.', null: false + t.integer 'step_number', null: false + t.index ['identifier'], name: 'index_better_together_wizard_step_definitions_on_identifier', unique: true + t.index %w[wizard_id step_number], name: 'index_wizard_step_definitions_on_wizard_id_and_step_number', + unique: true + t.index ['wizard_id'], name: 'by_step_definition_wizard' + end + + create_table 'better_together_wizard_steps', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.uuid 'wizard_id', null: false + t.uuid 'wizard_step_definition_id', null: false + t.uuid 'creator_id' + t.string 'identifier', limit: 100, null: false + t.boolean 'completed', default: false + t.integer 'step_number', null: false + t.index ['creator_id'], name: 'by_step_creator' + t.index ['identifier'], name: 'by_step_identifier' + t.index %w[wizard_id identifier creator_id], name: 'index_unique_wizard_steps', unique: true, + where: '(completed IS FALSE)' + t.index %w[wizard_id step_number], name: 'index_wizard_steps_on_wizard_id_and_step_number' + t.index ['wizard_id'], name: 'by_step_wizard' + t.index ['wizard_step_definition_id'], name: 'by_step_wizard_step_definition' + end + + create_table 'better_together_wizards', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'identifier', limit: 100, null: false + t.boolean 'protected', default: false, null: false + t.integer 'max_completions', default: 0, null: false + t.integer 'current_completions', default: 0, null: false + t.datetime 'first_completed_at' + t.datetime 'last_completed_at' + t.text 'success_message', default: 'Thank you. You have successfully completed the wizard', null: false + t.string 'success_path', default: '/', null: false + t.index ['identifier'], name: 'index_better_together_wizards_on_identifier', unique: true + end + + create_table 'friendly_id_slugs', force: :cascade do |t| + t.string 'slug', null: false + t.uuid 'sluggable_id', null: false + t.string 'sluggable_type', null: false + t.string 'scope' + t.integer 'lock_version', default: 0, null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'locale', null: false + t.index ['locale'], name: 'index_friendly_id_slugs_on_locale' + t.index %w[slug sluggable_type locale], name: 'index_friendly_id_slugs_on_slug_and_sluggable_type_and_locale' + t.index %w[slug sluggable_type scope locale], name: 'index_friendly_id_slugs_unique', unique: true + t.index %w[sluggable_type sluggable_id], name: 'by_sluggable' + end + + create_table 'mobility_string_translations', force: :cascade do |t| + t.string 'locale', null: false + t.string 'key', null: false + t.string 'value' + t.string 'translatable_type' + t.uuid 'translatable_id' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index %w[translatable_id translatable_type key], + name: 'index_mobility_string_translations_on_translatable_attribute' + t.index %w[translatable_id translatable_type locale key], + name: 'index_mobility_string_translations_on_keys', unique: true + t.index %w[translatable_type key value locale], name: 'index_mobility_string_translations_on_query_keys' + end + + create_table 'mobility_text_translations', force: :cascade do |t| + t.string 'locale', null: false + t.string 'key', null: false + t.text 'value' + t.string 'translatable_type' + t.uuid 'translatable_id' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index %w[translatable_id translatable_type key], + name: 'index_mobility_text_translations_on_translatable_attribute' + t.index %w[translatable_id translatable_type locale key], + name: 'index_mobility_text_translations_on_keys', unique: true + end + + create_table 'noticed_events', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.string 'type' + t.string 'record_type' + t.uuid 'record_id' + t.jsonb 'params' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'notifications_count' + t.index %w[record_type record_id], name: 'index_noticed_events_on_record' + end + + create_table 'noticed_notifications', id: :uuid, default: -> { 'gen_random_uuid()' }, force: :cascade do |t| + t.string 'type' + t.uuid 'event_id', null: false + t.string 'recipient_type', null: false + t.uuid 'recipient_id', null: false + t.datetime 'read_at', precision: nil + t.datetime 'seen_at', precision: nil + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['event_id'], name: 'index_noticed_notifications_on_event_id' + t.index %w[recipient_type recipient_id], name: 'index_noticed_notifications_on_recipient' + end + + add_foreign_key 'active_storage_attachments', 'active_storage_blobs', column: 'blob_id' + add_foreign_key 'active_storage_variant_records', 'active_storage_blobs', column: 'blob_id' + add_foreign_key 'better_together_addresses', 'better_together_contact_details', column: 'contact_detail_id' + add_foreign_key 'better_together_agreement_participants', 'better_together_agreements', column: 'agreement_id' + add_foreign_key 'better_together_agreement_participants', 'better_together_people', column: 'person_id' + add_foreign_key 'better_together_agreement_terms', 'better_together_agreements', column: 'agreement_id' + add_foreign_key 'better_together_agreements', 'better_together_pages', column: 'page_id' + add_foreign_key 'better_together_agreements', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_ai_log_translations', 'better_together_people', column: 'initiator_id' + add_foreign_key 'better_together_authorships', 'better_together_people', column: 'author_id' + add_foreign_key 'better_together_calendar_entries', 'better_together_calendars', column: 'calendar_id' + add_foreign_key 'better_together_calendar_entries', 'better_together_events', column: 'event_id' + add_foreign_key 'better_together_calendars', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_calendars', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_calls_for_interest', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_categorizations', 'better_together_categories', column: 'category_id' + add_foreign_key 'better_together_comments', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_communities', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_content_blocks', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_content_page_blocks', 'better_together_content_blocks', column: 'block_id' + add_foreign_key 'better_together_content_page_blocks', 'better_together_pages', column: 'page_id' + add_foreign_key 'better_together_content_platform_blocks', 'better_together_content_blocks', column: 'block_id' + add_foreign_key 'better_together_content_platform_blocks', 'better_together_platforms', column: 'platform_id' + add_foreign_key 'better_together_conversation_participants', 'better_together_conversations', + column: 'conversation_id' + add_foreign_key 'better_together_conversation_participants', 'better_together_people', column: 'person_id' + add_foreign_key 'better_together_conversations', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_email_addresses', 'better_together_contact_details', column: 'contact_detail_id' + add_foreign_key 'better_together_event_attendances', 'better_together_events', column: 'event_id' + add_foreign_key 'better_together_event_attendances', 'better_together_people', column: 'person_id' + add_foreign_key 'better_together_event_hosts', 'better_together_events', column: 'event_id' + add_foreign_key 'better_together_events', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_geography_continents', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_geography_countries', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_geography_country_continents', 'better_together_geography_continents', + column: 'continent_id' + add_foreign_key 'better_together_geography_country_continents', 'better_together_geography_countries', + column: 'country_id' + add_foreign_key 'better_together_geography_geospatial_spaces', 'better_together_geography_spaces', column: 'space_id' + add_foreign_key 'better_together_geography_locatable_locations', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_geography_maps', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_geography_region_settlements', 'better_together_geography_regions', + column: 'region_id' + add_foreign_key 'better_together_geography_region_settlements', 'better_together_geography_settlements', + column: 'settlement_id' + add_foreign_key 'better_together_geography_regions', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_geography_regions', 'better_together_geography_countries', column: 'country_id' + add_foreign_key 'better_together_geography_regions', 'better_together_geography_states', column: 'state_id' + add_foreign_key 'better_together_geography_settlements', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_geography_settlements', 'better_together_geography_countries', column: 'country_id' + add_foreign_key 'better_together_geography_settlements', 'better_together_geography_states', column: 'state_id' + add_foreign_key 'better_together_geography_spaces', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_geography_states', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_geography_states', 'better_together_geography_countries', column: 'country_id' + add_foreign_key 'better_together_infrastructure_building_connections', 'better_together_infrastructure_buildings', + column: 'building_id' + add_foreign_key 'better_together_infrastructure_buildings', 'better_together_addresses', column: 'address_id' + add_foreign_key 'better_together_infrastructure_buildings', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_infrastructure_buildings', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_infrastructure_floors', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_infrastructure_floors', 'better_together_infrastructure_buildings', + column: 'building_id' + add_foreign_key 'better_together_infrastructure_floors', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_infrastructure_rooms', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_infrastructure_rooms', 'better_together_infrastructure_floors', column: 'floor_id' + add_foreign_key 'better_together_infrastructure_rooms', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_invitations', 'better_together_roles', column: 'role_id' + add_foreign_key 'better_together_joatu_agreements', 'better_together_joatu_offers', column: 'offer_id' + add_foreign_key 'better_together_joatu_agreements', 'better_together_joatu_requests', column: 'request_id' + add_foreign_key 'better_together_joatu_offers', 'better_together_addresses', column: 'address_id' + add_foreign_key 'better_together_joatu_offers', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_joatu_requests', 'better_together_addresses', column: 'address_id' + add_foreign_key 'better_together_joatu_requests', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_joatu_response_links', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_messages', 'better_together_conversations', column: 'conversation_id' + add_foreign_key 'better_together_messages', 'better_together_people', column: 'sender_id' + add_foreign_key 'better_together_navigation_items', 'better_together_navigation_areas', column: 'navigation_area_id' + add_foreign_key 'better_together_navigation_items', 'better_together_navigation_items', column: 'parent_id' + add_foreign_key 'better_together_pages', 'better_together_navigation_areas', column: 'sidebar_nav_id' + add_foreign_key 'better_together_people', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_person_blocks', 'better_together_people', column: 'blocked_id' + add_foreign_key 'better_together_person_blocks', 'better_together_people', column: 'blocker_id' + add_foreign_key 'better_together_person_community_memberships', 'better_together_communities', column: 'joinable_id' + add_foreign_key 'better_together_person_community_memberships', 'better_together_people', column: 'member_id' + add_foreign_key 'better_together_person_community_memberships', 'better_together_roles', column: 'role_id' + add_foreign_key 'better_together_person_platform_integrations', 'better_together_people', column: 'person_id' + add_foreign_key 'better_together_person_platform_integrations', 'better_together_platforms', column: 'platform_id' + add_foreign_key 'better_together_person_platform_integrations', 'better_together_users', column: 'user_id' + add_foreign_key 'better_together_person_platform_memberships', 'better_together_people', column: 'member_id' + add_foreign_key 'better_together_person_platform_memberships', 'better_together_platforms', column: 'joinable_id' + add_foreign_key 'better_together_person_platform_memberships', 'better_together_roles', column: 'role_id' + add_foreign_key 'better_together_phone_numbers', 'better_together_contact_details', column: 'contact_detail_id' + add_foreign_key 'better_together_places', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_places', 'better_together_geography_spaces', column: 'space_id' + add_foreign_key 'better_together_places', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_platform_invitations', 'better_together_people', column: 'invitee_id' + add_foreign_key 'better_together_platform_invitations', 'better_together_people', column: 'inviter_id' + add_foreign_key 'better_together_platform_invitations', 'better_together_platforms', column: 'invitable_id' + add_foreign_key 'better_together_platform_invitations', 'better_together_roles', column: 'community_role_id' + add_foreign_key 'better_together_platform_invitations', 'better_together_roles', column: 'platform_role_id' + add_foreign_key 'better_together_platforms', 'better_together_communities', column: 'community_id' + add_foreign_key 'better_together_posts', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_reports', 'better_together_people', column: 'reporter_id' + add_foreign_key 'better_together_role_resource_permissions', 'better_together_resource_permissions', + column: 'resource_permission_id' + add_foreign_key 'better_together_role_resource_permissions', 'better_together_roles', column: 'role_id' + add_foreign_key 'better_together_social_media_accounts', 'better_together_contact_details', + column: 'contact_detail_id' + add_foreign_key 'better_together_uploads', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_website_links', 'better_together_contact_details', column: 'contact_detail_id' + add_foreign_key 'better_together_wizard_step_definitions', 'better_together_wizards', column: 'wizard_id' + add_foreign_key 'better_together_wizard_steps', 'better_together_people', column: 'creator_id' + add_foreign_key 'better_together_wizard_steps', 'better_together_wizard_step_definitions', + column: 'wizard_step_definition_id' + add_foreign_key 'better_together_wizard_steps', 'better_together_wizards', column: 'wizard_id' end diff --git a/spec/factories/better_together/people.rb b/spec/factories/better_together/people.rb index 769866e94..7ca0fa51b 100644 --- a/spec/factories/better_together/people.rb +++ b/spec/factories/better_together/people.rb @@ -4,7 +4,8 @@ module BetterTogether FactoryBot.define do - factory :better_together_person, class: Person, aliases: %i[person inviter invitee creator author] do + factory 'better_together/person', class: Person, + aliases: %i[better_together_person person inviter invitee creator author] do id { Faker::Internet.uuid } name { Faker::Name.name } description { Faker::Lorem.paragraph(sentence_count: 3) } diff --git a/spec/factories/better_together/person_platform_integrations.rb b/spec/factories/better_together/person_platform_integrations.rb new file mode 100644 index 000000000..c8b2bc895 --- /dev/null +++ b/spec/factories/better_together/person_platform_integrations.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :better_together_person_platform_integration, + class: 'BetterTogether::PersonPlatformIntegration', + aliases: %i[person_platform_integration] do + provider { 'MyString' } + uid { 'MyString' } + access_token { 'MyString' } + access_token_secret { 'MyString' } + profile_url { 'MyString' } + user + person { user.person } + end +end diff --git a/spec/factories/better_together/seeds.rb b/spec/factories/better_together/seeds.rb new file mode 100644 index 000000000..e7389c842 --- /dev/null +++ b/spec/factories/better_together/seeds.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :better_together_seed, class: 'BetterTogether::Seed' do + id { SecureRandom.uuid } + version { '1.0' } + created_by { 'Better Together Solutions' } + seeded_at { Time.current } + description { 'This is a generic seed for testing purposes.' } + + origin do + { + 'contributors' => [ + { 'name' => 'Test Contributor', 'role' => 'Tester', 'contact' => 'test@example.com', + 'organization' => 'Test Org' } + ], + 'platforms' => [ + { 'name' => 'Community Engine', 'version' => '1.0', 'url' => 'https://bebettertogether.ca' } + ], + 'license' => 'LGPLv3', + 'usage_notes' => 'This seed is for test purposes only.' + } + end + + payload do + { + version: '1.0', + generic_data: { + name: 'Generic Seed', + description: 'This is a placeholder seed.' + } + } + end + end +end diff --git a/spec/factories/better_together/users.rb b/spec/factories/better_together/users.rb index ac65cd73d..f988e97c0 100644 --- a/spec/factories/better_together/users.rb +++ b/spec/factories/better_together/users.rb @@ -30,5 +30,12 @@ ) end end + + before :create do |user| + user.build_person_identification( + agent: user, + identity: create(:person) + ) + end end end diff --git a/spec/factories/better_together/wizard_step_definitions.rb b/spec/factories/better_together/wizard_step_definitions.rb index 540dbefd1..2c7f8e52b 100644 --- a/spec/factories/better_together/wizard_step_definitions.rb +++ b/spec/factories/better_together/wizard_step_definitions.rb @@ -3,9 +3,9 @@ # spec/factories/wizard_step_definitions.rb FactoryBot.define do - factory :better_together_wizard_step_definition, + factory 'better_together/wizard_step_definition', class: 'BetterTogether::WizardStepDefinition', - aliases: %i[wizard_step_definition] do + aliases: %i[better_together_wizard_step_definition wizard_step_definition] do id { SecureRandom.uuid } wizard { create(:wizard) } name { Faker::Lorem.unique.sentence(word_count: 3) } @@ -14,7 +14,7 @@ template { "template_#{Faker::Lorem.word}" } form_class { "FormClass#{Faker::Lorem.word}" } message { 'Please complete this next step.' } - step_number { Faker::Number.unique.between(from: 1, to: 50) } + step_number { Faker::Number.unique.between(from: 1, to: 500) } protected { Faker::Boolean.boolean } end end diff --git a/spec/factories/better_together/wizard_steps.rb b/spec/factories/better_together/wizard_steps.rb index a2c7c71dd..99b736830 100644 --- a/spec/factories/better_together/wizard_steps.rb +++ b/spec/factories/better_together/wizard_steps.rb @@ -3,9 +3,9 @@ # spec/factories/wizard_steps.rb FactoryBot.define do - factory :better_together_wizard_step, + factory 'better_together/wizard_step', class: 'BetterTogether::WizardStep', - aliases: %i[wizard_step] do + aliases: %i[better_together_wizard_step wizard_step] do id { SecureRandom.uuid } wizard_step_definition wizard { wizard_step_definition.wizard } diff --git a/spec/factories/better_together/wizards.rb b/spec/factories/better_together/wizards.rb index 59faf9513..33e00c0e7 100644 --- a/spec/factories/better_together/wizards.rb +++ b/spec/factories/better_together/wizards.rb @@ -3,9 +3,9 @@ # spec/factories/wizards.rb FactoryBot.define do - factory :better_together_wizard, + factory 'better_together/wizard', class: 'BetterTogether::Wizard', - aliases: %i[wizard] do + aliases: %i[better_together_wizard wizard] do id { SecureRandom.uuid } name { Faker::Lorem.sentence(word_count: 3) } identifier { name.parameterize } diff --git a/spec/features/setup_wizard_spec.rb b/spec/features/setup_wizard_spec.rb index 92830e721..b47568127 100644 --- a/spec/features/setup_wizard_spec.rb +++ b/spec/features/setup_wizard_spec.rb @@ -12,6 +12,9 @@ # Start at the root and verify redirection to the wizard visit '/' + expect(current_path).to eq(better_together.home_page_path(locale: I18n.locale)) + + visit better_together.new_user_session_path(locale: I18n.locale) expect(current_path).to eq(better_together.setup_wizard_step_platform_details_path(locale: I18n.locale)) expect(page).to have_content("Please configure your platform's details below") diff --git a/spec/helpers/better_together/person_platform_integrations_helper_spec.rb b/spec/helpers/better_together/person_platform_integrations_helper_spec.rb new file mode 100644 index 000000000..4e37ccc97 --- /dev/null +++ b/spec/helpers/better_together/person_platform_integrations_helper_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the BetterTogether::PersonPlatformIntegrationsHelper. For example: +# +# describe BetterTogether::PersonPlatformIntegrationsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe BetterTogether::PersonPlatformIntegrationsHelper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/better_together/seeds_helper_spec.rb b/spec/helpers/better_together/seeds_helper_spec.rb new file mode 100644 index 000000000..e81117d91 --- /dev/null +++ b/spec/helpers/better_together/seeds_helper_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the SeedsHelper. For example: +# +# describe SeedsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +module BetterTogether + RSpec.describe SeedsHelper do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/better_together/person_platform_integration_spec.rb b/spec/models/better_together/person_platform_integration_spec.rb new file mode 100644 index 000000000..3ad0127eb --- /dev/null +++ b/spec/models/better_together/person_platform_integration_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BetterTogether::PersonPlatformIntegration do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/better_together/person_spec.rb b/spec/models/better_together/person_spec.rb index 60e42167b..3e173fdcc 100644 --- a/spec/models/better_together/person_spec.rb +++ b/spec/models/better_together/person_spec.rb @@ -15,7 +15,8 @@ module BetterTogether it_behaves_like 'a friendly slugged record' it_behaves_like 'an identity' it_behaves_like 'has_id' - # it_behaves_like 'an author model' + it_behaves_like 'an author model' + it_behaves_like 'a seedable model' describe 'ActiveModel validations' do it { is_expected.to validate_presence_of(:name) } diff --git a/spec/models/better_together/seed_spec.rb b/spec/models/better_together/seed_spec.rb new file mode 100644 index 000000000..f2cbff92f --- /dev/null +++ b/spec/models/better_together/seed_spec.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BetterTogether::Seed do + subject(:seed) { build(:better_together_seed) } + + describe 'validations' do + it { is_expected.to validate_presence_of(:type) } + it { is_expected.to validate_presence_of(:version) } + it { is_expected.to validate_presence_of(:created_by) } + it { is_expected.to validate_presence_of(:seeded_at) } + it { is_expected.to validate_presence_of(:description) } + it { is_expected.to validate_presence_of(:origin) } + it { is_expected.to validate_presence_of(:payload) } + end + + describe '#export' do + it 'returns the complete structured seed data' do + expect(seed.export.keys.first).to eq('better_together') + end + end + + describe '#export_yaml' do + it 'generates valid YAML' do + yaml = seed.export_yaml + expect(yaml).to include('better_together') + end + end + + it 'returns the contributors from origin' do + expect(seed.contributors.first['name']).to eq('Test Contributor') + end + + it 'returns the platforms from origin' do + expect(seed.platforms.first['name']).to eq('Community Engine') + end + + describe 'scopes' do + before do + create(:better_together_seed, identifier: 'generic_seed') + create(:better_together_seed, identifier: 'home_page', type: 'BetterTogether::Seed') + end + + it 'filters by type' do + expect(described_class.by_type('BetterTogether::Seed').count).to eq(2) + end + + it 'filters by identifier' do + expect(described_class.by_identifier('home_page').count).to eq(1) + end + end + + # ------------------------------------------------------------------- + # Specs for .load_seed + # ------------------------------------------------------------------- + describe '.load_seed' do + let(:valid_seed_data) do + { + 'better_together' => { + 'version' => '1.0', + 'seed' => { + 'type' => 'BetterTogether::Seed', + 'identifier' => 'from_test', + 'created_by' => 'Test Creator', + 'created_at' => '2025-03-04T12:00:00Z', + 'description' => 'A seed from tests', + 'origin' => { + 'contributors' => [], + 'platforms' => [], + 'license' => 'LGPLv3', + 'usage_notes' => 'Test usage only.' + } + }, + 'payload_key' => 'payload_value' + } + } + end + + let(:file_path) { '/fake/absolute/path/host_setup_wizard.yml' } + + before do + # Default everything to false/unset, override if needed + allow(File).to receive(:exist?).and_return(false) + allow(YAML).to receive(:load_file).and_call_original + end + + context 'when the source is a direct file path' do + # rubocop:todo RSpec/NestedGroups + context 'and the file exists' do # rubocop:todo RSpec/ContextWording, RSpec/NestedGroups + # rubocop:enable RSpec/NestedGroups + before do + allow(File).to receive(:exist?).with(file_path).and_return(true) + allow(YAML).to receive(:load_file).with(file_path).and_return(valid_seed_data) + end + + it 'imports the seed and returns a BetterTogether::Seed record' do # rubocop:todo RSpec/MultipleExpectations + result = described_class.load_seed(file_path) + expect(result).to be_a(described_class) + expect(result.identifier).to eq('from_test') + expect(result.payload[:payload_key]).to eq('payload_value') + end + end + + # rubocop:todo RSpec/NestedGroups + context 'but the file does not exist' do # rubocop:todo RSpec/ContextWording, RSpec/NestedGroups + # rubocop:enable RSpec/NestedGroups + it 'falls back to namespace logic and raises an error' do + expect do + described_class.load_seed(file_path) + end.to raise_error(RuntimeError, /Seed file not found for/) + end + end + + context 'when YAML loading raises an error' do # rubocop:todo RSpec/NestedGroups + before do + allow(File).to receive(:exist?).with(file_path).and_return(true) + allow(YAML).to receive(:load_file).with(file_path).and_raise(StandardError, 'Bad YAML') + end + + it 'raises a descriptive error' do + expect do + described_class.load_seed(file_path) + end.to raise_error(RuntimeError, /Error loading seed from file.*Bad YAML/) + end + end + end + + context 'when the source is a namespace' do + let(:namespace) { 'better_together/wizards/host_setup_wizard' } + let(:full_path) { Rails.root.join('config', 'seeds', "#{namespace}.yml").to_s } + + # rubocop:todo RSpec/NestedGroups + context 'and the file exists' do # rubocop:todo RSpec/ContextWording, RSpec/NestedGroups + # rubocop:enable RSpec/NestedGroups + before do + allow(File).to receive(:exist?).with(namespace).and_return(false) + allow(File).to receive(:exist?).with(full_path).and_return(true) + allow(YAML).to receive(:load_file).with(full_path).and_return(valid_seed_data) + end + + it 'imports the seed from the namespace path' do # rubocop:todo RSpec/MultipleExpectations + result = described_class.load_seed(namespace) + expect(result).to be_a(described_class) + expect(result.identifier).to eq('from_test') + end + end + + # rubocop:todo RSpec/NestedGroups + context 'but the file does not exist' do # rubocop:todo RSpec/ContextWording, RSpec/NestedGroups + # rubocop:enable RSpec/NestedGroups + before do + allow(File).to receive(:exist?).with(namespace).and_return(false) + allow(File).to receive(:exist?).with(full_path).and_return(false) + end + + it 'raises a file-not-found error' do + expect do + described_class.load_seed(namespace) + end.to raise_error(RuntimeError, /Seed file not found for/) + end + end + + context 'when YAML loading raises an error' do # rubocop:todo RSpec/NestedGroups + before do + allow(File).to receive(:exist?).with(namespace).and_return(false) + allow(File).to receive(:exist?).with(full_path).and_return(true) + allow(YAML).to receive(:load_file).with(full_path).and_raise(StandardError, 'YAML parse error') + end + + it 'raises a descriptive error' do + expect do + described_class.load_seed(namespace) + end.to raise_error(RuntimeError, /Error loading seed from namespace.*YAML parse error/) + end + end + end + end + + # ------------------------------------------------------------------- + # Specs for Active Storage attachment + # ------------------------------------------------------------------- + describe 'Active Storage YAML attachment' do + let(:seed) do + # create a valid, persisted seed so that we can test the attachment + create(:better_together_seed) + end + + it 'attaches a YAML file after creation' do # rubocop:todo RSpec/NoExpectationExample + # seed.reload # Ensures the record reloads from the DB after the commit callback + # expect(seed.yaml_file).to be_attached + + # # Optional: Check content type and file content + # expect(seed.yaml_file.content_type).to eq('text/yaml') + # downloaded_data = seed.yaml_file.download + # expect(downloaded_data).to include('better_together') + end + end +end diff --git a/spec/models/better_together/wizard_spec.rb b/spec/models/better_together/wizard_spec.rb index fcb2e60f2..d16ec4534 100644 --- a/spec/models/better_together/wizard_spec.rb +++ b/spec/models/better_together/wizard_spec.rb @@ -14,6 +14,8 @@ module BetterTogether end end + it_behaves_like 'a seedable model' + describe 'ActiveRecord associations' do it { is_expected.to have_many(:wizard_step_definitions).dependent(:destroy) } it { is_expected.to have_many(:wizard_steps).dependent(:destroy) } diff --git a/spec/models/better_together/wizard_step_definition_spec.rb b/spec/models/better_together/wizard_step_definition_spec.rb index 48d90443e..98d6a95de 100644 --- a/spec/models/better_together/wizard_step_definition_spec.rb +++ b/spec/models/better_together/wizard_step_definition_spec.rb @@ -16,6 +16,8 @@ module BetterTogether end end + it_behaves_like 'a seedable model' + describe 'ActiveRecord associations' do it { is_expected.to belong_to(:wizard) } it { is_expected.to have_many(:wizard_steps) } diff --git a/spec/models/better_together/wizard_step_spec.rb b/spec/models/better_together/wizard_step_spec.rb index 2c5c65064..fb587a130 100644 --- a/spec/models/better_together/wizard_step_spec.rb +++ b/spec/models/better_together/wizard_step_spec.rb @@ -16,6 +16,8 @@ module BetterTogether end end + it_behaves_like 'a seedable model' + describe 'ActiveRecord associations' do # it { is_expected.to belong_to(:wizard) } it { diff --git a/spec/requests/better_together/person_platform_integrations_spec.rb b/spec/requests/better_together/person_platform_integrations_spec.rb new file mode 100644 index 000000000..769d9ed3d --- /dev/null +++ b/spec/requests/better_together/person_platform_integrations_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to test the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. + +RSpec.describe '/better_together/authorizations' do + # This should return the minimal set of attributes required to create a valid + # BetterTogether::PersonPlatformIntegration. As you add validations to BetterTogether::PersonPlatformIntegration, + # be sure to adjust the attributes here as well. + let(:valid_attributes) do + skip('Add a hash of attributes valid for your model') + end + + let(:invalid_attributes) do + skip('Add a hash of attributes invalid for your model') + end + + describe 'GET /index' do # rubocop:todo RSpec/RepeatedExampleGroupBody + it 'renders a successful response' do # rubocop:todo RSpec/NoExpectationExample + # BetterTogether::PersonPlatformIntegration.create! valid_attributes + # get person_platform_integrations_url + # expect(response).to be_successful + end + end + + describe 'GET /show' do # rubocop:todo RSpec/RepeatedExampleGroupBody + it 'renders a successful response' do # rubocop:todo RSpec/NoExpectationExample + # authorization = BetterTogether::PersonPlatformIntegration.create! valid_attributes + # get person_platform_integration_url(authorization) + # expect(response).to be_successful + end + end + + describe 'GET /new' do # rubocop:todo RSpec/RepeatedExampleGroupBody + it 'renders a successful response' do # rubocop:todo RSpec/NoExpectationExample + # get new_person_platform_integration_url + # expect(response).to be_successful + end + end + + describe 'GET /edit' do # rubocop:todo RSpec/RepeatedExampleGroupBody + it 'renders a successful response' do # rubocop:todo RSpec/NoExpectationExample + # authorization = BetterTogether::PersonPlatformIntegration.create! valid_attributes + # get edit_person_platform_integration_url(authorization) + # expect(response).to be_successful + end + end + + describe 'POST /create' do + context 'with valid parameters' do + # rubocop:todo RSpec/RepeatedExample + it 'creates a new BetterTogether::PersonPlatformIntegration' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect do + # post person_platform_integrations_url, params: { person_platform_integration: valid_attributes } + # end.to change(BetterTogether::PersonPlatformIntegration, :count).by(1) + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'redirects to the created person_platform_integration' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # post person_platform_integrations_url, params: { person_platform_integration: valid_attributes } + # expect(response).to + # redirect_to(person_platform_integration_url(BetterTogether::PersonPlatformIntegration.last)) + end + # rubocop:enable RSpec/RepeatedExample + end + + context 'with invalid parameters' do + # rubocop:todo RSpec/RepeatedExample + it 'does not create a new BetterTogether::PersonPlatformIntegration' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect do + # post person_platform_integrations_url, params: { person_platform_integration: invalid_attributes } + # end.to change(BetterTogether::PersonPlatformIntegration, :count).by(0) + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + # rubocop:todo RSpec/NoExpectationExample + it "renders a response with 422 status (i.e. to display the 'new' template)" do + # post person_platform_integrations_url, params: { person_platform_integration: invalid_attributes } + # expect(response).to have_http_status(:unprocessable_entity) + end + # rubocop:enable RSpec/NoExpectationExample + # rubocop:enable RSpec/RepeatedExample + end + end + + describe 'PATCH /update' do + context 'with valid parameters' do + let(:new_attributes) do + skip('Add a hash of attributes valid for your model') + end + + # rubocop:todo RSpec/RepeatedExample + it 'updates the requested person_platform_integration' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # authorization = BetterTogether::PersonPlatformIntegration.create! valid_attributes + # patch person_platform_integration_url(authorization), params: { person_platform_integration: new_attributes } + # authorization.reload + # skip('Add assertions for updated state') + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'redirects to the person_platform_integration' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # authorization = BetterTogether::PersonPlatformIntegration.create! valid_attributes + # patch person_platform_integration_url(authorization), params: { person_platform_integration: new_attributes } + # authorization.reload + # expect(response).to redirect_to(person_platform_integration_url(authorization)) + end + # rubocop:enable RSpec/RepeatedExample + end + + context 'with invalid parameters' do + # rubocop:todo RSpec/NoExpectationExample + it "renders a response with 422 status (i.e. to display the 'edit' template)" do + # authorization = BetterTogether::PersonPlatformIntegration.create! valid_attributes + # patch person_platform_integration_url(authorization), + # params: { person_platform_integration: invalid_attributes } + # expect(response).to have_http_status(:unprocessable_entity) + end + # rubocop:enable RSpec/NoExpectationExample + end + end + + describe 'DELETE /destroy' do + # rubocop:todo RSpec/RepeatedExample + it 'destroys the requested person_platform_integration' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # authorization = BetterTogether::PersonPlatformIntegration.create! valid_attributes + # expect do + # delete person_platform_integration_url(authorization) + # end.to change(BetterTogether::PersonPlatformIntegration, :count).by(-1) + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'redirects to the person_platform_integrations list' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # authorization = BetterTogether::PersonPlatformIntegration.create! valid_attributes + # delete person_platform_integration_url(authorization) + # expect(response).to redirect_to(person_platform_integrations_url) + end + # rubocop:enable RSpec/RepeatedExample + end +end diff --git a/spec/routing/better_together/person_platform_integrations_routing_spec.rb b/spec/routing/better_together/person_platform_integrations_routing_spec.rb new file mode 100644 index 000000000..c1047e2a5 --- /dev/null +++ b/spec/routing/better_together/person_platform_integrations_routing_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BetterTogether::PersonPlatformIntegrationsController do + describe 'routing' do + # rubocop:todo RSpec/RepeatedExample + it 'routes to #index' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect(get: '/better_together/authorizations').to route_to('better_together/authorizations#index') + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'routes to #new' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect(get: '/better_together/authorizations/new').to route_to('better_together/authorizations#new') + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'routes to #show' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect(get: '/better_together/authorizations/1').to route_to('better_together/authorizations#show', id: '1') + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'routes to #edit' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect(get: '/better_together/authorizations/1/edit').toroute_to('better_together/authorizations#edit', id: '1') + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'routes to #create' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect(post: '/better_together/authorizations').to route_to('better_together/authorizations#create') + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'routes to #update via PUT' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect(put: '/better_together/authorizations/1').to route_to('better_together/authorizations#update', id: '1') + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'routes to #update via PATCH' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect(patch: '/better_together/authorizations/1').to route_to('better_together/authorizations#update', id: '1') + end + # rubocop:enable RSpec/RepeatedExample + + # rubocop:todo RSpec/RepeatedExample + it 'routes to #destroy' do # rubocop:todo RSpec/NoExpectationExample, RSpec/RepeatedExample + # expect(delete: '/better_together/authorizations/1').toroute_to('better_together/authorizations#destroy',id: '1') + end + # rubocop:enable RSpec/RepeatedExample + end +end diff --git a/spec/support/shared_examples/a_seedable_model.rb b/spec/support/shared_examples/a_seedable_model.rb new file mode 100644 index 000000000..40bb36bae --- /dev/null +++ b/spec/support/shared_examples/a_seedable_model.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a seedable model' do + it 'includes the Seedable concern' do + expect(described_class.ancestors).to include(BetterTogether::Seedable) + end + + describe 'Seedable instance methods' do + # Use create(...) so the record is persisted in the test database + let(:record) { create(described_class.name.underscore.to_sym) } + + it 'responds to #plant' do + expect(record).to respond_to(:plant) + end + + it 'responds to #export_as_seed' do + expect(record).to respond_to(:export_as_seed) + end + + it 'responds to #export_as_seed_yaml' do + expect(record).to respond_to(:export_as_seed_yaml) + end + + describe '#export_as_seed' do + it 'returns a hash with the default root key' do + seed_hash = record.export_as_seed + expect(seed_hash.keys).to include(BetterTogether::Seed::DEFAULT_ROOT_KEY) + end + + it 'includes the record data under :record (or your chosen key)' do + seed_hash = record.export_as_seed + root_key = seed_hash.keys.first + expect(seed_hash[root_key]).to have_key(:record) + end + end + + describe '#export_as_seed_yaml' do + it 'returns a valid YAML string' do # rubocop:todo RSpec/MultipleExpectations + yaml_str = record.export_as_seed_yaml + expect(yaml_str).to be_a(String) + expect(yaml_str).to include(BetterTogether::Seed::DEFAULT_ROOT_KEY.to_s) + end + end + end + + describe 'Seedable class methods' do + let(:records) { build_list(described_class.name.underscore.to_sym, 3) } + + it 'responds to .export_collection_as_seed' do + expect(described_class).to respond_to(:export_collection_as_seed) + end + + it 'responds to .export_collection_as_seed_yaml' do + expect(described_class).to respond_to(:export_collection_as_seed_yaml) + end + + describe '.export_collection_as_seed' do + it 'returns a hash with the default root key' do + collection_hash = described_class.export_collection_as_seed(records) + expect(collection_hash.keys).to include(BetterTogether::Seed::DEFAULT_ROOT_KEY) + end + + it 'includes an array of records under :records' do # rubocop:todo RSpec/MultipleExpectations + collection_hash = described_class.export_collection_as_seed(records) + root_key = collection_hash.keys.first + expect(collection_hash[root_key]).to have_key(:records) + expect(collection_hash[root_key][:records]).to be_an(Array) + expect(collection_hash[root_key][:records].size).to eq(records.size) + end + end + + describe '.export_collection_as_seed_yaml' do + it 'returns a valid YAML string' do # rubocop:todo RSpec/MultipleExpectations + yaml_str = described_class.export_collection_as_seed_yaml(records) + expect(yaml_str).to be_a(String) + expect(yaml_str).to include(BetterTogether::Seed::DEFAULT_ROOT_KEY.to_s) + end + end + end +end diff --git a/spec/views/better_together/person_platform_integrations/edit.html.erb_spec.rb b/spec/views/better_together/person_platform_integrations/edit.html.erb_spec.rb new file mode 100644 index 000000000..0ec7fbfd1 --- /dev/null +++ b/spec/views/better_together/person_platform_integrations/edit.html.erb_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'better_together/authorizations/edit' do + let(:person_platform_integration) do + create(:person_platform_integration) + end + + before do + assign(:person_platform_integration, person_platform_integration) + end + + it 'renders the edit person_platform_integration form' do # rubocop:todo RSpec/NoExpectationExample + # render + + # assert_select 'form[action=?][method=?]', person_platform_integration_path(person_platform_integration), 'post' do + # assert_select 'input[name=?]', 'person_platform_integration[provider]' + + # assert_select 'input[name=?]', 'person_platform_integration[uid]' + + # assert_select 'input[name=?]', 'person_platform_integration[access_token]' + + # assert_select 'input[name=?]', 'person_platform_integration[access_secret]' + + # assert_select 'input[name=?]', 'person_platform_integration[profile_url]' + + # assert_select 'input[name=?]', 'person_platform_integration[user_id]' + # assert_select 'input[name=?]', 'person_platform_integration[person_id]' + # end + end +end diff --git a/spec/views/better_together/person_platform_integrations/index.html.erb_spec.rb b/spec/views/better_together/person_platform_integrations/index.html.erb_spec.rb new file mode 100644 index 000000000..2c2211363 --- /dev/null +++ b/spec/views/better_together/person_platform_integrations/index.html.erb_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'better_together/authorizations/index' do + before do + assign(:person_platform_integrations, create_list(:person_platform_integration, 3)) + end + + it 'renders a list of better_together/authorizations' do # rubocop:todo RSpec/NoExpectationExample + # render + # cell_selector = Rails::VERSION::STRING >= '7' ? 'div>p' : 'tr>td' + # assert_select cell_selector, text: Regexp.new('Provider'.to_s), count: 2 + # assert_select cell_selector, text: Regexp.new('Uid'.to_s), count: 2 + # assert_select cell_selector, text: Regexp.new('Token'.to_s), count: 2 + # assert_select cell_selector, text: Regexp.new('Secret'.to_s), count: 2 + # assert_select cell_selector, text: Regexp.new('Profile Url'.to_s), count: 2 + # assert_select cell_selector, text: Regexp.new(nil.to_s), count: 2 + end +end diff --git a/spec/views/better_together/person_platform_integrations/new.html.erb_spec.rb b/spec/views/better_together/person_platform_integrations/new.html.erb_spec.rb new file mode 100644 index 000000000..ea22b1842 --- /dev/null +++ b/spec/views/better_together/person_platform_integrations/new.html.erb_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'better_together/authorizations/new' do + before do + assign(:person_platform_integration, create(:person_platform_integration)) + end + + it 'renders new person_platform_integration form' do # rubocop:todo RSpec/NoExpectationExample + # render + + # assert_select 'form[action=?][method=?]', person_platform_integrations_path, 'post' do + # assert_select 'input[name=?]', 'person_platform_integration[provider]' + + # assert_select 'input[name=?]', 'person_platform_integration[uid]' + + # assert_select 'input[name=?]', 'person_platform_integration[token]' + + # assert_select 'input[name=?]', 'person_platform_integration[secret]' + + # assert_select 'input[name=?]', 'person_platform_integration[profile_url]' + + # assert_select 'input[name=?]', 'person_platform_integration[user_id]' + # end + end +end diff --git a/spec/views/better_together/person_platform_integrations/show.html.erb_spec.rb b/spec/views/better_together/person_platform_integrations/show.html.erb_spec.rb new file mode 100644 index 000000000..2102b8627 --- /dev/null +++ b/spec/views/better_together/person_platform_integrations/show.html.erb_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'better_together/authorizations/show' do + before do + assign(:person_platform_integration, create(:person_platform_integration)) + end + + it 'renders attributes in

' do # rubocop:todo RSpec/NoExpectationExample + # render + # expect(rendered).to match(/Provider/) + # expect(rendered).to match(/Uid/) + # expect(rendered).to match(/Token/) + # expect(rendered).to match(/Secret/) + # expect(rendered).to match(/Profile Url/) + # expect(rendered).to match(//) + end +end diff --git a/spec/views/better_together/seeds/edit.html.erb_spec.rb b/spec/views/better_together/seeds/edit.html.erb_spec.rb new file mode 100644 index 000000000..453c336b5 --- /dev/null +++ b/spec/views/better_together/seeds/edit.html.erb_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'seeds/edit' do + let(:seed) do + create(:better_together_seed) + end + + before do + assign(:seed, seed) + end + + it 'renders the edit seed form' do # rubocop:todo RSpec/NoExpectationExample + # render + + # assert_select "form[action=?][method=?]", seed_path(seed), "post" do + # end + end +end diff --git a/spec/views/better_together/seeds/index.html.erb_spec.rb b/spec/views/better_together/seeds/index.html.erb_spec.rb new file mode 100644 index 000000000..52f2ca336 --- /dev/null +++ b/spec/views/better_together/seeds/index.html.erb_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'seeds/index' do + before do + assign(:seeds, [ + create(:better_together_seed), + create(:better_together_seed) + ]) + end + + it 'renders a list of seeds' do # rubocop:todo RSpec/NoExpectationExample + # render + # cell_selector = 'div>p' + end +end diff --git a/spec/views/better_together/seeds/new.html.erb_spec.rb b/spec/views/better_together/seeds/new.html.erb_spec.rb new file mode 100644 index 000000000..c95369385 --- /dev/null +++ b/spec/views/better_together/seeds/new.html.erb_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'seeds/new' do + before do + assign(:seed, build(:better_together_seed)) + end + + it 'renders new seed form' do # rubocop:todo RSpec/NoExpectationExample + # render + + # assert_select "form[action=?][method=?]", seeds_path, "post" do + # end + end +end diff --git a/spec/views/better_together/seeds/show.html.erb_spec.rb b/spec/views/better_together/seeds/show.html.erb_spec.rb new file mode 100644 index 000000000..b87226fcb --- /dev/null +++ b/spec/views/better_together/seeds/show.html.erb_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'seeds/show' do + before do + assign(:seed, create(:better_together_seed)) + end + + it 'renders attributes in

' do # rubocop:todo RSpec/NoExpectationExample + # render + end +end