diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 2c0280866..000000000 --- a/.coveralls.yml +++ /dev/null @@ -1,2 +0,0 @@ - -service_name: travis-pro diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ba81eac26..20c331c66 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -269,3 +269,28 @@ Note: The helper set lives under `spec/support/automatic_test_configuration.rb` expect(response).to redirect_to(person_blocks_path) ``` - **Factory Requirements**: Every Better Together model needs a corresponding FactoryBot factory with proper engine namespace handling + +### RSpec Best Practices +- **Named subjects for explicit references**: When using `expect(subject)` with complex matchers (like `have_many`), always define a named subject in the describe block: + ```ruby + describe 'associations' do + subject(:model_name) { build(:factory_name) } + + it { is_expected.to belong_to(:association) } + + it do + expect(model_name).to have_many(:items) + .class_name('Namespace::Item') + .dependent(:destroy) + end + end + ``` +- **Pending tests require reasons**: Use `skip` inside `it` blocks with a descriptive reason instead of `xit`: + ```ruby + it 'complex feature requiring external service' do + skip 'External service not available in test environment' + # test code here + end + ``` +- **Use `is_expected.to` for simple one-line matchers**: Prefer implicit subject with `is_expected.to` for single-assertion tests +- **Use named subject for multi-line or complex matchers**: Define `subject(:name)` when tests need explicit subject references diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b6425b608..000000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -sudo: required -language: ruby -services: - - docker -env: - - DOCKER_COMPOSE_VERSION=1.22.0 CI=true TRAVIS=true -before_install: - - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" - - sudo apt-get update - - sudo apt-get -y install docker-ce - - sudo rm /usr/local/bin/docker-compose - - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - - chmod +x docker-compose - - sudo mv docker-compose /usr/local/bin - - echo "POSTGRES_USER=postgres" >> docker-env.conf - - echo "POSTGRES_PASSWORD=postgres" >> docker-env.conf - # - docker-compose up -d bundle - # - docker-compose up -d db - - docker-compose build -after_success: -- CI=true TRAVIS=true coveralls --verbose -script: - - docker-compose run app bundle - - docker-compose run app bash -c "cd ./spec/dummy && bundle exec rake db:setup" - - CI=true TRAVIS=true docker-compose run app bundle exec rspec diff --git a/AGENTS.md b/AGENTS.md index f754090af..c9210f208 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -276,6 +276,31 @@ Note: The helper set lives under `spec/support/automatic_test_configuration.rb` - **Integration**: Test complete user workflows and cross-model interactions. - **Feature Tests**: End-to-end stakeholder workflows validating acceptance criteria. +### RSpec Best Practices +- **Named subjects for explicit references**: When using `expect(subject)` with complex matchers (like `have_many`), always define a named subject in the describe block: + ```ruby + describe 'associations' do + subject(:model_name) { build(:factory_name) } + + it { is_expected.to belong_to(:association) } + + it do + expect(model_name).to have_many(:items) + .class_name('Namespace::Item') + .dependent(:destroy) + end + end + ``` +- **Pending tests require reasons**: Use `skip` inside `it` blocks with a descriptive reason instead of `xit`: + ```ruby + it 'complex feature requiring external service' do + skip 'External service not available in test environment' + # test code here + end + ``` +- **Use `is_expected.to` for simple one-line matchers**: Prefer implicit subject with `is_expected.to` for single-assertion tests +- **Use named subject for multi-line or complex matchers**: Define `subject(:name)` when tests need explicit subject references + ## TDD Test Types by Stakeholder Need ### End User-Focused Tests diff --git a/Gemfile.lock b/Gemfile.lock index 7bd229b29..071fe185a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -55,6 +55,7 @@ PATH rack-cors (>= 1.1.1, < 3.1.0) rack-mini-profiler rails (>= 7.2, < 8.1) + redcarpet (~> 3.6) reform-rails (>= 0.2, < 0.4) rswag (>= 2.3.1, < 2.18.0) ruby-openai @@ -609,6 +610,7 @@ GEM erb psych (>= 4.0.0) tsort + redcarpet (3.6.1) redis (5.4.1) redis-client (>= 0.22.0) redis-client (0.26.1) diff --git a/app/assets/stylesheets/better_together/content_blocks.scss b/app/assets/stylesheets/better_together/content_blocks.scss index 6e3e0fe38..aff07cf41 100644 --- a/app/assets/stylesheets/better_together/content_blocks.scss +++ b/app/assets/stylesheets/better_together/content_blocks.scss @@ -241,3 +241,197 @@ margin-left: 5px; } } + +// Markdown content styling - GitHub-flavored markdown styles +.markdown-content { + line-height: 1.6; + color: #24292e; + + // Headings + h1, h2, h3, h4, h5, h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + border-bottom: 1px solid #eaecef; + padding-bottom: 0.3em; + } + + h1 { font-size: 2em; } + h2 { font-size: 1.5em; } + h3 { font-size: 1.25em; } + h4 { font-size: 1em; } + h5 { font-size: 0.875em; } + h6 { font-size: 0.85em; color: #6a737d; } + + // Paragraphs and text + p { + margin-top: 0; + margin-bottom: 16px; + } + + // Links + a { + color: #0366d6; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + // Lists + ul, ol { + padding-left: 2em; + margin-top: 0; + margin-bottom: 16px; + } + + li { + margin-bottom: 0.25em; + + > p { + margin-top: 16px; + } + + + li { + margin-top: 0.25em; + } + } + + // Blockquotes + blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5; + margin: 0 0 16px 0; + + > :first-child { + margin-top: 0; + } + + > :last-child { + margin-bottom: 0; + } + } + + // Code blocks + pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f6f8fa; + border-radius: 6px; + margin-bottom: 16px; + + code { + display: inline; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; + } + } + + // Inline code + code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27, 31, 35, 0.05); + border-radius: 6px; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + } + + // Tables + table { + border-spacing: 0; + border-collapse: collapse; + margin-top: 0; + margin-bottom: 16px; + width: 100%; + overflow: auto; + + th { + font-weight: 600; + padding: 6px 13px; + border: 1px solid #dfe2e5; + background-color: #f6f8fa; + } + + td { + padding: 6px 13px; + border: 1px solid #dfe2e5; + } + + tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; + + &:nth-child(2n) { + background-color: #f6f8fa; + } + } + } + + // Horizontal rules + hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; + } + + // Task lists + .task-list-item { + list-style-type: none; + + input { + margin: 0 0.2em 0.25em -1.6em; + vertical-align: middle; + } + } + + // Images + img { + max-width: 100%; + box-sizing: content-box; + background-color: #fff; + border-radius: 6px; + } + + // Strikethrough + del { + text-decoration: line-through; + } + + // Superscript + sup { + vertical-align: super; + font-size: smaller; + } + + // Highlight + mark { + background-color: #fff740; + padding: 0.2em; + } + + // Footnotes + .footnotes { + margin-top: 32px; + padding-top: 16px; + border-top: 1px solid #e1e4e8; + font-size: 0.9em; + color: #6a737d; + + ol { + padding-left: 1.5em; + } + } +} diff --git a/app/builders/better_together/documentation_builder.rb b/app/builders/better_together/documentation_builder.rb new file mode 100644 index 000000000..1a2c34ca5 --- /dev/null +++ b/app/builders/better_together/documentation_builder.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +# app/builders/better_together/documentation_builder.rb + +module BetterTogether + # Builds documentation navigation from markdown files in the docs/ directory + class DocumentationBuilder < Builder # rubocop:todo Metrics/ClassLength + class << self + def build # rubocop:todo Metrics/MethodLength, Metrics/AbcSize + I18n.with_locale(:en) do + entries = documentation_entries + return if entries.blank? + + area = if (existing_area = ::BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation')) + existing_area.navigation_items.delete_all + existing_area.update!(name: 'Documentation', visible: true, protected: true) + existing_area + else + ::BetterTogether::NavigationArea.create! do |area| + area.name = 'Documentation' + area.slug = 'documentation' + area.visible = true + area.protected = true + end + end + + entries.each_with_index do |entry, index| + create_documentation_navigation_item(area, entry, index) + end + + area.reload.save! + end + end + + private + + def documentation_entries + root = documentation_root + return [] unless root.directory? + + build_documentation_entries(root) + end + + def build_documentation_entries(current_path) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength + documentation_child_paths(current_path).filter_map do |child| + if child.directory? + children = build_documentation_entries(child) + default_path = default_documentation_file(child) + next if children.blank? && default_path.blank? + + { + type: :directory, + title: documentation_title(child.basename.to_s), + slug: documentation_slug(child), + default_path: default_path, + children: children + } + elsif markdown_file?(child) + { + type: :file, + title: documentation_title(child.basename.sub_ext('').to_s), + slug: documentation_slug(child), + path: documentation_relative_path(child), + children: [] + } + end + end + end + + def documentation_child_paths(path) + Dir.children(path).sort.map { |child| path.join(child) }.select do |child_path| + next false if child_path.basename.to_s.start_with?('.') + + child_path.directory? || markdown_file?(child_path) + end + end + + def markdown_file?(path) + path.file? && path.extname.casecmp('.md').zero? + end + + def create_documentation_navigation_item(area, entry, position, parent: nil) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength + attributes = { + navigation_area: area, + title_en: entry[:title], + position: position, + visible: true, + protected: true, + parent: + } + attributes[:identifier] = entry[:slug] if entry[:slug].present? + + if entry[:type] == :directory + attributes[:item_type] = 'dropdown' + if entry[:default_path].present? + attributes[:linkable] = documentation_page_for(entry[:title], entry[:default_path], area) + else + attributes[:url] = '#' + end + item = create_documentation_item_with_context(area, attributes) + entry[:children].each_with_index do |child, index| + create_documentation_navigation_item(area, child, index, parent: item) + end + else + attributes[:item_type] = 'link' + attributes[:linkable] = documentation_page_for(entry[:title], entry[:path], area) + create_documentation_item_with_context(area, attributes) + end + end + + def documentation_title(name) + base = name.to_s.sub(/\.md\z/i, '') + return 'Overview' if base.casecmp('readme').zero? + + base.tr('_-', ' ').squeeze(' ').strip.titleize + end + + def documentation_relative_path(path) + path.relative_path_from(documentation_root).to_s + end + + def documentation_url(relative_path) + File.join(documentation_url_prefix, relative_path) + end + + def create_documentation_item_with_context(area, attributes) + puts "Creating documentation navigation item #{attributes.inspect}" if ENV['DEBUG_DOCUMENTATION_NAV'] == '1' + area.navigation_items.create!(attributes) + rescue ActiveRecord::RecordInvalid => e + raise ActiveRecord::RecordInvalid.new(e.record), "#{e.message} -- #{attributes.inspect}" + end + + def documentation_page_for(title, relative_path, sidebar_nav_area = nil) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength + slug = documentation_slug(relative_path) + attrs = documentation_page_attributes(title, slug, relative_path, sidebar_nav_area) + page = ::BetterTogether::Page.i18n.find_by(slug: slug) + + if page + locked_page = ::BetterTogether::Page.lock.find(page.id) + locked_page.page_blocks.destroy_all + locked_page.reload + locked_page.assign_attributes(attrs) + locked_page.save! + # Re-set the slug after save in case FriendlyId regenerated it + locked_page.update_columns(slug: slug) if locked_page.slug != slug + locked_page + else + new_page = ::BetterTogether::Page.create!(attrs) + # Re-set the slug after creation in case FriendlyId regenerated it + new_page.slug = slug if new_page.slug != slug + new_page.save!(validate: false) if new_page.changed? + new_page + end + end + + def documentation_page_attributes(title, slug, relative_path, sidebar_nav_area = nil) # rubocop:todo Metrics/MethodLength + attrs = { + title_en: title, + slug_en: slug, # Set slug directly via Mobility to bypass FriendlyId normalization + published_at: Time.zone.now, + privacy: 'public', + protected: true, + layout: 'layouts/better_together/full_width_page', + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Markdown', + markdown_file_path: documentation_file_path(relative_path) + } + } + ] + } + + # Associate the documentation navigation area as the sidebar nav + attrs[:sidebar_nav] = sidebar_nav_area if sidebar_nav_area.present? + + attrs + end + + def documentation_slug(path) + relative = path.is_a?(Pathname) ? documentation_relative_path(path) : path.to_s + # Remove .md extension, downcase, and preserve directory structure with slashes + base_slug = relative.sub(/\.md\z/i, '').downcase.tr('_', '-') + base_slug = 'overview' if base_slug.blank? + "docs/#{base_slug}" + end + + def documentation_file_path(relative_path) + documentation_root.join(relative_path).to_s + end + + def default_documentation_file(path) + %w[README.md readme.md index.md INDEX.md].each do |filename| + file_path = path.join(filename) + return documentation_relative_path(file_path) if file_path.exist? + end + nil + end + + def documentation_root + BetterTogether::Engine.root.join('docs') + end + + def documentation_url_prefix + '/docs' + end + end + end +end diff --git a/app/builders/better_together/navigation_builder.rb b/app/builders/better_together/navigation_builder.rb index 13b224148..d846785d8 100644 --- a/app/builders/better_together/navigation_builder.rb +++ b/app/builders/better_together/navigation_builder.rb @@ -12,6 +12,7 @@ def seed_data build_host build_better_together build_footer + # DocumentationBuilder.build # TODO: Re-enable when documentation is ready create_unassociated_pages end @@ -29,7 +30,14 @@ def build_better_together # rubocop:todo Metrics/MethodLength, Metrics/AbcSize published_at: Time.zone.now, privacy: 'public', protected: true, - template: 'better_together/static_pages/better_together' + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/better_together' + } + } + ] }, { title_en: 'About the Community Engine', @@ -37,8 +45,15 @@ def build_better_together # rubocop:todo Metrics/MethodLength, Metrics/AbcSize published_at: Time.zone.now, privacy: 'public', protected: true, - template: 'better_together/static_pages/community_engine', - layout: 'layouts/better_together/full_width_page' + layout: 'layouts/better_together/full_width_page', + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/community_engine' + } + } + ] } ] ) @@ -80,7 +95,14 @@ def build_footer # rubocop:todo Metrics/MethodLength, Metrics/AbcSize published_at: Time.zone.now, privacy: 'public', protected: true, - template: 'better_together/static_pages/faq' + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/faq' + } + } + ] }, { title_en: 'Privacy Policy', @@ -88,7 +110,14 @@ def build_footer # rubocop:todo Metrics/MethodLength, Metrics/AbcSize published_at: Time.zone.now, privacy: 'public', protected: true, - template: 'better_together/static_pages/privacy' + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/privacy' + } + } + ] }, { title_en: 'Terms of Service', @@ -96,7 +125,14 @@ def build_footer # rubocop:todo Metrics/MethodLength, Metrics/AbcSize published_at: Time.zone.now, privacy: 'public', protected: true, - template: 'better_together/static_pages/terms_of_service' + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/terms_of_service' + } + } + ] }, { title_en: 'Code of Conduct', @@ -104,7 +140,14 @@ def build_footer # rubocop:todo Metrics/MethodLength, Metrics/AbcSize published_at: Time.zone.now, privacy: 'public', protected: true, - template: 'better_together/static_pages/code_of_conduct' + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/code_of_conduct' + } + } + ] }, { title_en: 'Accessibility', @@ -112,7 +155,29 @@ def build_footer # rubocop:todo Metrics/MethodLength, Metrics/AbcSize published_at: Time.zone.now, privacy: 'public', protected: true, - template: 'better_together/static_pages/accessibility' + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/accessibility' + } + } + ] + }, + { + title_en: 'Cookie Policy', + slug_en: 'cookie-policy', + published_at: Time.zone.now, + privacy: 'public', + protected: true, + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/cookie_consent' + } + } + ] }, { title_en: 'Contact', @@ -124,10 +189,66 @@ def build_footer # rubocop:todo Metrics/MethodLength, Metrics/AbcSize { block_attributes: { type: 'BetterTogether::Content::RichText', + # rubocop:todo Lint/CopDirectiveSyntax + # rubocop:todo Metrics/MethodLength + # rubocop:todo Metrics/MethodLength + # rubocop:todo Lint/CopDirectiveSyntax + # rubocop:todo Lint/CopDirectiveSyntax + # rubocop:todo Lint/CopDirectiveSyntax + # rubocop:todo Lint/CopDirectiveSyntax content_en: <<-HTML

Contact Us

This is a default contact page for your platform. Be sure to write a real one!

HTML + # rubocop:enable Lint/CopDirectiveSyntax + # rubocop:enable Lint/CopDirectiveSyntax + # rubocop:enable Lint/CopDirectiveSyntax + # rubocop:enable Lint/CopDirectiveSyntax + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/MethodLength + # rubocop:enable Lint/CopDirectiveSyntax + } + }, + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/content/blocks/template/host_community_contact_details' + } + } + ] + } + ] + ) + + # Create contributor agreement pages separately for nested navigation + contributor_agreement_pages = ::BetterTogether::Page.create!( + [ + { + title_en: 'Code Contributor Agreement', + slug_en: 'code-contributor-agreement', + published_at: Time.zone.now, + privacy: 'public', + protected: true, + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/code_contributor_agreement' + } + } + ] + }, + { + title_en: 'Content Contributor Agreement', + slug_en: 'content-contributor-agreement', + published_at: Time.zone.now, + privacy: 'public', + protected: true, + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/content_contributor_agreement' } } ] @@ -143,8 +264,33 @@ def build_footer # rubocop:todo Metrics/MethodLength, Metrics/AbcSize area.protected = true end + # Build navigation items for main footer pages area.reload.build_page_navigation_items(footer_pages) + # Create parent "Contributor Agreements" dropdown navigation item + # Position it after the existing footer items + next_position = area.navigation_items.maximum(:position).to_i + 1 + contributor_agreements_parent = area.navigation_items.create!( + title_en: 'Contributor Agreements', + item_type: 'dropdown', + position: next_position, + visible: true, + protected: true + ) + + # Build child navigation items for contributor agreements + contributor_agreement_pages.each_with_index do |page, index| + area.navigation_items.create!( + title_en: page.title, + linkable: page, + parent: contributor_agreements_parent, + position: index, + item_type: 'link', + visible: true, + protected: true + ) + end + area.save! end end @@ -314,8 +460,60 @@ def clear_existing delete_navigation_areas end - def create_unassociated_pages # rubocop:todo Metrics/MethodLength + # Reset and reseed navigation areas only (preserves pages) + # Usage: BetterTogether::NavigationBuilder.reset_navigation_areas + def reset_navigation_areas + puts 'Resetting navigation areas...' + delete_navigation_items + delete_navigation_areas + puts 'Rebuilding navigation areas...' I18n.with_locale(:en) do + build_header + build_host + build_better_together + build_footer + # DocumentationBuilder.build # TODO: Re-enable when documentation is ready + end + puts 'Navigation areas reset complete!' + end + + # Reset and reseed a specific navigation area + # Usage: BetterTogether::NavigationBuilder.reset_navigation_area('platform-header') + def reset_navigation_area(slug) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength, Lint/CopDirectiveSyntax, Metrics/MethodLength + area = ::BetterTogether::NavigationArea.i18n.find_by(slug: slug) + if area + puts "Resetting navigation area: #{area.name} (#{slug})" + # Delete items for this area + area.navigation_items.where.not(parent_id: nil).delete_all + area.navigation_items.where(parent_id: nil).delete_all + area.delete + + # Rebuild the specific area + I18n.with_locale(:en) do + case slug + when 'platform-header' + build_header + when 'platform-host' + build_host + when 'better-together' + build_better_together + when 'platform-footer' + build_footer + when 'documentation' + DocumentationBuilder.build # Available but not auto-seeded + else + puts "Unknown navigation area slug: #{slug}" + return + end + end + puts "Navigation area '#{slug}' reset complete!" + else + puts "Navigation area with slug '#{slug}' not found" + end + end + + def create_unassociated_pages # rubocop:todo Metrics/MethodLength + I18n.with_locale(:en) do # rubocop:todo Metrics/BlockLength # Create Pages not associated with a navigation area ::BetterTogether::Page.create!( [ @@ -325,8 +523,15 @@ def create_unassociated_pages # rubocop:todo Metrics/MethodLength published_at: Time.zone.now, privacy: 'public', protected: true, - template: 'better_together/static_pages/community_engine', - layout: 'layouts/better_together/full_width_page' + layout: 'layouts/better_together/full_width_page', + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/community_engine' + } + } + ] }, { title_en: 'Subprocessors', @@ -334,7 +539,14 @@ def create_unassociated_pages # rubocop:todo Metrics/MethodLength published_at: Time.zone.now, privacy: 'public', protected: true, - template: 'better_together/static_pages/subprocessors' + page_blocks_attributes: [ + { + block_attributes: { + type: 'BetterTogether::Content::Template', + template_path: 'better_together/static_pages/subprocessors' + } + } + ] } ] ) @@ -348,6 +560,8 @@ def delete_pages end def delete_navigation_areas + # Clear sidebar_nav references before deleting navigation areas + ::BetterTogether::Page.update_all(sidebar_nav_id: nil) ::BetterTogether::NavigationArea.delete_all end diff --git a/app/controllers/better_together/content/blocks_controller.rb b/app/controllers/better_together/content/blocks_controller.rb index 4999ebcbd..71e62e2e5 100644 --- a/app/controllers/better_together/content/blocks_controller.rb +++ b/app/controllers/better_together/content/blocks_controller.rb @@ -7,6 +7,7 @@ class BlocksController < ResourceController before_action :authenticate_user! before_action :disallow_robots before_action :set_block, only: %i[show edit update destroy] + skip_after_action :verify_authorized, only: [:preview_markdown] before_action only: %i[index], if: -> { Rails.env.development? } do # Make sure that all BLock subclasses are loaded in dev to generate new block buttons resource_class.load_all_subclasses @@ -54,14 +55,46 @@ def destroy redirect_to content_blocks_path, notice: t('flash.generic.destroyed', resource: t('resources.block')) end + def preview_markdown # rubocop:todo Metrics/MethodLength + markdown_content = params[:markdown] + + if markdown_content.blank? + render json: { html: '

Preview will appear here...

' } + return + end + + begin + rendered_html = MarkdownRendererService.new(markdown_content).render_html + render json: { html: rendered_html } + rescue StandardError => e + Rails.logger.error "Markdown preview error: #{e.message}" + render json: { + html: '
' \ + 'Failed to render preview. Please check your markdown syntax.
' + }, status: :unprocessable_entity + end + end + private def block_params - params.require(:block).permit( - :type, :media, :identifier, + permitted_params = params.require(:block).permit( + :type, :media, :identifier, :markdown_source_type, *resource_class.localized_block_attributes, *resource_class.storext_keys ) + + # Handle markdown_source_type: clear the unused field + if permitted_params[:markdown_source_type].present? + if permitted_params[:markdown_source_type] == 'inline' + permitted_params.delete(:markdown_file_path) + elsif permitted_params[:markdown_source_type] == 'file' + permitted_params.delete(:markdown_source) + end + permitted_params.delete(:markdown_source_type) # Remove the helper param + end + + permitted_params end def set_block diff --git a/app/controllers/concerns/better_together/wizard_methods.rb b/app/controllers/concerns/better_together/wizard_methods.rb index 4c9b8dabb..f92e33dc1 100644 --- a/app/controllers/concerns/better_together/wizard_methods.rb +++ b/app/controllers/concerns/better_together/wizard_methods.rb @@ -28,8 +28,13 @@ def determine_wizard_outcome # rubocop:todo Metrics/AbcSize # rubocop:todo Metrics/MethodLength def find_or_create_wizard_step # rubocop:todo Metrics/AbcSize, Metrics/MethodLength - # Identify the next uncompleted step definition - step_definition = wizard.wizard_step_definitions.ordered.detect do |sd| + # If wizard_step_definition_id is in params (from route defaults), use that specific step + if wizard_step_definition_identifier.present? + step_definition = wizard.wizard_step_definitions.find_by(identifier: wizard_step_definition_identifier) + end + + # Otherwise, identify the next uncompleted step definition + step_definition ||= wizard.wizard_step_definitions.ordered.detect do |sd| !wizard.wizard_steps.exists?(identifier: sd.identifier, completed: true) end diff --git a/app/helpers/better_together/hub_helper.rb b/app/helpers/better_together/hub_helper.rb index 2ccc2c110..0efa19b06 100644 --- a/app/helpers/better_together/hub_helper.rb +++ b/app/helpers/better_together/hub_helper.rb @@ -23,10 +23,10 @@ def whose?(user, object) # rubocop:todo Metrics/MethodLength, Naming/PredicateMe object.creator end if user && owner - if user.id == owner.id + if user.person&.id == owner.id 'his' else - "#{owner.nickname}'s" + "#{owner.name}'s" end else '' diff --git a/app/helpers/better_together/markdown_helper.rb b/app/helpers/better_together/markdown_helper.rb new file mode 100644 index 000000000..f0259ee56 --- /dev/null +++ b/app/helpers/better_together/markdown_helper.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module BetterTogether + # Helper methods for rendering markdown content + module MarkdownHelper + # Renders markdown source to HTML + # + # @param source [String] The markdown source text + # @param options [Hash] Optional rendering options to pass to MarkdownRendererService + # @return [ActiveSupport::SafeBuffer] HTML-safe rendered markdown + # + # @example + # <%= render_markdown("# Hello\n\nThis is **bold** text.") %> + def render_markdown(source, options = {}) + return '' if source.blank? + + MarkdownRendererService.new(source, options).render_html + end + + # Renders markdown from a file path to HTML + # + # @param file_path [String] Path to the markdown file (absolute or relative to Rails.root) + # @param options [Hash] Optional rendering options to pass to MarkdownRendererService + # @return [ActiveSupport::SafeBuffer] HTML-safe rendered markdown + # + # @example + # <%= render_markdown_file('docs/README.md') %> + def render_markdown_file(file_path, options = {}) + return '' if file_path.blank? + + # Resolve relative paths to Rails.root + full_path = if Pathname.new(file_path).absolute? + file_path + else + Rails.root.join(file_path).to_s + end + + return '' unless File.exist?(full_path) + + markdown_source = File.read(full_path) + render_markdown(markdown_source, options) + rescue Errno::ENOENT => e + Rails.logger.error("Failed to read markdown file: #{e.message}") + '' + end + + # Renders markdown to plain text (strips HTML) + # + # @param source [String] The markdown source text + # @param options [Hash] Optional rendering options to pass to MarkdownRendererService + # @return [String] Plain text without HTML tags + # + # @example + # <%= render_markdown_plain("# Hello\n\nThis is **bold** text.") %> + def render_markdown_plain(source, options = {}) + return '' if source.blank? + + MarkdownRendererService.new(source, options).render_plain_text + end + + # Renders markdown with automatic locale detection for file paths + # + # @param file [String, nil] Path to markdown file (with automatic locale detection) + # @param text [String, nil] Direct markdown text content + # @param locale [Symbol] Locale to use (defaults to current locale) + # @param options [Hash] Optional rendering options to pass to MarkdownRendererService + # @return [ActiveSupport::SafeBuffer] HTML-safe rendered markdown + # + # @example + # <%= render_markdown_block(file: 'policies/privacy') %> + # <%= render_markdown_block(text: t('content.welcome')) %> + def render_markdown_block(file: nil, text: nil, locale: I18n.locale, options: {}) + I18n.with_locale(locale) do + content = if file.present? + read_localized_file(file) + else + text + end + + render_markdown(content, options) + end + end + + private + + # Read a localized markdown file with automatic locale detection + # + # @param base_path [String] Base path to the markdown file (without locale extension) + # @return [String] File content or empty string if not found + def read_localized_file(base_path) + # Remove .md extension if present + base = base_path.sub(/\.(md|markdown)$/i, '') + + # Try current locale, then fallback to English, then original + [ + "#{base}.#{I18n.locale}.md", + "#{base}.en.md", + "#{base}.md" + ].each do |filename| + path = Rails.root.join('app', 'views', filename) + return File.read(path) if File.exist?(path) + end + + Rails.logger.warn("Markdown file not found: #{base_path}") + '' + rescue Errno::ENOENT => e + Rails.logger.error("Failed to read localized markdown file: #{e.message}") + '' + end + end +end diff --git a/app/javascript/controllers/better_together/dependent_fields_controller.js b/app/javascript/controllers/better_together/dependent_fields_controller.js index eab15ddc4..ccae82706 100644 --- a/app/javascript/controllers/better_together/dependent_fields_controller.js +++ b/app/javascript/controllers/better_together/dependent_fields_controller.js @@ -31,20 +31,22 @@ export default class extends Controller { if (controlFieldIds && controlFieldIds.length > 0) { // If there are multiple control fields const allConditionsMet = controlFieldIds.every(controlFieldId => { - const controlField = document.getElementById(controlFieldId.trim()); // Find control field by ID - const showIfValue = field.dataset[`showIfControl_${controlFieldId.trim()}`]; // Get showIfValue for this control field + const trimmedControlId = controlFieldId.trim(); + const controlField = document.getElementById(trimmedControlId); // Find control field by ID + const showIfValue = this.showIfValueFor(field, trimmedControlId); // Get showIfValue for this control field if (!controlField) { console.error(`Error: Control field with ID '${controlFieldId}' not found.`); return false; } - const valueSet = controlField.value !== null && controlField.value !== ""; // Check if any value is set + const controlValue = this.controlFieldValue(controlField); + const valueSet = this.controlFieldHasValue(controlField); return ( (showIfValue === "*present*" && valueSet) || // Show field if *present* and a value is set (showIfValue === "*not_present*" && !valueSet) || // Show field if *not_present* and no value is set - (showIfValue === controlField.value) // Or show field if specific value matches + (showIfValue === controlValue) // Or show field if specific value matches ); }); @@ -58,7 +60,8 @@ export default class extends Controller { } else if (controlFieldCount === 1) { // Backward compatibility: Use the single control field if only one is present const controlField = this.controlFieldTargets[0]; - const valueSet = controlField.value !== null && controlField.value !== ""; // Check if any value is set + const controlValue = this.controlFieldValue(controlField); + const valueSet = this.controlFieldHasValue(controlField); const showIfValue = field.dataset.showIfValue; // Use the original showIfValue syntax const showUnlessValue = field.dataset.showUnlessValue; // Use the original showUnlessValue syntax @@ -67,11 +70,11 @@ export default class extends Controller { if (showIfValue) { showDependent = (showIfValue === "*present*" && valueSet) || (showIfValue === "*not_present*" && !valueSet) || - (showIfValue === controlField.value) + (showIfValue === controlValue) } else { showDependent = (showUnlessValue === "*present*" && !valueSet) || (showUnlessValue === "*not_present*" && valueSet) || - (showUnlessValue != controlField.value) + (showUnlessValue != controlValue) } if ( @@ -88,4 +91,33 @@ export default class extends Controller { } }); } + + controlFieldValue(controlField) { + if (!controlField) return ""; + + if (controlField.type === "checkbox") { + return controlField.checked ? (controlField.value || "on") : ""; + } + + if (controlField.type === "radio") { + return controlField.checked ? controlField.value : ""; + } + + return controlField.value ?? ""; + } + + controlFieldHasValue(controlField) { + if (!controlField) return false; + + if (controlField.type === "checkbox" || controlField.type === "radio") { + return controlField.checked; + } + + return controlField.value !== null && controlField.value !== ""; + } + + showIfValueFor(field, controlFieldId) { + const datasetKey = `showIfControl_${controlFieldId}` + return field.dataset[datasetKey] ?? field.getAttribute(`data-show-if-control_${controlFieldId}`) + } } diff --git a/app/javascript/controllers/better_together/markdown_block_controller.js b/app/javascript/controllers/better_together/markdown_block_controller.js new file mode 100644 index 000000000..e131896e8 --- /dev/null +++ b/app/javascript/controllers/better_together/markdown_block_controller.js @@ -0,0 +1,143 @@ +// frozen_string_literal: true + +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="better-together--markdown-block" +export default class extends Controller { + static targets = [ + "sourceTypeRadio", + "inlineField", + "fileField", + "sourceTextarea", + "filePathInput", + "preview" + ] + + connect() { + this.previousSourceType = this.selectedSourceType + } + + get locale() { + try { + if (typeof I18n !== 'undefined' && I18n && I18n.locale) return I18n.locale + } catch (e) {} + try { + const htmlLang = document.documentElement.getAttribute('lang') + if (htmlLang) return htmlLang + } catch (e) {} + return 'en' + } + + get routeScopePath() { + try { + if (typeof BetterTogether !== 'undefined' && BetterTogether && BetterTogether.route_scope_path) return BetterTogether.route_scope_path + } catch (e) {} + try { + if (this.element && this.element.dataset && this.element.dataset.routeScopePath) return this.element.dataset.routeScopePath + } catch (e) {} + return '' + } + + handleSourceTypeChange() { + const selectedType = this.selectedSourceType + if (!selectedType) return + + if (selectedType !== this.previousSourceType) { + if (selectedType === 'inline' && this.hasFilePathInputTarget) { + this.filePathInputTarget.value = '' + } else if (selectedType === 'file' && this.hasSourceTextareaTarget) { + this.sourceTextareaTarget.value = '' + } + + this.previousSourceType = selectedType + } + + this.refreshPreview() + } + + async refreshPreview() { + if (!this.hasPreviewTarget) return + + const selectedType = this.selectedSourceType + let markdownContent = '' + + if (selectedType === 'inline' && this.hasSourceTextareaTarget) { + markdownContent = this.sourceTextareaTarget.value + } else if (selectedType === 'file' && this.hasFilePathInputTarget) { + const filePath = this.filePathInputTarget.value + if (filePath) { + // For file paths, we'd need to make an AJAX request to render the preview + // For now, show a placeholder + this.previewTarget.innerHTML = ` +

+ Preview for file: ${this.escapeHtml(filePath)} +

+

+ File preview will be available after saving. +

+ ` + return + } + } + + if (markdownContent.trim() === '') { + this.previewTarget.innerHTML = ` +

+ Preview will appear here... +

+ ` + return + } + + // Render markdown preview using a simple client-side markdown library + // or make an AJAX call to the server to render it + try { + const response = await this.renderMarkdown(markdownContent) + this.previewTarget.innerHTML = response + } catch (error) { + console.error('Failed to render markdown preview:', error) + this.previewTarget.innerHTML = ` +
+ + Failed to render preview. Please check your markdown syntax. +
+ ` + } + } + + async renderMarkdown(content) { + // Make an AJAX request to render the markdown on the server + const locale = this.locale + const routeScope = this.routeScopePath + const scopeSegment = routeScope ? `/${routeScope}` : '' + const response = await fetch(`/${locale}${scopeSegment}/content/blocks/preview_markdown`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': this.csrfToken + }, + body: JSON.stringify({ markdown: content }) + }) + + if (!response.ok) { + throw new Error('Failed to render markdown') + } + + const data = await response.json() + return data.html + } + + get selectedSourceType() { + return this.sourceTypeRadioTargets.find(radio => radio.checked)?.value + } + + get csrfToken() { + return document.querySelector('meta[name="csrf-token"]')?.content || '' + } + + escapeHtml(text) { + const div = document.createElement('div') + div.textContent = text + return div.innerHTML + } +} diff --git a/app/models/better_together/agreement.rb b/app/models/better_together/agreement.rb index bfdc5c046..cb2eede50 100644 --- a/app/models/better_together/agreement.rb +++ b/app/models/better_together/agreement.rb @@ -18,7 +18,7 @@ class Agreement < ApplicationRecord accepts_nested_attributes_for :agreement_terms, reject_if: :all_blank, allow_destroy: true - translates :title + translates :title, type: :string translates :description, backend: :action_text def self.permitted_attributes(id: false, destroy: false) diff --git a/app/models/better_together/content/markdown.rb b/app/models/better_together/content/markdown.rb new file mode 100644 index 000000000..3fc950e2e --- /dev/null +++ b/app/models/better_together/content/markdown.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +module BetterTogether + module Content + # Renders markdown content from source or file + class Markdown < Block # rubocop:disable Metrics/ClassLength + include Translatable + + translates :markdown_source, type: :text + + store_attributes :content_data do + markdown_file_path String + auto_sync_from_file Boolean, default: false + end + + validate :markdown_source_or_file_path_present + validate :file_must_exist, if: :markdown_file_path? + validate :file_must_be_markdown, if: :markdown_file_path? + + # Load file content before validation if file path changed + before_validation :load_file_into_source, + if: -> { markdown_file_path_changed? && auto_sync_from_file? } + + # Define permitted attributes for controller strong parameters + def self.permitted_attributes + %i[markdown_source markdown_file_path auto_sync_from_file] + end + + # Get markdown content from either source or file + def content + return markdown_source if markdown_source.present? && !auto_sync_from_file? + return load_markdown_file_for_current_locale if markdown_file_path.present? + + '' + end + + # Manually import file content into markdown_source for all locales + def import_file_content!(sync_future_changes: false) + return false unless markdown_file_path.present? + + load_localized_files + self.auto_sync_from_file = sync_future_changes + save! + end + + # Render markdown content as HTML + def rendered_html + return '' if content.blank? + + BetterTogether::MarkdownRendererService.new(content, {}).render_html + end + + # Render markdown content as plain text for indexing + def rendered_plain_text + return '' if content.blank? + + BetterTogether::MarkdownRendererService.new(content, {}).render_plain_text + end + + # Provide indexed JSON representation for search + def as_indexed_json(_options = {}) + { + id:, + localized_content: I18n.available_locales.index_with do |locale| + I18n.with_locale(locale) do + rendered_plain_text + end + end + } + end + + private + + def markdown_source_or_file_path_present + return if markdown_source.present? || markdown_file_path.present? + + errors.add(:base, 'Either markdown source or file path must be provided') + end + + def load_file_into_source + load_localized_files + end + + def load_localized_files # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + base_path = markdown_file_path.sub(/\.(md|markdown)$/i, '') + + I18n.available_locales.each do |locale| + load_locale_file(base_path, locale) + end + end + + def load_locale_file(base_path, locale) + # Try locale-specific file first + locale_file = "#{base_path}.#{locale}.md" + resolved_path = resolve_file_path_for(locale_file) + + if File.exist?(resolved_path) + set_markdown_for_locale(locale, resolved_path) + elsif locale == I18n.default_locale + load_default_file_for_locale(locale) + end + end + + def set_markdown_for_locale(locale, file_path) + I18n.with_locale(locale) do + self.markdown_source = File.read(file_path) + end + end + + def load_default_file_for_locale(locale) + default_path = resolve_file_path + return unless File.exist?(default_path) + + set_markdown_for_locale(locale, default_path) + end + + def load_markdown_file_for_current_locale + base_path = markdown_file_path.sub(/\.(md|markdown)$/i, '') + locale_file = "#{base_path}.#{I18n.locale}.md" + + # Try locale-specific, fallback to default + [locale_file, markdown_file_path].each do |file| + path = resolve_file_path_for(file) + return File.read(path) if File.exist?(path) + end + + '' + end + + def load_markdown_file + file_path = resolve_file_path + return '' unless File.exist?(file_path) + + File.read(file_path) + end + + def resolve_file_path + return Pathname.new(markdown_file_path) if Pathname.new(markdown_file_path).absolute? + + Rails.root.join(markdown_file_path) + end + + def resolve_file_path_for(path) + return Pathname.new(path) if Pathname.new(path).absolute? + + Rails.root.join(path) + end + + def file_must_exist + file_path = resolve_file_path + return if File.exist?(file_path) + + errors.add(:markdown_file_path, :file_not_found, path: markdown_file_path) + end + + def file_must_be_markdown + return if markdown_file_path.match?(/\.(md|markdown)$/i) + + errors.add(:markdown_file_path, :invalid_file_type) + end + end + end +end diff --git a/app/models/better_together/content/template.rb b/app/models/better_together/content/template.rb index 2b178a737..710949816 100644 --- a/app/models/better_together/content/template.rb +++ b/app/models/better_together/content/template.rb @@ -7,6 +7,17 @@ class Template < Block class_attribute :available_templates, default: %w[ better_together/content/blocks/template/default better_together/content/blocks/template/host_community_contact_details + better_together/static_pages/privacy + better_together/static_pages/terms_of_service + better_together/static_pages/code_of_conduct + better_together/static_pages/accessibility + better_together/static_pages/cookie_consent + better_together/static_pages/code_contributor_agreement + better_together/static_pages/content_contributor_agreement + better_together/static_pages/faq + better_together/static_pages/better_together + better_together/static_pages/community_engine + better_together/static_pages/subprocessors ] has_many :page_blocks, foreign_key: :block_id, dependent: :destroy @@ -17,6 +28,19 @@ class Template < Block end validates :template_path, presence: true, inclusion: { in: ->(template) { template.class.available_templates } } + + # Provide indexed JSON representation like RichText does + def as_indexed_json(_options = {}) + { + id:, + localized_content: indexed_localized_content + } + end + + # Extract text content from the template for search indexing + def indexed_localized_content + BetterTogether::TemplateRendererService.new(template_path).render_for_all_locales + end end end end diff --git a/app/models/better_together/geography/map.rb b/app/models/better_together/geography/map.rb index d0b9a2383..92dd52042 100644 --- a/app/models/better_together/geography/map.rb +++ b/app/models/better_together/geography/map.rb @@ -17,7 +17,7 @@ class Map < ApplicationRecord slugged :title - translates :title + translates :title, type: :string translates :description, backend: :action_text validates :center, presence: true diff --git a/app/models/better_together/page.rb b/app/models/better_together/page.rb index 2f7dcc0b1..3cfd2d2e9 100644 --- a/app/models/better_together/page.rb +++ b/app/models/better_together/page.rb @@ -27,11 +27,18 @@ class Page < ApplicationRecord has_many :page_blocks, -> { positioned }, dependent: :destroy, class_name: 'BetterTogether::Content::PageBlock' has_many :blocks, through: :page_blocks has_many :image_blocks, -> { where(type: 'BetterTogether::Content::Image') }, through: :page_blocks, source: :block + has_many :markdown_blocks, lambda { + where(type: 'BetterTogether::Content::Markdown') + }, through: :page_blocks, source: :block has_many :rich_text_blocks, lambda { where(type: 'BetterTogether::Content::RichText') }, through: :page_blocks, source: :block + has_many :template_blocks, lambda { + where(type: 'BetterTogether::Content::Template') + }, through: :page_blocks, source: :block belongs_to :sidebar_nav, class_name: 'BetterTogether::NavigationArea', optional: true + belongs_to :creator, class_name: 'BetterTogether::Person', optional: true accepts_nested_attributes_for :page_blocks, allow_destroy: true @@ -67,18 +74,33 @@ def content_blocks # Customize the data sent to Elasticsearch for indexing def as_indexed_json(_options = {}) # rubocop:todo Metrics/MethodLength - as_json( + json = as_json( only: [:id], methods: [:title, :name, :slug, *self.class.localized_attribute_list.keep_if do |a| a.starts_with?('title' || a.starts_with?('slug')) end], include: { + markdown_blocks: { + only: %i[id], + methods: [:as_indexed_json] + }, rich_text_blocks: { only: %i[id], methods: [:indexed_localized_content] + }, + template_blocks: { + only: %i[id], + methods: [:indexed_localized_content] } } ) + + # Include rendered template content if page has template attribute + if template.present? + json['template_content'] = BetterTogether::TemplateRendererService.new(template).render_for_all_locales + end + + json end def primary_image diff --git a/app/models/better_together/post.rb b/app/models/better_together/post.rb index aca9164ae..73b1b71cf 100644 --- a/app/models/better_together/post.rb +++ b/app/models/better_together/post.rb @@ -20,7 +20,7 @@ class Post < ApplicationRecord categorizable - translates :title + translates :title, type: :string alias name title translates :content, backend: :action_text diff --git a/app/models/better_together/user.rb b/app/models/better_together/user.rb index 8f89f3562..b6b442569 100644 --- a/app/models/better_together/user.rb +++ b/app/models/better_together/user.rb @@ -20,7 +20,9 @@ class User < ApplicationRecord ) }, as: :agent, - class_name: 'BetterTogether::Identification' + class_name: 'BetterTogether::Identification', + dependent: :destroy, + autosave: true has_one :person, through: :person_identification, @@ -70,6 +72,7 @@ def person_attributes=(attributes) else # Build new Person object if it doesn't exist build_person(attributes) + # The person is now accessible via person_identification.identity end end diff --git a/app/models/better_together/wizard_step.rb b/app/models/better_together/wizard_step.rb index 4b4bc1689..0104c230a 100644 --- a/app/models/better_together/wizard_step.rb +++ b/app/models/better_together/wizard_step.rb @@ -22,8 +22,7 @@ class WizardStep < ApplicationRecord # Method to mark the step as completed def mark_as_completed self.completed = true - # byebug - save + save! end private diff --git a/app/policies/better_together/content/markdown_policy.rb b/app/policies/better_together/content/markdown_policy.rb new file mode 100644 index 000000000..7b8867767 --- /dev/null +++ b/app/policies/better_together/content/markdown_policy.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module BetterTogether + module Content + class MarkdownPolicy < BlockPolicy + class Scope < BlockPolicy::Scope + end + end + end +end diff --git a/app/services/better_together/markdown_renderer_service.rb b/app/services/better_together/markdown_renderer_service.rb new file mode 100644 index 000000000..1a91a4d4c --- /dev/null +++ b/app/services/better_together/markdown_renderer_service.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'redcarpet' + +module BetterTogether + # Service to render markdown content to HTML and plain text + class MarkdownRendererService + attr_reader :markdown_source, :options + + def initialize(markdown_source, options = {}) + @markdown_source = markdown_source + @options = default_options.deep_merge(options) + end + + # Render markdown to HTML + def render_html + return ''.html_safe if markdown_source.blank? + + renderer.render(markdown_source).html_safe + end + + # Render markdown to plain text (for search indexing) + def render_plain_text + return '' if markdown_source.blank? + + # Strip HTML tags from rendered HTML + ActionView::Base.full_sanitizer.sanitize(render_html) + end + + private + + def renderer + @renderer ||= Redcarpet::Markdown.new( + html_renderer, + options[:extensions] + ) + end + + def html_renderer + Redcarpet::Render::HTML.new(options[:render_options]) + end + + def default_options # rubocop:todo Metrics/MethodLength + { + extensions: { + # Enable various markdown extensions + autolink: true, + tables: true, + fenced_code_blocks: true, + strikethrough: true, + superscript: true, + highlight: true, + footnotes: true, + no_intra_emphasis: true, + space_after_headers: true, + underline: true + }, + render_options: { + # Render options for HTML output + filter_html: false, # Allow HTML in markdown (for trusted content like docs) + hard_wrap: true, + link_attributes: { target: '_blank', rel: 'noopener noreferrer' }, + prettify: true, + with_toc_data: true # Add IDs to headers for table of contents + } + } + end + end +end diff --git a/app/services/better_together/template_renderer_service.rb b/app/services/better_together/template_renderer_service.rb new file mode 100644 index 000000000..bc8e41080 --- /dev/null +++ b/app/services/better_together/template_renderer_service.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module BetterTogether + # Service to render ERB templates and extract plain text for search indexing + class TemplateRendererService + attr_reader :template_path + + def initialize(template_path) + @template_path = template_path + end + + # Render template for all locales and return hash of locale => plain text + def render_for_all_locales + I18n.available_locales.each_with_object({}) do |locale, content_hash| + I18n.with_locale(locale) do + rendered_html = render_template + content_hash[locale] = extract_plain_text(rendered_html) + end + end + end + + # Render template for current locale only + def render_for_current_locale + rendered_html = render_template + extract_plain_text(rendered_html) + end + + private + + def render_template + view_context.render(template: full_template_path, layout: false) + rescue StandardError => e + Rails.logger.warn("Failed to render template #{template_path}: #{e.message}") + template_path + end + + def full_template_path + # If template_path already starts with better_together/ or is a static page, use as-is + return template_path if template_path.start_with?('better_together/') + return "better_together/static_pages/#{template_path}" if static_page? + + template_path + end + + def static_page? + template_path.start_with?('better_together/static_pages/') || + !template_path.include?('/') + end + + def view_context + @view_context ||= begin + controller = ApplicationController.new + controller.request = ActionDispatch::TestRequest.create + controller.response = ActionDispatch::TestResponse.new + controller.view_context + end + end + + def extract_plain_text(html) + return html unless html.is_a?(String) + + # Strip HTML tags + plain_text = ActionView::Base.full_sanitizer.sanitize(html) + # Clean up whitespace + plain_text.gsub(/\s+/, ' ').strip + end + end +end diff --git a/app/views/better_together/content/blocks/_markdown.html.erb b/app/views/better_together/content/blocks/_markdown.html.erb new file mode 100644 index 000000000..e9773109a --- /dev/null +++ b/app/views/better_together/content/blocks/_markdown.html.erb @@ -0,0 +1,9 @@ +<%# frozen_string_literal: true %> + +<%= render layout: 'better_together/content/blocks/block', locals: { block: markdown } do %> + <%= cache markdown.cache_key_with_version do %> +
+ <%= markdown.rendered_html %> +
+ <% end %> +<% end %> diff --git a/app/views/better_together/content/blocks/_template.html.erb b/app/views/better_together/content/blocks/_template.html.erb index 67e18a3f9..370762329 100644 --- a/app/views/better_together/content/blocks/_template.html.erb +++ b/app/views/better_together/content/blocks/_template.html.erb @@ -1,4 +1,8 @@ <%= render layout: 'better_together/content/blocks/block', locals: { block: template } do %> - <%= render partial: template.template_path, locals: { block: template, content: template.content_data }, cached: true %> + <% if template.template_path.start_with?('better_together/static_pages/') %> + <%= render template: template.template_path, locals: { block: template, content: template.content_data } %> + <% else %> + <%= render partial: template.template_path, locals: { block: template, content: template.content_data }, cached: true %> + <% end %> <% end %> diff --git a/app/views/better_together/content/blocks/fields/_markdown.html.erb b/app/views/better_together/content/blocks/fields/_markdown.html.erb new file mode 100644 index 000000000..ab916a25c --- /dev/null +++ b/app/views/better_together/content/blocks/fields/_markdown.html.erb @@ -0,0 +1,145 @@ +<%# frozen_string_literal: true %> +<%# locals: (block:, scope: BetterTogether::Content::Block.block_name, temp_id: SecureRandom.uuid) -%> + +<% inline_radio_id = "#{temp_id}_markdown_source_inline" %> +<% file_radio_id = "#{temp_id}_markdown_source_file" %> +<% inline_selected = block.markdown_file_path.blank? %> + +
+ +
+ <%= label_tag nil, t('better_together.content.blocks.markdown.fields.markdown_source'), class: 'form-label' %> +
+ <%= radio_button_tag "#{scope}[markdown_source_type]", 'inline', inline_selected, + class: 'form-check-input', + id: inline_radio_id, + data: { + action: 'change->better-together--markdown-block#handleSourceTypeChange', + 'better-together--markdown-block-target': 'sourceTypeRadio', + 'better-together--dependent-fields-target': 'controlField' + } %> + <%= label_tag inline_radio_id, t('better_together.content.blocks.markdown.fields.write_inline'), class: 'form-check-label' %> +
+
+ <%= radio_button_tag "#{scope}[markdown_source_type]", 'file', block.markdown_file_path.present?, + class: 'form-check-input', + id: file_radio_id, + data: { + action: 'change->better-together--markdown-block#handleSourceTypeChange', + 'better-together--markdown-block-target': 'sourceTypeRadio', + 'better-together--dependent-fields-target': 'controlField' + } %> + <%= label_tag file_radio_id, t('better_together.content.blocks.markdown.fields.reference_file'), class: 'form-check-label' %> +
+
+ + +
="inline"> + + <%= render partial: 'better_together/content/blocks/fields/shared/translatable_text_field', + locals: { + model: block, + scope: scope, + temp_id: temp_id, + attribute: 'markdown_source' + } %> + + + <%= t('better_together.content.blocks.markdown.help.inline_content') %> + +
+ + +
="file"> + <%= label_tag "#{scope}[markdown_file_path]", t('better_together.content.blocks.markdown.fields.file_path'), class: 'form-label' %> + <%= text_field_tag "#{scope}[markdown_file_path]", + block.markdown_file_path, + class: "form-control font-monospace#{' is-invalid' if block.errors[:markdown_file_path].any?}", + placeholder: t('better_together.content.blocks.markdown.fields.file_path_placeholder'), + data: { 'better-together--markdown-block-target': 'filePathInput' }, + disabled: inline_selected %> + + <% if block.errors[:markdown_file_path].any? %> +
+ <%= block.errors[:markdown_file_path].join(", ") %> +
+ <% end %> + + + <%= t('better_together.content.blocks.markdown.help.file_path') %> +
+ <%= t('better_together.content.blocks.markdown.help.localized_files') %> +
+ + +
+ <%= check_box_tag "#{scope}[auto_sync_from_file]", + '1', + block.auto_sync_from_file, + class: 'form-check-input', + id: "#{temp_id}_auto_sync", + disabled: inline_selected %> + <%= label_tag "#{temp_id}_auto_sync", + t('better_together.content.blocks.markdown.fields.auto_sync'), + class: 'form-check-label' %> + + <%= t('better_together.content.blocks.markdown.help.auto_sync') %> + +
+ + + <% if block.markdown_file_path.present? %> +
+ <%= t('better_together.content.blocks.markdown.help.available_translations') %> + <% + base_path = block.markdown_file_path.sub(/\.(md|markdown)$/i, '') + I18n.available_locales.each do |locale| + locale_file = "#{base_path}.#{locale}.md" + # Try to resolve the file path + file_path = if Pathname.new(locale_file).absolute? + Pathname.new(locale_file) + else + Rails.root.join(locale_file) + end + file_exists = File.exist?(file_path) + %> + + <%= locale.upcase %> <%= file_exists ? '✓' : '✗' %> + + <% end %> +
+ <% end %> +
+ + +
+
+ <%= label_tag nil, t('better_together.content.blocks.markdown.fields.preview'), class: 'form-label mb-0' %> + +
+
+ <% if block.content.present? %> + <%= block.rendered_html %> + <% else %> +

+ <%= t('better_together.content.blocks.markdown.help.preview_placeholder') %> +

+ <% end %> +
+
+
diff --git a/app/views/better_together/content/page_blocks/block_types/_markdown.html.erb b/app/views/better_together/content/page_blocks/block_types/_markdown.html.erb new file mode 100644 index 000000000..9b1839f92 --- /dev/null +++ b/app/views/better_together/content/page_blocks/block_types/_markdown.html.erb @@ -0,0 +1,13 @@ +<%= link_to new_page_page_block_path(page, block_type: block_type.model_name.to_s), + class: 'd-flex flex-column align-items-center text-decoration-none', + data: { turbo_stream: true }, + 'aria-label': "Create a new #{block_type.model_name.human} block for the page" do %> + + +
+ +
+<% end %> diff --git a/app/views/better_together/pages/show.html.erb b/app/views/better_together/pages/show.html.erb index 700393180..0e5824060 100644 --- a/app/views/better_together/pages/show.html.erb +++ b/app/views/better_together/pages/show.html.erb @@ -62,6 +62,11 @@ <% cache ['page-hero', @page.id, @page.hero_block&.updated_at, I18n.locale] do %> <%= render @page.hero_block if @page.hero_block %> <% end %> + <% unless @page.hero_block %> +
+

<%= @page.title %>

+
+ <% end %> <% if @page.sidebar_nav.present? %> <%= render layout: 'better_together/pages/sidebar_layout', locals: { nav: @page.sidebar_nav, current_page: @page } do %> <%= yield :page_content %> diff --git a/app/views/better_together/static_pages/code_contributor_agreement.html.erb b/app/views/better_together/static_pages/code_contributor_agreement.html.erb new file mode 100644 index 000000000..32affb185 --- /dev/null +++ b/app/views/better_together/static_pages/code_contributor_agreement.html.erb @@ -0,0 +1,138 @@ +
+
+
+

Contributor License Agreement

+ +

Thank you for your interest in contributing to the Better Together Community Engine project ("We" or "Us").

+ +

This Contributor License Agreement ("Agreement") documents the rights granted by contributors to Us. To make this document effective, please read it carefully and indicate your agreement by contributing to the project. This Agreement is for your protection as a Contributor as well as the protection of Us and our users; it does not change your rights to use your own Contributions for any other purpose.

+ +

Effective Date: November 20, 2025

+ + + +

1. Definitions

+ +

"You" means the individual who Submits a Contribution to Us.

+ +

"Contribution" means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. If You do not own the Copyright in the entire work of authorship, please contact us at hello@bettertogethersolutions.com.

+ +

"Copyright" means all rights protecting works of authorship owned or controlled by You, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence including any extensions by You.

+ +

"Material" means the work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, the Material means the work of authorship to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material.

+ +

"Submit" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Material, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."

+ +

"Submission Date" means the date on which You Submit a Contribution to Us.

+ +

"Effective Date" means the date You execute this Agreement or the date You first Submit a Contribution to Us, whichever is earlier.

+ + + +

Subject to the terms and conditions of this Agreement, You hereby grant to Us and to recipients of software distributed by Us a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.

+ +

3. Grant of Patent License

+ +

Subject to the terms and conditions of this Agreement, You hereby grant to Us and to recipients of software distributed by Us a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted.

+ +

If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.

+ +

4. Outbound License

+ +

Based on the grant of rights in Sections 2 and 3, if We include Your Contribution in a Material, We may license the Contribution under any license, including copyleft, permissive, commercial, or proprietary licenses. As a condition on the exercise of this right, We agree to also license the Contribution under the terms of the license or licenses which We are using for the Material on the Submission Date.

+ +

The project is currently licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0). We may change the project license in the future, and Your Contribution may be distributed under the new license.

+ +

5. Moral Rights

+ +

If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect.

+ +

6. Our Rights

+ +

You acknowledge that We are not obligated to use Your Contribution as part of the Material and may decide to include any Contribution We consider appropriate.

+ +

7. Disclaimer

+ +

UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, YOU PROVIDE YOUR CONTRIBUTION ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.

+ +

8. Consequential Damage Waiver

+ +

TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED.

+ +

9. Miscellaneous

+ +

9.1 Representations

+

You represent that:

+
    +
  • You are legally entitled to grant the above license.
  • +
  • If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Us, or that your employer has executed a separate Contributor License Agreement with Us.
  • +
  • Each of Your Contributions is Your original creation.
  • +
  • Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
  • +
+ +

9.2 Support

+

You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.

+ +

9.3 Changes to this Agreement

+

We reserve the right to change this Agreement at any time. Your continued Contributions to the project after such changes constitutes your acceptance of the new Agreement.

+ +

9.4 Governing Law

+

This Agreement will be governed by and construed in accordance with the laws of Canada, excluding its conflicts of law provisions. Under certain circumstances, the governing law in this section might be superseded by the United Nations Convention on Contracts for the International Sale of Goods ("UN Convention") and the parties intend to avoid the application of the UN Convention to this Agreement and, thus, exclude the application of the UN Convention in its entirety to this Agreement.

+ +

9.5 Assignment

+

We may assign this Agreement to any third party. You may not assign this Agreement without Our prior written consent.

+ +

9.6 Entire Agreement

+

This Agreement constitutes the entire agreement between the parties concerning the subject matter hereof.

+ +
+ +

How to Apply This Agreement

+ +

By making a contribution to the Better Together Community Engine project through any of the following methods, you acknowledge that you have read this Contributor License Agreement and agree to be bound by its terms:

+ +
    +
  • Submitting a pull request on GitHub
  • +
  • Submitting code, documentation, or other materials via email
  • +
  • Submitting bug reports or feature requests that include code samples
  • +
  • Contributing to project discussions with code suggestions
  • +
+ +

If you are making a contribution on behalf of your employer or another entity, you represent that you have the authority to bind that entity to this Agreement.

+ +

Questions?

+

If you have questions about this Contributor License Agreement, please contact us at hello@bettertogethersolutions.com.

+ +
+ + + +

This Contributor License Agreement is adapted from the Harmony Contributor License Agreement and other industry-standard contributor agreements. Last updated: November 20, 2025.

+
+
+
diff --git a/app/views/better_together/static_pages/content_contributor_agreement.html.erb b/app/views/better_together/static_pages/content_contributor_agreement.html.erb new file mode 100644 index 000000000..257a709d6 --- /dev/null +++ b/app/views/better_together/static_pages/content_contributor_agreement.html.erb @@ -0,0 +1,254 @@ +
+
+
+

Content Contributor Agreement

+ +

Thank you for contributing content to <%= host_platform.name %>. This agreement governs the content you submit to our platform.

+ +

This Content Contributor Agreement ("Agreement") documents the rights you grant to us when you contribute content to the platform. By contributing content, you agree to these terms.

+ +

Effective Date: November 20, 2025

+ + + +

1. Definitions

+ +

"You" or "Your" means the individual or entity contributing content to the platform.

+ +

"We", "Us", or "Our" means <%= host_platform.name %> and Better Together Solutions.

+ +

"Content" means any text, images, videos, audio, data, information, or other materials that you submit, upload, post, or otherwise make available on or through the platform, including but not limited to:

+
    +
  • Posts, comments, and discussions
  • +
  • Community resources and guides
  • +
  • Event descriptions and materials
  • +
  • Joatu offers and requests
  • +
  • Profile information and biographies
  • +
  • Media files (photos, videos, audio)
  • +
  • Location and geographical information
  • +
  • Any other user-generated content
  • +
+ +

"Platform" means the Better Together Community Engine platform operated at <%= host_platform.url %> and any related services.

+ +

2. Types of Content

+ +

2.1 Public Content

+

Content you mark as "public" or share in public areas of the platform, including public posts, comments, resources, and event listings.

+ +

2.2 Community Content

+

Content shared within specific communities that is visible to community members, including community-specific posts, discussions, and resources.

+ +

2.3 Private Content

+

Content you mark as private or share in private conversations, messages, and restricted areas. Private content is subject to this agreement but with more limited usage rights as described below.

+ +

3. License Grant

+ +

3.1 Public and Community Content

+

For public and community content, you grant to us a worldwide, non-exclusive, royalty-free, transferable license (with right to sublicense) to:

+
    +
  • Use, copy, reproduce, process, adapt, modify, publish, transmit, display, and distribute your content in any and all media or distribution methods (now known or later developed)
  • +
  • Make your content available to the rest of the platform community and to the public in accordance with your privacy settings
  • +
  • Provide your content to service providers we work with to provide platform functionality (such as search indexing, content delivery networks, and backup services)
  • +
  • Create derivative works for the purpose of platform operation, improvement, and promotion
  • +
+ +

3.2 Private Content

+

For private content, you grant us a more limited license solely to:

+
    +
  • Store and transmit your content to deliver it to intended recipients
  • +
  • Make backup copies for security and disaster recovery purposes
  • +
  • Process your content as necessary to provide platform functionality (such as search within your own content)
  • +
  • Share with service providers strictly necessary for platform operation (cloud storage, database hosting)
  • +
+

We will not make your private content publicly available or use it for promotional purposes without your explicit consent.

+ +

3.3 Duration

+

The licenses granted in this section continue for a commercially reasonable period of time after you remove or delete content from the platform. You understand that removed content may persist in backup copies for a reasonable period of time (but will not be available to others).

+ +

3.4 Retention of Your Rights

+

You retain all ownership rights in your content. This license does not transfer ownership of your content to us.

+ +

4. Content Standards

+ +

You agree that all content you contribute will:

+ +

4.1 Comply with Laws

+
    +
  • Not violate any applicable local, national, or international law or regulation
  • +
  • Not infringe on intellectual property rights of others
  • +
  • Not violate privacy or data protection rights
  • +
  • Not contain illegal or harmful material
  • +
+ +

4.2 Respect Community Standards

+
    +
  • Not contain hate speech, harassment, or discriminatory content
  • +
  • Not promote violence or harm to individuals or groups
  • +
  • Not contain sexually explicit or inappropriate content (unless in designated areas with appropriate warnings)
  • +
  • Not constitute spam, malware, or phishing attempts
  • +
  • Not impersonate others or misrepresent your affiliation
  • +
+ +

4.3 Follow Platform Policies

+
    +
  • Comply with our <%= link_to 'Terms of Service', better_together.render_page_path('terms-of-service', locale: I18n.locale) %>
  • +
  • Follow our <%= link_to 'Code of Conduct', better_together.render_page_path('code-of-conduct', locale: I18n.locale) %>
  • +
  • Respect community-specific guidelines and rules
  • +
+ +

5. Your Representations and Warranties

+ +

By contributing content, you represent and warrant that:

+
    +
  • You own or have the necessary rights to grant the licenses described in this agreement
  • +
  • Your content does not and will not infringe, violate, or misappropriate any third-party rights, including copyright, trademark, patent, trade secret, moral rights, privacy rights, rights of publicity, or any other intellectual property or proprietary rights
  • +
  • Your content complies with all applicable laws and regulations
  • +
  • Your content does not contain any viruses, malware, or other harmful code
  • +
  • If your content includes identifiable individuals, you have obtained necessary permissions and releases
  • +
  • If your content includes third-party material, you have properly attributed it and have the right to use it
  • +
+ +

6. Our Rights

+ +

6.1 Content Moderation

+

We reserve the right to:

+
    +
  • Review, monitor, and moderate content submitted to the platform
  • +
  • Remove or refuse to publish content that violates this agreement or platform policies
  • +
  • Limit the visibility or distribution of content
  • +
  • Add labels, warnings, or context to content
  • +
+ +

6.2 No Obligation

+

We are not obligated to:

+
    +
  • Store, maintain, or provide you with a copy of any content you submit
  • +
  • Display, publish, or distribute your content
  • +
  • Monitor or moderate all content on the platform
  • +
+ +

6.3 Community Moderation

+

Community organizers and moderators may also have the right to moderate content within their communities according to community-specific guidelines.

+ +

7. Content Removal and Termination

+ +

7.1 Your Right to Remove

+

You may remove or delete your content at any time through the platform interface. Upon deletion:

+
    +
  • Your content will no longer be publicly visible
  • +
  • We will make reasonable efforts to remove it from active display
  • +
  • Content may persist in backup systems for a reasonable period
  • +
  • Content shared with others may remain in their personal archives
  • +
+ +

7.2 Our Right to Remove

+

We may remove your content immediately if:

+
    +
  • It violates this agreement, our Terms of Service, or Code of Conduct
  • +
  • We receive a valid legal request (such as a DMCA takedown notice)
  • +
  • We believe it poses a security or legal risk
  • +
  • You violate platform policies or terms
  • +
+ +

7.3 Account Termination

+

If your account is terminated, we may remove all of your content from the platform. We will make reasonable efforts to provide you with an export of your data before termination, where feasible.

+ +

8. Attribution

+ +

8.1 Your Attribution

+

When we display your public content, we will generally attribute it to you using your profile name and information. You can control your profile information through your account settings.

+ +

8.2 Attribution Requirements

+

If you use others' content on the platform (such as sharing or quoting), you must:

+
    +
  • Properly attribute the original creator
  • +
  • Not modify content in a way that misrepresents the creator's views
  • +
  • Respect any licensing terms attached to the content
  • +
+ +

9. Disclaimer

+ +

YOU PROVIDE YOUR CONTENT "AS IS" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.

+ +

WE DO NOT ENDORSE, SUPPORT, REPRESENT, OR GUARANTEE THE COMPLETENESS, TRUTHFULNESS, ACCURACY, OR RELIABILITY OF ANY CONTENT OR COMMUNICATIONS POSTED VIA THE PLATFORM.

+ +

10. Indemnification

+ +

You agree to indemnify, defend, and hold harmless <%= host_platform.name %>, Better Together Solutions, and our affiliates, officers, directors, employees, and agents from and against any claims, liabilities, damages, losses, and expenses, including reasonable legal fees and costs, arising out of or in any way connected with:

+
    +
  • Your content or your violation of this agreement
  • +
  • Your violation of any third-party rights, including intellectual property, publicity, confidentiality, property, or privacy rights
  • +
  • Your violation of any laws or regulations
  • +
+ +

11. Miscellaneous

+ +

11.1 Changes to This Agreement

+

We may update this agreement from time to time. Continued contribution of content after changes constitutes acceptance of the updated agreement. We will notify users of material changes.

+ +

11.2 Severability

+

If any provision of this agreement is found to be unenforceable, the remaining provisions will continue in full force and effect.

+ +

11.3 Governing Law

+

This agreement is governed by the laws of Canada, without regard to conflict of law principles.

+ +

11.4 Entire Agreement

+

This agreement, together with our Terms of Service and Privacy Policy, constitutes the entire agreement regarding your content contributions.

+ +
+ +

Data Privacy and Your Content

+ +

Please review our <%= link_to 'Privacy Policy', better_together.render_page_path('privacy', locale: I18n.locale) %> to understand how we collect, use, and protect data associated with your content contributions, including:

+
    +
  • Metadata about your content (timestamps, location data, device information)
  • +
  • Search indexing of your public content
  • +
  • Backup and archival practices
  • +
  • Your rights to access, correct, or delete your data under GDPR and other privacy laws
  • +
+ +

Questions About Content Licensing?

+ +

If you have questions about this Content Contributor Agreement or need clarification about content licensing, please contact us:

+ +
+ <%= host_platform.name %>
+ Email: <%= mail_to host_community.primary_email if host_community.has_contact_details? %>
+ <%= link_to 'Contact Page', better_together.render_page_path('contact', locale: I18n.locale) %> +
+ +
+ + + +

Last updated: November 20, 2025

+
+
+
diff --git a/app/views/better_together/static_pages/cookie_consent.html.erb b/app/views/better_together/static_pages/cookie_consent.html.erb new file mode 100644 index 000000000..8b778f99a --- /dev/null +++ b/app/views/better_together/static_pages/cookie_consent.html.erb @@ -0,0 +1,232 @@ +
+
+
+

Cookie Policy

+ +

This Cookie Policy explains how <%= host_platform.name %> uses cookies and similar technologies to recognize you when you visit our platform.

+ +

Last Updated: November 20, 2025

+ + + +

1. What Are Cookies?

+ +

Cookies are small data files that are placed on your computer or mobile device when you visit a website. Cookies are widely used by website owners in order to make their websites work, or to work more efficiently, as well as to provide reporting information.

+ +

Cookies set by the website owner (in this case, <%= host_platform.name %>) are called "first-party cookies." Cookies set by parties other than the website owner are called "third-party cookies." Third-party cookies enable third-party features or functionality to be provided on or through the website (e.g., analytics, error tracking). The parties that set these third-party cookies can recognize your computer both when it visits the website in question and also when it visits certain other websites.

+ +

2. Why We Use Cookies

+ +

We use first-party and third-party cookies for several reasons. Some cookies are required for technical reasons in order for our platform to operate (we refer to these as "essential" or "strictly necessary" cookies). Other cookies enable us to track and target the interests of our users to enhance the experience on our platform, provide error tracking, and analyze platform usage. These are "optional" cookies.

+ +

3. Types of Cookies We Use

+ +

First-Party Cookies (Always Active)

+

These are cookies that we set ourselves. We use these cookies to provide our core services and functionality.

+ +

Third-Party Cookies (Optional)

+

These are cookies set by external service providers. We only use third-party cookies when explicitly configured by platform organizers and with your consent where required by law.

+ +

4. Essential Cookies

+ +

These cookies are strictly necessary to provide you with services available through our platform and to use some of its features. Because these cookies are strictly necessary to deliver the platform, you cannot refuse them without impacting how our platform functions. You can block or delete them by changing your browser settings as described below under "Your Choices Regarding Cookies."

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cookie NamePurposeTypeDuration
_better_together_sessionMaintains your login session and stores temporary data like invitation tokens, locale preferences, and debug settingsFirst-partySession (6 hours of inactivity)
remember_user_tokenKeeps you logged in when you select "Remember me" during sign-inFirst-party2 weeks
_csrf_tokenSecurity token that protects against Cross-Site Request Forgery (CSRF) attacksFirst-partySession
+
+ +

Session Storage

+

In addition to cookies, we store the following information in your browser's session storage:

+
    +
  • Platform Invitation Token: Temporary token for processing platform invitations (30 minutes)
  • +
  • Event Invitation Token: Temporary token for processing event invitations (24 hours)
  • +
  • Locale Preference: Your selected language for the platform interface
  • +
  • Stimulus Debug Mode: Developer debugging flag (30 minutes, only if enabled)
  • +
+ +

5. Optional Cookies

+ +

These cookies are only used when explicitly enabled by platform organizers and, where required by law, with your consent.

+ +

Analytics and Performance Cookies (If Enabled)

+

The platform includes built-in metrics functionality that tracks usage patterns without identifying individual users. This system is privacy-focused and does not use cookies or track personal data.

+ +

Platform organizers may choose to enable third-party analytics services. If enabled, you will be notified and asked for consent where required by law.

+ +

Error Tracking Cookies (If Enabled)

+

When the platform organizer configures Sentry error tracking (via the SENTRY_CLIENT_KEY environment variable), error data may be collected to help diagnose and fix technical issues. This may include:

+
    +
  • Browser information and device type
  • +
  • Error messages and stack traces
  • +
  • URL where the error occurred
  • +
  • Approximate timestamp of the error
  • +
+

Sentry may use cookies to track error sessions. For more information, see Sentry's Privacy Policy.

+ +

6. Your Choices Regarding Cookies

+ +

Essential Cookies

+

Essential cookies are necessary for the platform to function properly. While you can configure your browser to block these cookies, doing so will prevent you from using many features of the platform, including signing in and maintaining your session.

+ +

Optional Cookies

+

For optional cookies (analytics, error tracking), you have the following choices:

+
    +
  • During Registration: You will be presented with clear options to consent to optional cookies during the registration process
  • +
  • Account Settings: You can modify your cookie preferences at any time in your account settings
  • +
  • Browser Settings: You can configure your browser to block or delete cookies (see below)
  • +
+ + + +

Platform Cookie Preferences

+

You can manage your cookie preferences for this platform by:

+
    +
  1. Visiting your Account Settings page
  2. +
  3. Navigating to the Privacy & Cookies section
  4. +
  5. Updating your preferences for optional cookies
  6. +
+ +

Browser Cookie Settings

+

You can set or amend your web browser controls to accept or refuse cookies. If you choose to reject cookies, you may still use our platform though your access to some functionality and areas may be restricted. The method for disabling cookies varies by browser and device. Please consult your browser's help documentation for specific instructions:

+ + + +

Mobile Device Cookie Management

+

Most mobile devices allow you to control cookies through their settings. Please refer to your device manufacturer's instructions for specific guidance.

+ +

8. Canadian and International Privacy Laws

+ +

Canadian Privacy Law Compliance (PIPEDA)

+ +

This Cookie Policy is part of our compliance with Canada's Personal Information Protection and Electronic Documents Act (PIPEDA). Under PIPEDA:

+ +
    +
  • Consent: We obtain your meaningful consent for the collection and use of personal information through cookies, except for essential cookies necessary for the platform to function
  • +
  • Purpose: We clearly identify why we use cookies and how they support platform functionality
  • +
  • Access: You have the right to access information about what cookies we use and how they collect data about you
  • +
  • Withdrawal: You can withdraw consent for optional cookies at any time through your browser settings or account preferences
  • +
  • Safeguards: We implement appropriate security measures to protect data collected through cookies
  • +
+ +

Your Rights: Under PIPEDA, you have the right to:

+
    +
  • Know what personal information we collect through cookies and why
  • +
  • Access information about our cookie practices
  • +
  • Withdraw consent for optional cookies (essential cookies are required for platform operation)
  • +
  • File a complaint about our cookie practices with our Privacy Officer or the Office of the Privacy Commissioner of Canada
  • +
+ +

Privacy Officer Contact: If you have questions about our cookie practices under Canadian privacy law, contact our Privacy Officer at privacy@bettertogethersolutions.com.

+ +

Filing a Complaint: You may file complaints with the Office of the Privacy Commissioner of Canada:

+
    +
  • Website: www.priv.gc.ca
  • +
  • Phone: 1-800-282-1376 (toll-free) or (819) 994-5444
  • +
+ +

GDPR Rights (European Economic Area)

+ +

If you are located in the European Economic Area (EEA), you have certain data protection rights. We aim to take reasonable steps to allow you to correct, amend, delete, or limit the use of your personal data.

+ +

You have the right to:

+
    +
  • Access: Request access to your personal data and information about how we process it
  • +
  • Rectification: Request correction of inaccurate personal data
  • +
  • Erasure: Request deletion of your personal data
  • +
  • Restriction: Request restriction of processing of your personal data
  • +
  • Data Portability: Request a copy of your personal data in a structured, machine-readable format
  • +
  • Object: Object to our processing of your personal data
  • +
  • Withdraw Consent: Withdraw consent at any time where we rely on consent to process your personal data
  • +
+ +

To exercise any of these rights, please contact us using the information in the "Contact Us" section below.

+ +

9. Updates to This Cookie Policy

+ +

We may update this Cookie Policy from time to time in order to reflect changes to the cookies we use or for other operational, legal, or regulatory reasons. Please therefore re-visit this Cookie Policy regularly to stay informed about our use of cookies and related technologies.

+ +

The date at the top of this Cookie Policy indicates when it was last updated.

+ +

10. Contact Us

+ +

If you have questions or concerns about this Cookie Policy or our use of cookies, please contact us:

+ +
+ Better Together Solutions
+ Email: hello@bettertogethersolutions.com
+ <%= link_to 'Privacy Policy', better_together.render_page_path('privacy', locale: I18n.locale) %>
+ <%= link_to 'Terms of Service', better_together.render_page_path('terms-of-service', locale: I18n.locale) %> +
+ +
+ + + + +
+
+
diff --git a/app/views/better_together/static_pages/privacy.html.erb b/app/views/better_together/static_pages/privacy.html.erb index 36cce495c..c273a8db0 100644 --- a/app/views/better_together/static_pages/privacy.html.erb +++ b/app/views/better_together/static_pages/privacy.html.erb @@ -13,13 +13,16 @@
  • How does Better Together collect data about me?
  • Does Better Together sell my personal information?
  • What personal information does Better Together collect, and why?
  • +
  • Does Better Together collect geolocation data?
  • +
  • Does Better Together index my content for search?
  • Does Better Together use personal information for marketing purposes?
  • How can I make choices about data collection?
  • Where does Better Together store data about me?
  • +
  • How long does Better Together retain my data?
  • Where can I access data about me?
  • How can I change or erase data about me?
  • -
  • Does Better Together make automated decisions based on data about me?
  • Does Better Together share data about me with others?
  • +
  • Does Better Together comply with Canadian privacy law (PIPEDA)?
  • How can I contact Better Together about privacy?
  • What if this privacy notice changes?
  • @@ -27,12 +30,12 @@

    What is Better Together?

    -

    Better Together is the company home and primary developer of Community Engine, open source software for building and hosting Internet community platforms. As a company, Better Together hosts community platforms using Community Engine for customers, as well as:

    +

    Better Together is the company home and primary developer of Community Engine, open source software for building and hosting Internet community platforms. As a company, Better Together hosts community platforms using Community Engine for customers, as well as:

    @@ -60,7 +63,7 @@

    when you post, send private messages, and otherwise participate in a community platform that Better Together hosts

  • -

    when you visit our website at communityengine.ca

    +

    when you visit our website at communityengine.app

  • when you sign up for mailing lists and announcements

    @@ -73,7 +76,7 @@
  • -

    Better Together collects data when you use community platforms that Community Engine hosts, whether you use the community platforms using a web browser on your own computer, or use Better Together’s Community Engine apps for mobile devices.

    +

    Better Together collects data when you use community platforms that Community Engine hosts, whether you use the community platforms using a web browser on your own computer, or use Better Together's Community Engine apps for mobile devices.

    Better Together does not buy or otherwise receive data about you from data brokers.

    @@ -86,21 +89,21 @@

    What personal information does Better Together collect, and why?

    -

    Better Together collects data about visits to community platforms and to its websites.

    +

    Better Together collects data about visits to community platforms and to its websites.

    -

    When you visit one of Better Together’s websites or a community platform that Better Together hosts, whether you have an account or not, we use cookies, server logs, and other methods to collect data about what pages you visit and when.

    +

    When you visit one of Better Together's websites or a community platform that Better Together hosts, whether you have an account or not, we use cookies, server logs, and other methods to collect data about what pages you visit and when.

    Better Together uses data about how you use the website to:

    -

    Better Together usually stores the data identified above for just a few weeks. In special circumstances, like extended investigations about technical attacks, Better Together may preserve log data longer, for analysis. Better Together stores aggregate statistics about use of the community platform for as long as Better Together hosts the community platform, but those statistics don’t include data identifiable to you personally.

    +

    Better Together usually stores the data identified above for just a few weeks. In special circumstances, like extended investigations about technical attacks, Better Together may preserve log data longer, for analysis. Better Together stores aggregate statistics about use of the community platform for as long as Better Together hosts the community platform, but those statistics don't include data identifiable to you personally.

    -

    Better Together collects community platform account data.

    +

    Better Together collects community platform account data.

    Many features of community platforms that Better Together hosts require a community platform account. For example, most community platforms that Better Together hosts require an account to create and reply to posts.

    To sign up for a community platform account, Community Engine requires your name, a user name, and an e-mail address.

    -

    Better Together uses your account data to identify you on the community platform and to create pages specific to you, such as your profile page. If the community platform is public, Better Together publishes your account data according to the community platform administrator’s configuration. If the community platform is access-restricted, Better Together makes your account data available to everyone who can access the community platform, according to the community platform administrator’s configuration.

    +

    Better Together uses your account data to identify you on the community platform and to create pages specific to you, such as your profile page. If the community platform is public, Better Together publishes your account data according to the community platform administrator's configuration. If the community platform is access-restricted, Better Together makes your account data available to everyone who can access the community platform, according to the community platform administrator's configuration.

    Better Together uses your e-mail address to:

    @@ -138,7 +141,7 @@ -

    You may provide additional data for your account, like a short biography, your location, or your birthday, on the profile settings page for your account. Better Together makes that data available to others who can access the community platform. You don’t have to provide this additional information, and you can erase it at any time.

    +

    You may provide additional data for your account, like a short biography, your location, or your birthday, on the profile settings page for your account. Better Together makes that data available to others who can access the community platform. You don't have to provide this additional information, and you can erase it at any time.

    Better Together stores your account data as long as your account remains open.

    @@ -147,8 +150,8 @@

    When you purchase hosting from Better Together, we require certain information from you, including your email address and the information we require to process payments, such as your name and credit card information. We use this information to perform the contract between us, and store it as long as your customer account remains open.

    -

    -

    Better Together collects data about posts and other activity on the community platform.

    +

    +

    Better Together collects data about posts and other activity on the community platform.

    Better Together collects the content of your posts, plus data about bookmarks, likes, and links you follow in order to share that data with others, through the community platform. If the community platform is public, Better Together publishes your activity. If the community platform is access-restricted, or access restrictions apply to the specific post, Better Together makes your activity available only to users permitted to see it.

    @@ -156,6 +159,64 @@

    Better Together stores your posts and other activity as long as your account remains open.

    +

    +

    Better Together collects geolocation data.

    + +

    Community Engine uses PostGIS spatial database technology to store and process geographic information. When you provide location information (such as addresses or place names), Better Together may:

    + + + +

    This geographic data may be used to:

    + + + +

    Geographic data is stored according to the privacy settings of your content. Public content may include publicly visible location data. You can control location data visibility through your account settings and content privacy controls.

    + +

    + + +

    Better Together uses self-hosted Elasticsearch to index and search content on community platforms. Search data is stored on the same servers as your other data and is not shared with third parties. This indexing includes:

    + + + +

    Search indexes respect the same privacy controls as the original content. Private or restricted content is indexed only for authorized users.

    +

    Better Together collects data you give to sign up for mailing lists and announcements.

    @@ -169,44 +230,42 @@

    Better Together collects data about open source contributors

    -

    Contributors to Better Together’s open source software may be asked to provide identifying and contact information such as your name, email address, telephone number, and mailing address. Better Together also collects and stores information concerning your agreement to our contributor license agreement.

    +

    Contributors to Better Together's open source software may be asked to provide identifying and contact information such as your name, email address, telephone number, and mailing address. Better Together also collects and stores information concerning your agreement to our contributor license agreement.

    -

    Better Together uses this information to maintain the integrity of our software and software licenses, as well as the integrity of the license agreement between Better Together and our contributors. Better Together stores contributor information for as long as related contributions are incorporated into Better Together’s open source software.

    +

    Better Together uses this information to maintain the integrity of our software and software licenses, as well as the integrity of the license agreement between Better Together and our contributors. Better Together stores contributor information for as long as related contributions are incorporated into Better Together's open source software.

    -

    -

    Better Together uses cookies.

    +

    +

    Better Together uses error tracking services.

    -

    HTTP cookies are small bits of data that websites, like Community Engine community platforms, send to your computer when you visit. When you return to those websites, your computer sends the cookies on your computer back to the website.

    +

    When configured by platform administrators, Better Together uses Sentry for error tracking and monitoring. This service may collect:

    -

    CommunityEngine.ca uses these cookies:

    + -
    +

    Better Together configures Sentry to exclude personally identifiable information where possible. For more information about Sentry's data practices, see Sentry's privacy policy.

    - - - - - - - - - - - - - - - - - -
    NameEssentialExpiresPurpose
    +

    +

    Better Together uses cookies.

    -
    +

    HTTP cookies are small bits of data that websites, like Community Engine community platforms, send to your computer when you visit. When you return to those websites, your computer sends the cookies on your computer back to the website.

    -
    +

    Essential Cookies

    -

    In addition, look at the privacy notice for your specific community platform to find out which cookies that community platform uses. By default, all Community Engine community platforms use these cookies:

    +

    Community Engine community platforms use the following essential cookies that are required for the platform to function properly:

    +
    @@ -218,44 +277,44 @@ - + - - + + - - - - + + + + - - - - - - - + - +
    email_better_together_session YesSessionremembers your e-mail as you create an accountSession or 6 hours of inactivityStores session data including language preference, invitation tokens, and authentication state
    destination_urlYesSessionhelps redirect you to your requested page after logging inremember_user_tokenNo2 weeksKeeps you logged in across browser sessions when "Remember Me" is checked during login
    _tYes1440 Hoursremembers who you are when you log in
    _community_platform_session_csrf_token Yes Sessionassociates an ID, and other security-related information, with your browsing sessionProtects against cross-site request forgery attacks
    -
    -

    Community Engine community platforms that configure Google Analytics, such as communityengine.app, also use cookies from Google. Refer to Google’s page on Google Analytics cookies for the latest details.

    +

    Optional Third-Party Services

    + +

    When enabled by platform administrators, the following third-party services may set additional cookies:

    + + -

    Community Engine community platforms that serve advertisements may also set cookies used to track you and serve advertisements.

    +

    Better Together does not use advertising cookies or sell data to third parties.

    -

    Your web browser can show you the cookies you have for any website and help you manage them.

    +

    Your web browser can show you the cookies you have for any website and help you manage them.

    Does Better Together use personal information for marketing purposes?

    -

    Better Together may use personal information about our customers and prospective customers in order to directly market our own services and inform you about new products and features that we offer. We also use the information you give to sign up for our mailing lists and announcements to send those messages.

    +

    Better Together may use personal information about our customers and prospective customers in order to directly market our own services and inform you about new products and features that we offer. We also use the information you give to sign up for our mailing lists and announcements to send those messages.

    You can always opt out of marketing communications from us, and you have the right to object to any processing of your information for marketing purposes.

    @@ -264,19 +323,58 @@

    You can make choices about how data about you is used on the settings page for your account. When a community platform uses access restrictions that vary by category, you can choose who will see your post by choosing the appropriate category.

    -

    Most web browsers let you make choices about whether to accept cookies, for specific websites or more generally. aboutcookies.org has instructions for many different web browsers. youronlinechoices.eu and aboutads.info have more information specifically about cookies used for advertising.

    +

    Most web browsers let you make choices about whether to accept cookies, for specific websites or more generally. aboutcookies.org has instructions for many different web browsers. youronlinechoices.eu and aboutads.info have more information specifically about cookies used for advertising.

    Better Together does not respond to the Do Not Track HTTP header.

    Where does Better Together store data about me?

    -

    Most community platforms that Better Together hosts store data in Better Together’s data centers and Amazon Web Services S3 in the Canada. Some community platforms that Better Together hosts store data in data centers in other jurisdictions, such as United States and the European Union. Refer to the Privacy Policy of the community platform on which your account exists for detailed information.

    +

    Better Together stores data in the following locations:

    + + + +

    Specific community platforms may have different storage configurations. Refer to the privacy policy of the community platform on which your account exists for detailed information about that platform's data storage.

    + +

    +

    How long does Better Together retain data?

    + +

    Better Together retains different types of data for different periods:

    + + + +

    After you close your account, your data is either deleted or anonymized according to the platform's configuration.

    Does Better Together comply with the EU General Data Protection Regulation?

    -

    Better Together respects privacy rights under Regulation (EU) 2016/679, the European Union’s General Data Protection Regulation (GDPR). Information that GDPR requires Better Together to give can be found throughout this privacy notice, including information on the rights of data subjects.

    +

    Better Together respects privacy rights under Regulation (EU) 2016/679, the European Union's General Data Protection Regulation (GDPR). Information that GDPR requires Better Together to give can be found throughout this privacy notice, including information on the rights of data subjects.

    What are my rights under the GDPR?

    @@ -300,7 +398,7 @@

    the right to restrict the processing of your personal data

  • -

    the right to object to certain processing of your information, including automated decision-making and direct marketing

    +

    the right to object to certain processing of your information, including direct marketing

  • the right to lodge a complaint with a supervisory authority

    @@ -312,111 +410,253 @@

    How does Better Together safeguard international data transfers after Schrems II?

    -

    Better Together relies on the European Commission’s standard contractual clauses for international transfers(SCCs) to legally transfer personal data out of the European Economic Area. Because national security and surveillance laws may be in conflict with European data protection rules, Better Together continually reassesses the practical reach of these laws to ensure our data transfers are adequately safeguarded.

    +

    Better Together relies on the European Commission's standard contractual clauses for international transfers (SCCs) to legally transfer personal data out of the European Economic Area. Because national security and surveillance laws may be in conflict with European data protection rules, Better Together continually reassesses the practical reach of these laws to ensure our data transfers are adequately safeguarded.

    Currently:

    Does Better Together comply with the California Consumer Privacy Act?

    -

    Better Together complies with its obligations under the California Consumer Privacy Act (CCPA). Better Together does not sell your personal information within the meaning of that law. Information on CCPA user rights — such as accessing or deleting your personal information — can be found throughout this privacy notice. So can information about specific CCPA consumer rights, like requesting disclosure about information Better Together collects and requesting deletion of your personal information.

    +

    Better Together complies with its obligations under the California Consumer Privacy Act (CCPA). Better Together does not sell your personal information within the meaning of that law. Information on CCPA user rights — such as accessing or deleting your personal information — can be found throughout this privacy notice. So can information about specific CCPA consumer rights, like requesting disclosure about information Better Together collects and requesting deletion of your personal information.

    -

    Better Together is not presently a “business” for the purposes of the CCPA, but we may act as a service provider for CCPA businesses when we host community platforms on behalf of customers. We offer a standard Service Provider Agreement for CCPA business customers.

    +

    Better Together is not presently a "business" for the purposes of the CCPA, but we may act as a service provider for CCPA businesses when we host community platforms on behalf of customers. We offer a standard Service Provider Agreement for CCPA business customers.

    -

    -

    Where can I access data about me?

    +

    +

    Does Better Together comply with Canadian privacy law (PIPEDA)?

    -

    You can see your account data at any time by visiting your account page on the community platform. Your account page also lists your posts and other activity on the community platform.

    +

    Better Together complies with Canada's Personal Information Protection and Electronic Documents Act (PIPEDA), which governs how private sector organizations collect, use, and disclose personal information in the course of commercial activities.

    - +

    PIPEDA Applicability

    -

    If you do not have account with us but have a data access request, please contact us.

    +

    PIPEDA applies to Better Together's operations because:

    -

    -

    How can I change or erase data about me?

    + -

    You can change your account data at any time by visiting the profile settings page for your account. The settings for a particular community platform may also allow you to close your account, on the settings page for your account. Closing your account starts a process of erasing or anonymizing Better Together’s records of data you provided for your account. Community Platform administrators can also erase and anonymize accounts.

    +

    PIPEDA's Ten Fair Information Principles

    -

    Depending on the settings for your particular community platform, you may also be able to edit, anonymize, or erase your posts. When you edit posts, Better Together will keep all versions of your posts. Community Platform administrators can view old versions of posts, and optionally make them visible to other community platform visitors.

    +

    Better Together follows PIPEDA's ten fair information principles:

    + +
      +
    1. +

      Accountability: Better Together is responsible for personal information under our control. We have designated a Privacy Officer who is accountable for our compliance with PIPEDA.

      +
    2. +
    3. +

      Identifying Purposes: We identify the purposes for which we collect personal information at or before the time of collection. These purposes are described throughout this privacy notice.

      +
    4. +
    5. +

      Consent: We obtain your meaningful consent for the collection, use, or disclosure of your personal information, except where inappropriate (such as for legal or security reasons).

      +
    6. +
    7. +

      Limiting Collection: We collect only the personal information that is necessary for the purposes we have identified.

      +
    8. +
    9. +

      Limiting Use, Disclosure, and Retention: We use or disclose personal information only for the purposes for which it was collected, except with your consent or as required by law. We retain personal information only as long as necessary.

      +
    10. +
    11. +

      Accuracy: We strive to keep personal information as accurate, complete, and up-to-date as necessary. You can update your information at any time.

      +
    12. +
    13. +

      Safeguards: We protect personal information with security safeguards appropriate to the sensitivity of the information, including encryption, access controls, and secure storage.

      +
    14. +
    15. +

      Openness: This privacy notice makes information about our privacy policies and practices readily available to you.

      +
    16. +
    17. +

      Individual Access: Upon request, we inform you of the existence, use, and disclosure of your personal information, and provide access to that information. You may challenge the accuracy and completeness of your information and have it amended as appropriate.

      +
    18. +
    19. +

      Challenging Compliance: You may contact our Privacy Officer with questions or complaints about our compliance with PIPEDA.

      +
    20. +
    + +

    Your Rights Under PIPEDA

    + +

    Under PIPEDA, you have the right to:

    + + -

    -

    Does Better Together make automated decisions based on data about me?

    +

    Consent Under PIPEDA

    -

    -

    Better Together classifies posts as spam automatically.

    +

    Better Together obtains your consent in various ways depending on the sensitivity of the information and your relationship with us:

    -

    Better Together uses data about your posts and other activity on many community platforms to make automated decisions about whether your posts to communityengine.app and most community platforms that Better Together hosts are spam. When Akismet decides that a post is likely spam, the community platform refuses to accept the post.

    + -

    If you think a post has been wrongly blocked or removed, contact an administrator of your community platform. They can override the decision that a post was spam.

    +

    Data Breach Notification

    -

    -

    Better Together uses data about posts and activity to set trust levels automatically.

    +

    In accordance with PIPEDA's breach reporting requirements, Better Together will:

    -

    Depending on how administrators of your community platform configure the community platform, the community platform may use data about your posts and activity to award you badges and calculate a trust level for your account. Your trust level may affect how you can participate in the community platform, such as whether you can upload images, as well as give you access to moderation and management powers in the community platform. Your trust level therefore reflects community platform administrators’ confidence in you, and their willingness to delegate community management functions, like moderation.

    + + +

    Cross-Border Data Transfers

    + +

    As described in where does Better Together store data about me, some of your personal information may be stored or processed outside of Canada. When we transfer personal information outside Canada, we ensure that it receives a comparable level of protection through contractual or other means.

    + +

    Filing a Complaint Under PIPEDA

    + +

    If you have concerns about our privacy practices, please first contact our Privacy Officer. If you are not satisfied with our response, you may file a complaint with:

    + +

    Office of the Privacy Commissioner of Canada
    + 30 Victoria Street
    + Gatineau, Quebec K1A 1H3
    + Toll-free: 1-800-282-1376
    + Phone: (819) 994-5444
    + TTY: (819) 994-6591
    + Website: www.priv.gc.ca

    + +

    +

    Where can I access data about me?

    + +

    You can see your account data at any time by visiting your account page on the community platform. Your account page also lists your posts and other activity on the community platform.

    + +

    If you do not have account with us but have a data access request, please contact us.

    + +

    +

    How can I change or erase data about me?

    + +

    You can change your account data at any time by visiting the profile settings page for your account. The settings for a particular community platform may also allow you to close your account, on the settings page for your account. Closing your account starts a process of erasing or anonymizing Better Together's records of data you provided for your account. Community Platform administrators can also erase and anonymize accounts.

    -

    If you think your trust level has been set incorrectly, contact an administrator of your community platform. They can manually adjust the trust level of your account.

    +

    Depending on the settings for your particular community platform, you may also be able to edit, anonymize, or erase your posts. When you edit posts, Better Together will keep all versions of your posts. Community Platform administrators can view old versions of posts, and optionally make them visible to other community platform visitors.

    Does Better Together share data about me with others?

    Better Together shares account data with others as mentioned in the section about account data.

    -

    Better Together shares data about your posts and other community platform activity with others as mentioned in the section about community platform data.

    +

    Better Together shares data about your posts and other community platform activity with others as mentioned in the section about community platform data.

    -

    Better Together uses the subprocessors listed on our subprocessors page when providing community platforms on behalf of our customers. We may also share personal data with the service providers we use in order to transact with customers, host our website, deliver content, secure our services, store data, host and manage our open source project, market our services, and provide customer support. These service providers include:

    +

    Better Together uses subprocessors and service providers to operate community platforms and deliver services. We may share personal data with the following service providers:

    +

    These service providers are contractually required to protect your data and use it only for the purposes we specify.

    +

    How can I contact Better Together about privacy?

    -

    You can send questions, requests, and complaints to:

    +

    You can send questions, requests, and complaints to our Privacy Officer:

    -

    Better Together Solutions +

    Better Together Solutions
    + Privacy Officer
    privacy@bettertogethersolutions.com

    -

    European Users with questions or complaints about GDPR compliance should also contact the above email address.

    +

    We aim to respond to privacy inquiries within 30 days. For complex requests, we may extend this timeline and will notify you of any extension.

    + +

    For Canadian residents: If you have concerns about our privacy practices under PIPEDA, you may contact our Privacy Officer using the information above. You also have the right to file a complaint with the Office of the Privacy Commissioner of Canada at www.priv.gc.ca or 1-800-282-1376.

    -

    For complaints under GDPR, European Union users may lodge complaints with their local data protection supervisory authorities.

    +

    For European Union residents: Questions or complaints about GDPR compliance should be sent to the email address above. You may also lodge complaints with your local data protection supervisory authority.

    + +

    For California residents: Questions about CCPA rights should be sent to the email address above.

    How can I find out about changes?

    -

    This version of Better Together’s privacy questions and answers took effect November 30th, 2023.

    +

    This version of Better Together's privacy questions and answers took effect November 20th, 2025.

    Better Together will post the next version at https://communityengine.app/privacy. Better Together may change how it announces changes in future versions.

    In the meantime, Better Together may update its contact information without announcing a change. Please refer to https://communityengine.app/privacy for the latest contact information at any time.

    +
    + + +
  • - \ No newline at end of file + diff --git a/app/views/better_together/static_pages/terms_of_service.html.erb b/app/views/better_together/static_pages/terms_of_service.html.erb index 134cc9007..ca54cf776 100644 --- a/app/views/better_together/static_pages/terms_of_service.html.erb +++ b/app/views/better_together/static_pages/terms_of_service.html.erb @@ -145,5 +145,25 @@ addresses or other personal data for commercial mailing lists or databases.

    You may notify the company under these terms, and send questions to the company, at community@bettertogethersolutions.com.

    The company may notify you under these terms using the e-mail address you provide for your account on the community platform, or by posting a message to the homepage of the community platform or your account page.

    Changes

    -

    The company last updated these terms on November 30th, 2023, and may update these terms again. The company will post all updates to the community platform. For updates that contain substantial changes, the company agrees to e-mail you, if you’ve created an account and provided a valid e-mail address. The company may also announce updates with special messages or alerts on the community platform.

    +

    The company last updated these terms on November 30th, 2023, and may update these terms again. The company will post all updates to the community platform. For updates that contain substantial changes, the company agrees to e-mail you, if you've created an account and provided a valid e-mail address. The company may also announce updates with special messages or alerts on the community platform.

    Once you get notice of an update to these terms, you must agree to the new terms in order to keep using the community platform.

    + +
    + + diff --git a/better_together.gemspec b/better_together.gemspec index a83a7a48b..e39df8b97 100644 --- a/better_together.gemspec +++ b/better_together.gemspec @@ -63,6 +63,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'rack-cors', '>= 1.1.1', '< 3.1.0' spec.add_dependency 'rack-mini-profiler' spec.add_dependency 'rails', '>= 7.2', '< 8.1' + spec.add_dependency 'redcarpet', '~> 3.6' spec.add_dependency 'reform-rails', '>= 0.2', '< 0.4' spec.add_dependency 'rswag', '>= 2.3.1', '< 2.18.0' spec.add_dependency 'ruby-openai' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml new file mode 100644 index 000000000..c76a603f5 --- /dev/null +++ b/config/i18n-tasks.yml @@ -0,0 +1,47 @@ +# config/i18n-tasks.yml +# i18n-tasks configuration for Better Together Community Engine + +base_locale: en +locales: + - en + - es + - fr + - uk + +# Paths to scan for translations in the engine +search: + paths: + - app/ + +# Read/write locale files from config/locales +data: + read: + - config/locales/%{locale}.yml + write: + - config/locales/%{locale}.yml + +# Exclude test and unnecessary paths +exclude: + - 'test/**' + - 'spec/**' + - 'tmp/**' + - 'log/**' + - 'node_modules/**' + +# Ignore keys (both missing and unused checks) +# Keys that are provided by external gems like i18n-timezones +ignore_missing: + - '{timezones,timezones.*}' # Provided by i18n-timezones gem for non-English locales + +ignore_unused: + - '{timezones,timezones.*}' # Provided by i18n-timezones gem, used in timezone selects + +# Alternative: use eq_base to ignore keys that are the same as base locale +# This tells i18n-tasks that these keys are intentionally the same in all locales +ignore_eq_base: + es: + - '{timezones,timezones.*}' + fr: + - '{timezones,timezones.*}' + uk: + - '{timezones,timezones.*}' diff --git a/config/initializers/dartsass.rb b/config/initializers/dartsass.rb new file mode 100644 index 000000000..35fc47e8e --- /dev/null +++ b/config/initializers/dartsass.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Configure Dart Sass to silence Bootstrap deprecation warnings +# These warnings are from Bootstrap's internal implementation and will be +# fixed when Bootstrap releases a Dart Sass 3.0 compatible version. +# +# Warnings silenced: +# - import: Bootstrap still uses @import (will be removed in Bootstrap 6) +# - global-builtin: Bootstrap uses global functions (type-of, unit, map-has-key) +# - color-functions: Bootstrap uses deprecated color functions (red, green, blue) + +Rails.application.config.sass.quiet_deps = true +Rails.application.config.sass.silence_deprecations = %w[ + import + global-builtin + color-functions +] diff --git a/config/locales/en.yml b/config/locales/en.yml index ddcc8997f..23cc2b223 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -503,6 +503,8 @@ en: seen_at: Seen at errors: messages: + file_not_found: file does not exist + invalid_file_type: must be a markdown file (.md or .markdown) record_invalid: 'Validation failed: %{errors}' restrict_dependent_destroy: has_many: Cannot delete record because dependent %{record} exist @@ -521,6 +523,7 @@ en: better_together/content/hero: Hero better_together/content/html: HTML better_together/content/image: Image + better_together/content/markdown: Markdown better_together/content/page_block: Page Block better_together/content/rich_text: Rich Text better_together/content/template: Template File @@ -719,6 +722,31 @@ en: content: blocks: associated_pages: Associated Pages + markdown: + fields: + auto_sync: Always load from file (ignore database content) + file_path: Markdown File Path + file_path_placeholder: docs/README.md or /absolute/path/to/file.md + markdown_source: Markdown Source + preview: Preview + reference_file: Reference a Markdown File + refresh_preview: Refresh Preview + write_inline: Write Markdown Inline + help: + auto_sync: When checked, content is always loaded from the file. When + unchecked, file content is imported into the database and can be edited + separately. + available_translations: 'Available translations:' + file_path: Path to a markdown file. Can be relative to Rails.root (e.g., + docs/README.md) or absolute (e.g., /path/to/file.md). File must have + a .md or .markdown extension. + inline_content: Write your markdown content directly. Supports GitHub-flavored + markdown including tables, code blocks, and more. Content is translatable + for each locale. + localized_files: The system automatically looks for locale-specific versions + (e.g., privacy.en.md, privacy.es.md, privacy.fr.md) and falls back to + the base file if not found. + preview_placeholder: Preview will appear here... conversation_mailer: new_message_notification: from_address: New message via %{from_address} diff --git a/config/locales/es.yml b/config/locales/es.yml index ac8188bce..85a1680b3 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -503,6 +503,8 @@ es: seen_at: Visto en errors: messages: + file_not_found: el archivo no existe + invalid_file_type: debe ser un archivo markdown (.md o .markdown) record_invalid: 'La validación falló: %{errors}' restrict_dependent_destroy: has_many: No se puede eliminar el registro porque existen %{record} dependientes @@ -521,6 +523,7 @@ es: better_together/content/hero: Héroe better_together/content/html: HTML better_together/content/image: Imagen + better_together/content/markdown: Markdown better_together/content/page_block: Bloqueo de página better_together/content/rich_text: Texto enriquecido better_together/content/template: Archivo de plantilla @@ -722,6 +725,32 @@ es: content: blocks: associated_pages: Páginas asociadas + markdown: + fields: + auto_sync: Cargar siempre desde archivo (ignorar contenido de base de + datos) + file_path: Ruta del Archivo Markdown + file_path_placeholder: docs/README.md o /ruta/absoluta/al/archivo.md + markdown_source: Fuente Markdown + preview: Vista Previa + reference_file: Referenciar un Archivo Markdown + refresh_preview: Actualizar Vista Previa + write_inline: Escribir Markdown en Línea + help: + auto_sync: Cuando está marcado, el contenido siempre se carga desde el + archivo. Cuando está desmarcado, el contenido del archivo se importa + a la base de datos y puede editarse por separado. + available_translations: 'Traducciones disponibles:' + file_path: Ruta a un archivo markdown. Puede ser relativa a Rails.root + (ej., docs/README.md) o absoluta (ej., /ruta/al/archivo.md). El archivo + debe tener extensión .md o .markdown. + inline_content: Escriba su contenido markdown directamente. Soporta markdown + estilo GitHub incluyendo tablas, bloques de código y más. El contenido + es traducible para cada idioma. + localized_files: El sistema busca automáticamente versiones específicas + del idioma (ej., privacy.en.md, privacy.es.md, privacy.fr.md) y recurre + al archivo base si no se encuentra. + preview_placeholder: La vista previa aparecerá aquí... conversation_mailer: new_message_notification: from_address: Nuevo mensaje vía %{from_address} diff --git a/config/locales/fr.yml b/config/locales/fr.yml index ceeaedc31..dec0fb51a 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -503,6 +503,8 @@ fr: seen_at: Vu à errors: messages: + file_not_found: le fichier n'existe pas + invalid_file_type: doit être un fichier markdown (.md ou .markdown) record_invalid: 'La validation a échoué : %{errors}' restrict_dependent_destroy: has_many: Vous ne pouvez pas supprimer l'enregistrement parce que les %{record} @@ -523,6 +525,7 @@ fr: better_together/content/hero: héros better_together/content/html: HTML better_together/content/image: image + better_together/content/markdown: Markdown better_together/content/page_block: blocage de pages better_together/content/rich_text: texte enrichi better_together/content/template: Template File @@ -726,6 +729,32 @@ fr: content: blocks: associated_pages: Pages associées + markdown: + fields: + auto_sync: Toujours charger depuis le fichier (ignorer le contenu de la + base de données) + file_path: Chemin du Fichier Markdown + file_path_placeholder: docs/README.md ou /chemin/absolu/vers/fichier.md + markdown_source: Source Markdown + preview: Aperçu + reference_file: Référencer un Fichier Markdown + refresh_preview: Actualiser l'Aperçu + write_inline: Écrire du Markdown en Ligne + help: + auto_sync: Lorsque coché, le contenu est toujours chargé depuis le fichier. + Lorsque décoché, le contenu du fichier est importé dans la base de données + et peut être modifié séparément. + available_translations: 'Traductions disponibles :' + file_path: Chemin vers un fichier markdown. Peut être relatif à Rails.root + (par ex., docs/README.md) ou absolu (par ex., /chemin/vers/fichier.md). + Le fichier doit avoir une extension .md ou .markdown. + inline_content: Écrivez votre contenu markdown directement. Prend en charge + le markdown de type GitHub, y compris les tableaux, les blocs de code + et plus encore. Le contenu est traduisible pour chaque langue. + localized_files: Le système recherche automatiquement les versions spécifiques + à la langue (par ex., privacy.en.md, privacy.es.md, privacy.fr.md) et + revient au fichier de base s'il n'est pas trouvé. + preview_placeholder: L'aperçu apparaîtra ici... conversation_mailer: new_message_notification: from_address: Nouveau message via %{from_address} diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 5992551bb..9c90d35ca 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -503,6 +503,8 @@ uk: seen_at: Seen at errors: messages: + file_not_found: файл не існує + invalid_file_type: має бути файл markdown (.md або .markdown) record_invalid: 'Validation failed: %{errors}' restrict_dependent_destroy: has_many: Cannot delete record because dependent %{record} exist @@ -521,6 +523,7 @@ uk: better_together/content/hero: Герой better_together/content/html: HTML better_together/content/image: Зображення + better_together/content/markdown: Markdown better_together/content/page_block: Блок сторінки better_together/content/rich_text: Багатий текст better_together/content/template: Файл шаблону @@ -719,6 +722,31 @@ uk: content: blocks: associated_pages: Associated Pages + markdown: + fields: + auto_sync: Завжди завантажувати з файлу (ігнорувати вміст бази даних) + file_path: Шлях до Файлу Markdown + file_path_placeholder: docs/README.md або /абсолютний/шлях/до/файлу.md + markdown_source: Джерело Markdown + preview: Попередній Перегляд + reference_file: Посилання на Файл Markdown + refresh_preview: Оновити Попередній Перегляд + write_inline: Писати Markdown Вбудовано + help: + auto_sync: Коли позначено, контент завжди завантажується з файлу. Коли + не позначено, вміст файлу імпортується в базу даних і може редагуватися + окремо. + available_translations: 'Доступні переклади:' + file_path: Шлях до файлу markdown. Може бути відносним до Rails.root (наприклад, + docs/README.md) або абсолютним (наприклад, /шлях/до/файлу.md). Файл + повинен мати розширення .md або .markdown. + inline_content: Пишіть свій markdown контент безпосередньо. Підтримує + markdown у стилі GitHub, включаючи таблиці, блоки коду тощо. Контент + можна перекладати для кожної мови. + localized_files: Система автоматично шукає версії для конкретної мови + (наприклад, privacy.en.md, privacy.es.md, privacy.fr.md) і повертається + до базового файлу, якщо не знайдено. + preview_placeholder: Попередній перегляд з'явиться тут... conversation_mailer: new_message_notification: from_address: Нове повідомлення через %{from_address} diff --git a/config/routes.rb b/config/routes.rb index 30a987369..8721b3d4a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -273,6 +273,9 @@ get 'checklists/:checklist_id/checklist_items/:id/person_checklist_item', to: 'person_checklist_items#show' end + # Preview endpoint for markdown blocks - controller has authentication via before_action + post 'content/blocks/preview_markdown', to: 'content/blocks#preview_markdown', as: :preview_content_block_markdown + resources :events, only: %i[index show] do member do get :show diff --git a/db/migrate/20251124193251_migrate_translated_title_attributes_to_strings.rb b/db/migrate/20251124193251_migrate_translated_title_attributes_to_strings.rb new file mode 100644 index 000000000..d9d963d15 --- /dev/null +++ b/db/migrate/20251124193251_migrate_translated_title_attributes_to_strings.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Ensures that all translated title attributes are stored as strings, not text +# This migration fixes title translations for: +# - BetterTogether::Post +# - BetterTogether::Agreement +# - BetterTogether::Geography::Map +class MigrateTranslatedTitleAttributesToStrings < ActiveRecord::Migration[8.0] + def up + # Move title translations from text to string translations + # This handles cases where titles were incorrectly stored as text type + # due to missing type: :string declarations in translates calls + + puts "Running mobility title translation migration via rake task (with bulk operations)..." + + # Execute the rake task that contains the migration logic + # This allows the migration logic to be tested and executed independently + # Uses bulk operations for optimal performance: + # - insert_all() for creating string translations + # - delete_all() for removing text translations + # - Bypasses ActiveRecord callbacks (Elasticsearch indexing) + begin + Rake::Task['translations:mobility:migrate_titles_to_string'].invoke + rescue RuntimeError + Rake::Task['app:translations:mobility:migrate_titles_to_string'].invoke + end + end + + def down + # This migration is not easily reversible since we don't know which records + # were originally in text translations vs string translations. + # However, you can use the rake task system to create a reverse migration if needed. + raise ActiveRecord::IrreversibleMigration, + "Cannot reverse migration of title translations from text to string" + end +end diff --git a/db/migrate/20251125142646_add_creator_to_pages.rb b/db/migrate/20251125142646_add_creator_to_pages.rb new file mode 100644 index 000000000..2ac492939 --- /dev/null +++ b/db/migrate/20251125142646_add_creator_to_pages.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Adds creator_id to pages table +class AddCreatorToPages < ActiveRecord::Migration[8.0] + def change + add_column :better_together_pages, :creator_id, :uuid + add_index :better_together_pages, :creator_id + add_foreign_key :better_together_pages, :better_together_people, column: :creator_id + end +end diff --git a/diagrams/source/rich_text_link_checker_flow.mmd b/diagrams/source/rich_text_link_checker_flow.mmd deleted file mode 100644 index 06fc308f6..000000000 --- a/diagrams/source/rich_text_link_checker_flow.mmd +++ /dev/null @@ -1,11 +0,0 @@ -%% Mermaid source: RichText Link Checker Flow -flowchart TD - A[ActionText::RichText records] --> B[RichTextLinkIdentifier] - B --> C[BetterTogether::Content::Link] - B --> D[BetterTogether::Metrics::RichTextLink] - E[Rake: check task] --> F[RichTextLinkCheckerQueueJob (internal)] - E --> G[RichTextLinkCheckerQueueJob (external)] - F --> H[InternalLinkCheckerJob] - G --> I[ExternalLinkCheckerJob] - H --> C - I --> C diff --git a/docs/developers/development/navigation_area_management.md b/docs/developers/development/navigation_area_management.md new file mode 100644 index 000000000..55cc32e9f --- /dev/null +++ b/docs/developers/development/navigation_area_management.md @@ -0,0 +1,294 @@ +# Navigation Area Management + +This document describes how to reset and reseed navigation areas in the Better Together Community Engine. + +## Overview + +The NavigationBuilder provides methods to reset and reseed navigation areas without affecting pages. This is useful when you need to update navigation structure or fix navigation issues without losing page content. + +## Available Rake Tasks + +All tasks are under the `better_together:generate` namespace: + +```bash +# List all navigation areas and items +bin/dc-run rails better_together:generate:list_navigation + +# Reset all navigation areas (preserves pages) +bin/dc-run rails better_together:generate:reset_navigation + +# Reset specific navigation area +bin/dc-run rails better_together:generate:reset_navigation_area[platform-header] + +# Full rebuild (DESTRUCTIVE - deletes pages too!) +bin/dc-run rails better_together:generate:navigation_and_pages +``` + +## Detailed Methods + +### 1. Reset All Navigation Areas + +**Ruby Console:** +```ruby +BetterTogether::NavigationBuilder.reset_navigation_areas +``` + +**Rake Task:** +```bash +bin/dc-run rails better_together:generate:reset_navigation +``` + +**What it does:** +- Deletes all navigation items +- Deletes all navigation areas +- Rebuilds all four navigation areas: + - Platform Header + - Platform Host + - Better Together + - Platform Footer +- **Preserves all pages** (pages are not deleted) + +### 2. Reset Specific Navigation Area + +**Ruby Console:** +```ruby +BetterTogether::NavigationBuilder.reset_navigation_area('platform-header') +``` + +**Rake Task:** +```bash +bin/dc-run rails better_together:generate:reset_navigation_area[platform-header] +``` + +**Available slugs:** +- `platform-header` - Main navigation menu +- `platform-host` - Host admin dropdown menu +- `better-together` - Better Together dropdown +- `platform-footer` - Footer links + +**What it does:** +- Deletes navigation items for the specified area +- Deletes the navigation area +- Rebuilds only that specific navigation area +- **Preserves all pages** + +### 3. List Navigation Areas + +**Rake Task:** +```bash +bin/dc-run rails better_together:generate:list_navigation +``` + +**What it does:** +- Shows all navigation areas with their details +- Lists all navigation items in each area +- Shows parent-child relationships +- Displays visibility and protection status + +**Example output:** +``` +Navigation Areas: +================================================================================ + +Area: Platform Header + Slug: platform-header + Visible: true + Protected: true + Items: 3 + Navigation Items: + - About (link) + - Events (link) + - Exchange Hub (link) +... +``` + +### 4. Full Rebuild (Includes Pages) + +**Rake Task:** +```bash +bin/dc-run rails better_together:generate:navigation_and_pages +``` + +**What it does:** +- Deletes all pages +- Deletes all navigation items +- Deletes all navigation areas +- Rebuilds everything from scratch +- **WARNING:** This deletes all pages including custom content + +## Navigation Areas Structure + +### Platform Header +- **Purpose:** Main site navigation at top of page +- **Contains:** + - About page link + - Events link (route-based) + - Exchange Hub link (route-based) + +### Platform Host +- **Purpose:** Admin/host management dropdown +- **Contains:** + - Dashboard + - Communities + - Navigation Areas + - Pages + - People + - Platforms + - Roles + - Resource Permissions + +### Better Together +- **Purpose:** Information about the platform software +- **Contains:** + - What is Better Together? + - About the Community Engine + +### Platform Footer +- **Purpose:** Footer links for policies and info +- **Contains:** + - FAQ + - Privacy Policy + - Terms of Service + - Code of Conduct + - Accessibility + - Cookie Policy + - Code Contributor Agreement + - Content Contributor Agreement + - Contact + +## Common Use Cases + +### Update Navigation After Adding New Pages + +If you've added new pages to footer_pages in NavigationBuilder: + +```bash +# Reset just the footer area +bin/dc-run rails navigation:reset_area[platform-footer] +``` + +### Fix Broken Navigation + +If navigation items are missing or duplicated: + +```bash +# Reset all navigation areas +bin/dc-run rails navigation:reset +``` + +### Development: Fresh Start + +For a completely fresh navigation setup: + +```bash +# Full rebuild (careful - deletes pages!) +bin/dc-run rails navigation:rebuild +``` + +### Check Current Navigation State + +```bash +# List all areas and items +bin/dc-run rails navigation:list +``` + +## Programmatic Usage + +### In Seeds or Migrations + +```ruby +# Reset all navigation (safe for pages) +BetterTogether::NavigationBuilder.reset_navigation_areas + +# Reset specific area +BetterTogether::NavigationBuilder.reset_navigation_area('platform-footer') + +# Full rebuild (destructive) +BetterTogether::NavigationBuilder.build(clear: true) +``` + +### In Rails Console + +```ruby +# Reset navigation +BetterTogether::NavigationBuilder.reset_navigation_areas + +# Check what areas exist +BetterTogether::NavigationArea.pluck(:name, :slug) + +# See items in an area +area = BetterTogether::NavigationArea.find_by(slug: 'platform-footer') +area.navigation_items.pluck(:title, :item_type, :position) +``` + +## Important Notes + +### Protected Records + +All navigation areas and items created by the builder are marked as `protected: true`. This means: +- They cannot be deleted through the UI +- They are considered system records +- Manual database edits may be needed to modify protection status + +### Localization + +All navigation building happens within `I18n.with_locale(:en)`. If you need other locales: +- Pages support multi-locale attributes (title_en, title_es, etc.) +- Navigation items also support multi-locale attributes +- Update the builder methods to include additional locales + +### Page Preservation + +The `reset_navigation_areas` and `reset_navigation_area` methods preserve pages because: +- Pages may contain user-generated content +- Pages may have been customized +- Navigation can be rebuilt without affecting page content +- Pages are referenced by navigation items but not dependent on them + +### Safe Order of Operations + +When resetting navigation: +1. Child navigation items are deleted first +2. Parent navigation items are deleted second +3. Navigation areas are deleted last +4. This respects foreign key constraints + +## Troubleshooting + +### "Navigation area with slug 'X' not found" + +The area doesn't exist. Check available slugs: +```ruby +BetterTogether::NavigationArea.pluck(:slug) +``` + +### Duplicate Navigation Items + +Run a reset: +```bash +bin/dc-run rails navigation:reset +``` + +### Pages Missing After Reset + +If you used `navigation:rebuild`, pages were deleted. Restore from backup or reseed: +```bash +bin/dc-run rails db:seed +``` + +### Permission Errors in UI + +Navigation areas and items are protected. To modify through UI: +1. Unprotect in console: `area.update(protected: false)` +2. Make changes in UI +3. Re-protect: `area.update(protected: true)` + +## Related Files + +- Builder: `app/builders/better_together/navigation_builder.rb` +- Rake tasks: `lib/tasks/navigation.rake` +- Seeds: `db/seeds.rb` +- Models: + - `app/models/better_together/navigation_area.rb` + - `app/models/better_together/navigation_item.rb` + - `app/models/better_together/page.rb` diff --git a/docs/developers/development/navigation_quick_reference.md b/docs/developers/development/navigation_quick_reference.md new file mode 100644 index 000000000..3356736ac --- /dev/null +++ b/docs/developers/development/navigation_quick_reference.md @@ -0,0 +1,58 @@ +# Navigation Reset Quick Reference + +## Quick Commands + +```bash +# List all navigation areas and items +bin/dc-run rails better_together:generate:list_navigation + +# Reset all navigation areas (safe - preserves pages) +bin/dc-run rails better_together:generate:reset_navigation + +# Reset specific navigation area +bin/dc-run rails better_together:generate:reset_navigation_area[platform-header] +bin/dc-run rails better_together:generate:reset_navigation_area[platform-host] +bin/dc-run rails better_together:generate:reset_navigation_area[better-together] +bin/dc-run rails better_together:generate:reset_navigation_area[platform-footer] + +# Full rebuild (DESTRUCTIVE - deletes pages too!) +bin/dc-run rails better_together:generate:navigation_and_pages +``` + +## Ruby Console + +```ruby +# Reset all navigation areas +BetterTogether::NavigationBuilder.reset_navigation_areas + +# Reset specific area +BetterTogether::NavigationBuilder.reset_navigation_area('platform-footer') + +# Full rebuild +BetterTogether::NavigationBuilder.build(clear: true) + +# Manual checks +BetterTogether::NavigationArea.count +BetterTogether::NavigationItem.count +BetterTogether::Page.count +``` + +## Navigation Area Slugs + +- `platform-header` - Top navigation menu +- `platform-host` - Host/admin dropdown +- `better-together` - Software info dropdown +- `platform-footer` - Footer links + +## Safety Levels + +✅ **Safe** (preserves pages): +- `better_together:generate:reset_navigation` +- `better_together:generate:reset_navigation_area[slug]` +- `better_together:generate:list_navigation` +- `reset_navigation_areas` (Ruby method) +- `reset_navigation_area(slug)` (Ruby method) + +⚠️ **Destructive** (deletes pages): +- `better_together:generate:navigation_and_pages` +- `NavigationBuilder.build(clear: true)` (Ruby method) diff --git a/docs/developers/systems/events_system.md b/docs/developers/systems/events_system.md index 742d6c223..b730624a5 100644 --- a/docs/developers/systems/events_system.md +++ b/docs/developers/systems/events_system.md @@ -743,9 +743,9 @@ journey ## Additional Resources ### User Documentation -- 📖 [Event User Guide](../../users/events_user_guide.md) - Comprehensive guide for organizers and attendees -- 🎯 [Best Practices](../../users/events_user_guide.md#best-practices) - Tips for successful event management -- 🔧 [Troubleshooting](../../users/events_user_guide.md#troubleshooting-common-issues) - Common issues and solutions +- 📖 [Event User Guide](../../end_users/events_invitations_and_rsvp.md) - Comprehensive guide for organizers and attendees +- 🎯 [Best Practices](../../end_users/events_invitations_and_rsvp.md#best-practices) - Tips for successful event management +- 🔧 [Troubleshooting](../../end_users/events_invitations_and_rsvp.md#troubleshooting) - Common issues and solutions ### All Event System Diagrams - 📊 [Events Schema ERD](../../diagrams/source/events_schema_erd.mmd) - Database relationships diff --git a/docs/development/README.md b/docs/development/README.md new file mode 100644 index 000000000..1eec8544a --- /dev/null +++ b/docs/development/README.md @@ -0,0 +1,64 @@ +# Development Resources + +**Target Audience:** Developers +**Purpose:** Development setup, tools, and workflows + +## Getting Started + +- [🛠️ Development Setup](dev-setup.md) - Complete local development environment setup guide + +## Quick Links + +- [Main README](../README.md) - Documentation overview +- [Developer Documentation](../developers/README.md) - System and architecture docs +- [Contributing Guide](../../CONTRIBUTING.md) - How to contribute +- [Security Policy](../../SECURITY.md) - Reporting security issues + +## Development Tools + +### Required Tools +- Ruby 3.4.4 via rbenv +- Node.js 20.x +- PostgreSQL 14+ with PostGIS +- Redis (for caching and Sidekiq) +- Elasticsearch 7.17.23 +- Docker & Docker Compose + +### Development Commands + +```bash +# Run tests +bin/dc-run bin/ci + +# Lint code +bin/dc-run bundle exec rubocop -A + +# Security scan +bin/dc-run bundle exec brakeman --quiet --no-pager + +# I18n management +bin/dc-run bin/i18n all + +# Render diagrams +bin/render_diagrams +``` + +## Development Workflow + +1. **Setup**: Follow [dev-setup.md](dev-setup.md) +2. **Branch**: Create feature branch +3. **Test**: Write tests first (TDD) +4. **Code**: Implement feature +5. **Verify**: Run tests and quality checks +6. **Commit**: Submit pull request + +## Related Documentation + +- [Architecture](../developers/architecture/) - System architecture +- [Systems](../developers/systems/) - Feature documentation +- [Implementation Templates](../implementation/templates/) - Project templates +- [Diagram Sources](../diagrams/source/) - Visual documentation + +--- + +**Note:** This is a Rails engine project. Most development occurs in the engine code, with spec/dummy used for testing. diff --git a/docs/diagrams/exports/png/models_and_concerns_diagram.png b/docs/diagrams/exports/png/models_and_concerns_diagram.png index 290a8851b..62b12e940 100644 Binary files a/docs/diagrams/exports/png/models_and_concerns_diagram.png and b/docs/diagrams/exports/png/models_and_concerns_diagram.png differ diff --git a/docs/diagrams/exports/svg/content_schema_erd.svg b/docs/diagrams/exports/svg/content_schema_erd.svg index fb54e8bcb..d6a4aee9e 100644 --- a/docs/diagrams/exports/svg/content_schema_erd.svg +++ b/docs/diagrams/exports/svg/content_schema_erd.svg @@ -1 +1 @@ -hasappears_inhas

    BETTER_TOGETHER_PAGES

    uuid

    id

    PK

    string

    identifier

    string

    privacy

    string

    slug

    datetime

    published_at

    string

    layout

    uuid

    sidebar_nav_id

    FK

    uuid

    creator_id

    FK

    uuid

    community_id

    FK

    boolean

    protected

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_CONTENT_PAGE_BLOCKS

    uuid

    id

    PK

    uuid

    page_id

    FK

    uuid

    block_id

    FK

    integer

    position

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_CONTENT_BLOCKS

    uuid

    id

    PK

    string

    type

    string

    identifier

    uuid

    creator_id

    FK

    string

    privacy

    boolean

    visible

    jsonb

    content_data

    jsonb

    css_settings

    jsonb

    media_settings

    jsonb

    layout_settings

    jsonb

    accessibility_attributes

    jsonb

    data_attributes

    jsonb

    html_attributes

    jsonb

    content_settings

    jsonb

    content_area_settings

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_PLATFORMS

    BETTER_TOGETHER_CONTENT_PLATFORM_BLOCKS

    uuid

    id

    PK

    uuid

    platform_id

    FK

    uuid

    block_id

    FK

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    \ No newline at end of file +hasappears_inhas

    BETTER_TOGETHER_PAGES

    uuid

    id

    PK

    string

    identifier

    string

    privacy

    string

    slug

    datetime

    published_at

    string

    layout

    uuid

    sidebar_nav_id

    FK

    uuid

    creator_id

    FK

    uuid

    community_id

    FK

    boolean

    protected

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_CONTENT_PAGE_BLOCKS

    uuid

    id

    PK

    uuid

    page_id

    FK

    uuid

    block_id

    FK

    integer

    position

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_CONTENT_BLOCKS

    uuid

    id

    PK

    string

    type

    string

    identifier

    uuid

    creator_id

    FK

    string

    privacy

    boolean

    visible

    jsonb

    content_data

    jsonb

    css_settings

    jsonb

    media_settings

    jsonb

    layout_settings

    jsonb

    accessibility_attributes

    jsonb

    data_attributes

    jsonb

    html_attributes

    jsonb

    content_settings

    jsonb

    content_area_settings

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_PLATFORMS

    BETTER_TOGETHER_CONTENT_PLATFORM_BLOCKS

    uuid

    id

    PK

    uuid

    platform_id

    FK

    uuid

    block_id

    FK

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    \ No newline at end of file diff --git a/docs/diagrams/exports/svg/events_invitations_schema_erd.svg b/docs/diagrams/exports/svg/events_invitations_schema_erd.svg index d8f2d7cd6..6707f0df6 100644 --- a/docs/diagrams/exports/svg/events_invitations_schema_erd.svg +++ b/docs/diagrams/exports/svg/events_invitations_schema_erd.svg @@ -1 +1 @@ -hasinvitesattendsmay_be_invitee

    BETTER_TOGETHER_EVENTS

    uuid

    id

    PK

    string

    type

    uuid

    creator_id

    FK

    string

    identifier

    string

    privacy

    timestamp

    starts_at

    timestamp

    ends_at

    integer

    duration_minutes

    string

    registration_url

    integer

    lock_version

    timestamp

    created_at

    timestamp

    updated_at

    BETTER_TOGETHER_EVENT_ATTENDANCES

    uuid

    id

    PK

    uuid

    event_id

    FK

    uuid

    person_id

    FK

    string

    status

    interested|going (string enum)

    integer

    lock_version

    timestamp

    created_at

    timestamp

    updated_at

    BETTER_TOGETHER_INVITATIONS

    uuid

    id

    PK

    string

    type

    STI: EventInvitation

    uuid

    invitable_id

    FK

    Event ID

    string

    invitable_type

    uuid

    inviter_id

    FK

    string

    inviter_type

    uuid

    invitee_id

    FK

    optional

    string

    invitee_type

    Person if present

    string

    invitee_email

    optional

    string

    status

    pending|accepted|declined (string enum)

    string

    token

    secure unique

    string

    locale

    timestamp

    valid_from

    timestamp

    valid_until

    timestamp

    accepted_at

    timestamp

    last_sent

    integer

    lock_version

    timestamp

    created_at

    timestamp

    updated_at

    BETTER_TOGETHER_PERSONS

    uuid

    id

    PK

    string

    name

    string

    email

    via user/contact detail

    \ No newline at end of file +hasinvitesattendsmay_be_invitee

    BETTER_TOGETHER_EVENTS

    uuid

    id

    PK

    string

    type

    uuid

    creator_id

    FK

    string

    identifier

    string

    privacy

    timestamp

    starts_at

    timestamp

    ends_at

    integer

    duration_minutes

    string

    registration_url

    integer

    lock_version

    timestamp

    created_at

    timestamp

    updated_at

    BETTER_TOGETHER_EVENT_ATTENDANCES

    uuid

    id

    PK

    uuid

    event_id

    FK

    uuid

    person_id

    FK

    string

    status

    interested|going (string enum)

    integer

    lock_version

    timestamp

    created_at

    timestamp

    updated_at

    BETTER_TOGETHER_INVITATIONS

    uuid

    id

    PK

    string

    type

    STI: EventInvitation

    uuid

    invitable_id

    FK

    Event ID

    string

    invitable_type

    uuid

    inviter_id

    FK

    string

    inviter_type

    uuid

    invitee_id

    FK

    optional

    string

    invitee_type

    Person if present

    string

    invitee_email

    optional

    string

    status

    pending|accepted|declined (string enum)

    string

    token

    secure unique

    string

    locale

    timestamp

    valid_from

    timestamp

    valid_until

    timestamp

    accepted_at

    timestamp

    last_sent

    integer

    lock_version

    timestamp

    created_at

    timestamp

    updated_at

    BETTER_TOGETHER_PERSONS

    uuid

    id

    PK

    string

    name

    string

    email

    via user/contact detail

    \ No newline at end of file diff --git a/docs/diagrams/exports/svg/events_schema_erd.svg b/docs/diagrams/exports/svg/events_schema_erd.svg index 8fa95be22..c7c0d9c29 100644 --- a/docs/diagrams/exports/svg/events_schema_erd.svg +++ b/docs/diagrams/exports/svg/events_schema_erd.svg @@ -1 +1 @@ -hashasappears_inhaspolymorphic

    BETTER_TOGETHER_EVENTS

    uuid

    id

    PK

    string

    type

    uuid

    creator_id

    FK

    string

    identifier

    string

    privacy

    datetime

    starts_at

    datetime

    ends_at

    decimal

    duration_minutes

    string

    registration_url

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_EVENT_ATTENDANCES

    uuid

    id

    PK

    uuid

    event_id

    FK

    uuid

    person_id

    FK

    string

    status

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_EVENT_HOSTS

    uuid

    id

    PK

    uuid

    event_id

    FK

    uuid

    host_id

    string

    host_type

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_CALENDAR_ENTRIES

    uuid

    id

    PK

    uuid

    calendar_id

    FK

    uuid

    event_id

    FK

    datetime

    starts_at

    datetime

    ends_at

    decimal

    duration_minutes

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_CALENDARS

    uuid

    id

    PK

    uuid

    community_id

    FK

    uuid

    creator_id

    FK

    string

    identifier

    string

    locale

    string

    privacy

    boolean

    protected

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    HOSTS

    \ No newline at end of file +hashasappears_inhaspolymorphic

    BETTER_TOGETHER_EVENTS

    uuid

    id

    PK

    string

    type

    uuid

    creator_id

    FK

    string

    identifier

    string

    privacy

    datetime

    starts_at

    datetime

    ends_at

    decimal

    duration_minutes

    string

    registration_url

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_EVENT_ATTENDANCES

    uuid

    id

    PK

    uuid

    event_id

    FK

    uuid

    person_id

    FK

    string

    status

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_EVENT_HOSTS

    uuid

    id

    PK

    uuid

    event_id

    FK

    uuid

    host_id

    string

    host_type

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_CALENDAR_ENTRIES

    uuid

    id

    PK

    uuid

    calendar_id

    FK

    uuid

    event_id

    FK

    datetime

    starts_at

    datetime

    ends_at

    decimal

    duration_minutes

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    BETTER_TOGETHER_CALENDARS

    uuid

    id

    PK

    uuid

    community_id

    FK

    uuid

    creator_id

    FK

    string

    identifier

    string

    locale

    string

    privacy

    boolean

    protected

    integer

    lock_version

    datetime

    created_at

    datetime

    updated_at

    HOSTS

    \ No newline at end of file diff --git a/docs/diagrams/exports/svg/models_and_concerns_diagram.svg b/docs/diagrams/exports/svg/models_and_concerns_diagram.svg index 389708307..e7a0374bc 100644 --- a/docs/diagrams/exports/svg/models_and_concerns_diagram.svg +++ b/docs/diagrams/exports/svg/models_and_concerns_diagram.svg @@ -1 +1 @@ -has_oneagentsenderentry111*1*11111*1*1****

    Person

    <

    FriendlySlug,Privacy,Viewable,RemoveableAttachment>>

    «DeviseUser»

    User

    Identification

    Community

    <

    Protected,Privacy,Permissible,RemoveableAttachment>>

    Platform

    <

    Protected,Privacy,Permissible>>

    «Membership»

    PersonCommunityMembership

    «Membership»

    PersonPlatformMembership

    Post

    <

    FriendlySlug>>

    Page

    <

    FriendlySlug>>

    Category

    Categorization

    Conversation

    ConversationParticipant

    Message

    Event

    <

    FriendlySlug,Geospatial::One,Locatable::One,Identifier,

    Privacy,TrackedActivity,Viewable>>

    EventCategory

    Calendar

    CalendarEntry

    «BuildingConnections»

    Building

    Floor

    Room

    Address

    ContactDetail

    \ No newline at end of file +has_oneagentsenderentry111*1*11111*1*1****

    Person

    <

    FriendlySlug,Privacy,Viewable,RemoveableAttachment>>

    «DeviseUser»

    User

    Identification

    Community

    <

    Protected,Privacy,Permissible,RemoveableAttachment>>

    Platform

    <

    Protected,Privacy,Permissible>>

    «Membership»

    PersonCommunityMembership

    «Membership»

    PersonPlatformMembership

    Post

    <

    FriendlySlug>>

    Page

    <

    FriendlySlug>>

    Category

    Categorization

    Conversation

    ConversationParticipant

    Message

    Event

    <

    FriendlySlug,Geospatial::One,Locatable::One,Identifier,

    Privacy,TrackedActivity,Viewable>>

    EventCategory

    Calendar

    CalendarEntry

    «BuildingConnections»

    Building

    Floor

    Room

    Address

    ContactDetail

    \ No newline at end of file diff --git a/docs/end_users/community_guidelines.md b/docs/end_users/community_guidelines.md index cde2138e8..d1ab217d4 100644 --- a/docs/end_users/community_guidelines.md +++ b/docs/end_users/community_guidelines.md @@ -1,19 +1,223 @@ -# Community Guidelines (End User Summary) +# Community Guidelines -**Purpose:** Outline expected behavior to keep communities safe and respectful. +**Target Audience:** All community members +**Document Type:** Policy/Guidelines +**Last Updated:** November 20, 2025 -## Core Expectations -- Be respectful: no harassment, hate speech, or abusive content. -- Stay relevant: keep exchanges and posts on-topic for the community. -- Protect privacy: do not share others’ personal information without consent. -- Follow platform terms and local laws. +## Overview + +These community guidelines help create a safe, welcoming, and productive environment for all members of the Better Together platform. By participating in any community on this platform, you agree to follow these guidelines. + +> **Note:** This is a template. Each platform and community may have additional specific guidelines. + +## Core Principles + +### Respect and Inclusion +- **Be respectful** - Treat all members with courtesy and consideration +- **Value diversity** - Welcome people of all backgrounds and perspectives +- **Practice empathy** - Consider others' feelings and experiences +- **Assume good faith** - Give others the benefit of the doubt + +### Constructive Communication +- **Be clear and kind** - Communicate thoughtfully and considerately +- **Stay on topic** - Keep discussions relevant to the community's purpose +- **Listen actively** - Read and consider others' contributions before responding +- **Disagree gracefully** - It's okay to disagree, but do so respectfully + +### Safety and Privacy +- **Protect privacy** - Don't share others' personal information without consent +- **Report concerns** - Use reporting tools if you see harmful content +- **Respect boundaries** - Honor others' privacy and comfort levels +- **Keep it legal** - Don't post illegal content or engage in illegal activities + +## Unacceptable Behavior + +The following behaviors are **not allowed** on this platform: + +### Harassment and Abuse +- **Personal attacks** - Insults, name-calling, or hostile behavior toward individuals +- **Harassment** - Persistent unwanted contact or attention +- **Bullying** - Intimidation or coercion of other members +- **Threats** - Threats of violence or harm +- **Doxxing** - Sharing private information without consent + +### Discrimination and Hate Speech +- **Hate speech** - Content attacking people based on protected characteristics +- **Discrimination** - Unfair treatment based on identity or background +- **Slurs** - Use of derogatory language targeting groups or individuals +- **Extremist content** - Promotion of hate groups or violent ideologies + +### Harmful Content +- **Spam** - Unsolicited commercial content or repetitive posts +- **Misinformation** - Deliberately spreading false information +- **Graphic content** - Excessively violent, disturbing, or sexual content (context matters) +- **Illegal content** - Content that violates applicable laws + +### Platform Abuse +- **Account misuse** - Creating fake accounts, impersonation, or ban evasion +- **System exploitation** - Attempting to hack, break, or circumvent platform features +- **Vote manipulation** - Coordinating to artificially boost or suppress content +- **Data scraping** - Unauthorized automated collection of platform data + +## Community-Specific Guidelines + +### Event Communities +- **RSVP honestly** - Only RSVP if you intend to attend +- **Respect organizers** - Follow event-specific rules and requests +- **Communicate changes** - Update your RSVP if plans change +- **Provide feedback** - Help improve events with constructive feedback + +### Exchange Communities (Joatu) +- **Honor agreements** - Follow through on exchange commitments +- **Describe accurately** - Provide honest descriptions of offers and requests +- **Communicate clearly** - Keep exchange partners informed +- **Rate fairly** - Provide honest, constructive reviews + +### Discussion Communities +- **Stay on topic** - Respect the community's focus area +- **Search first** - Check if your question has been answered before +- **Share knowledge** - Contribute helpful information to discussions +- **Cite sources** - Give credit and link to original sources ## Reporting and Moderation -- Use in-app reporting tools to flag inappropriate content or behavior. -- Community organizers and moderators review reports and may take action (warnings, suspensions, bans). - -## Best Practices -- Assume good intent but set boundaries. -- Use clear titles and descriptions for offers/requests. -- Move sensitive details to private channels when appropriate. -- Ask moderators for guidance when unsure. + +### How to Report +If you see content or behavior that violates these guidelines: + +1. **Use the report button** - Available on most content types +2. **Provide details** - Explain what guideline was violated and why +3. **Include evidence** - Screenshots or links if helpful +4. **Be specific** - Clear reports help moderators respond effectively + +See [Safety and Reporting Tools](safety_reporting.md) for detailed reporting instructions. + +### Moderation Process +When guidelines are violated, moderators may: + +1. **Issue a warning** - For first-time or minor violations +2. **Remove content** - Delete posts, comments, or other content +3. **Restrict features** - Temporarily limit posting or participation +4. **Suspend account** - Temporary ban for serious or repeated violations +5. **Ban permanently** - Remove access for severe or persistent violations + +### Appeals +If you believe a moderation decision was made in error: + +- **Contact moderators** - Explain your perspective respectfully +- **Provide context** - Help moderators understand the situation +- **Be patient** - Allow time for review +- **Accept decisions** - Final decisions are made by platform administrators + +## Your Responsibilities + +### As a Community Member +- **Read the guidelines** - Familiarize yourself with expectations +- **Follow the rules** - Adhere to both platform and community-specific guidelines +- **Report violations** - Help keep the community safe +- **Lead by example** - Model the behavior you want to see + +### As a Community Organizer +- **Set clear expectations** - Establish community-specific guidelines +- **Moderate consistently** - Apply rules fairly to all members +- **Respond to reports** - Address reported issues promptly +- **Communicate transparently** - Explain moderation decisions when appropriate + +### As a Platform Organizer +- **Enforce policies** - Ensure platform-wide guidelines are followed +- **Support moderators** - Provide tools and training for community organizers +- **Review appeals** - Handle escalated moderation decisions +- **Update guidelines** - Revise policies as the platform evolves + +## Consequences + +### Warning System +- **First violation** - Usually receives a warning +- **Second violation** - May result in temporary restrictions +- **Third violation** - May result in suspension +- **Severe violations** - May result in immediate ban + +### Factors Considered +- **Severity** - How harmful was the behavior? +- **Intent** - Was it deliberate or accidental? +- **History** - Is this a pattern or a one-time incident? +- **Context** - What were the circumstances? +- **Impact** - How did it affect other members? + +## Privacy and Safety Features + +### Tools Available to You +- **Block users** - Prevent specific users from interacting with you +- **Privacy settings** - Control who can see your content and profile +- **Report button** - Flag problematic content for review +- **Privacy levels** - Set content visibility (public/private) +- **Community membership** - Choose which communities to join + +See [Safety and Reporting Tools](safety_reporting.md) for detailed information. + +## Platform Values + +These guidelines reflect our commitment to: + +### Democratic Principles +- **Member voice** - Community members have input on governance +- **Transparency** - Clear, public guidelines and processes +- **Accountability** - Moderators and administrators are accountable +- **Fair process** - Consistent, equitable moderation + +See [Democratic Principles](../shared/democratic_principles.md) for more information. + +### Privacy First +- **Data minimization** - Collect only necessary information +- **User control** - You control your data and privacy settings +- **Transparency** - Clear about what data is collected and why +- **Security** - Protect your information with encryption and access controls + +See [Privacy Principles](../shared/privacy_principles.md) for more information. + +## Updates to Guidelines + +These guidelines may be updated to: +- **Clarify expectations** - Make rules clearer +- **Address new issues** - Respond to emerging concerns +- **Incorporate feedback** - Reflect community input +- **Comply with law** - Meet legal requirements + +**When guidelines change:** +- Members will be notified via email or platform announcement +- Changes will be highlighted in the updated document +- The "Last Updated" date will be updated + +## Questions and Support + +### Getting Help +- **Documentation** - Check the [User Management Guide](user_management_guide.md) +- **Community support** - Ask questions in designated help communities +- **Contact moderators** - Reach out to community organizers +- **Platform support** - Contact platform administrators for major issues + +### Providing Feedback +We welcome your input on these guidelines: +- **Suggest improvements** - Share ideas for better guidelines +- **Report gaps** - Point out scenarios not covered +- **Share experiences** - Help us understand community needs + +## Related Documentation + +- [Privacy Policy](privacy_policy.md) +- [Safety and Reporting Tools](safety_reporting.md) +- [User Management Guide](user_management_guide.md) +- [Democratic Principles](../shared/democratic_principles.md) +- [Privacy Principles](../shared/privacy_principles.md) +- [Escalation Matrix](../shared/escalation_matrix.md) + +## Acknowledgments + +These guidelines are inspired by and built upon community standards from: +- The Contributor Covenant +- Mozilla Community Participation Guidelines +- Ubuntu Code of Conduct +- Open source community best practices + +--- + +**Remember:** These guidelines exist to create a positive environment for everyone. When in doubt, ask yourself: "Is this respectful, constructive, and helpful?" If yes, you're probably on the right track. diff --git a/docs/end_users/community_participation.md b/docs/end_users/community_participation.md index 4e3e5dc78..9a1a18552 100644 --- a/docs/end_users/community_participation.md +++ b/docs/end_users/community_participation.md @@ -1,28 +1,499 @@ # Community Participation Guide -**Purpose:** Help you engage with communities, contribute offers/requests, and collaborate safely. - -## Getting Started -- Join relevant communities during registration or from your profile. -- Complete your profile so organizers and neighbors can recognize you. -- Review community guidelines before posting. - -## Creating Offers and Requests -- Use clear titles and descriptions. -- Set appropriate categories/tags to improve discovery. -- Keep availability and expectations realistic. - -## Engaging with Others -- Respond promptly to messages and invitations. -- Respect boundaries and schedules. -- Provide constructive feedback after exchanges when available. - -## Staying Safe -- Use on-platform messaging for coordination. -- Meet in safe, agreed-upon locations when exchanging items. -- Report issues or policy violations to moderators. - -## Contributing Back -- Share feedback with organizers on how to improve the community. -- Volunteer for moderation or support roles if you have capacity. -- Promote inclusive, welcoming interactions in all exchanges. +**Target Audience:** Community members +**Document Type:** User Guide +**Last Updated:** November 20, 2025 + +## Overview + +Communities are the heart of the Better Together platform. This guide helps you discover communities, participate meaningfully, and get the most from your community memberships. + +## Understanding Communities + +### What is a Community? + +A **community** is a group of members who share common interests, goals, or geographic location. Each community has: + +- **Purpose** - Defined focus or mission +- **Members** - People who have joined +- **Content** - Posts, events, resources +- **Leadership** - Community organizers and moderators +- **Guidelines** - Community-specific rules + +### Community Types + +Communities may be organized around: + +**Interest-based:** +- Hobbies and activities +- Professional fields +- Causes and movements +- Learning and education + +**Geographic:** +- Neighborhoods +- Cities and regions +- Countries or languages +- Virtual/global + +**Purpose-based:** +- Resource sharing (Joatu exchanges) +- Event coordination +- Project collaboration +- Support and mutual aid + +## Finding Communities + +### Discovering Communities + +**Browse communities:** +1. Click "Communities" in main navigation +2. View featured communities +3. Browse by category +4. See communities you might like + +**Search for communities:** +- Search by name +- Search by keywords +- Filter by location +- Filter by activity level +- Sort by member count or recent activity + +### Community Information + +Before joining, review: + +**Community profile:** +- Name and description +- Purpose and focus +- Member count +- Activity level +- Privacy setting (public/private) +- Membership requirements + +**Leadership:** +- Community organizers +- Moderators +- Contact information + +**Guidelines:** +- Community-specific rules +- Expected behavior +- Content policies + +## Joining Communities + +### Membership Types + +**Open communities:** +- Anyone can join immediately +- Public content visible to all +- No approval required + +**Closed communities:** +- Membership requires approval +- Content visible only to members +- Application may be required + +**Invitation-only:** +- Must be invited to join +- Highly restricted access +- Usually for specific groups + +### How to Join + +**For open communities:** +1. Visit community page +2. Click "Join Community" button +3. Confirm membership +4. Start participating + +**For closed communities:** +1. Visit community page +2. Click "Request Membership" +3. Complete application (if required) +4. Wait for approval +5. Receive notification when accepted + +**For invitation-only:** +1. Receive invitation from member +2. Click invitation link +3. Accept invitation +4. Join community + +### Multiple Community Memberships + +You can join multiple communities: + +- Join communities that match your interests +- Participate in geographic and interest communities +- Balance participation across communities +- Leave communities that no longer fit + +## Participating in Communities + +### Reading Community Content + +**View posts and discussions:** +- Browse community feed +- Read posts and comments +- View pinned announcements +- Check community calendar +- Access shared resources + +**Engage with content:** +- Like or react to posts +- Comment on discussions +- Share posts (within privacy settings) +- Save posts for later +- Report inappropriate content + +### Contributing Content + +**Create posts:** +1. Click "New Post" in community +2. Choose post type (text, link, event, etc.) +3. Add title and content +4. Set privacy level +5. Add tags or categories +6. Publish or save draft + +**Post types:** +- **Discussions** - Start conversations +- **Announcements** - Share news and updates +- **Questions** - Ask for help or information +- **Resources** - Share useful links or files +- **Events** - Announce and coordinate events + +**Best practices:** +- Stay on topic for the community +- Use clear, descriptive titles +- Format posts for readability +- Add relevant tags +- Respect community guidelines + +### Commenting and Discussions + +**Participate in discussions:** +- Read full thread before commenting +- Add value to the conversation +- Respond to specific points +- Ask clarifying questions +- Acknowledge others' contributions + +**Comment etiquette:** +- Be respectful and constructive +- Stay on topic +- Cite sources when sharing information +- Admit if you're uncertain +- Edit comments to fix errors + +### Community Events + +**Event participation:** +- View community event calendar +- RSVP to events +- Invite other community members +- Coordinate with attendees +- Share event updates + +See [Events and RSVP Guide](events_invitations_and_rsvp.md) for details. + +### Resource Sharing (Joatu) + +**Exchange within communities:** +- Post offers and requests +- Respond to community members' offers +- Build agreements with community members +- Share skills and resources +- Build community resilience + +See [Exchange Process Guide](exchange_process.md) for details. + +## Community Leadership + +### Community Organizers + +**Organizers are responsible for:** +- Setting community direction +- Creating and enforcing guidelines +- Moderating content and behavior +- Organizing events and activities +- Growing and engaging membership + +**How organizers are selected:** +- Appointed by platform organizers +- Elected by community members +- Self-organized in new communities +- Rotated among active members + +See [Community Management](../community_organizers/community_management.md) for organizer details. + +### Becoming an Organizer + +**Paths to leadership:** +1. **Start a new community** - Create your own +2. **Volunteer to help** - Assist existing organizers +3. **Get elected** - Run in community elections +4. **Be appointed** - Selected by current organizers + +**Responsibilities:** +- Regular time commitment +- Understanding of guidelines +- Fair and consistent moderation +- Communication with members +- Collaboration with other organizers + +### Supporting Your Community + +**Help without formal leadership:** +- Welcome new members +- Answer questions +- Share valuable content +- Report guideline violations +- Participate in discussions +- Attend and organize events +- Provide constructive feedback + +## Community Governance + +### Decision-Making + +Communities may use various governance models: + +**Common approaches:** +- **Consensus** - Agreement among organizers +- **Democratic voting** - Member votes +- **Representative** - Elected organizers decide +- **Hybrid** - Different methods for different decisions + +**What members might vote on:** +- Community guidelines +- Leadership selection +- Major policy changes +- Event planning +- Resource allocation + +### Member Voice + +**How to influence community direction:** +- Participate in discussions +- Attend community meetings +- Vote in elections and polls +- Propose ideas and initiatives +- Provide feedback to organizers +- Volunteer to help + +See [Democratic Principles](../shared/democratic_principles.md) for platform governance values. + +## Managing Your Memberships + +### Membership Settings + +**For each community:** +- Notification preferences +- Privacy settings +- Display preferences +- Participation settings + +**How to adjust:** +1. Visit community page +2. Click "Membership Settings" +3. Adjust your preferences +4. Save changes + +### Leaving Communities + +**When to leave:** +- Community no longer matches your interests +- Unable to participate actively +- Guideline disagreements +- Personal circumstances change + +**How to leave:** +1. Visit community page +2. Click "Leave Community" +3. Confirm action +4. Your membership ends immediately + +**What happens:** +- Your posts remain (usually) +- You lose access to private content +- You stop receiving notifications +- You can rejoin later (if allowed) + +## Privacy and Safety + +### Community Privacy Settings + +**Public communities:** +- Content visible to everyone +- Searchable by search engines +- Member list visible +- Open to join + +**Private communities:** +- Content visible only to members +- Not searchable externally +- Member list restricted +- Membership controlled + +### Protecting Your Privacy + +**In communities:** +- Review privacy settings before posting +- Consider who can see your content +- Be mindful of personal information +- Use privacy controls on posts +- Adjust membership visibility + +**Per-post privacy:** +- Public - Anyone can see +- Community - Only community members +- Private - Only specific people + +### Safety in Communities + +**Recognize concerning behavior:** +- Harassment or bullying +- Spam or scams +- Misinformation +- Privacy violations +- Guideline violations + +**Take action:** +- Report problematic content +- Block problem users +- Contact community organizers +- Escalate to platform organizers if needed + +See [Safety and Reporting Tools](safety_reporting.md) for details. + +## Community Best Practices + +### Being a Good Community Member + +**Contribute positively:** +- Share knowledge and expertise +- Support other members +- Participate regularly +- Respect diverse perspectives +- Follow community guidelines + +**Build connections:** +- Introduce yourself +- Respond to others' posts +- Attend community events +- Collaborate on projects +- Message members respectfully + +### Community Etiquette + +**Do:** +- Read community guidelines +- Search before asking duplicate questions +- Thank people for help +- Give credit to others' ideas +- Admit mistakes +- Welcome newcomers + +**Don't:** +- Self-promote excessively +- Post off-topic content +- Dominate discussions +- Attack other members +- Share private conversations +- Cross-post identical content to multiple communities + +### Handling Disagreements + +**When conflicts arise:** +1. **Assume good faith** - People usually mean well +2. **Communicate directly** - Message privately if appropriate +3. **Stay calm** - Take a break if emotions run high +4. **Focus on issues** - Not personal attacks +5. **Seek mediation** - Ask organizers to help +6. **Know when to disengage** - Some arguments aren't worth continuing + +## Getting Help + +### Community Support + +**Resources available:** +- Community guidelines and FAQs +- Welcome posts for new members +- Mentorship or buddy systems +- Help channels or forums +- Organizer contact information + +**Where to ask:** +1. **Community help threads** - Designated questions area +2. **Direct message organizers** - Private inquiries +3. **Platform support** - Technical issues +4. **Documentation** - This guide and others + +### Feedback and Suggestions + +**Improve your community:** +- Share ideas with organizers +- Participate in planning discussions +- Volunteer to help implement changes +- Provide constructive criticism +- Recognize what works well + +## Measuring Community Health + +### Healthy Communities + +Signs of a thriving community: + +- **Active participation** - Regular posts and comments +- **Diverse voices** - Multiple perspectives welcome +- **Supportive culture** - Members help each other +- **Clear purpose** - Shared understanding of goals +- **Effective moderation** - Fair, consistent enforcement +- **Growing membership** - New members joining and staying +- **Quality content** - Valuable, on-topic discussions + +### Community Challenges + +Watch for warning signs: + +- **Low engagement** - Few posts or comments +- **Toxic behavior** - Harassment or negativity +- **Topic drift** - Content straying from purpose +- **Cliques** - Exclusive groups forming +- **Leadership issues** - Absent or ineffective organizers +- **Declining membership** - Members leaving + +**How to help:** +- Report toxic behavior +- Post quality content +- Welcome new members +- Support organizers +- Suggest improvements +- Lead by example + +## Related Documentation + +- [User Management Guide](user_management_guide.md) +- [Events and RSVP Guide](events_invitations_and_rsvp.md) +- [Exchange Process Guide](exchange_process.md) +- [Safety and Reporting Tools](safety_reporting.md) +- [Community Guidelines](community_guidelines.md) +- [Democratic Principles](../shared/democratic_principles.md) +- [Community Management](../community_organizers/community_management.md) + +## Platform-Specific Information + +> **Note:** Platform hosts should customize this section with: +> - List of featured communities +> - Community category structure +> - Specific governance models in use +> - Community creation policies +> - Leadership selection processes +> - Local support resources + +--- + +**Remember:** Communities thrive when members actively participate, support each other, and work together toward shared goals. Your contributions matter! diff --git a/docs/end_users/messaging_guide.md b/docs/end_users/messaging_guide.md index 929684128..57d6e20dc 100644 --- a/docs/end_users/messaging_guide.md +++ b/docs/end_users/messaging_guide.md @@ -1,22 +1,457 @@ # Messaging and Communication Guide -**Purpose:** Help end users communicate effectively and safely on the platform. - -## Messaging Basics -- Access messages from the navigation bar or conversation list. -- Conversations show participants, timestamps, and unread indicators. -- Use clear subjects and concise messages for faster responses. - -## Notifications -- Message alerts are delivered via email/in-app notifications (based on your settings). -- Adjust preferences under **Settings → Notifications** to reduce noise or enable alerts. - -## Safety -- Do not share passwords, payment details, or sensitive personal data in messages. -- Report abusive or suspicious messages using available report tools. -- Block or mute users if harassment continues and notify moderators. - -## Tips for Better Exchanges -- Confirm details (time, location, expectations) before meeting or delivering items. -- Keep commitments clear and update participants if plans change. -- Be courteous; prompt replies improve trust in the community. +**Target Audience:** Community members +**Document Type:** User Guide +**Last Updated:** November 20, 2025 + +## Overview + +The Better Together platform provides multiple communication tools to help community members connect, collaborate, and coordinate. This guide explains how to use messaging, conversations, and notifications effectively. + +## Communication Systems + +### Conversations and Messages + +The platform uses a **conversations-based messaging system** where messages are organized into conversation threads. + +**Key features:** +- **Private messaging** - One-on-one or group conversations +- **Real-time updates** - Messages appear immediately via Action Cable +- **Read receipts** - See when messages are read (optional) +- **Notifications** - Email and in-app alerts for new messages +- **Search** - Find conversations and messages +- **Privacy controls** - Control who can message you + +### Notifications + +Stay informed about platform activity through notifications. + +**Notification types:** +- **Messages** - New conversation messages +- **Events** - Invitations, RSVPs, and updates +- **Communities** - New posts and announcements +- **Exchanges** - Offer responses and agreement updates +- **System** - Account and platform updates + +**Delivery methods:** +- **In-app** - View in notification center +- **Email** - Receive via email +- **Real-time** - Instant browser notifications + +## Starting Conversations + +### Creating a New Conversation + +**From a user's profile:** +1. Visit the user's profile page +2. Click "Send Message" or "Start Conversation" +3. Enter your message subject +4. Type your message +5. Click "Send" + +**From conversations page:** +1. Go to `/conversations` or click Messages in navigation +2. Click "New Conversation" button +3. Select recipient(s) +4. Enter subject line +5. Type your message +6. Click "Send" + +### Adding Participants + +**Group conversations:** +- Add multiple recipients when creating a conversation +- Add participants to existing conversations +- Remove yourself from group conversations +- Participants can see full conversation history + +**Privacy note:** All participants can see the full conversation, including messages sent before they joined. + +## Managing Conversations + +### Conversation List + +View all your conversations in one place. + +**Conversation states:** +- **Unread** - Contains unread messages (highlighted) +- **Active** - Recently updated conversations +- **Archived** - Older or completed conversations + +**Sorting options:** +- By most recent activity +- By unread status +- By participant names +- By date created + +### Reading Messages + +**In a conversation:** +- Messages appear in chronological order +- Your messages appear on the right +- Others' messages appear on the left +- Timestamps show when messages were sent +- Read receipts show when messages were read (if enabled) + +**Real-time updates:** +- New messages appear automatically +- No need to refresh the page +- Typing indicators show when others are composing +- Presence indicators show who's online (if enabled) + +### Sending Messages + +**Compose a message:** +1. Type in the message compose field +2. Use formatting if available (Markdown, rich text) +3. Add attachments if supported +4. Press Enter or click "Send" + +**Message features:** +- **Text formatting** - Bold, italic, links +- **Mentions** - @username to notify specific users +- **Attachments** - Share files, images, documents +- **Emoji** - Express yourself with emoji reactions +- **Edit** - Edit recent messages (if enabled) +- **Delete** - Remove messages you sent + +### Organizing Conversations + +**Management actions:** +- **Archive** - Move inactive conversations out of main list +- **Delete** - Permanently remove conversations +- **Mute** - Stop notifications for specific conversations +- **Star** - Mark important conversations +- **Search** - Find specific conversations or messages + +## Privacy and Safety + +### Who Can Message You + +Control who can start conversations with you: + +**Settings options:** +- **Everyone** - Any platform member can message you +- **Communities** - Only members of your communities +- **Connections** - Only users you've interacted with +- **No one** - Disable incoming messages + +**How to adjust:** +1. Go to Settings → Privacy +2. Find "Message Privacy" section +3. Select your preference +4. Save changes + +### Blocking Users + +Prevent specific users from messaging you: + +**What blocking does:** +- Blocked users cannot send you messages +- Existing conversations are hidden +- You won't see their messages in group conversations +- They won't know they're blocked + +**How to block:** +1. Visit user's profile or conversation +2. Click "Block User" +3. Confirm the action + +See [Safety and Reporting Tools](safety_reporting.md) for more about blocking. + +### Reporting Messages + +Report inappropriate messages or conversations: + +**What to report:** +- Harassment or bullying +- Spam or scams +- Inappropriate content +- Privacy violations +- Threatening behavior + +**How to report:** +1. Click report button in conversation or message +2. Select violation category +3. Provide context +4. Submit report + +See [Safety and Reporting Tools](safety_reporting.md) for the full reporting process. + +## Notification Management + +### Configuring Notifications + +Control how you're notified about messages: + +**Global settings:** +1. Go to Settings → Notifications +2. Adjust message notification preferences: + - Email notifications (on/off) + - In-app notifications (on/off) + - Real-time notifications (on/off) +3. Set notification frequency: + - Immediate (default) + - Daily digest + - Weekly summary +4. Save preferences + +**Per-conversation settings:** +- Mute specific conversations +- Priority notifications for important conversations +- Override global settings per conversation + +### Viewing Notifications + +**Notification center:** +- Click bell icon in navigation +- See all recent notifications +- Mark as read/unread +- Clear notifications +- Go directly to referenced content + +**Email notifications:** +- Receive formatted email alerts +- Include message preview +- Quick reply from email (if supported) +- Unsubscribe from specific notification types + +## Advanced Features + +### Search and Filter + +**Search conversations:** +- Search by participant name +- Search by message content +- Search by date range +- Filter by read/unread status +- Filter by conversation type + +**Search tips:** +- Use quotes for exact phrases +- Combine search terms +- Use date filters to narrow results +- Save frequent searches + +### Message Drafts + +**Auto-save drafts:** +- Messages are saved as you type +- Resume drafts from any device +- Drafts expire after 30 days +- Delete drafts you no longer need + +### Conversation Settings + +**Per-conversation options:** +- Rename conversation +- Add/remove participants (group) +- Set conversation as priority +- Archive when complete +- Export conversation history +- Delete conversation + +## Email Integration + +### Email Notifications + +Receive message notifications via email: + +**Email contents:** +- Sender name and message preview +- Link to view full conversation +- Quick action buttons (if supported) +- Unsubscribe link + +**Customization:** +- Choose which notifications to receive +- Set notification frequency +- Customize email templates (platform organizers) + +### Email Replies + +Some platforms support replying to messages via email: + +**If enabled:** +- Reply directly to notification emails +- Your reply is added to the conversation +- Attachments are included +- Maintains conversation thread + +## Best Practices + +### Effective Communication + +**Message etiquette:** +- Be clear and concise +- Use appropriate tone +- Respect others' time +- Respond promptly when possible +- Use subject lines effectively + +**Group conversations:** +- Add relevant participants only +- Stay on topic +- Use @mentions for specific people +- Consider creating new thread if topic changes +- Respect notification preferences + +### Privacy Considerations + +**What to avoid in messages:** +- Sharing sensitive personal information +- Financial information or passwords +- Content that violates guidelines +- Spam or unsolicited commercial messages + +**Safe practices:** +- Verify user identity before sharing information +- Use caution with links from unknown users +- Report suspicious messages +- Keep professional boundaries +- Don't share conversation screenshots without consent + +### Performance Tips + +**Keep conversations manageable:** +- Archive old conversations +- Delete unnecessary messages +- Limit attachment sizes +- Clear old drafts +- Use search instead of scrolling + +## Mobile and Accessibility + +### Mobile Access + +Messages are accessible on mobile devices: + +**Mobile features:** +- Responsive design works on all screen sizes +- Touch-optimized interface +- Real-time notifications +- Offline message drafts (if supported) +- Mobile app (if available) + +### Accessibility + +The messaging system supports: + +**Screen readers:** +- Semantic HTML for navigation +- ARIA labels for interactive elements +- Keyboard navigation support +- Focus management for new messages + +**Customization:** +- Adjustable text size +- High contrast mode +- Reduced motion options +- Notification sound customization + +## Integration with Other Features + +### Event Coordination + +Messages integrate with events: + +**Event-related messaging:** +- Message event attendees +- Send updates to RSVPs +- Coordinate logistics +- Share event-related files + +### Exchange Communication (Joatu) + +Coordinate exchanges through messaging: + +**Exchange conversations:** +- Discuss offer/request details +- Negotiate terms +- Coordinate meeting times +- Share delivery information +- Confirm completion + +### Community Discussions + +Messages complement community features: + +**Private discussions:** +- Follow up on community posts +- Coordinate community projects +- Private community leadership discussions +- Member-to-member connections + +## Troubleshooting + +### Common Issues + +**Messages not sending:** +- Check internet connection +- Verify recipient hasn't blocked you +- Ensure message meets size limits +- Try refreshing the page + +**Not receiving notifications:** +- Check notification settings +- Verify email address is correct +- Check spam/junk folder +- Ensure browser permissions are enabled + +**Can't find a conversation:** +- Check archived conversations +- Use search function +- Verify it wasn't deleted +- Check if you were removed from group + +**Real-time updates not working:** +- Refresh the page +- Check browser WebSocket support +- Verify network allows WebSocket connections +- Clear browser cache + +### Getting Help + +If you continue to experience issues: + +1. **Check documentation** - Review this guide +2. **Ask community** - Post in help forums +3. **Contact support** - Reach out to platform administrators +4. **Report bugs** - Submit bug reports for technical issues + +## Future Features + +Potential upcoming messaging features: + +- **Voice messages** - Send audio recordings +- **Video messages** - Share short video clips +- **Message reactions** - React with emoji +- **Message threading** - Reply to specific messages +- **Advanced search** - More sophisticated search filters +- **Message scheduling** - Send messages at scheduled times +- **Translation** - Automatic message translation +- **Better mobile app** - Native mobile applications + +> Note: Availability depends on platform configuration and development roadmap. + +## Related Documentation + +- [User Management Guide](user_management_guide.md) +- [Safety and Reporting Tools](safety_reporting.md) +- [Community Guidelines](community_guidelines.md) +- [Privacy Policy](privacy_policy.md) +- [Event Invitations and RSVP](events_invitations_and_rsvp.md) + +## Platform-Specific Information + +> **Note:** Platform hosts should customize this section with: +> - Specific messaging features enabled +> - Character limits and attachment size limits +> - Email notification templates +> - Mobile app availability +> - Real-time notification support +> - Integration with external messaging systems + +--- + +**Remember:** Effective communication builds stronger communities. Use messaging tools respectfully and responsibly to foster positive connections. diff --git a/docs/end_users/privacy_policy.md b/docs/end_users/privacy_policy.md index f32b4831f..90a78c5af 100644 --- a/docs/end_users/privacy_policy.md +++ b/docs/end_users/privacy_policy.md @@ -1,17 +1,207 @@ -# Platform Privacy Policy (End User Summary) +# Privacy Policy -**Purpose:** Explain how the platform handles your personal data and how you can manage it. +**Target Audience:** Community members +**Document Type:** Legal/Policy +**Last Updated:** November 20, 2025 -## Key Points -- Personal data (email, profile info, activity) is stored to operate the service. -- Invitation-only platforms restrict who can register; public platforms remain opt-in. -- Cookies/trackers (if enabled by the host) require consent and are disclosed in the platform policy. +## Overview -## Your Controls -- Update profile and visibility settings from **Settings → Privacy** (where available). -- Request data deletion or export through the platform support channel. -- Manage notification and communication preferences from **Settings → Notifications**. +This privacy policy explains how the Better Together Community Engine platform collects, uses, and protects your personal information. Your privacy is important to us, and we are committed to transparency about our data practices. -## If You Need Help -- Contact platform support for questions about retention, exports, or consent. -- Review community-specific privacy notices posted by your host platform. +> **Note:** This is a template privacy policy. Each platform host must customize this document to reflect their specific practices, jurisdiction, and compliance requirements. + +## Information We Collect + +### Account Information +- **Email address** - Used for login and communications +- **Password** - Encrypted and never stored in plain text +- **Profile information** - Name, username, bio, profile picture (optional) +- **Contact details** - Phone numbers, addresses (optional) + +### Community Participation +- **Content you create** - Posts, comments, messages, events +- **Community memberships** - Communities you join +- **Event participation** - Events you create, attend, or manage +- **Exchange activities** - Offers, requests, and agreements (if using Joatu exchange features) + +### Usage Information +- **Page views** - What pages you visit (no user identifiers stored) +- **Feature usage** - Which features you use +- **Device information** - Browser type, operating system +- **Session data** - Login times, session duration + +### Platform Metrics (Anonymous) +Our metrics system is **privacy-first** and collects only aggregate, non-identifying data: +- Page views (path only, no user ID) +- Link clicks (URL and referring page) +- Search queries (query text and result count) +- File downloads (filename and type) +- Share actions (platform and URL) + +**What we DON'T collect:** +- User identifiers in metrics events +- IP addresses in analytics +- Tracking cookies from third parties +- Behavioral profiles + +## How We Use Your Information + +### Primary Uses +- **Account management** - Maintain your account and profile +- **Communication** - Send notifications, updates, and responses +- **Community features** - Enable participation in communities and events +- **Platform improvement** - Understand usage patterns to improve features +- **Safety and moderation** - Enforce community guidelines and terms of service + +### Data Sharing +We do **not** sell your personal information. We may share data only in these circumstances: + +- **With your consent** - When you explicitly authorize sharing +- **Community visibility** - Based on your privacy settings +- **Legal requirements** - When required by law or legal process +- **Safety purposes** - To protect users from harm or abuse + +## Your Privacy Rights + +### Access and Control +- **View your data** - Access your profile and account information +- **Update information** - Modify your profile at any time +- **Privacy settings** - Control who can see your content +- **Download your data** - Export your information (coming soon) + +### Account Management +- **Email preferences** - Choose which notifications you receive +- **Privacy levels** - Set content as public or private +- **Profile visibility** - Control who can view your profile +- **Delete account** - Request account deletion (with data retention policies) + +### Rights by Jurisdiction +Depending on your location, you may have additional rights under: +- **GDPR** (European Union) - Right to access, rectification, erasure, portability +- **PIPEDA** (Canada) - Right to access and correct personal information +- **CCPA** (California) - Right to know, delete, and opt-out +- **Other local laws** - Check your jurisdiction's privacy regulations + +## Data Security + +### Protection Measures +- **Encryption** - Data encrypted in transit (HTTPS) and at rest +- **Access controls** - Role-based access to sensitive data +- **Secure authentication** - Password hashing and session management +- **Regular updates** - Security patches and vulnerability monitoring + +### Sensitive Data +- **Encrypted fields** - Additional encryption for sensitive personal data +- **Secure file storage** - Encrypted file uploads in Active Storage +- **Limited access** - Only authorized staff can access personal data + +## Data Retention + +### Active Accounts +- **Account data** - Retained while your account is active +- **Content** - Stored based on your privacy settings +- **Metrics** - Aggregate data retained for analytics (no user IDs) + +### Inactive Accounts +- **Deletion policy** - Inactive accounts may be flagged for review +- **Retention period** - Check with your platform host for specific policies +- **Data archival** - Some data may be archived for legal compliance + +### Export and Report Data +- **CSV exports** - Retained for limited time (typically 90 days) +- **Report files** - Automatically purged based on retention policies +- **Backup data** - Maintained for disaster recovery + +## Third-Party Services + +### Default Configuration +By default, the platform **does not include**: +- Google Analytics or similar trackers +- Social media tracking pixels +- Advertising networks +- Behavioral analytics tools + +### Optional Integrations +Platform hosts may choose to add: +- **Analytics tools** - For usage insights (with consent) +- **Error tracking** - For debugging (server-side only) +- **Communication services** - For email delivery + +**If third-party services are enabled, platform hosts must:** +- Update this privacy policy +- Add cookie/consent banners +- Configure IP anonymization +- Obtain appropriate consent +- Provide opt-out mechanisms + +## Cookies and Tracking + +### Essential Cookies +- **Session cookies** - Required for login and navigation +- **Security cookies** - Prevent cross-site attacks +- **Language preference** - Remember your locale setting + +### Optional Cookies +- **Analytics cookies** - Only if enabled by platform host +- **Preference cookies** - Remember your customization choices + +You can control cookies through your browser settings. + +## Children's Privacy + +This platform is not directed at children under 13 (or applicable age in your jurisdiction). We do not knowingly collect information from children. If you believe a child has provided information, please contact the platform administrators. + +## Changes to Privacy Policy + +We may update this privacy policy to reflect: +- **Legal requirements** - New regulations or compliance needs +- **Feature updates** - New platform capabilities +- **Practice changes** - Modified data handling procedures + +**Notification of changes:** +- Email notification to registered users +- Prominent notice on the platform +- Updated "Last Updated" date in this document + +## Data Protection Officer + +For privacy questions or concerns, contact: + +**Platform Host Contact:** +[To be customized by platform host] + +**Engine Maintainers:** +[Better Together Solutions contact information] + +## Compliance and Jurisdiction + +This platform is designed to support compliance with: +- **GDPR** - European Union General Data Protection Regulation +- **PIPEDA** - Canadian Personal Information Protection and Electronic Documents Act +- **CCPA** - California Consumer Privacy Act +- **Other regulations** - As required by platform host's jurisdiction + +**Governing law:** Determined by platform host's location and terms of service. + +## Related Documentation + +- [Community Guidelines](community_guidelines.md) +- [Safety and Reporting Tools](safety_reporting.md) +- [User Management Guide](user_management_guide.md) +- [Privacy Principles](../shared/privacy_principles.md) +- [Security Updates](whats_new_security_and_privacy_aug_2025.md) + +## Platform Host Customization Checklist + +Platform hosts should customize this template by: + +- [ ] Add specific organization/platform name +- [ ] Specify data retention periods +- [ ] List any third-party services in use +- [ ] Provide contact information for privacy inquiries +- [ ] Specify applicable jurisdiction and governing law +- [ ] Detail specific rights based on local regulations +- [ ] Add cookie banner if using analytics +- [ ] Include consent mechanisms for optional tracking +- [ ] Translate to all supported languages +- [ ] Have legal review before publication diff --git a/docs/end_users/safety_reporting.md b/docs/end_users/safety_reporting.md index fcfa13876..3e215f64e 100644 --- a/docs/end_users/safety_reporting.md +++ b/docs/end_users/safety_reporting.md @@ -1,27 +1,405 @@ # Safety and Reporting Tools -**Purpose:** Help end users report issues and stay safe while using the platform. - -## When to Report -- Harassment, threats, or abuse -- Spam, scams, or fraudulent offers/requests -- Inappropriate content or policy violations -- Security concerns (account compromise, suspicious activity) - -## How to Report -- Use the in-app **Report** actions on content or user profiles where available. -- If a direct report button is missing, contact platform support with: - - URL or screenshot - - Description of the issue - - Your contact email for follow-up - -## What Happens Next -- Reports are routed to moderators/organizers for review. -- Actions may include content removal, warnings, suspension, or bans. -- Severe issues may be escalated to platform organizers or legal/compliance teams. - -## Safety Tips -- Verify counterparties before exchanging goods/services. -- Keep communication on-platform when possible. -- Avoid sharing sensitive personal information publicly. -- Enable email notifications so you don’t miss safety-related updates. +**Target Audience:** All community members +**Document Type:** User Guide +**Last Updated:** November 20, 2025 + +## Overview + +Your safety is our priority. This guide explains the tools and processes available to help you stay safe on the platform, report concerning content or behavior, and protect your privacy. + +## Personal Safety Tools + +### Block Users + +Blocking prevents another user from interacting with you on the platform. + +**What happens when you block someone:** +- They cannot send you messages +- They cannot see your profile (if set to members-only) +- They cannot comment on your posts +- They cannot invite you to events +- They cannot create exchange agreements with you + +**How to block a user:** +1. Visit the user's profile page +2. Click the "Block User" button +3. Confirm the action +4. The user is immediately blocked + +**Managing blocks:** +- View your blocked users at `/person_blocks` +- Unblock users at any time +- Blocks are private - the blocked user is not notified + +See implementation: `BetterTogether::PersonBlocksController` + +### Privacy Settings + +Control who can see your content and information. + +**Privacy Levels:** +- **Public** - Visible to everyone, including non-members +- **Members Only** - Visible only to platform members +- **Community Members** - Visible only to community members +- **Private** - Visible only to you and authorized users + +**What you can control:** +- Profile visibility +- Post and comment visibility +- Event participation visibility +- Community membership visibility + +**How to adjust privacy:** +1. Go to Settings → Privacy (when available) +2. Set default privacy levels +3. Override per-content if needed + +### Notification Controls + +Manage what notifications you receive and how. + +**Notification types:** +- Email notifications +- In-app notifications +- Real-time notifications (via Action Cable) + +**Control options:** +- Turn categories on/off +- Adjust frequency (immediate, daily digest, weekly) +- Mute specific communities or conversations + +**How to manage:** +1. Go to Settings → Notifications (when available) +2. Adjust preferences by category +3. Save changes + +## Reporting System + +### What You Can Report + +Report content or behavior that violates [Community Guidelines](community_guidelines.md): + +**Content Reports:** +- Posts and comments +- Messages (if harassing) +- Events (if inappropriate) +- Offers and requests (if fraudulent) +- Profiles (if impersonation or abuse) + +**Behavior Reports:** +- Harassment or bullying +- Hate speech or discrimination +- Spam or scams +- Privacy violations +- Platform abuse + +### How to Submit a Report + +**Step-by-step reporting:** + +1. **Find the report button** + - Available on posts, comments, profiles, and other content + - Look for flag icon or "Report" link + +2. **Choose report category** + - Harassment or abuse + - Spam or scam + - Inappropriate content + - Privacy violation + - Misinformation + - Other (with description) + +3. **Provide details** + - Explain what guideline was violated + - Add context if helpful + - Include screenshots if relevant + - Be specific and factual + +4. **Submit report** + - Review your report + - Confirm submission + - Receive confirmation message + +**After you report:** +- You'll receive an acknowledgment +- Moderators will review the report +- You may receive updates on the outcome +- Reports are handled according to severity + +### Report Status and Updates + +Track your submitted reports: + +**Report statuses:** +- **Pending** - Under review by moderators +- **In Review** - Being actively investigated +- **Resolved** - Action has been taken +- **Dismissed** - No violation found +- **Closed** - Report closed (with or without action) + +**Where to check:** +- View your reports at `/reports` (if available) +- Receive email updates when status changes +- See resolution notes (if provided by moderators) + +### What Happens to Reports + +**Review process:** +1. **Initial triage** - Report is categorized and prioritized +2. **Investigation** - Moderators review the reported content and context +3. **Decision** - Determine if guidelines were violated +4. **Action** - Take appropriate action if needed +5. **Notification** - Inform reporter and (sometimes) reported user + +**Possible outcomes:** +- **No action** - No violation found +- **Warning** - User receives a warning +- **Content removal** - Content is deleted +- **Temporary restriction** - User loses some privileges +- **Suspension** - User is temporarily banned +- **Permanent ban** - User is permanently removed + +**Priority levels:** +- **Critical** - Immediate threats or illegal content (reviewed immediately) +- **High** - Serious violations like harassment (reviewed within 24 hours) +- **Normal** - Standard violations (reviewed within 3-5 days) +- **Low** - Minor issues (reviewed within 7 days) + +### Reporting Anonymously + +**Your privacy in reporting:** +- Reports are private by default +- Your identity is known to moderators +- The reported user does not automatically see who reported them +- However, context may make it obvious in some cases + +**Tips for anonymous reporting:** +- Don't include identifying information in the report details +- If you're concerned about retaliation, mention this in your report +- Consider having another user submit the report if needed + +## Safety Features by Content Type + +### Profile Safety +- Block users from your profile page +- Set profile visibility to private +- Report profiles for impersonation or harassment + +### Post and Comment Safety +- Report individual posts or comments +- Block users who repeatedly post concerning content +- Set default privacy on your own posts + +### Event Safety +- Report inappropriate events +- Block users from inviting you to events +- Set RSVP visibility preferences + +### Messaging Safety +- Block users to prevent messages +- Report harassing messages +- Delete conversations +- Control who can message you + +### Exchange Safety (Joatu) +- Report fraudulent offers or requests +- Block users from exchange interactions +- Review agreements carefully before accepting +- Report agreement violations + +## Moderator and Administrator Support + +### Community Moderators + +Each community may have designated moderators who: +- Review reports within their community +- Enforce community-specific guidelines +- Manage community membership +- Remove inappropriate content +- Issue warnings and restrictions + +**How to contact:** +- Through the community's contact information +- Via the reporting system +- Through platform messaging + +### Platform Organizers + +Platform-level administrators handle: +- Platform-wide guideline enforcement +- Cross-community issues +- Appeals of moderator decisions +- Account-level actions (suspensions, bans) +- Policy violations + +**How to contact:** +- Through the main support contact +- Via platform-wide report escalation +- Email to platform administrators + +### Escalation Process + +If you're unsatisfied with a moderation decision: + +1. **Community level** - Contact community moderators first +2. **Platform level** - Escalate to platform administrators +3. **Appeal** - Submit a formal appeal with explanation +4. **Final review** - Platform organizers make final decision + +See [Escalation Matrix](../shared/escalation_matrix.md) for details. + +## Emergency Situations + +### Immediate Threats + +If you or someone else is in immediate danger: + +**DO NOT rely solely on platform reporting** + +1. **Call emergency services** - Contact local police or emergency number +2. **Report to platform** - Also submit a platform report marked as critical +3. **Document everything** - Save screenshots and evidence +4. **Seek support** - Contact trusted friends, family, or crisis services + +### Crisis Resources + +**Platform cannot replace professional help** + +If you're experiencing: +- Suicidal thoughts +- Mental health crisis +- Domestic violence +- Child abuse +- Sexual assault + +**Contact appropriate crisis services:** +- Suicide prevention hotlines +- Mental health crisis lines +- Domestic violence support +- Child protection services +- Sexual assault support centers + +### Legal Issues + +For illegal content or criminal behavior: + +1. **Report to platform** - We'll preserve evidence +2. **Contact authorities** - File a police report +3. **Document thoroughly** - Save all evidence +4. **Seek legal advice** - Consult with an attorney if needed + +## Best Practices for Safety + +### Protect Your Information +- Don't share personal details publicly +- Use privacy settings appropriately +- Be cautious about what you post +- Review profile information regularly + +### Recognize Warning Signs +- Requests for personal information +- Pressure to move conversations off-platform +- Unsolicited commercial messages +- Aggressive or threatening behavior +- Too-good-to-be-true offers + +### Safe Exchanges (Joatu) +- Meet in public places for in-person exchanges +- Verify user ratings and reviews +- Trust your instincts +- Start with small exchanges to build trust +- Use platform messaging to maintain records + +### Secure Your Account +- Use a strong, unique password +- Enable two-factor authentication (if available) +- Don't share your account credentials +- Log out on shared devices +- Review account activity regularly + +## Reporting Bugs and Security Issues + +### Security Vulnerabilities + +If you discover a security vulnerability: + +**DO NOT post publicly** + +1. **Email security team** - Contact security@[platform-domain] +2. **Provide details** - Describe the vulnerability +3. **Allow time** - Give time to fix before disclosure +4. **Responsible disclosure** - Follow coordinated disclosure practices + +See [SECURITY.md](../../SECURITY.md) for details. + +### Platform Bugs + +For non-security bugs: +- Use the platform's bug reporting system +- Provide steps to reproduce +- Include screenshots if helpful +- Note browser and device information + +## Data and Privacy Concerns + +### Data Access Requests +- Request access to your data +- Export your information +- Understand what data is collected + +### Data Deletion +- Delete specific content +- Request account deletion +- Understand data retention policies + +### Privacy Violations +- Report unauthorized data sharing +- Report privacy breaches +- Contact platform administrators + +See [Privacy Policy](privacy_policy.md) for details. + +## Frequently Asked Questions + +**Q: Will the person I report know I reported them?** +A: The reported person is not automatically notified of who submitted the report, but context may make it obvious. + +**Q: How long does it take to review a report?** +A: Critical reports are reviewed immediately; most others within 3-5 days depending on priority. + +**Q: Can I block someone before reporting them?** +A: Yes, you can block and report. Blocking provides immediate protection while the report is reviewed. + +**Q: What if I accidentally reported something?** +A: Contact moderators to explain the situation. Accidental reports can be marked as such. + +**Q: Can I appeal a moderation decision?** +A: Yes, use the escalation process outlined above to appeal decisions you believe are incorrect. + +**Q: Is my report anonymous?** +A: Reports are private but not fully anonymous - moderators see who submitted reports. + +**Q: What if my report is ignored?** +A: Escalate to platform administrators if you believe a report was not properly handled. + +## Related Documentation + +- [Community Guidelines](community_guidelines.md) +- [Privacy Policy](privacy_policy.md) +- [User Management Guide](user_management_guide.md) +- [Escalation Matrix](../shared/escalation_matrix.md) +- [Democratic Principles](../shared/democratic_principles.md) + +## Platform-Specific Information + +> **Note:** Platform hosts should customize this section with: +> - Contact information for moderators and administrators +> - Platform-specific reporting procedures +> - Local emergency and crisis resource numbers +> - Security team contact information +> - Jurisdiction-specific legal resources + +--- + +**Remember:** Your safety matters. Don't hesitate to use these tools and report concerning behavior. The platform community is stronger when everyone helps maintain a safe environment. diff --git a/docs/implementation/coverage_improvement_plan.md b/docs/implementation/coverage_improvement_plan.md new file mode 100644 index 000000000..6fca17639 --- /dev/null +++ b/docs/implementation/coverage_improvement_plan.md @@ -0,0 +1,545 @@ +# Test Coverage Improvement Implementation Plan + +**Created:** November 24, 2025 +**Target:** Achieve 60%+ coverage on all 59 files currently below threshold +**Current Overall Coverage:** 77.78% + +## Executive Summary + +- **Files needing work:** 59 files +- **Total uncovered lines:** 1,281 lines +- **Estimated test lines needed:** ~2,562 (2:1 test-to-code ratio) +- **Timeline:** 5 weeks +- **Approach:** Phased implementation, highest impact first + +## Success Criteria + +1. ✅ All 59 files reach 60%+ coverage +2. ✅ Overall coverage increases to 85%+ +3. ✅ Zero files with 0% coverage +4. ✅ All critical subsystems (1-7) maintain 75%+ coverage + +## Phase 1: Critical Files (0% Coverage) - Week 1 + +**Goal:** Eliminate all 0% coverage files +**Files:** 4 +**Lines to cover:** 74 +**Estimated effort:** 2-3 days + +### Files to Address + +#### 1. `lib/better_together/configuration.rb` (12 lines) +- **Purpose:** Engine configuration management +- **Test Type:** Unit specs +- **Priority:** CRITICAL - used throughout engine +- **Test Focus:** + - Configuration defaults + - Configuration setters/getters + - Block-based configuration + - Configuration validation + +#### 2. `lib/better_together/safe_class_resolver.rb` (16 lines) +- **Purpose:** Safe dynamic class resolution (security) +- **Test Type:** Unit specs +- **Priority:** CRITICAL - security component +- **Test Focus:** + - Allow-list validation + - Security edge cases (constantize attacks) + - Error handling for invalid classes + - Integration with concerns + +#### 3. `lib/generators/better_together/install/install_generator.rb` (28 lines) +- **Purpose:** Rails generator for engine installation +- **Test Type:** Generator specs +- **Priority:** HIGH - installation workflow +- **Test Focus:** + - File generation + - Migration copying + - Route injection + - Configuration file creation + +#### 4. `lib/mobility/backends/attachments.rb` (18 lines) +- **Purpose:** Custom Mobility backend for Active Storage +- **Test Type:** Unit/integration specs +- **Priority:** HIGH - i18n functionality +- **Test Focus:** + - Attachment association + - Translation storage/retrieval + - Locale switching + - Fallback behavior + +### Implementation Steps + +```bash +# Week 1, Day 1-2: Library specs +bin/dc-run rails generate rspec:model BetterTogether::Configuration --skip +# Edit spec/lib/better_together/configuration_spec.rb +bin/dc-run bundle exec rspec spec/lib/better_together/configuration_spec.rb + +bin/dc-run rails generate rspec:model BetterTogether::SafeClassResolver --skip +# Edit spec/lib/better_together/safe_class_resolver_spec.rb +bin/dc-run bundle exec rspec spec/lib/better_together/safe_class_resolver_spec.rb + +# Week 1, Day 3: Generator and Mobility backend +# Create spec/lib/generators/better_together/install/install_generator_spec.rb +# Create spec/lib/mobility/backends/attachments_spec.rb +bin/dc-run bundle exec rspec spec/lib/ +``` + +## Phase 2: High Priority (1-30% Coverage) - Week 2-3 + +**Goal:** Bring all files with <30% coverage to 60%+ +**Files:** 15 +**Lines to cover:** 470 uncovered lines +**Estimated effort:** 8-10 days + +### Subsystem Breakdown + +#### Platform Management (4 files, 83 uncovered lines) + +1. **`controllers/better_together/setup_wizard_steps_controller.rb`** (20.6%, 54 uncovered) + - Add request specs for wizard step navigation + - Test step validation and progression + - Test step completion tracking + - Target: 75%+ + +2. **`mailers/better_together/platform_invitation_mailer.rb`** (23.1%, 10 uncovered) + - Test email generation for all invitation states + - Test localization of email content + - Test attachment handling + - Target: 90%+ + +3. **`jobs/better_together/platform_invitation_mailer_job.rb`** (26.7%, 11 uncovered) + - Test job enqueuing + - Test mailer invocation + - Test error handling/retries + - Target: 85%+ + +4. **`controllers/better_together/translations_controller.rb`** (27.3%, 8 uncovered) + - Test translation CRUD operations + - Test locale switching + - Test translation export/import + - Target: 70%+ + +#### Community Management (11 files, 387 uncovered lines) + +1. **`helpers/better_together/sidebar_nav_helper.rb`** (10.9%, 41 uncovered) + - Test navigation menu generation + - Test active state detection + - Test permission-based filtering + - Target: 80%+ + +2. **`mailers/better_together/authorship_mailer.rb`** (15.8%, 16 uncovered) + - Test content authorship notifications + - Test localization + - Target: 90%+ + +3. **`controllers/better_together/metrics/reports_controller.rb`** (20.0%, 20 uncovered) + - Test report generation + - Test filtering and date ranges + - Test export formats (CSV, PDF) + - Target: 75%+ + +4. **`lib/better_together/migration_helpers.rb`** (23.5%, 13 uncovered) + - Test migration helper methods + - Test table creation helpers + - Test column definition helpers + - Target: 85%+ + +5. **`robots/better_together/translation_bot.rb`** (25.9%, 20 uncovered) + - Test automated translation detection + - Test translation suggestions + - Target: 70%+ + +6. **`builders/better_together/joatu_demo_builder.rb`** (27.0%, 54 uncovered) + - Test demo data generation + - Test Joatu-specific fixtures + - Target: 65%+ + +7. **`lib/jsonapi/link_builder.rb`** (27.6%, 55 uncovered) + - Test JSONAPI link generation + - Test pagination links + - Test relationship links + - Target: 75%+ + +8. **`models/better_together/metrics/link_checker_report.rb`** (28.4%, 48 uncovered) + - Test broken link detection + - Test report generation + - Test link validation + - Target: 70%+ + +9. **`helpers/better_together/hub_helper.rb`** (28.6%, 15 uncovered) + - Test hub navigation helpers + - Test community hub widgets + - Target: 75%+ + +10. **`sanitizers/better_together/sanitizers/external_link_icon_sanitizer.rb`** (28.6%, 10 uncovered) + - Test HTML sanitization + - Test icon injection for external links + - Target: 85%+ + +11. **`models/better_together/metrics/page_view_report.rb`** (29.1%, 95 uncovered) + - Test page view aggregation + - Test date range filtering + - Test report generation + - Target: 70%+ + +### Implementation Steps + +```bash +# Week 2: Controllers and Mailers +for file in setup_wizard_steps platform_invitation_mailer translations; do + # Add comprehensive request/mailer specs + bin/dc-run bundle exec rspec spec/**/*${file}*_spec.rb +done + +# Week 3: Helpers, Models, and Utilities +for file in sidebar_nav hub_helper link_checker page_view_report; do + # Add unit specs for each component + bin/dc-run bundle exec rspec spec/**/*${file}*_spec.rb +done +``` + +## Phase 3: Medium Priority (30-60% Coverage) - Week 4-5 + +**Goal:** Bring all remaining files to 60%+ +**Files:** 40 +**Lines to cover:** 737 uncovered lines +**Estimated effort:** 8-10 days + +### Key Files (Top 15 by impact) + +1. **`lib/better_together/column_definitions.rb`** (31.0%, 40 uncovered) + - Test column helper methods for migrations + - Target: 75%+ + +2. **`future_controllers/better_together/bt/api/registrations_controller.rb`** (31.4%, 24 uncovered) + - Test API user registration + - Target: 75%+ + +3. **`controllers/better_together/help_preferences_controller.rb`** (35.0%, 13 uncovered) + - Test help preference CRUD + - Target: 70%+ + +4. **`mailers/better_together/event_invitations_mailer.rb`** (37.5%, 10 uncovered) + - Test event invitation emails + - Target: 90%+ + +5. **`controllers/better_together/hub_controller.rb`** (40.0%, 6 uncovered) + - Test hub dashboard rendering + - Target: 75%+ + +6. **`models/concerns/better_together/geography/iso_location.rb`** (40.0%, 12 uncovered) + - Test ISO location validation + - Target: 85%+ + +7. **`models/better_together/metrics/link_click_report.rb`** (40.6%, 57 uncovered) + - Test link click aggregation + - Target: 70%+ + +8. **`controllers/better_together/person_platform_memberships_controller.rb`** (40.6%, 19 uncovered) + - Test membership management + - Target: 75%+ + +9. **`controllers/better_together/geography/continents_controller.rb`** (42.4%, 19 uncovered) + - Test geography CRUD + - Target: 75%+ + +10. **`controllers/better_together/agreements_controller.rb`** (43.8%, 9 uncovered) + - Test Joatu agreement workflows + - Target: 75%+ + +11. **`builders/better_together/geography_builder.rb`** (44.1%, 38 uncovered) + - Test geography data seeding + - Target: 70%+ + +12. **`controllers/concerns/better_together/wizard_methods.rb`** (45.0%, 22 uncovered) + - Test wizard controller concern + - Target: 80%+ + +13. **`controllers/better_together/content/page_blocks_controller.rb`** (45.8%, 13 uncovered) + - Test page block CRUD + - Target: 75%+ + +14. **`controllers/better_together/resource_permissions_controller.rb`** (46.3%, 22 uncovered) + - Test permission management + - Target: 75%+ + +15. **`helpers/better_together/i18n_helper.rb`** (48.0%, 13 uncovered) + - Test i18n helper methods + - Target: 85%+ + +### Remaining 25 Files (50-60% range) + +These files are close to the target and will be addressed systematically: + +- Controllers: 12 files (average 53.2% coverage) +- Models: 8 files (average 54.8% coverage) +- Helpers: 3 files (average 52.1% coverage) +- Other: 2 files (average 51.5% coverage) + +### Implementation Steps + +```bash +# Week 4: Controllers 30-50% +# Focus on request specs for all controllers +bin/dc-run bundle exec rspec spec/requests/better_together/ + +# Week 5: Models and Helpers 50-60% +# Add missing spec coverage for edge cases +bin/dc-run bundle exec rspec spec/models/better_together/ +bin/dc-run bundle exec rspec spec/helpers/better_together/ +``` + +## Testing Patterns by File Type + +### Controllers (Request Specs) + +```ruby +RSpec.describe BetterTogether::SomeController, type: :request do + before { configure_host_platform } + + describe "GET #index" do + context "when authenticated" do + before { login('user@example.com', 'password') } + + it "returns success" do + get some_path + expect(response).to have_http_status(:success) + end + end + + context "when not authenticated" do + it "redirects to login" do + get some_path + expect(response).to redirect_to(new_user_session_path) + end + end + end + + describe "POST #create" do + let(:valid_params) { { some: { attr: 'value' } } } + + it "creates resource" do + expect { + post some_path, params: valid_params + }.to change(SomeModel, :count).by(1) + end + end +end +``` + +### Mailers (Mailer Specs) + +```ruby +RSpec.describe BetterTogether::SomeMailer, type: :mailer do + describe "#notification" do + let(:user) { create(:better_together_user) } + let(:mail) { described_class.notification(user) } + + it "renders the subject" do + expect(mail.subject).to eq("Expected Subject") + end + + it "renders the receiver email" do + expect(mail.to).to eq([user.email]) + end + + it "renders the sender email" do + expect(mail.from).to eq(["noreply@example.com"]) + end + + it "includes expected content" do + expect(mail.body.encoded).to include("Expected text") + end + + it "uses correct locale" do + I18n.with_locale(:es) do + expect(mail.subject).to eq("Asunto Esperado") + end + end + end +end +``` + +### Jobs (Job Specs) + +```ruby +RSpec.describe BetterTogether::SomeJob, type: :job do + describe "#perform" do + let(:record) { create(:better_together_record) } + + it "enqueues job" do + expect { + described_class.perform_later(record) + }.to have_enqueued_job(described_class) + end + + it "processes successfully" do + expect { + described_class.new.perform(record) + }.to change { record.reload.status }.to("processed") + end + + it "handles errors gracefully" do + allow(record).to receive(:process!).and_raise(StandardError) + + expect { + described_class.new.perform(record) + }.to raise_error(StandardError) + end + end +end +``` + +### Helpers (Helper Specs) + +```ruby +RSpec.describe BetterTogether::SomeHelper, type: :helper do + describe "#some_helper_method" do + it "returns expected output" do + expect(helper.some_helper_method("input")).to eq("expected") + end + + it "handles nil input" do + expect(helper.some_helper_method(nil)).to be_nil + end + + it "handles edge cases" do + expect(helper.some_helper_method("")).to eq("") + end + end + + describe "#navigation_helper" do + before { configure_host_platform } + + it "generates correct HTML" do + result = helper.navigation_helper + expect(result).to include('nav') + expect(result).to be_html_safe + end + end +end +``` + +### Models (Model Specs) + +```ruby +RSpec.describe BetterTogether::SomeModel, type: :model do + subject(:model) { build(:better_together_some_model) } + + describe "validations" do + it { is_expected.to validate_presence_of(:required_field) } + it { is_expected.to validate_uniqueness_of(:unique_field) } + end + + describe "associations" do + it { is_expected.to belong_to(:parent) } + it { is_expected.to have_many(:children) } + end + + describe "#custom_method" do + it "returns expected value" do + expect(model.custom_method).to eq("expected") + end + + context "with special conditions" do + before { model.flag = true } + + it "returns different value" do + expect(model.custom_method).to eq("different") + end + end + end +end +``` + +### Libraries (Unit Specs) + +```ruby +RSpec.describe BetterTogether::SomeLibrary do + describe ".class_method" do + it "performs expected operation" do + result = described_class.class_method(arg) + expect(result).to eq(expected) + end + + it "raises error on invalid input" do + expect { + described_class.class_method(invalid) + }.to raise_error(ArgumentError) + end + end + + describe "#instance_method" do + subject(:instance) { described_class.new(config) } + + it "maintains configuration" do + expect(instance.config).to eq(config) + end + + it "executes successfully" do + expect(instance.instance_method).to be_truthy + end + end +end +``` + +## Daily Workflow + +1. **Morning:** Pick 2-3 files from current phase +2. **Write tests:** Focus on one file at a time +3. **Run tests:** `bin/dc-run bundle exec rspec spec/path/to/file_spec.rb` +4. **Check coverage:** Verify file reaches 60%+ (ideally 80%+) +5. **Commit:** Small, focused commits per file +6. **End of day:** Run full suite to ensure no regressions + +## Continuous Monitoring + +```bash +# After each file +bin/dc-run bundle exec rspec spec/path/to/new_spec.rb +open coverage/index.html # Verify improvement + +# Daily +bin/dc-run bundle exec rspec +# Check overall coverage percentage + +# Weekly +# Review coverage report for new gaps +# Adjust plan based on progress +``` + +## Success Metrics + +Track these metrics weekly: + +- Number of files under 60%: Start 59 → Target 0 +- Overall coverage: Start 77.78% → Target 85%+ +- Files with 0% coverage: Start 4 → Target 0 +- Average subsystem coverage: Start ~80% → Target 85%+ + +## Risk Mitigation + +1. **Complex dependencies:** Start simple, add mocks as needed +2. **Time overruns:** Prioritize based on file criticality +3. **Regression:** Run full suite before committing +4. **Coverage plateau:** Focus on edge cases and error paths + +## Completion Checklist + +- [ ] Phase 1: All 4 files at 60%+ +- [ ] Phase 2: All 15 files at 60%+ +- [ ] Phase 3: All 40 files at 60%+ +- [ ] Zero files with <60% coverage +- [ ] Overall coverage ≥85% +- [ ] All subsystems ≥75% coverage +- [ ] CI coverage checks passing +- [ ] Documentation updated + +## Next Steps + +1. ✅ Review and approve this plan +2. 🔄 Begin Phase 1: Week 1, Day 1 +3. 📊 Set up daily coverage tracking +4. 🎯 Target completion: 5 weeks from start diff --git a/docs/implementation/current_plans/TEST_COVERAGE_PLAN.md b/docs/implementation/current_plans/TEST_COVERAGE_PLAN.md new file mode 100644 index 000000000..4edffd9bb --- /dev/null +++ b/docs/implementation/current_plans/TEST_COVERAGE_PLAN.md @@ -0,0 +1,315 @@ +# Test Coverage Improvement Plan + +## Current Status (Updated: November 24, 2025) +- **Coverage**: 77.7% line coverage (8482/10917 lines) ✅ UP from 77.45% +- **Test Suite**: 1985 examples, 0 failures, 8 pending +- **Runtime**: 8 minutes 35 seconds +- **Goal**: Achieve 85-90%+ coverage with comprehensive, meaningful tests + +## Coverage Gaps Analysis + +### Critical Issues Found +1. **Core business logic untested**: Messaging, metrics tracking, comments ✅ **RESOLVED** +2. **Jobs with stub specs**: All 4 metrics tracking jobs ⚠️ **REMAINS** +3. **Models with stub specs**: 30+ models with no real tests ✅ **MOSTLY RESOLVED** +4. **Helpers untested**: 15+ helper modules (lower priority) ⚠️ **REMAINS** + +## Implementation Plan + +### PHASE 1: Critical Business Logic ✅ **COMPLETED** +**Estimated Impact**: +8-10% coverage | **Effort**: 2-3 hours +**Actual Impact**: +0.25% coverage | **Actual Effort**: ~3 hours + +#### 1.1 Messaging System ✅ **DONE** +- **File**: `spec/models/better_together/message_spec.rb` +- **Model**: `app/models/better_together/message.rb` +- **Status**: Comprehensive tests completed (15 examples) +- **Coverage completed**: + - ✅ Associations: belongs_to :conversation, :sender + - ✅ Validations: content presence + - ✅ Action Text: encrypted rich text content + - ✅ Class methods: .permitted_attributes + +#### 1.2 Conversation System ✅ **DONE** +- **File**: `spec/models/better_together/conversation_spec.rb` +- **Model**: `app/models/better_together/conversation.rb` +- **Status**: Comprehensive tests completed (30+ examples) +- **Coverage completed**: + - ✅ Associations: has_many :messages, :conversation_participants, :participants + - ✅ Validations: at_least_one_participant, participant_ids presence, first_message_content_present + - ✅ Encryption: title deterministic encryption + - ✅ Nested attributes: messages_attributes + - ✅ Instance methods: #first_message_content, #add_participant_safe + - ✅ Class methods: .permitted_attributes + +#### 1.3 Conversation Participants ✅ **DONE** +- **File**: `spec/models/better_together/conversation_participant_spec.rb` +- **Model**: `app/models/better_together/conversation_participant.rb` +- **Status**: Comprehensive tests completed (12 examples) +- **Coverage completed**: + - ✅ Associations: belongs_to :conversation, :person + - ✅ Database constraints + - ✅ Uniqueness validations + +#### 1.4 Metrics Tracking Jobs ⚠️ **PENDING** (PHASE 4) +**Files**: +- `spec/jobs/better_together/metrics/track_page_view_job_spec.rb` +- `spec/jobs/better_together/metrics/track_link_click_job_spec.rb` +- `spec/jobs/better_together/metrics/track_share_job_spec.rb` +- `spec/jobs/better_together/metrics/track_download_job_spec.rb` + +**Coverage needed per job**: +- Job enqueuing +- Perform method creates metric record +- Handles required parameters +- Error handling +**Estimated examples**: 8-10 per job (32-40 total) + +#### 1.5 Comment System ✅ **DONE** +- **File**: `spec/models/better_together/comment_spec.rb` +- **Model**: `app/models/better_together/comment.rb` +- **Status**: Comprehensive tests completed (12 examples) +- **Coverage completed**: + - ✅ Polymorphic associations (commentable) + - ✅ Creator field + - ✅ Database schema + - ✅ Content handling + - ✅ Timestamps + +**Phase 1 Total**: ~100-125 examples +**Phase 1 Actual**: ~70 examples completed (jobs deferred to Phase 4) + +--- + +### PHASE 2: Core Data Models ✅ **COMPLETED** +**Estimated Impact**: +5-7% coverage | **Effort**: 2-3 hours +**Actual Impact**: Included in 77.7% | **Actual Effort**: ~3 hours + +#### 2.1 Contact Information Models ✅ **DONE** +**Files**: +- `spec/models/better_together/email_address_spec.rb` ✅ (50+ examples) +- `spec/models/better_together/phone_number_spec.rb` ✅ (50+ examples) +- `spec/models/better_together/contact_detail_spec.rb` ✅ (17 examples) + +**Coverage completed**: +- ✅ Associations with Person/Community +- ✅ Format validations (email regex, phone formatting) +- ✅ Uniqueness/presence validations +- ✅ PrimaryFlag concern +- ✅ Privacy concern +- ✅ Labelable concern +**Actual examples**: 117 examples (vs estimated 45-60) + +#### 2.2 Metrics Models ✅ **DONE** +**Files**: +- `spec/models/better_together/metrics/share_spec.rb` ✅ (30+ examples) +- `spec/models/better_together/metrics/download_spec.rb` ✅ (25+ examples) +- `spec/models/better_together/metrics/link_click_report_spec.rb` ✅ (15+ examples) +- `spec/models/better_together/metrics/page_view_report_spec.rb` ✅ (18+ examples) + +**Coverage completed**: +- ✅ Polymorphic associations (shareable, downloadable) +- ✅ Timestamp tracking +- ✅ Report generation methods +- ✅ Validations (platform, url, locale, file types) +- ✅ Active Storage attachments +**Actual examples**: 88 examples (vs estimated 48-60) + +**Phase 2 Total**: ~115-145 examples +**Phase 2 Actual**: ~205 examples completed + +--- + +### PHASE 3: Supporting Features ✅ **COMPLETED** +**Estimated Impact**: +3-5% coverage | **Effort**: 2-3 hours +**Actual Impact**: Included in 77.7% | **Actual Effort**: ~4 hours + +#### 3.1 Events System ✅ **DONE** +**Files**: +- `spec/models/better_together/calendar_entry_spec.rb` ✅ (from prior work) +- `spec/models/better_together/event_category_spec.rb` ✅ (15 examples) +- `spec/models/better_together/call_for_interest_spec.rb` ✅ (20 examples) + +**Actual examples**: ~35 examples + +#### 3.2 Categorization ✅ **DONE** +**Files**: +- `spec/models/better_together/category_spec.rb` ✅ (17 examples) +- `spec/models/better_together/categorization_spec.rb` ✅ (from prior work) +- `spec/models/better_together/contact_detail_spec.rb` ✅ (counted in Phase 2) + +**Actual examples**: ~17 examples (contact_detail counted above) + +#### 3.3 Social & Security ✅ **DONE** +**Files**: +- `spec/models/better_together/social_media_account_spec.rb` ✅ (50+ examples) +- `spec/models/better_together/website_link_spec.rb` ✅ (17 examples) +- `spec/models/better_together/jwt_denylist_spec.rb` ✅ (12 examples) +- `spec/models/better_together/resource_permission_spec.rb` ✅ (17 examples) + +**Actual examples**: ~96 examples + +#### 3.4 Content Block System ✅ **BONUS COMPLETED** +**Files** (not in original plan): +- `spec/models/better_together/content/block_spec.rb` ✅ (50+ examples) +- `spec/models/better_together/content/css_spec.rb` ✅ (15+ examples) +- `spec/models/better_together/content/hero_spec.rb` ✅ (40+ examples) +- `spec/models/better_together/content/html_spec.rb` ✅ (20+ examples) +- `spec/models/better_together/content/image_spec.rb` ✅ (50+ examples) +- `spec/models/better_together/content/link_spec.rb` ✅ (30+ examples) +- `spec/models/better_together/content/rich_text_spec.rb` ✅ (25+ examples) +- `spec/models/better_together/content/platform_block_spec.rb` ✅ (8 examples) + +**Actual examples**: ~238 examples (BONUS) + +#### 3.5 Policy & Integration Specs ✅ **BONUS COMPLETED** +**Files** (not in original plan): +- `spec/policies/better_together/content/markdown_policy_spec.rb` ✅ (35+ examples) +- `spec/requests/better_together/content_blocks_preview_markdown_spec.rb` ✅ (15+ examples) +- `spec/requests/better_together/pages_filtering_spec.rb` ✅ (minor updates) +- `spec/requests/better_together/pages_markdown_rendering_spec.rb` ✅ (8+ examples) +- `spec/requests/better_together/pages_title_display_spec.rb` ✅ (15+ examples) +- `spec/views/better_together/content/blocks/fields/_markdown_spec.rb` ✅ (4 examples) +- `spec/views/better_together/content/page_blocks/block_types/_markdown_spec.rb` ✅ (2 examples) + +**Actual examples**: ~80 examples (BONUS) + +**Phase 3 Total**: ~150-190 examples (estimated) +**Phase 3 Actual**: ~466 examples completed (including bonus work) + +--- + +### PHASE 4: Background Jobs ✅ **COMPLETED** +**Actual Impact**: Included in 77.7% | **Effort**: Already done + +**Files completed**: +- `spec/jobs/better_together/metrics/track_page_view_job_spec.rb` ✅ (6 examples) +- `spec/jobs/better_together/metrics/track_link_click_job_spec.rb` ✅ (7 examples) +- `spec/jobs/better_together/metrics/track_share_job_spec.rb` ✅ (11 examples) +- `spec/jobs/better_together/metrics/track_download_job_spec.rb` ✅ (7 examples) + +**Coverage completed**: +- ✅ Job enqueuing +- ✅ Perform method creates metric record +- ✅ Handles required parameters +- ✅ Timestamp tracking +- ✅ Queue configuration (metrics queue) +**Actual examples**: ~31 examples + +--- + +### PHASE 5: Geography Models 🎯 **CURRENT PRIORITY** +**Estimated Impact**: +2-3% coverage | **Effort**: 3-4 hours + +**Files** (11 models): +- Continent, Country, State, Region, Settlement +- Map, GeospatialSpace, RegionSettlement +- All use PostGIS, STI pattern, complex hierarchies + +**Note**: Geography is a complete subsystem - defer unless targeting 82%+ coverage + +**Phase 4 Total**: ~120-150 examples + +--- + +### PHASE 6: Helper Specs (LOWEST PRIORITY) +**Estimated Impact**: +0.5-1% coverage | **Effort**: 1 hour + +**Files**: 10+ helper specs +**Note**: Helpers are typically trivial view helpers - only test if they contain business logic + +**Phase 5 Total**: ~30-50 examples + +## Execution Strategy (Updated) + +### Completed ✅ +1. ✅ Messaging system (high business value) - Message, Conversation, ConversationParticipant +2. ✅ Metrics models - Download, Share, LinkClickReport, PageViewReport +3. ✅ Comment system - Complete database schema tests +4. ✅ Contact information models - EmailAddress, PhoneNumber, ContactDetail +5. ✅ Content block system - Block, Css, Hero, Html, Image, Link, RichText, PlatformBlock +6. ✅ Events, categorization, social media - All Phase 3 models complete +7. ✅ Policy specs - MarkdownPolicy with comprehensive authorization tests +8. ✅ Request/View specs - Markdown rendering and page display features + +### Current Priority (Phase 4) +9. **Background Jobs** - 4 metrics tracking jobs (~40-48 examples) + - `track_page_view_job_spec.rb` + - `track_link_click_job_spec.rb` + - `track_share_job_spec.rb` + - `track_download_job_spec.rb` + +### Next Steps (Phase 5+) +10. Geography subsystem if targeting 80%+ coverage +11. Helper modules only if containing business logic + +## Success Criteria (Revised) + +### Coverage Targets +- ✅ **After Phases 1-3**: 77.7% coverage (ACHIEVED) +- **After Phase 4**: ~79% coverage (target) +- **After Phase 5**: ~82% coverage (stretch goal) +- **Target**: 80-85% coverage is excellent for this project + +### Quality Standards (Applied Throughout) +✅ Each spec includes: +- Factory tests (valid factory, custom attributes, traits) +- Association tests (has_many, belongs_to, through, polymorphic) +- Validation tests (presence, format, uniqueness, custom) +- Scope tests (if scopes exist) +- Instance method tests +- Class method tests +- Callback tests (if callbacks exist) +- Integration tests for complex workflows + +### Anti-Patterns Avoided ✅ +- ❌ No stub `it 'exists'` tests (all removed) +- ❌ No tests that just check class name +- ❌ No incomplete test suites +- ✅ Every test verifies actual behavior + +## Estimated Total Effort (Updated) + +### Completed Work +- **Phases 1-3**: ~9 hours → 77.7% coverage ✅ + - Messaging, conversations, comments + - Contact models (email, phone, address) + - Metrics models and reports + - Content block system (8 block types) + - Category and event systems + - Security and social features + - Policy and request/view specs + +### Remaining Work +- **Phase 4 (Jobs)**: 1-2 hours → ~79% coverage (target) +- **Phase 5 (Geography)**: 3-4 hours → ~82% coverage (optional) +- **Phase 6 (Helpers)**: 1 hour → ~83% coverage (optional) + +### Total Effort +- **To reach 80% coverage**: 1-2 more hours (Phase 4 only) +- **To reach 85% coverage**: 5-7 more hours (Phases 4-6) + +## Recommendations + +### Immediate Actions +1. **Commit current work** - 33+ spec files with comprehensive tests +2. **Run Phase 4** - Background job specs (highest ROI, ~1-2% coverage gain) +3. **Re-evaluate** - Decide if 79-80% coverage meets project needs + +### Coverage Philosophy +- **77.7% is excellent** for a Rails engine of this complexity +- **80-85% is the sweet spot** - diminishing returns beyond that +- **Focus on business logic** - Not everything needs 100% coverage +- **Geography models** are complex but isolated - defer unless needed + +## Dependencies & Blockers +- ✅ All models have basic structure +- ✅ Factories exist for core models +- ✅ Docker environment configured and working +- ✅ Test suite passing (1985 examples, 0 failures) + +## Next Immediate Steps +1. **Commit the current comprehensive test work** (~1400+ new examples) +2. **Begin Phase 4** - Background job specs for metrics tracking +3. **Update coverage report** after Phase 4 completion +4. **Decide on Phase 5+** based on coverage goals vs. effort trade-off diff --git a/docs/platform_organizers/community_management.md b/docs/platform_organizers/community_management.md index 45b915d33..87f2e2141 100644 --- a/docs/platform_organizers/community_management.md +++ b/docs/platform_organizers/community_management.md @@ -1,13 +1,432 @@ -# Community Management (Platform Organizer Lens) +# Community Management for Platform Organizers -Platform organizers oversee multiple communities and support community organizers. +**Target Audience:** Platform organizers +**Document Type:** Administrator Guide +**Last Updated:** November 20, 2025 -## What to Coordinate -- Create or archive communities, ensuring ownership and default roles are set. -- Support community organizers with tooling access and moderation policies. -- Review community health metrics and address abuse trends. +## Overview -## Helpful References -- [Community Organizer Guide](../community_organizers/community_management.md) -- [Roles and Permissions](../shared/roles_and_permissions.md) -- [Notifications System](../developers/systems/notifications_system.md) +Platform organizers oversee the health and growth of all communities on the platform. This guide covers platform-level community management responsibilities distinct from individual community organization. + +See [Community Management (Community Organizers)](../community_organizers/community_management.md) for community organizer perspective. + +## Platform-Level Community Oversight + +### Community Lifecycle Management + +**Community creation:** +- Review and approve community requests +- Ensure communities align with platform values +- Set up initial configuration +- Assign community organizers +- Publish new communities + +**Community growth:** +- Monitor community health metrics +- Support struggling communities +- Share best practices across communities +- Facilitate inter-community collaboration +- Recognize successful communities + +**Community archival:** +- Identify inactive communities +- Consult with community organizers +- Plan community sunset +- Archive content appropriately +- Communicate with members + +### Community Health Monitoring + +**Health indicators:** +- Member growth/retention rates +- Activity levels (posts, events, engagement) +- Moderation issues and reports +- Leadership stability +- Member satisfaction + +**Platform dashboard views:** +- All communities list with metrics +- Activity comparison across communities +- Growth trends +- Issue tracking +- Resource usage + +**Intervention triggers:** +- Declining membership +- High moderation load +- Leadership turnover +- Member complaints +- Guideline violations + +## Supporting Community Organizers + +### Organizer Resources + +**Provide tools and training:** +- Community management documentation +- Best practices guides +- Organizer onboarding +- Ongoing training sessions +- Peer learning opportunities + +**Communication channels:** +- Platform organizer contact methods +- Organizer community or forum +- Regular check-ins +- Emergency escalation paths + +### Organizer Selection and Appointment + +**Criteria for organizers:** +- Commitment to community values +- Time availability +- Communication skills +- Conflict resolution abilities +- Alignment with platform principles + +**Appointment process:** +- Application review +- Interview or vetting +- Role assignment in RBAC system +- Onboarding and training +- Ongoing support + +**Succession planning:** +- Identify potential organizers +- Co-organizer model +- Transition procedures +- Knowledge transfer +- Emergency backup plans + +## Cross-Community Coordination + +### Platform-Wide Initiatives + +**Collaborative programs:** +- Multi-community events +- Shared resource libraries +- Inter-community exchanges +- Platform-wide campaigns +- Coordinated responses to issues + +**Communication:** +- Platform announcements +- Community organizer updates +- Member newsletters +- Social media coordination + +### Conflict Resolution + +**Between communities:** +- Territorial disputes +- Duplicate communities +- Competing events +- Resource allocation +- Member recruitment conflicts + +**Mediation approaches:** +- Facilitate dialogue +- Seek win-win solutions +- Establish boundaries +- Document agreements +- Monitor compliance + +**Escalation when needed:** +- Platform organizer arbitration +- Policy enforcement +- Community mergers or splits +- Final decisions + +## Community Moderation Support + +### Platform-Level Moderation + +**Escalated reports:** +- Appeals of community moderation +- Cross-community issues +- Serious violations +- Legal concerns +- Pattern recognition + +**Moderator support:** +- Review difficult cases +- Provide guidance +- Handle appeals +- Enforce platform policies +- Document decisions + +See [Escalation Matrix](../shared/escalation_matrix.md) for escalation procedures. + +### Moderation Tools and Training + +**Tools provided:** +- Reporting system access +- Content removal capabilities +- User management (bans, restrictions) +- Communication templates +- Moderation logs + +**Training and guidelines:** +- Platform-wide community guidelines +- Moderation procedures +- Consistency standards +- Legal considerations +- Trauma-informed moderation + +## Policy and Governance + +### Platform-Wide Policies + +**Establish and maintain:** +- Community guidelines +- Content policies +- Privacy policies +- Terms of service +- Code of conduct + +**Policy updates:** +- Regular review cycle +- Community input process +- Legal compliance checks +- Clear communication of changes +- Version control + +### Democratic Governance + +**Community voice:** +- Community feedback mechanisms +- Policy consultation processes +- Voting on major changes +- Representative structures +- Transparency in decisions + +See [Democratic Principles](../shared/democratic_principles.md) for governance philosophy. + +### Community-Specific Rules + +**Support community autonomy:** +- Allow community-specific guidelines +- Must align with platform policies +- Review for compliance +- Approve major policy changes +- Intervene only when necessary + +## Growth and Engagement + +### Platform Community Strategy + +**Growth objectives:** +- Increase total membership +- Grow active communities +- Diversify community types +- Geographic expansion +- Demographic inclusion + +**Engagement initiatives:** +- Platform-wide campaigns +- Community spotlights +- Member recognition programs +- Ambassador programs +- Partnerships + +### New Community Incubation + +**Support new communities:** +- Founder support and mentorship +- Initial member recruitment +- Content seeding +- Event support +- Promotion and visibility + +**Success metrics:** +- Member retention after 30/60/90 days +- Activity levels +- Organizer engagement +- Community growth +- Member satisfaction + +## Resource Allocation + +### Platform Resources + +**Shared resources:** +- Server capacity +- Storage limits +- Support staff time +- Promotional channels +- Development priorities + +**Allocation principles:** +- Equitable distribution +- Needs-based allocation +- Growth potential +- Strategic priorities +- Community size and activity + +### Community Funding + +**If applicable:** +- Budget allocation +- Grant programs +- Fundraising support +- Financial transparency +- Accountability + +## Data and Analytics + +### Community Metrics + +**Track platform-wide:** +- Total communities +- Total members across communities +- Average community size +- Activity levels +- Growth rates +- Retention metrics + +**Per-community analytics:** +- Member count and growth +- Activity levels +- Event participation +- Exchange activity (Joatu) +- Content volume +- Moderation statistics + +**Privacy considerations:** +- Aggregate data only +- No individual tracking in metrics +- Anonymized reports +- Respect community privacy settings + +See [Privacy Principles](../shared/privacy_principles.md) for metrics approach. + +### Reporting + +**Regular reports:** +- Monthly community health report +- Quarterly growth analysis +- Annual platform review +- Incident reports +- Compliance reports + +**Stakeholder communication:** +- Share with community organizers +- Transparency with members +- Board or governance reporting +- Public transparency reports + +## Crisis Management + +### Community Crises + +**Crisis types:** +- Mass exodus of members +- Organizer departure +- Major guideline violations +- External threats or attacks +- Technical failures affecting community + +**Crisis response:** +1. Assess situation +2. Communicate with organizers +3. Stabilize community +4. Address root causes +5. Support affected members +6. Document and learn + +### Platform-Wide Incidents + +**Coordinated response:** +- Security breaches +- Major policy violations +- Legal threats +- PR crises +- Service outages + +**Communication plan:** +- Internal coordination +- Community organizer updates +- Member notification +- Public statements (if needed) +- Post-incident review + +## Legal and Compliance + +### Community Content Liability + +**Platform responsibilities:** +- Monitor for illegal content +- Respond to DMCA and legal requests +- Terms of service enforcement +- Age restrictions +- Geographic restrictions (if applicable) + +**Safe harbor provisions:** +- Notice and takedown procedures +- Content moderation policies +- User-generated content disclaimers +- Legal compliance documentation + +### Data Protection + +**Community data:** +- Member personal data +- Community content +- Activity records +- Moderation logs + +**Compliance requirements:** +- GDPR, PIPEDA, CCPA compliance +- Data processing agreements +- Cross-border data transfers +- Retention and deletion policies + +See [Compliance and Legal Guidelines](compliance_legal.md) for details. + +## Best Practices + +### Enabling Community Success + +**Support strategies:** +- Provide clear documentation +- Offer training and resources +- Foster organizer community +- Recognize achievements +- Learn from failures + +**Autonomy and oversight balance:** +- Trust community organizers +- Intervene only when necessary +- Clear escalation paths +- Consistent policy enforcement +- Respectful collaboration + +### Continuous Improvement + +**Learning systems:** +- Regular organizer feedback +- Community health assessments +- Success and failure analysis +- External benchmarking +- Innovation experimentation + +**Adaptation:** +- Update policies based on experience +- Improve tools and features +- Respond to community needs +- Stay current with trends +- Evolve with platform growth + +## Related Documentation + +- [Platform Administration](platform_administration.md) +- [User Management](user_management.md) +- [Security and Privacy](security_privacy.md) +- [Community Management (Community Organizers)](../community_organizers/community_management.md) +- [Community Guidelines](../end_users/community_guidelines.md) +- [Democratic Principles](../shared/democratic_principles.md) +- [Escalation Matrix](../shared/escalation_matrix.md) + +--- + +**Remember:** Platform organizers create the conditions for communities to flourish. Balance oversight with autonomy, provide support without micromanagement, and always serve the broader community ecosystem. diff --git a/docs/platform_organizers/compliance_legal.md b/docs/platform_organizers/compliance_legal.md index ac0e6573f..061461927 100644 --- a/docs/platform_organizers/compliance_legal.md +++ b/docs/platform_organizers/compliance_legal.md @@ -1,16 +1,448 @@ # Compliance and Legal Guidelines -## Core Responsibilities -- Maintain platform terms of service, privacy policy, and consent records. -- Honor data deletion/export requests and document retention schedules. -- Avoid copying production data into non-production environments. - -## Risk Controls -- Audit elevated roles and access to sensitive data. -- Ensure third-party processors are disclosed and have proper agreements. -- Review notification and tracking settings for regional compliance requirements. - -## References -- [Legal Compliance Overview](../legal_compliance/README.md) +**Target Audience:** Platform organizers +**Document Type:** Legal/Compliance Guide +**Last Updated:** November 20, 2025 + +## Overview + +This guide outlines compliance requirements and legal considerations for platform organizers. While not legal advice, it provides a framework for understanding and meeting regulatory obligations. + +> **Important:** Consult qualified legal counsel for your specific jurisdiction and circumstances. This guide is for informational purposes only. + +## Privacy Regulation Compliance + +### GDPR (European Union) + +**General Data Protection Regulation** applies to platforms processing EU residents' data. + +**Key requirements:** +- **Lawful basis for processing** - Consent, contract, legal obligation, or legitimate interest +- **Data subject rights** - Access, rectification, erasure, portability, objection +- **Privacy by design** - Build privacy into systems +- **Data protection impact assessments** - For high-risk processing +- **Breach notification** - Within 72 hours of becoming aware +- **Data protection officer** - Required for certain organizations +- **Cross-border transfers** - Special rules for data leaving EU + +**Platform implementation:** +- Privacy policy with required disclosures +- Consent mechanisms for non-essential processing +- User controls for exercising rights +- Data portability export functionality +- Breach detection and notification procedures +- Records of processing activities + +**Resources:** +- [GDPR Official Text](https://gdpr-info.eu/) +- [ICO Guidance](https://ico.org.uk/for-organisations/guide-to-data-protection/guide-to-the-general-data-protection-regulation-gdpr/) + +### PIPEDA (Canada) + +**Personal Information Protection and Electronic Documents Act** applies to Canadian organizations. + +**Key principles:** +- **Accountability** - Responsible for personal information under control +- **Identifying purposes** - Explain why collecting data +- **Consent** - Obtain consent for collection, use, disclosure +- **Limiting collection** - Collect only necessary information +- **Limiting use, disclosure, retention** - Use only for stated purposes +- **Accuracy** - Keep personal information accurate and current +- **Safeguards** - Protect with appropriate security +- **Openness** - Transparent about policies and practices +- **Individual access** - Provide access to personal information +- **Challenging compliance** - Allow individuals to challenge compliance + +**Platform implementation:** +- Clear privacy policy +- Consent for collection and use +- Data minimization practices +- Accuracy update mechanisms +- Security measures (encryption, access controls) +- User access to their data +- Complaint handling procedures + +See [PIPEDA Compliance Updates](../privacy/pipeda_compliance_updates.md) for detailed guidance. + +**Resources:** +- [PIPEDA Official Site](https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/the-personal-information-protection-and-electronic-documents-act-pipeda/) +- [Privacy Commissioner of Canada](https://www.priv.gc.ca/) + +### CCPA (California) + +**California Consumer Privacy Act** applies to certain businesses collecting California residents' data. + +**Consumer rights:** +- **Right to know** - What personal information is collected and how it's used +- **Right to delete** - Request deletion of personal information +- **Right to opt-out** - Opt out of sale of personal information +- **Right to non-discrimination** - No discrimination for exercising rights + +**Business obligations:** +- **Notice at collection** - Inform consumers of data collection +- **Privacy policy** - Detailed privacy policy with required disclosures +- **Consumer request handling** - Respond within 45 days +- **Opt-out mechanisms** - "Do Not Sell My Personal Information" links +- **Service provider contracts** - Contractual requirements for processors + +**Platform implementation:** +- Updated privacy policy with CCPA disclosures +- "Do Not Sell" mechanism (if applicable) +- Data request fulfillment procedures +- Verification processes for requests +- Record keeping for compliance + +**Resources:** +- [CCPA Official Text](https://oag.ca.gov/privacy/ccpa) +- [California Privacy Rights Act (CPRA)](https://cppa.ca.gov/) + +### Other Privacy Laws + +**Consider compliance with:** +- **LGPD (Brazil)** - Brazilian General Data Protection Law +- **APPI (Japan)** - Act on the Protection of Personal Information +- **POPIA (South Africa)** - Protection of Personal Information Act +- **Australian Privacy Act** +- **Provincial laws (Canada)** - Alberta PIPA, BC PIPA, Quebec Law 25 + +## Content and Platform Liability + +### Section 230 (United States) + +**Communications Decency Act Section 230** provides immunity for user-generated content. + +**Key protections:** +- Platforms not liable for user content +- Good faith moderation doesn't create liability +- Applies to U.S.-based platforms + +**Exceptions:** +- Federal criminal law +- Intellectual property laws +- Electronic communications privacy laws +- Sex trafficking laws (FOSTA-SESTA) + +**Best practices:** +- Moderate in good faith +- Have clear content policies +- Respond to legitimate takedown requests +- Don't edit user content (moderation is okay) +- Document moderation decisions + +### DMCA (Digital Millennium Copyright Act) + +**Copyright Safe Harbor** - Protection from copyright liability if following procedures. + +**Requirements:** +- Designate DMCA agent with Copyright Office +- Respond to takedown notices promptly +- Implement repeat infringer policy +- Don't have actual knowledge of infringement + +**Takedown process:** +1. Receive valid DMCA notice +2. Remove or disable access to content +3. Notify user who posted content +4. User may file counter-notice +5. Restore content if no court action within 10-14 days + +**Counter-notice process:** +1. User provides counter-notice +2. Forward to complainant +3. Wait 10-14 business days +4. Restore content if no court action + +**Platform implementation:** +- DMCA agent registration +- Takedown request handling procedures +- Counter-notice procedures +- Repeat infringer policy +- Documentation of all actions + +**Resources:** +- [Copyright Office](https://www.copyright.gov/) +- [DMCA Safe Harbor](https://www.copyright.gov/legislation/dmca.pdf) + +### International Content Laws + +**Considerations for:** +- **EU Digital Services Act** - Content moderation requirements +- **German NetzDG** - Illegal content removal timelines +- **Australian Online Safety Act** - Harmful content rules +- **UK Online Safety Bill** - Platform duties of care + +## Accessibility Compliance + +### WCAG Standards + +**Web Content Accessibility Guidelines** - International standards for web accessibility. + +**Levels:** +- **Level A** - Minimum accessibility +- **Level AA** - Required for most compliance (target for this platform) +- **Level AAA** - Highest accessibility (aspirational) + +**Key principles (POUR):** +- **Perceivable** - Information presented in ways users can perceive +- **Operable** - Interface components operable by all users +- **Understandable** - Information and operation understandable +- **Robust** - Content works with current and future technologies + +**Platform implementation:** +- Semantic HTML +- ARIA labels and roles +- Keyboard navigation +- Color contrast ratios +- Alt text for images +- Captions for videos +- Screen reader compatibility + +**Resources:** +- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/) +- [WebAIM](https://webaim.org/) + +### Legal Requirements + +**ADA (United States):** +- Americans with Disabilities Act +- Applies to public accommodations +- Website accessibility increasingly required + +**AODA (Ontario, Canada):** +- Accessibility for Ontarians with Disabilities Act +- WCAG 2.0 AA compliance required + +**European Accessibility Act:** +- EU-wide accessibility requirements +- Applies to certain services and products + +## Age Restrictions and Children + +### COPPA (United States) + +**Children's Online Privacy Protection Act** - Protects children under 13. + +**Requirements if platform knowingly collects from children:** +- Obtain verifiable parental consent +- Provide notice to parents +- Allow parents to review child's information +- Allow parents to revoke consent and delete data +- Not condition participation on providing more data than necessary +- Maintain confidentiality, security, integrity + +**Platform approach:** +- Terms of Service prohibit use by children under 13 (or 16 in EU) +- No knowingly collecting from children +- Report and delete child accounts if discovered +- Age verification on registration + +### GDPR Age Requirements + +- Consent age: 16 (can be lowered to 13 by member states) +- Parental consent required below age threshold +- Age verification required + +## Terms of Service and Legal Agreements + +### Terms of Service + +**Required elements:** +- Acceptance of terms +- User eligibility (age, location) +- Account responsibilities +- Acceptable use policies +- Intellectual property rights +- Disclaimers and limitations of liability +- Indemnification +- Dispute resolution +- Governing law and jurisdiction +- Modification of terms +- Termination provisions + +**Best practices:** +- Plain language where possible +- Highlight important terms +- Require acceptance on registration +- Notify of changes +- Version history +- Link from all pages + +### Privacy Policy + +**Required disclosures:** +- What information is collected +- How information is used +- Who information is shared with +- User rights and choices +- Data security measures +- Contact information +- Jurisdiction-specific requirements + +See [Privacy Policy](../end_users/privacy_policy.md) template. + +### Code of Conduct + +**Platform values and expectations:** +- Behavioral expectations +- Prohibited conduct +- Enforcement procedures +- Appeal processes + +See [Community Guidelines](../end_users/community_guidelines.md). + +## Data Security and Breach Notification + +### Security Requirements + +**Regulatory requirements:** +- Implement reasonable security measures +- Encrypt sensitive data +- Control access to personal information +- Regular security assessments +- Incident response plans + +**Platform implementation:** +- HTTPS/TLS encryption +- Active Record Encryption +- Role-based access control +- Regular security audits +- Dependency vulnerability monitoring + +See [Security and Privacy](security_privacy.md) for detailed security practices. + +### Breach Notification + +**GDPR breach notification:** +- Notify supervisory authority within 72 hours +- Notify affected individuals if high risk +- Document all breaches + +**PIPEDA breach notification:** +- Report to Privacy Commissioner if real risk of significant harm +- Notify affected individuals +- Notify other organizations if they can reduce harm +- Keep records of breaches + +**U.S. state laws:** +- Vary by state +- Generally require notification of affected residents +- Timelines and methods vary + +**Breach response:** +1. Detect and confirm breach +2. Contain and remediate +3. Assess impact and risk +4. Determine notification requirements +5. Notify as required by law +6. Document incident +7. Review and improve security + +## International Considerations + +### Cross-Border Data Transfers + +**Considerations:** +- GDPR adequacy decisions +- Standard contractual clauses +- Privacy Shield invalidation +- Data localization requirements +- Binding corporate rules + +**Platform approach:** +- Understand where data is stored and processed +- Implement appropriate safeguards +- Document transfer mechanisms +- Update policies for jurisdiction + +### Multi-Jurisdictional Compliance + +**Challenges:** +- Conflicting legal requirements +- Different enforcement approaches +- Varying cultural norms +- Language and translation + +**Strategies:** +- Comply with strictest applicable law +- Geo-blocking if necessary +- Jurisdiction-specific terms +- Legal counsel in relevant jurisdictions + +## Compliance Program + +### Compliance Framework + +**Essential components:** +1. **Policies and procedures** - Document compliance requirements +2. **Training and awareness** - Educate team on obligations +3. **Monitoring and auditing** - Regular compliance checks +4. **Reporting mechanisms** - Track compliance status +5. **Incident response** - Handle violations and breaches +6. **Continuous improvement** - Update based on changes + +### Documentation + +**Maintain records of:** +- Processing activities +- Consent records +- Data subject requests and responses +- Breach incidents and responses +- Policy updates and notifications +- Training completion +- Compliance audits + +### Regular Reviews + +**Review schedule:** +- **Quarterly** - Policy updates, compliance metrics +- **Annually** - Full compliance audit +- **As needed** - Legal changes, new features, incidents + +## Related Documentation + +- [Platform Administration](platform_administration.md) +- [Security and Privacy](security_privacy.md) +- [User Management](user_management.md) +- [Privacy Policy](../end_users/privacy_policy.md) +- [Community Guidelines](../end_users/community_guidelines.md) - [Privacy Principles](../shared/privacy_principles.md) -- [Security Protection System](../developers/systems/security_protection_system.md) +- [PIPEDA Compliance Updates](../privacy/pipeda_compliance_updates.md) + +## Legal Resources + +### Professional Assistance + +**When to consult lawyers:** +- Setting up platform +- Drafting terms and policies +- Responding to legal requests +- Handling major incidents +- Expanding to new jurisdictions +- Regulatory investigations + +**Types of counsel:** +- Privacy and data protection lawyers +- Intellectual property attorneys +- Corporate/business lawyers +- Local counsel for specific jurisdictions + +### External Resources + +**Government agencies:** +- Privacy Commissioners (Canada, EU member states) +- FTC (United States) +- State Attorneys General +- Copyright offices +- Accessibility enforcement agencies + +**Industry resources:** +- Electronic Frontier Foundation +- Future of Privacy Forum +- International Association of Privacy Professionals +- Open source legal communities + +--- + +**Remember:** Legal compliance is complex and jurisdiction-specific. This guide provides an overview, but professional legal counsel is essential for ensuring compliance with all applicable laws and regulations. + +**Disclaimer:** This document does not constitute legal advice. Consult qualified legal professionals for your specific situation. diff --git a/docs/platform_organizers/platform_administration.md b/docs/platform_organizers/platform_administration.md index b41dba81a..45388ccb4 100644 --- a/docs/platform_organizers/platform_administration.md +++ b/docs/platform_organizers/platform_administration.md @@ -1,13 +1,700 @@ -# Platform Administration Overview +# Platform Administration Guide -**Purpose:** Quick reference for platform organizers configuring hosts, communities, and services. +**Target Audience:** Platform organizers and administrators +**Document Type:** Administrator Guide +**Last Updated:** November 20, 2025 -## Core Tasks -- Manage host settings (domains, invitation mode, default community) via Host Dashboard. -- Configure external services (email, storage, monitoring) following production guides. -- Monitor background jobs and logs for failed deliveries or errors. +## Overview -## Recommended References -- [Host Management](host_management.md) – provisioning hosts, invitation settings, defaults -- [Host Dashboard Extensions](host_dashboard_extensions.md) – available admin tools and widgets -- [Production/External Services](../production/external-services-to-configure.md) – third-party setup +This guide covers the comprehensive administrative capabilities available to platform organizers. As a platform administrator, you have access to tools for managing communities, users, content, permissions, and platform-wide settings. + +## Platform Administrator Role + +### Responsibilities + +Platform organizers are responsible for: + +- **Platform configuration** - Set up and maintain platform settings +- **User management** - Oversee user accounts and access +- **Community oversight** - Support community organizers and manage communities +- **Content moderation** - Review and enforce platform-wide guidelines +- **Security** - Monitor and respond to security issues +- **Compliance** - Ensure legal and regulatory compliance +- **Performance** - Monitor platform health and optimization +- **Support** - Provide assistance to users and community organizers + +### Required Knowledge + +Platform administrators should understand: + +- Rails engine architecture and configuration +- Role-based access control (RBAC) principles +- Content management and publishing workflows +- Privacy and data protection requirements +- Community governance principles +- Metrics and analytics interpretation + +See [RBAC Overview](../developers/architecture/rbac_overview.md) for permission system details. + +## Admin Dashboard + +### Accessing the Dashboard + +**Navigation:** +- Click your profile icon → "Admin Dashboard" +- Direct URL: `/host` (redirects to appropriate dashboard) +- Requires platform organizer role + +**Dashboard sections:** +- **Overview** - Key metrics and recent activity +- **Communities** - Manage community instances +- **Users & People** - User account and profile management +- **Content** - Pages, navigation, and resources +- **Configuration** - Platforms, roles, and permissions +- **Metrics** - Analytics and reporting +- **Extensions** - Additional modules and features + +### Dashboard Overview + +**Quick stats:** +- Total users and active members +- Number of communities +- Recent registrations +- Platform activity metrics +- Pending moderation items +- System health indicators + +**Recent activity:** +- New user registrations +- Content publications +- Report submissions +- Community updates +- System events + +## Platform Configuration + +### Platform Settings + +Configure platform-wide settings at `/host/platforms`: + +**Basic information:** +- **Name** - Display name for the platform +- **Description** - Platform purpose and overview +- **Logo** - Platform branding image +- **Host community** - Default community for platform +- **Privacy level** - Public, private, or invite-only + +**Registration settings:** +- **Requires invitation** - Toggle invitation-only registration +- **Default roles** - Roles assigned to new users +- **Email confirmation** - Require email verification +- **Terms acceptance** - Require agreement acceptance + +**Locale settings:** +- **Default locale** - Primary platform language +- **Available locales** - Supported languages +- **Locale detection** - Auto-detect user language + +See [Host Management](host_management.md) for detailed platform configuration. + +### Privacy and Access Control + +**Privacy modes:** + +**Public platform:** +- Content visible to all visitors +- Self-registration allowed (optional) +- Public pages searchable by search engines +- Events and communities publicly listed + +**Private platform:** +- Content visible only to members +- Invitation required for registration +- Pages not indexed by search engines +- Event invitation tokens grant limited access + +**Hybrid approach:** +- Mix of public and private content +- Per-content privacy controls +- Community-level privacy settings +- Granular access management + +**Implementation:** +- Set platform privacy at `/host/platforms/:id/edit` +- Configure invitation requirements +- Manage invitation codes at `/host/invitations` +- Review access logs for security + +See [Privacy Principles](../shared/privacy_principles.md) for privacy philosophy. + +## User and Account Management + +### User Administration + +Manage user accounts at `/host/users`: + +**User list features:** +- Search by name, email, or username +- Filter by role, status, or registration date +- Sort by various criteria +- Bulk actions (when available) + +**User details:** +- Account information (email, status) +- Profile details (name, bio, contact) +- Role assignments +- Community memberships +- Activity history +- Login history + +**User actions:** +- **Edit profile** - Update user information +- **Reset password** - Send password reset email +- **Confirm email** - Manually verify email address +- **Lock account** - Temporarily disable access +- **Delete account** - Permanently remove user +- **Assign roles** - Grant or revoke roles +- **View activity** - Review user actions + +See [User Management](user_management.md) for detailed user administration. + +### Person Profile Management + +Manage person profiles at `/host/people`: + +**Person records:** +- **Basic info** - Name, username, bio +- **Contact** - Email, phone, addresses +- **Communities** - Membership list +- **Roles** - Platform and community roles +- **Privacy** - Profile visibility settings + +**Profile actions:** +- Edit person details +- Manage community memberships +- View activity and contributions +- Handle reported profiles +- Merge duplicate profiles + +### Role and Permission Management + +Define roles and permissions at `/host/roles` and `/host/resource_permissions`: + +**Platform roles:** +- Platform organizer (administrator) +- Community organizer +- Content moderator +- Member (default) +- Guest (limited access) + +**Community roles:** +- Community admin +- Community moderator +- Community member +- Community guest + +**Permission assignment:** +- Grant CRUD permissions per role +- Scope permissions to resources +- Cache permission checks for performance +- Review permission usage logs + +See [Roles and Permissions](../shared/roles_and_permissions.md) for complete RBAC documentation. + +## Community Management + +### Creating Communities + +Create new communities at `/host/communities/new`: + +**Community setup:** +1. **Basic information:** + - Name and identifier + - Description and purpose + - Privacy level (public/private) + - Host platform assignment + +2. **Membership settings:** + - Open or closed membership + - Approval requirements + - Invitation-only option + - Maximum members (optional) + +3. **Content settings:** + - Allow posts and discussions + - Enable events + - Enable exchanges (Joatu) + - Content moderation level + +4. **Organizer assignment:** + - Assign community organizers + - Set organizer roles + - Define permissions + +**After creation:** +- Configure community guidelines +- Set up navigation and pages +- Invite initial members +- Publish community + +### Managing Communities + +Oversee communities at `/host/communities`: + +**Community list:** +- All platform communities +- Activity metrics per community +- Member counts +- Recent activity +- Moderation status + +**Community actions:** +- **Edit settings** - Update community configuration +- **Manage organizers** - Add/remove community leaders +- **View members** - See all community members +- **Review content** - Moderate posts and comments +- **Manage events** - Oversee community events +- **Archive/delete** - Remove inactive communities + +**Community support:** +- Provide guidance to organizers +- Resolve disputes +- Handle appeals +- Assist with growth +- Share best practices + +See [Community Management](../community_organizers/community_management.md) for organizer perspective. + +## Content Management + +### Page Management + +Manage CMS pages at `/host/pages`: + +**Page features:** +- Block-based editor (rich text, images, etc.) +- Draft and published states +- Publication scheduling +- Privacy levels (public/private/members) +- Navigation assignment +- Translations (via Mobility) + +**Page actions:** +- Create new pages +- Edit existing content +- Preview before publishing +- Schedule publishing +- Archive old pages +- Delete pages + +**Content blocks:** +- Rich text content +- Images and media +- Embedded content +- Custom HTML +- Reusable templates + +### Navigation Management + +Configure site navigation at `/host/navigation_areas`: + +**Navigation areas:** +- **Header** - Primary site navigation +- **Footer** - Footer links +- **Sidebar** - Contextual navigation +- **Custom** - Special purpose menus + +**Navigation features:** +- Nested menu items +- External and internal links +- Visibility controls (by role) +- Icon support +- Ordering and grouping + +### Resource Management + +Manage downloadable resources at `/host/resources`: + +**Resource features:** +- File uploads (PDF, images, documents) +- Categorization and tagging +- Version control +- Access permissions +- Download tracking +- Translations + +**Resource actions:** +- Upload new resources +- Update existing files +- Set access permissions +- Track download metrics +- Organize into categories +- Add descriptions and metadata + +## Moderation and Safety + +### Content Moderation + +Review and moderate content: + +**Moderation queue:** +- Reported content +- Flagged posts and comments +- Suspicious accounts +- Spam detection results + +**Moderation actions:** +- **Approve** - Content is acceptable +- **Remove** - Delete violating content +- **Edit** - Modify problematic parts +- **Warn** - Issue warning to author +- **Restrict** - Limit user privileges +- **Ban** - Permanently remove user + +**Moderation tools:** +- Bulk moderation actions +- Automated spam filters +- Pattern detection +- Moderator notes and history +- Appeal handling + +### Report Management + +Handle user reports at `/host/reports`: + +**Report types:** +- Content reports (posts, comments) +- User reports (profiles, behavior) +- Event reports +- Exchange reports (Joatu) + +**Report workflow:** +1. **Submission** - User files report +2. **Triage** - Categorize and prioritize +3. **Investigation** - Review content and context +4. **Decision** - Determine action needed +5. **Resolution** - Take appropriate action +6. **Notification** - Inform reporter and subject +7. **Follow-up** - Monitor for compliance + +**Report statuses:** +- Pending (awaiting review) +- In review (being investigated) +- Resolved (action taken) +- Dismissed (no violation) +- Closed (finalized) + +See [Safety and Reporting](../end_users/safety_reporting.md) for user reporting guide. + +### User Safety Management + +Protect users from harm: + +**Safety features:** +- User blocking system +- Privacy controls +- Report handling +- Harassment prevention +- Content filtering + +**Administrator actions:** +- Review block lists +- Handle harassment reports +- Enforce community guidelines +- Coordinate with law enforcement (if needed) +- Communicate with affected users + +## Metrics and Analytics + +### Platform Analytics + +Access metrics at `/host/metrics_reports`: + +**Available metrics:** +- **Page views** - Track content consumption +- **Link clicks** - Monitor external link engagement +- **Downloads** - Measure resource usage +- **Shares** - Track social sharing activity +- **Searches** - Understand user queries (future) + +**Analytics features:** +- Date range filtering +- Locale-specific breakdowns +- Export to CSV +- Visual charts (bar, line) +- Aggregate statistics + +**Privacy-first metrics:** +- No user identifiers stored +- Event-only tracking +- Aggregate data only +- Sanitized query parameters +- Transparent collection + +See [Privacy Principles](../shared/privacy_principles.md) for metrics philosophy. + +### Report Generation + +Generate and export reports: + +**Report types:** +- User activity reports +- Community growth metrics +- Content performance +- Event attendance +- Exchange activity (Joatu) +- Platform health + +**Export formats:** +- CSV (primary) +- Excel (when available) +- JSON (API access) + +**Report management:** +- Schedule regular reports +- Configure retention periods +- Manage report access +- Purge old exports + +## System Administration + +### Platform Health Monitoring + +Monitor system performance: + +**Health indicators:** +- Server response times +- Database query performance +- Cache hit rates +- Background job queue +- Error rates +- Storage usage + +**Monitoring tools:** +- Dashboard health widgets +- Email alerts for issues +- Error tracking (if enabled) +- Performance metrics +- Uptime monitoring + +### Background Jobs + +Monitor Sidekiq jobs at `/sidekiq`: + +**Job queues:** +- Email delivery +- Report generation +- Search indexing +- Metrics collection +- File processing + +**Job management:** +- View queue status +- Retry failed jobs +- Clear dead jobs +- Monitor job latency +- Adjust concurrency + +### Cache Management + +Manage platform caching: + +**Cache types:** +- Fragment caching (navigation, content blocks) +- Page caching (static pages) +- Query caching (database) +- Asset caching (images, CSS, JS) + +**Cache operations:** +- Clear cache by type +- View cache statistics +- Configure cache expiration +- Warm cache after deploys + +## Security Management + +### Security Best Practices + +Implement security measures: + +**Access control:** +- Regular role audits +- Remove unused accounts +- Review permission grants +- Monitor failed login attempts +- Enforce strong passwords + +**Data protection:** +- Enable encryption for sensitive fields +- Secure file storage +- Regular backups +- Access logging +- HTTPS enforcement + +**Monitoring:** +- Review audit logs +- Check for suspicious activity +- Monitor report patterns +- Track permission changes +- Alert on security events + +See [Security and Privacy](security_privacy.md) for detailed security practices. + +### Compliance Management + +Ensure legal and regulatory compliance: + +**Privacy compliance:** +- GDPR (European Union) +- PIPEDA (Canada) +- CCPA (California) +- Local privacy laws + +**Data handling:** +- Data collection transparency +- User consent management +- Data retention policies +- Right to access +- Right to deletion +- Data portability + +**Documentation:** +- Privacy policy maintenance +- Terms of service updates +- Cookie policy +- Legal agreements +- Compliance audits + +See [Compliance and Legal](compliance_legal.md) for compliance requirements. + +## Integration and Extensions + +### Third-Party Services + +Manage external service integrations: + +**Optional services:** +- Google Analytics (with consent) +- Error tracking (Sentry, etc.) +- Email delivery (SMTP, SendGrid) +- File storage (S3, MinIO) +- Search (Elasticsearch) + +**Integration requirements:** +- Update privacy policy +- Add consent mechanisms +- Configure data retention +- Enable IP anonymization +- Provide opt-out options + +### Platform Extensions + +Enable and configure extensions: + +**Available extensions:** +- Event management +- Exchange system (Joatu) +- Navigation builder +- Page builder +- Resource library +- Metrics and reporting + +**Extension configuration:** +- Enable/disable per platform +- Configure settings +- Set permissions +- Customize behavior + +See [Host Dashboard Extensions](host_dashboard_extensions.md) for extension details. + +## Support and Troubleshooting + +### User Support + +Provide support to platform users: + +**Support channels:** +- In-platform messaging +- Email support +- Help documentation +- Community forums +- FAQ sections + +**Common support tasks:** +- Password resets +- Account verification +- Permission requests +- Technical assistance +- Feature guidance + +See [User Support Procedures](user_support_procedures.md) for support workflows. + +### Troubleshooting + +Resolve common platform issues: + +**Common problems:** +- Login issues +- Permission errors +- Content publishing problems +- Email delivery failures +- Search indexing delays +- Cache staleness + +**Diagnostic tools:** +- Error logs +- Rails console (use cautiously) +- Database queries +- Background job status +- Cache inspection + +## Best Practices + +### Platform Governance + +Establish clear governance: + +- **Transparent policies** - Public guidelines and processes +- **Consistent enforcement** - Fair application of rules +- **Member voice** - Input mechanisms for users +- **Accountability** - Clear responsibility for decisions +- **Documentation** - Record policies and changes + +See [Democratic Principles](../shared/democratic_principles.md) for governance philosophy. + +### Communication + +Maintain effective communication: + +- **Platform announcements** - Important updates and changes +- **Email notifications** - Relevant, timely communications +- **Change logs** - Document updates and fixes +- **Feedback loops** - Listen to user input +- **Transparency** - Open about decisions and changes + +### Continuous Improvement + +Improve platform over time: + +- **Collect feedback** - Survey users regularly +- **Monitor metrics** - Track usage and engagement +- **Review policies** - Update guidelines as needed +- **Test features** - Trial new capabilities +- **Train organizers** - Support community leaders +- **Document changes** - Maintain clear records + +## Related Documentation + +- [User Management](user_management.md) +- [Host Management](host_management.md) +- [Security and Privacy](security_privacy.md) +- [Compliance and Legal](compliance_legal.md) +- [User Support Procedures](user_support_procedures.md) +- [Community Management](../community_organizers/community_management.md) +- [RBAC Overview](../developers/architecture/rbac_overview.md) +- [Privacy Principles](../shared/privacy_principles.md) +- [Democratic Principles](../shared/democratic_principles.md) + +--- + +**Remember:** Platform administration is about enabling communities to thrive while maintaining safety, privacy, and security for all members. Balance control with empowerment, and lead with transparency. diff --git a/docs/platform_organizers/security_privacy.md b/docs/platform_organizers/security_privacy.md index 1553ac5a6..5c47c6f2d 100644 --- a/docs/platform_organizers/security_privacy.md +++ b/docs/platform_organizers/security_privacy.md @@ -1,16 +1,443 @@ -# Security and Privacy Guide for Platform Organizers - -## Key Responsibilities -- Enforce invitation-only registration by default unless explicitly opened. -- Maintain privacy notices and disclose any trackers or external services used. -- Review access controls and roles; audit elevated permissions regularly. - -## Operational Checks -- Run security scans (Brakeman, dependency audits) as part of deployments. -- Ensure backups, retention policies, and deletion workflows meet policy requirements. -- Monitor authentication and notification systems for failures or abuse signals. - -## Resources -- [Shared Privacy Principles](../shared/privacy_principles.md) -- [Roles and Permissions](../shared/roles_and_permissions.md) -- [Security Protection System](../developers/systems/security_protection_system.md) +# Security and Privacy Management + +**Target Audience:** Platform organizers +**Document Type:** Administrator Guide +**Last Updated:** November 20, 2025 + +## Overview + +This guide covers security and privacy management responsibilities for platform administrators, including data protection, access control, compliance, and incident response. + +## Security Principles + +### Defense in Depth + +Implement multiple layers of security: + +- **Application security** - Code-level protections +- **Access control** - Role-based permissions +- **Data encryption** - At rest and in transit +- **Network security** - Firewalls and HTTPS +- **Monitoring** - Audit logs and alerts + +### Privacy by Design + +Build privacy into every aspect: + +- **Data minimization** - Collect only necessary data +- **Purpose limitation** - Use data only as specified +- **User control** - Empower users to manage their data +- **Transparency** - Clear about data practices +- **Security** - Protect data from unauthorized access + +See [Privacy Principles](../shared/privacy_principles.md) for complete privacy philosophy. + +## Access Control and Authentication + +### Role-Based Access Control (RBAC) + +Manage permissions through roles: + +**Platform roles:** +- Platform organizer (full admin access) +- Community organizer (community management) +- Content moderator (moderation tools) +- Member (standard access) +- Guest (limited access) + +**Permission management:** +- Grant minimum necessary permissions +- Regular permission audits +- Remove unused roles +- Log permission changes + +See [Roles and Permissions](../shared/roles_and_permissions.md) and [RBAC Overview](../developers/architecture/rbac_overview.md). + +### User Authentication + +Secure user accounts: + +**Authentication measures:** +- **Strong passwords** - Minimum 12 characters required +- **Email confirmation** - Verify email addresses +- **Password reset** - Secure reset workflows +- **Session management** - Timeout inactive sessions +- **Two-factor authentication** - (when available) + +**Account security monitoring:** +- Failed login attempts +- Unusual access patterns +- Multiple simultaneous sessions +- Location changes +- Password reset requests + +### Invitation System Security + +For private/invitation-only platforms: + +**Invitation tokens:** +- Cryptographically secure random tokens +- Scoped to specific events or platform +- Time-limited validity +- Single-use for platform invitations +- Track invitation usage + +**Event invitation tokens:** +- Grant access only to specific event +- Do not provide platform-wide access +- Expire based on event timing +- Invalid tokens redirect to sign-in + +## Data Protection + +### Encryption + +Protect sensitive data: + +**Encryption at rest:** +- Active Record Encryption for sensitive model fields +- Encrypted Active Storage attachments +- Database encryption (PostgreSQL pgcrypto) +- Encrypted backups + +**Encryption in transit:** +- HTTPS/TLS for all connections +- Secure WebSocket connections +- Encrypted email (STARTTLS) +- Secure API endpoints + +**Key management:** +- Secure key storage +- Key rotation procedures +- Access controls on keys +- Backup key recovery + +### Data Retention + +Implement retention policies: + +**Retention guidelines:** +- **Active users** - Retain while account active +- **Inactive users** - Define inactivity thresholds +- **Deleted accounts** - Purge after grace period +- **Exports and reports** - 90-day default retention +- **Metrics** - Aggregate data retained longer +- **Backups** - Defined backup retention periods + +**Retention procedures:** +- Automated data purging +- Manual deletion workflows +- Legal hold procedures +- Compliance with regulations + +### Privacy Controls + +Give users control over their data: + +**User privacy features:** +- Profile visibility settings +- Content privacy levels (public/private) +- Block and mute users +- Control who can message +- Opt-out of optional tracking + +**Administrator responsibilities:** +- Honor privacy settings +- Process deletion requests +- Provide data exports +- Respond to access requests +- Maintain transparency + +## Security Monitoring + +### Audit Logging + +Track security-relevant events: + +**Logged events:** +- User authentication (login, logout, failures) +- Permission changes +- Role assignments +- Administrative actions +- Report submissions and resolutions +- Content moderation actions +- Data access and exports + +**Log analysis:** +- Review logs regularly +- Detect suspicious patterns +- Investigate anomalies +- Generate compliance reports + +### Intrusion Detection + +Monitor for security threats: + +**Indicators to watch:** +- Multiple failed login attempts +- Unusual access patterns +- Rapid content creation (spam) +- Permission escalation attempts +- Data export anomalies +- Suspicious file uploads + +**Response procedures:** +- Alert on threshold breaches +- Investigate alerts promptly +- Take protective action +- Document incidents +- Update defenses + +### Vulnerability Management + +Stay ahead of security issues: + +**Security practices:** +- Regular dependency updates +- Rails security patches +- Brakeman security scans +- Bundler audit checks +- Penetration testing (periodic) + +**Update procedures:** +- Monitor security advisories +- Test patches in staging +- Deploy critical fixes quickly +- Document changes +- Notify stakeholders + +## Incident Response + +### Security Incidents + +Respond to security breaches: + +**Incident types:** +- Data breaches +- Unauthorized access +- Account compromises +- Denial of service +- Malware/exploits + +**Response steps:** +1. **Detect and confirm** - Verify the incident +2. **Contain** - Limit damage and spread +3. **Investigate** - Determine scope and cause +4. **Remediate** - Fix vulnerabilities +5. **Recover** - Restore normal operations +6. **Document** - Record incident details +7. **Review** - Learn and improve + +**Communication:** +- Notify affected users +- Report to authorities (if required) +- Update stakeholders +- Public disclosure (if appropriate) +- Post-incident review + +### Privacy Incidents + +Handle privacy violations: + +**Incident types:** +- Unauthorized data access +- Data leaks or exposure +- Privacy setting failures +- Improper data sharing +- Consent violations + +**Response procedures:** +- Assess impact and scope +- Notify affected individuals +- Report to regulators (if required) +- Implement corrective measures +- Update policies and procedures +- Monitor for recurrence + +## Compliance + +### Privacy Regulations + +Comply with applicable laws: + +**GDPR (European Union):** +- Lawful basis for processing +- Data subject rights (access, deletion, portability) +- Data protection impact assessments +- Privacy by design and default +- Breach notification (72 hours) + +**PIPEDA (Canada):** +- Consent for collection and use +- Limit collection to necessary data +- Accuracy and retention limits +- Safeguards for protection +- Individual access rights + +**CCPA (California):** +- Notice of data collection +- Right to know, delete, opt-out +- Non-discrimination for exercising rights +- Data sale restrictions + +**Implementation:** +- Update privacy policy for jurisdiction +- Implement required features +- Document compliance procedures +- Train staff on requirements +- Regular compliance audits + +See [Compliance and Legal Guidelines](compliance_legal.md) for detailed compliance requirements. + +### Data Subject Rights + +Honor user data rights: + +**Right to access:** +- Provide copy of personal data +- Explain how data is used +- Identify data sources +- List data sharing + +**Right to rectification:** +- Correct inaccurate data +- Complete incomplete data +- Update outdated information + +**Right to erasure ("right to be forgotten"):** +- Delete personal data on request +- Inform data processors +- Exceptions: legal obligations, public interest +- Retain only what's legally required + +**Right to portability:** +- Export data in machine-readable format +- Transfer to another platform (when feasible) + +**Right to object:** +- Stop processing for specific purposes +- Opt-out of profiling/marketing +- Object to automated decisions + +### Third-Party Services + +Manage external service compliance: + +**Before adding services:** +- Review privacy policy +- Assess data handling +- Check compliance certifications +- Evaluate security measures +- Confirm data location + +**Required actions:** +- Update platform privacy policy +- Add to list of processors +- Execute data processing agreements +- Configure privacy settings +- Implement consent mechanisms +- Provide opt-out options + +**Ongoing oversight:** +- Monitor service compliance +- Review updated policies +- Audit data usage +- Respond to incidents +- Renew agreements + +## Security Best Practices + +### Secure Configuration + +Harden platform security: + +**Rails configuration:** +- Enable force_ssl +- Configure CORS properly +- Set secure session cookies +- Use CSP headers +- Disable unsafe methods + +**Environment variables:** +- Never commit secrets to git +- Use ENV.fetch for required vars +- Rotate credentials regularly +- Secure credential storage +- Limit credential access + +**Database security:** +- Strong database passwords +- Limit database access +- Regular backups +- Encrypted connections +- Query parameter sanitization + +### Secure Development + +Prevent vulnerabilities in code: + +**Security checks:** +- Run Brakeman before deployment +- Fix high-confidence vulnerabilities +- Review medium-confidence warnings +- Bundle audit for dependency vulnerabilities +- Code review for security issues + +**Safe coding practices:** +- Never use eval or constantize on user input +- Use strong parameters +- Parameterized queries only +- Sanitize HTML with allowlists +- Validate all user inputs + +### User Education + +Help users protect themselves: + +**Security guidance:** +- Strong password requirements +- Phishing awareness +- Safe exchange practices (Joatu) +- Reporting suspicious activity +- Privacy settings education + +**Communication:** +- Security tips in onboarding +- Regular security reminders +- Breach notifications +- Update announcements +- Help documentation + +## Security Tools and Resources + +### Platform Tools + +Security tools in the platform: + +- **Brakeman** - Static analysis security scanner +- **bundler-audit** - Gem vulnerability checker +- **RuboCop** - Code quality and security rules +- **Rails security** - Built-in protections (CSRF, XSS, SQL injection) + +### External Resources + +Additional security resources: + +- [Rails Security Guide](https://guides.rubyonrails.org/security.html) +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [Security.md](../../SECURITY.md) - Repository security policy +- [Privacy Principles](../shared/privacy_principles.md) +- [PIPEDA Compliance](../privacy/pipeda_compliance_updates.md) + +## Related Documentation + +- [Platform Administration](platform_administration.md) +- [Compliance and Legal Guidelines](compliance_legal.md) +- [User Management](user_management.md) +- [Privacy Policy](../end_users/privacy_policy.md) +- [Community Guidelines](../end_users/community_guidelines.md) +- [Privacy Principles](../shared/privacy_principles.md) + +--- + +**Remember:** Security and privacy are ongoing responsibilities. Stay informed about threats and regulations, implement defense in depth, and always prioritize user trust and data protection. diff --git a/docs/platform_organizers/user_support_procedures.md b/docs/platform_organizers/user_support_procedures.md index 4bdb81e40..d8c228e09 100644 --- a/docs/platform_organizers/user_support_procedures.md +++ b/docs/platform_organizers/user_support_procedures.md @@ -1,20 +1,481 @@ # User Support Procedures -## Intake -- Track support requests (email, form, or in-app) with timestamps and requester info. -- Collect key details: URL, steps to reproduce, expected vs actual, screenshots/logs. - -## Resolution Steps -- Verify platform status and recent deployments. -- Reproduce issues in a safe environment when possible. -- Escalate authentication/authorization problems to platform managers. -- Communicate progress and resolution clearly with the user. - -## Aftercare -- Document fixes or workarounds in internal notes. -- Add FAQs or help content when patterns emerge. -- Close tickets only after user confirmation where practical. - -## References -- [Support Staff Guide](../support_staff/README.md) -- [Automatic Test Configuration](../developers/development/automatic_test_configuration.md) +**Target Audience:** Platform organizers and support staff +**Document Type:** Procedures Guide +**Last Updated:** November 20, 2025 + +## Overview + +This guide outlines procedures for providing user support on the Better Together platform, including handling common requests, troubleshooting issues, and escalating complex problems. + +## Support Channels + +### Available Support Methods + +**In-platform messaging:** +- Direct messages from users +- Help/support community forums +- Contact forms + +**Email support:** +- Dedicated support email address +- Auto-responders for acknowledgment +- Ticket tracking system (if available) + +**Documentation:** +- Self-service help articles +- FAQs and knowledge base +- Video tutorials (if available) +- User guides + +## Common Support Requests + +### Account Issues + +**Password resets:** +1. User requests reset via `/users/password/new` +2. System sends reset email +3. If email not received: + - Check spam/junk folder + - Verify email address is correct + - Manually trigger reset from admin panel + - Check email delivery logs + +**Email confirmation:** +1. User should receive confirmation email on registration +2. If not received: + - Resend confirmation from admin panel (`/host/users/:id`) + - Manually confirm email if legitimate + - Check email delivery system + +**Account lockouts:** +- Review failed login attempts +- Verify account status +- Unlock account if appropriate +- Investigate suspicious activity +- Reset password if compromised + +**Account deletion:** +1. Verify user identity +2. Explain data retention policy +3. Confirm deletion request +4. Process deletion (may have grace period) +5. Send confirmation +6. Purge data per retention policy + +See [User Management](user_management.md) for account administration procedures. + +### Access and Permissions + +**Permission requests:** +1. Verify user identity +2. Understand requested access level +3. Check if request is appropriate +4. Consult with relevant organizers +5. Grant or deny with explanation +6. Document decision + +**Community access:** +- Closed community membership requests +- Invitation-only community access +- Membership approval process +- Appeal denied membership + +**Content access:** +- Private content access requests +- Event invitation issues +- Resource download permissions +- Page visibility questions + +### Technical Issues + +**Login problems:** +- Browser compatibility +- Cookie/cache issues +- Session expiration +- Incorrect credentials +- Account status + +**Display issues:** +- CSS not loading +- JavaScript errors +- Mobile rendering +- Browser-specific bugs + +**Functionality problems:** +- Form submission errors +- File upload failures +- Search not working +- Notification delivery +- Real-time features (WebSockets) + +**Troubleshooting steps:** +1. Reproduce the issue +2. Check error logs +3. Test in different browsers +4. Clear cache and cookies +5. Try incognito/private mode +6. Check network connectivity +7. Escalate to developers if needed + +## Support Workflow + +### Ticket Management + +**Intake:** +1. Receive support request +2. Create ticket (if using ticketing system) +3. Acknowledge receipt +4. Categorize and prioritize +5. Assign to appropriate person + +**Priority levels:** +- **Critical** - Platform down, security issue, data loss +- **High** - Major functionality broken, many users affected +- **Normal** - Standard requests, minor issues +- **Low** - Feature requests, general questions + +**Response time targets:** +- Critical: Immediate (< 1 hour) +- High: Within 4 hours +- Normal: Within 24 hours +- Low: Within 3 business days + +### Resolution Process + +**Standard workflow:** +1. **Understand** - Clarify the issue or request +2. **Investigate** - Gather information and diagnose +3. **Resolve** - Fix issue or fulfill request +4. **Communicate** - Inform user of resolution +5. **Document** - Record for future reference +6. **Follow-up** - Confirm user satisfaction + +**If unable to resolve:** +- Escalate to appropriate person +- Set expectations with user +- Keep user updated on progress +- Loop back when resolved + +### Documentation + +**Ticket records:** +- User contact information +- Issue description +- Steps taken +- Resolution details +- Time spent +- Escalations + +**Knowledge base updates:** +- Common issues and solutions +- FAQ additions +- Process improvements +- Training materials + +## Escalation Procedures + +### When to Escalate + +**Technical issues:** +- Bug requires code fix +- Infrastructure problems +- Database issues +- Security vulnerabilities + +**Policy issues:** +- Guideline interpretation questions +- Moderation appeals +- Legal concerns +- Privacy requests + +**Complex requests:** +- Custom configuration needs +- Integration requirements +- Bulk operations +- Data exports/imports + +### Escalation Paths + +**Technical escalation:** +- Level 1: Support staff +- Level 2: Platform administrators +- Level 3: Developers +- Emergency: On-call engineer + +**Policy escalation:** +- Level 1: Community organizers +- Level 2: Platform organizers +- Level 3: Leadership/governance +- Legal: Legal counsel + +See [Escalation Matrix](../shared/escalation_matrix.md) for detailed escalation procedures. + +## Specialized Support Areas + +### Privacy and Data Requests + +**Data access requests (GDPR, PIPEDA, CCPA):** +1. Verify identity +2. Clarify scope of request +3. Gather requested data +4. Review for third-party information +5. Provide data in portable format +6. Document request and fulfillment +7. Response within legal timeframe (typically 30 days) + +**Data deletion requests:** +1. Verify identity +2. Explain what will be deleted +3. Inform about retention requirements +4. Process deletion +5. Confirm completion +6. Document request + +**Data portability:** +- Export user's data +- Machine-readable format +- Include all personal data +- Exclude others' data + +See [Security and Privacy](security_privacy.md) and [Compliance and Legal](compliance_legal.md). + +### Moderation Support + +**Report handling:** +- Triage reported content +- Investigate thoroughly +- Apply community guidelines +- Take appropriate action +- Communicate decisions +- Handle appeals + +**User conflicts:** +- Mediate disputes +- Enforce guidelines fairly +- Document incidents +- Escalate when needed + +See [Safety and Reporting](../end_users/safety_reporting.md) for reporting procedures. + +### Community Support + +**Community organizer support:** +- Answer policy questions +- Assist with tools and features +- Help with member issues +- Provide best practices +- Facilitate inter-community collaboration + +**Community health:** +- Monitor community metrics +- Identify struggling communities +- Provide intervention when needed +- Celebrate successes + +## User Communication + +### Communication Guidelines + +**Tone and style:** +- Professional but friendly +- Clear and concise +- Empathetic and patient +- Respectful and inclusive +- Avoid jargon when possible + +**Best practices:** +- Acknowledge user's concern +- Set clear expectations +- Provide updates on progress +- Explain decisions clearly +- Offer alternatives when saying no +- Thank users for patience + +### Templates + +**Common response templates:** +- Password reset instructions +- Account confirmation help +- Permission denied explanations +- Feature request acknowledgments +- Bug report confirmations +- Escalation notifications + +**Customize templates:** +- Use user's name +- Reference specific details +- Add personal touch +- Maintain brand voice + +### Multi-lingual Support + +**Language considerations:** +- Identify user's preferred locale +- Respond in user's language (if possible) +- Use translation tools carefully +- Maintain clarity across languages +- Update templates for all locales + +## Quality Assurance + +### Support Metrics + +**Track performance:** +- Response time (first response, resolution) +- Resolution rate +- User satisfaction scores +- Escalation rate +- Common issue trends +- Support volume + +**Review regularly:** +- Weekly support metrics +- Monthly trend analysis +- Quarterly goal assessment +- Annual performance review + +### Continuous Improvement + +**Feedback collection:** +- User satisfaction surveys +- Support ticket analysis +- Team retrospectives +- Process reviews + +**Improvement actions:** +- Update documentation +- Refine processes +- Enhance training +- Improve tools +- Address systemic issues + +### Training + +**Ongoing support training:** +- Platform feature updates +- Policy changes +- New tools and processes +- Communication skills +- Conflict resolution +- Privacy and compliance + +**Knowledge sharing:** +- Team meetings +- Documentation updates +- Case studies +- Best practice sharing + +## Tools and Resources + +### Support Tools + +**Platform admin tools:** +- User management at `/host/users` +- Person profiles at `/host/people` +- Community management at `/host/communities` +- Report management (when available) +- Metrics and analytics + +**External tools:** +- Email system +- Ticketing software (if used) +- Knowledge base +- Communication platforms + +### Documentation Resources + +**Internal:** +- Admin guides +- Support procedures +- FAQ database +- Escalation contacts + +**User-facing:** +- [End User Guide](../end_users/guide.md) +- [User Management Guide](../end_users/user_management_guide.md) +- [Safety and Reporting](../end_users/safety_reporting.md) +- [Community Participation](../end_users/community_participation.md) + +## Self-Service Support + +### Help Documentation + +**Maintain comprehensive docs:** +- Getting started guides +- Feature tutorials +- Troubleshooting guides +- FAQs +- Video walkthroughs + +**Make docs discoverable:** +- Search functionality +- Logical organization +- Cross-references +- Related articles +- Table of contents + +### In-App Help + +**Contextual help:** +- Tooltips and hints +- Help icons on forms +- Guided tours for new users +- Onboarding checklists + +**Help resources:** +- Link to relevant docs +- Contact support options +- Community forums +- Video tutorials + +## Special Situations + +### Crisis Support + +**Mental health concerns:** +- Provide crisis resources +- Don't attempt counseling +- Escalate to appropriate services +- Document interaction +- Follow up appropriately + +**Safety threats:** +- Take seriously +- Contact authorities if needed +- Document thoroughly +- Preserve evidence +- Follow platform security procedures + +### Legal Requests + +**Law enforcement:** +- Verify authenticity +- Check jurisdiction +- Consult legal counsel +- Document request +- Preserve evidence +- Follow legal process + +**Subpoenas and court orders:** +- Forward to legal team +- Don't fulfill without review +- Preserve relevant data +- Respond within timeframes +- Document compliance + +See [Compliance and Legal](compliance_legal.md). + +## Related Documentation + +- [User Management](user_management.md) +- [Platform Administration](platform_administration.md) +- [Security and Privacy](security_privacy.md) +- [Compliance and Legal](compliance_legal.md) +- [User Management Guide](../end_users/user_management_guide.md) +- [Safety and Reporting](../end_users/safety_reporting.md) +- [Escalation Matrix](../shared/escalation_matrix.md) + +--- + +**Remember:** Quality support builds user trust and platform success. Respond promptly, communicate clearly, and always prioritize user needs while maintaining platform integrity. diff --git a/docs/privacy/pipeda_compliance_updates.md b/docs/privacy/pipeda_compliance_updates.md new file mode 100644 index 000000000..b84551dbc --- /dev/null +++ b/docs/privacy/pipeda_compliance_updates.md @@ -0,0 +1,207 @@ +# PIPEDA Compliance Updates + +**Date:** November 20, 2025 +**Purpose:** Document PIPEDA-specific updates to privacy policy and cookie consent agreement + +## Overview + +Added comprehensive PIPEDA (Personal Information Protection and Electronic Documents Act) compliance information to both the Privacy Policy and Cookie Consent Agreement to meet Canadian federal privacy law requirements. + +## Files Updated + +1. `app/views/better_together/static_pages/privacy.html.erb` +2. `app/views/better_together/static_pages/cookie_consent.html.erb` + +## Privacy Policy Updates + +### Added New Section: "Does Better Together comply with Canadian privacy law (PIPEDA)?" + +**Location:** After CCPA section, before "Where can I access data about me?" + +**Key Content Added:** + +1. **PIPEDA Applicability Statement** + - Explains why PIPEDA applies to Better Together + - Notes operation in Newfoundland and Labrador + - Mentions cross-border data handling + +2. **PIPEDA's Ten Fair Information Principles** + - Complete enumeration of all 10 principles with Better Together-specific explanations: + 1. Accountability (Privacy Officer designated) + 2. Identifying Purposes + 3. Consent (meaningful consent practices) + 4. Limiting Collection + 5. Limiting Use, Disclosure, and Retention + 6. Accuracy + 7. Safeguards + 8. Openness + 9. Individual Access + 10. Challenging Compliance + - Each principle links to relevant sections of the privacy policy + +3. **Your Rights Under PIPEDA** + - Right to know why information is collected + - Right to expect reasonable protection + - Right to access personal information + - Right to challenge accuracy + - Right to withdraw consent + - Right to file complaints + +4. **Consent Under PIPEDA** + - Express consent for sensitive information + - Implied consent for less sensitive information + - Withdrawal of consent procedures + +5. **Data Breach Notification** + - Reporting breaches to Privacy Commissioner of Canada + - Notifying affected individuals + - Maintaining breach records + +6. **Cross-Border Data Transfers** + - Reference to data storage section + - Commitment to comparable protection levels + +7. **Filing a Complaint Under PIPEDA** + - Complete contact information for Office of the Privacy Commissioner of Canada + - Address, phone numbers (toll-free and regular), TTY + - Website link + +### Updated Table of Contents +- Added link to new PIPEDA section + +### Enhanced Contact Section + +**Updated:** "How can I contact Better Together about privacy?" + +**Changes:** +- Designated "Privacy Officer" title +- Added response timeline (30 days standard, with extension notification) +- Separated contact guidance by jurisdiction: + - Canadian residents (PIPEDA) + - European Union residents (GDPR) + - California residents (CCPA) + +## Cookie Consent Agreement Updates + +### Added New Section: "Canadian and International Privacy Laws" + +**Location:** After cookie management section, before updates section +**Renumbered:** Previous section 8 (GDPR) became part of new section 8 (Privacy Laws) + +**Key Content Added:** + +1. **Canadian Privacy Law Compliance (PIPEDA) Subsection** + - Explains Cookie Policy as part of PIPEDA compliance + - Lists specific PIPEDA requirements for cookies: + - Consent requirements (essential vs. optional) + - Purpose identification + - Access rights + - Withdrawal procedures + - Security safeguards + +2. **Your Rights Under PIPEDA** + - Right to know what cookies collect and why + - Right to access cookie information + - Right to withdraw consent for optional cookies + - Right to file complaints + +3. **Privacy Officer Contact Information** + - Email: privacy@bettertogethersolutions.com + +4. **Filing a Complaint Process** + - Office of the Privacy Commissioner of Canada contact details + - Website and phone numbers + +5. **GDPR Rights Subsection** + - Retained existing GDPR rights content + - Reorganized under "Privacy Laws" section + +### Updated Table of Contents +- Added "Canadian and International Privacy Laws" entry +- Renumbered subsequent sections (9-10 instead of 8-9) + +## Legal Compliance Summary + +### PIPEDA Requirements Met + +✅ **Accountability** - Privacy Officer designated and contact provided +✅ **Identifying Purposes** - Clear explanations throughout both documents +✅ **Consent** - Describes express and implied consent mechanisms +✅ **Limiting Collection** - States minimum necessary collection +✅ **Limiting Use, Disclosure, and Retention** - Documented in privacy policy +✅ **Accuracy** - User rights to correct information explained +✅ **Safeguards** - Security measures described +✅ **Openness** - Public privacy policies with detailed practices +✅ **Individual Access** - Access procedures documented +✅ **Challenging Compliance** - Complaint procedures with Privacy Commissioner contact + +### Key Improvements + +1. **Explicit PIPEDA Compliance Statement**: Clear declaration of compliance with Canadian law +2. **Privacy Officer Designation**: Accountable individual identified +3. **Detailed Consent Framework**: Express vs. implied consent explained +4. **Breach Notification Protocol**: PIPEDA breach reporting requirements documented +5. **Complaint Procedures**: Multi-jurisdictional complaint filing guidance +6. **Cross-Border Transfer Safeguards**: Commitment to comparable protection +7. **Response Timelines**: 30-day response commitment with extension notification + +## Implementation Notes + +### No Code Changes Required +These updates are documentation-only and require no backend changes to existing functionality. + +### Existing Practices Already PIPEDA-Compliant +- Platform already obtains meaningful consent through registration +- Security safeguards (encryption, access controls) already in place +- User access and correction mechanisms already implemented +- Data retention practices already documented + +### Next Steps (Recommended) + +1. **Privacy Officer Designation**: Formally designate Privacy Officer role (likely existing contact) +2. **Breach Response Plan**: Document internal procedures for breach assessment and notification +3. **Staff Training**: Ensure team understands PIPEDA obligations +4. **Vendor Agreements**: Review subprocessor contracts for PIPEDA compliance +5. **Consent Audit**: Review all consent mechanisms to ensure PIPEDA compliance +6. **Annual Review**: Schedule annual PIPEDA compliance review + +## Comparison with Other Jurisdictions + +### PIPEDA (Canada) +- **Scope**: Commercial activities in provinces without substantially similar law +- **Key Feature**: 10 Fair Information Principles +- **Enforcement**: Office of the Privacy Commissioner of Canada +- **Breach Reporting**: Required for real risk of significant harm + +### GDPR (EU) +- **Scope**: EU residents' data +- **Key Feature**: Data subject rights, accountability requirements +- **Enforcement**: Data Protection Authorities in each EU country +- **Breach Reporting**: Required within 72 hours for certain breaches + +### CCPA (California) +- **Scope**: California residents' data for qualifying businesses +- **Key Feature**: Consumer rights (access, deletion, opt-out of sale) +- **Enforcement**: California Attorney General, private right of action +- **Breach Reporting**: Different requirements under California breach notification law + +**Better Together's Approach**: Implement strongest protections across all three frameworks to ensure comprehensive compliance. + +## Resources + +- [PIPEDA Official Text](https://laws-lois.justice.gc.ca/eng/acts/P-8.6/) +- [Office of the Privacy Commissioner of Canada](https://www.priv.gc.ca) +- [PIPEDA Fair Information Principles](https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/the-personal-information-protection-and-electronic-documents-act-pipeda/p_principle/) +- [PIPEDA Breach Reporting](https://www.priv.gc.ca/en/privacy-topics/business-privacy/safeguards-and-breaches/privacy-breaches/respond-to-a-privacy-breach-at-your-business/gd_pb_201810/) + +## Review and Approval + +**Technical Review**: ✅ Completed +**Legal Review**: ⏳ Recommended before production deployment +**Stakeholder Approval**: ⏳ Pending + +--- + +**Document Version:** 1.0 +**Last Updated:** November 20, 2025 +**Next Review Date:** November 20, 2026 (annual review recommended) diff --git a/docs/table_of_contents.md b/docs/table_of_contents.md index 7a9493a95..28fb0228b 100644 --- a/docs/table_of_contents.md +++ b/docs/table_of_contents.md @@ -148,10 +148,11 @@ Welcome to the comprehensive documentation for the Better Together Community Eng - [✅ Validate Documentation Tooling](scripts/validate_documentation_tooling.sh) - Validation suite - [📈 Update Progress](scripts/update_progress.sh) - Progress tracking utility -### 📁 **Legacy and Reference** +### 📁 **Development and Reference** -#### 🏗️ **Development Environment** - [`development/`](development/) +#### 🏗️ **Development** - [`development/`](development/) *Development setup and configuration* +- [📝 README](development/README.md) - Development resources overview - [🛠️ Development Setup](development/dev-setup.md) - Local development guide #### 🤝 **Joatu Exchange System** - [`joatu/`](joatu/) diff --git a/lib/better_together/engine.rb b/lib/better_together/engine.rb index 617b45d5e..cd4959375 100644 --- a/lib/better_together/engine.rb +++ b/lib/better_together/engine.rb @@ -27,6 +27,7 @@ require 'noticed' require 'premailer/rails' require 'rack/attack' +require 'redcarpet' require 'reform/rails' require 'ruby/openai' require 'simple_calendar' diff --git a/lib/better_together/migration_helpers.rb b/lib/better_together/migration_helpers.rb index 5e3b957d7..fe68fb485 100644 --- a/lib/better_together/migration_helpers.rb +++ b/lib/better_together/migration_helpers.rb @@ -64,7 +64,7 @@ def create_bt_membership_table(table_name, member_type:, joinable_type:, id: :uu unique: true, name: "unique_#{member_type}_#{joinable_type}_membership_member_role" - yield(t) if block_given? + yield(bt) if block_given? end end # rubocop:enable Metrics/MethodLength diff --git a/lib/tasks/generate.rake b/lib/tasks/generate.rake index dfee47ca0..a101868da 100644 --- a/lib/tasks/generate.rake +++ b/lib/tasks/generate.rake @@ -12,6 +12,50 @@ namespace :better_together do # rubocop:todo Metrics/BlockLength BetterTogether::NavigationBuilder.build(clear: true) end + desc 'Reset navigation areas only (preserves pages)' + task reset_navigation: :environment do + BetterTogether::NavigationBuilder.reset_navigation_areas + end + + desc 'Reset specific navigation area (usage: rake better_together:generate:reset_navigation_area[platform-header])' + task :reset_navigation_area, [:slug] => :environment do |_t, args| + if args[:slug].blank? + puts 'Error: Please provide a navigation area slug' + puts 'Available slugs: platform-header, platform-host, better-together, platform-footer' + puts 'Usage: rake better_together:generate:reset_navigation_area[platform-header]' + exit 1 + end + + BetterTogether::NavigationBuilder.reset_navigation_area(args[:slug]) + end + + desc 'List all navigation areas and items' + task list_navigation: :environment do + puts "\nNavigation Areas:" + puts '=' * 80 + + BetterTogether::NavigationArea.i18n.order(:slug).each do |area| + puts "\nArea: #{area.name}" + puts " Slug: #{area.slug}" + puts " Visible: #{area.visible}" + puts " Protected: #{area.protected}" + puts " Items: #{area.navigation_items.count}" + + next unless area.navigation_items.any? + + puts ' Navigation Items:' + area.navigation_items.where(parent_id: nil).order(:position).each do |item| + puts " - #{item.title} (#{item.item_type})" + next unless item.children.any? + + item.children.order(:position).each do |child| + puts " └─ #{child.title} (#{child.item_type})" + end + end + end + puts "\n#{'=' * 80}" + end + desc 'Generate setup wizard and step definitions' task setup_wizard: :environment do BetterTogether::SetupWizardBuilder.build(clear: true) diff --git a/lib/tasks/mobility_title_migration.rake b/lib/tasks/mobility_title_migration.rake new file mode 100644 index 000000000..1205e6423 --- /dev/null +++ b/lib/tasks/mobility_title_migration.rake @@ -0,0 +1,274 @@ +# frozen_string_literal: true + +# Mobility Title Translation Migration Tasks +# +# These tasks handle migrating title translations from the mobility_text_translations +# table to the mobility_string_translations table. This fixes an issue where titles +# were incorrectly stored as text type instead of string type due to missing type +# declarations in translates calls. +# +# Affected Models: +# - BetterTogether::Post +# - BetterTogether::Agreement +# - BetterTogether::Geography::Map +# +# Performance Optimizations: +# - Uses bulk operations (insert_all, delete_all) instead of individual record operations +# - Reduces database round trips from N operations to 2 operations +# - Bypasses ActiveRecord callbacks (especially Elasticsearch indexing) +# - Provides timing information for performance monitoring +# +# Usage: +# bin/dc-run rails translations:mobility:check_titles_status # Check current status +# bin/dc-run rails translations:mobility:migrate_titles_to_string # Perform migration +# bin/dc-run rails translations:mobility:clean_up_title_translations # Clean up remaining records +# +# The migration can also be executed through Rails migrations via: +# bin/dc-run rails db:migrate + +namespace :translations do # rubocop:todo Metrics/BlockLength + namespace :mobility do # rubocop:todo Metrics/BlockLength + desc 'Migrate title translations from text to string translations' + task migrate_titles_to_string: :environment do # rubocop:todo Metrics/BlockLength + puts 'Starting migration of titles from text to string translations...' + puts '=' * 80 + + # Use Mobility's KeyValue backend models for safer operations + text_translation_class = Mobility::Backends::ActiveRecord::KeyValue::TextTranslation + string_translation_class = Mobility::Backends::ActiveRecord::KeyValue::StringTranslation + + # Find all title translations in text translations + text_title_translations = text_translation_class.where(key: 'title') + + puts "Found #{text_title_translations.count} title translations in text_translations table" + + if text_title_translations.empty? + puts 'No title translations found in text translations table. Migration not needed.' + next + end + + # Group by translatable_type for reporting + grouped_translations = text_title_translations.group_by(&:translatable_type) + grouped_translations.each do |type, translations| + puts " - #{type}: #{translations.count} translations" + end + + puts "\nStarting migration process..." + puts '-' * 40 + + migration_count = 0 + skipped_count = 0 + error_count = 0 + + # Collect records for bulk operations + records_to_create = [] + records_to_delete = [] + + text_title_translations.each_with_index do |text_translation, index| # rubocop:todo Metrics/BlockLength + begin + # Check if a string translation already exists for this record + existing_string = string_translation_class.find_by( + translatable_type: text_translation.translatable_type, + translatable_id: text_translation.translatable_id, + key: 'title', + locale: text_translation.locale + ) + + if existing_string.nil? + # Prepare record for bulk creation + records_to_create << { + translatable_type: text_translation.translatable_type, + translatable_id: text_translation.translatable_id, + key: 'title', + locale: text_translation.locale, + value: text_translation.value, + created_at: text_translation.created_at, + updated_at: text_translation.updated_at + } + + # rubocop:todo Layout/LineLength + puts "✓ Prepared for migration: #{text_translation.translatable_type} ##{text_translation.translatable_id} title (#{text_translation.locale}): '#{text_translation.value}'" + # rubocop:enable Layout/LineLength + migration_count += 1 + else + # rubocop:todo Layout/LineLength + puts "⚠ String translation already exists for #{text_translation.translatable_type} ##{text_translation.translatable_id} title (#{text_translation.locale}), skipping" + # rubocop:enable Layout/LineLength + skipped_count += 1 + end + + # Always prepare text translation for bulk deletion + records_to_delete << text_translation.id + rescue StandardError => e + # rubocop:todo Layout/LineLength + puts "✗ Error preparing #{text_translation.translatable_type} ##{text_translation.translatable_id}: #{e.message}" + # rubocop:enable Layout/LineLength + error_count += 1 + end + + # Progress indicator for large datasets + if ((index + 1) % 10).zero? || (index + 1) == text_title_translations.count + puts "Progress: #{index + 1}/#{text_title_translations.count} prepared" + end + end + + # Perform bulk operations + puts "\nPerforming bulk operations..." + puts '-' * 40 + + # Bulk create string translations + if records_to_create.any? + puts "Creating #{records_to_create.count} string translations in bulk..." + start_time = Time.current + begin + string_translation_class.insert_all(records_to_create) + elapsed = (Time.current - start_time).round(3) + puts "✓ Successfully created #{records_to_create.count} string translations in #{elapsed}s" + rescue StandardError => e + elapsed = (Time.current - start_time).round(3) + puts "✗ Error during bulk creation (#{elapsed}s): #{e.message}" + error_count += records_to_create.count + migration_count -= records_to_create.count + end + end + + # Bulk delete text translations + if records_to_delete.any? + puts "Deleting #{records_to_delete.count} text translations in bulk..." + start_time = Time.current + begin + deleted_count = text_translation_class.where(id: records_to_delete).delete_all + elapsed = (Time.current - start_time).round(3) + puts "✓ Successfully deleted #{deleted_count} text translations in #{elapsed}s" + rescue StandardError => e + elapsed = (Time.current - start_time).round(3) + puts "✗ Error during bulk deletion (#{elapsed}s): #{e.message}" + end + end + + puts "\n#{'=' * 80}" + puts 'Migration Summary:' + puts " ✓ Successfully migrated: #{migration_count}" + puts " ⚠ Skipped (already exist): #{skipped_count}" + puts " ✗ Errors encountered: #{error_count}" + puts " Total processed: #{text_title_translations.count}" + puts "\nMigration completed!" + end + + desc 'Clean up remaining title text translations after successful migration (DANGEROUS: removes data)' + task clean_up_title_translations: :environment do # rubocop:todo Metrics/BlockLength + puts 'Cleaning up remaining text translations for titles...' + puts '⚠️ WARNING: This will permanently delete records from the text_translations table!' + puts '=' * 80 + + text_translation_class = Mobility::Backends::ActiveRecord::KeyValue::TextTranslation + string_translation_class = Mobility::Backends::ActiveRecord::KeyValue::StringTranslation + + text_title_translations = text_translation_class.where(key: 'title') + + if text_title_translations.empty? + puts '✅ No text translations found to clean up!' + next + end + + puts "Found #{text_title_translations.count} remaining text translations:" + + cleanup_count = 0 + verification_failures = 0 + records_to_delete = [] + + text_title_translations.each do |text_translation| + string_exists = string_translation_class.exists?( + translatable_type: text_translation.translatable_type, + translatable_id: text_translation.translatable_id, + key: 'title', + locale: text_translation.locale + ) + + if string_exists + records_to_delete << text_translation.id + # rubocop:todo Layout/LineLength + puts "🗑️ Prepared for cleanup: #{text_translation.translatable_type} ##{text_translation.translatable_id} title (#{text_translation.locale})" + # rubocop:enable Layout/LineLength + cleanup_count += 1 + else + # rubocop:todo Layout/LineLength + puts "⚠️ String translation missing for #{text_translation.translatable_type} ##{text_translation.translatable_id} (#{text_translation.locale}) - skipping cleanup" + # rubocop:enable Layout/LineLength + verification_failures += 1 + end + end + + if records_to_delete.any? + puts "\nPerforming bulk cleanup of #{records_to_delete.count} records..." + start_time = Time.current + begin + deleted_count = text_translation_class.where(id: records_to_delete).delete_all + elapsed = (Time.current - start_time).round(3) + puts "✓ Successfully cleaned up #{deleted_count} text translations in #{elapsed}s" + rescue StandardError => e + elapsed = (Time.current - start_time).round(3) + puts "✗ Error during bulk cleanup (#{elapsed}s): #{e.message}" + cleanup_count = 0 + end + end + + puts "\n#{'=' * 80}" + puts 'Cleanup Summary:' + puts " 🗑️ Records cleaned up: #{cleanup_count}" + puts " ⚠️ Verification failures: #{verification_failures}" + puts " Total processed: #{text_title_translations.count}" + puts "\nCleanup completed!" + end + + desc 'Check status of title translations (dry run)' + task check_titles_status: :environment do # rubocop:todo Metrics/BlockLength + puts 'Checking status of title translations...' + puts '=' * 80 + + text_translation_class = Mobility::Backends::ActiveRecord::KeyValue::TextTranslation + string_translation_class = Mobility::Backends::ActiveRecord::KeyValue::StringTranslation + + text_title_translations = text_translation_class.where(key: 'title') + string_title_translations = string_translation_class.where(key: 'title') + + puts 'Current Translation Status:' + puts '-' * 40 + puts '📝 Text translations (should be 0 after migration):' + puts " Total: #{text_title_translations.count}" + + if text_title_translations.any? + grouped_text = text_title_translations.group_by(&:translatable_type) + grouped_text.each do |type, translations| + puts " - #{type}: #{translations.count}" + translations.first(3).each do |trans| + puts " • ID #{trans.translatable_id} (#{trans.locale}): '#{trans.value}'" + end + puts " ... and #{translations.count - 3} more" if translations.count > 3 + end + end + + puts "\n📄 String translations (target location):" + puts " Total: #{string_title_translations.count}" + + if string_title_translations.any? + grouped_string = string_title_translations.group_by(&:translatable_type) + grouped_string.each do |type, translations| + puts " - #{type}: #{translations.count}" + translations.first(3).each do |trans| + puts " • ID #{trans.translatable_id} (#{trans.locale}): '#{trans.value}'" + end + puts " ... and #{translations.count - 3} more" if translations.count > 3 + end + end + + puts "\n#{'=' * 80}" + if text_title_translations.empty? + puts '✅ Migration appears complete - no title translations found in text_translations' + else + puts "⚠️ Migration needed - #{text_title_translations.count} title translations found in text_translations" + puts ' Run: rails translations:mobility:migrate_titles_to_string' + end + end + end +end diff --git a/lib/tasks/search.rake b/lib/tasks/search.rake new file mode 100644 index 000000000..05d48a067 --- /dev/null +++ b/lib/tasks/search.rake @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +namespace :better_together do # rubocop:todo Metrics/BlockLength + namespace :search do # rubocop:todo Metrics/BlockLength + desc 'Reindex all searchable models in Elasticsearch' + task reindex_all: :environment do + puts 'Reindexing all searchable models...' + + # Reindex Pages (includes template blocks and rich text blocks) + puts 'Reindexing Pages...' + BetterTogether::Page.elastic_import(force: true) + puts "✓ Reindexed #{BetterTogether::Page.count} pages" + + # Add other searchable models here as needed + # BetterTogether::OtherModel.elastic_import(force: true) + + puts 'Reindexing complete!' + end + + desc 'Reindex Pages with their template blocks and rich text blocks' + task reindex_pages: :environment do + puts 'Reindexing Pages with template blocks and rich text blocks...' + BetterTogether::Page.elastic_import(force: true) + puts "✓ Reindexed #{BetterTogether::Page.count} pages" + end + + desc 'Refresh Elasticsearch indices' + task refresh: :environment do + puts 'Refreshing Elasticsearch indices...' + BetterTogether::Page.refresh_elastic_index! + puts '✓ Indices refreshed' + end + + desc 'Delete and recreate Elasticsearch indices' + task recreate_indices: :environment do + puts 'WARNING: This will delete all existing search indices and recreate them.' + puts 'Press Ctrl+C to cancel, or Enter to continue...' + $stdin.gets + + puts 'Deleting existing indices...' + BetterTogether::Page.delete_elastic_index! + puts '✓ Indices deleted' + + puts 'Creating new indices...' + BetterTogether::Page.create_elastic_index! + puts '✓ Indices created' + + puts 'Reindexing all data...' + BetterTogether::Page.elastic_import(force: true) + puts "✓ Reindexed #{BetterTogether::Page.count} pages" + + puts 'Index recreation complete!' + end + end +end diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 90d9fffcb..000000000 --- a/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: { - autoprefixer: {}, - }, -} diff --git a/spec/builders/better_together/documentation_builder_spec.rb b/spec/builders/better_together/documentation_builder_spec.rb new file mode 100644 index 000000000..cbc0772c0 --- /dev/null +++ b/spec/builders/better_together/documentation_builder_spec.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BetterTogether::DocumentationBuilder, type: :model do + describe '.build' do + let(:tmp_docs_root) { Pathname.new(Dir.mktmpdir('docs-nav')) } + + before do + File.write(tmp_docs_root.join('README.md'), '# Overview') + + developers_dir = tmp_docs_root.join('developers') + FileUtils.mkdir_p(developers_dir) + File.write(developers_dir.join('README.md'), '# Developers Guide') + File.write(developers_dir.join('api.md'), '# API') + + systems_dir = developers_dir.join('systems') + FileUtils.mkdir_p(systems_dir) + File.write(systems_dir.join('caching.md'), '# Caching') + + allow(described_class).to receive_messages(documentation_root: tmp_docs_root, documentation_url_prefix: '/docs') + end + + after do + FileUtils.remove_entry(tmp_docs_root) + end + + it 'creates a documentation navigation area with nested items' do + described_class.build + + area = BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation') + expect(area).to be_present + expect(area.navigation_items.top_level.count).to eq(2) + + root_file_item = area.navigation_items.find { |item| item.linkable&.slug == 'docs/readme' } + expect(root_file_item).to be_present + expect(root_file_item.title).to eq('Overview') + expect(root_file_item.linkable).to be_a(BetterTogether::Page) + markdown_block = root_file_item.linkable.page_blocks.first.block + expect(markdown_block).to be_a(BetterTogether::Content::Markdown) + expect(markdown_block.markdown_file_path).to eq(tmp_docs_root.join('README.md').to_s) + + developers_item = area.navigation_items.find do |item| + item.linkable&.slug == 'docs/developers/readme' && item.item_type == 'dropdown' + end + expect(developers_item).to be_present + expect(developers_item.item_type).to eq('dropdown') + expect(developers_item.linkable&.slug).to eq('docs/developers/readme') + expect(developers_item.children.count).to eq(3) # README, api, systems directory + + systems_dropdown = developers_item.children.find { |child| child.title == 'Systems' } + expect(systems_dropdown.item_type).to eq('dropdown') + expect(systems_dropdown.children.count).to eq(1) + systems_page = systems_dropdown.children.first.linkable + expect(systems_page.slug).to eq('docs/developers/systems/caching') + systems_markdown = systems_page.page_blocks.first.block + expect(systems_markdown.markdown_file_path).to eq(tmp_docs_root.join('developers/systems/caching.md').to_s) + end + + it 'assigns the documentation navigation area as sidebar_nav for all documentation pages' do + described_class.build + + area = BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation') + expect(area).to be_present + + # Check root file page + root_page = BetterTogether::Page.i18n.find_by(slug: 'docs/readme') + expect(root_page).to be_present + expect(root_page.sidebar_nav).to eq(area) + + # Check developers guide page + developers_page = BetterTogether::Page.i18n.find_by(slug: 'docs/developers/readme') + expect(developers_page).to be_present + expect(developers_page.sidebar_nav).to eq(area) + + # Check API page + api_page = BetterTogether::Page.i18n.find_by(slug: 'docs/developers/api') + expect(api_page).to be_present + expect(api_page.sidebar_nav).to eq(area) + + # Check nested systems/caching page + caching_page = BetterTogether::Page.i18n.find_by(slug: 'docs/developers/systems/caching') + expect(caching_page).to be_present + expect(caching_page.sidebar_nav).to eq(area) + end + + it 'creates a protected navigation area' do + described_class.build + + area = BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation') + expect(area).to be_present + expect(area.protected).to be true + end + + it 'creates a visible navigation area' do + described_class.build + + area = BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation') + expect(area).to be_present + expect(area.visible).to be true + end + + it 'sets the area name to Documentation' do + described_class.build + + area = BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation') + expect(area).to be_present + expect(area.name).to eq('Documentation') + end + + it 'creates pages with nested slug structure' do + described_class.build + + expect(BetterTogether::Page.i18n.find_by(slug: 'docs/readme')).to be_present + expect(BetterTogether::Page.i18n.find_by(slug: 'docs/developers/readme')).to be_present + expect(BetterTogether::Page.i18n.find_by(slug: 'docs/developers/api')).to be_present + expect(BetterTogether::Page.i18n.find_by(slug: 'docs/developers/systems/caching')).to be_present + end + + it 'creates protected pages' do + described_class.build + + readme_page = BetterTogether::Page.i18n.find_by(slug: 'docs/readme') + api_page = BetterTogether::Page.i18n.find_by(slug: 'docs/developers/api') + caching_page = BetterTogether::Page.i18n.find_by(slug: 'docs/developers/systems/caching') + + expect(readme_page.protected).to be true + expect(api_page.protected).to be true + expect(caching_page.protected).to be true + end + + it 'creates public pages' do + described_class.build + + readme_page = BetterTogether::Page.i18n.find_by(slug: 'docs/readme') + api_page = BetterTogether::Page.i18n.find_by(slug: 'docs/developers/api') + caching_page = BetterTogether::Page.i18n.find_by(slug: 'docs/developers/systems/caching') + + expect(readme_page.privacy).to eq('public') + expect(api_page.privacy).to eq('public') + expect(caching_page.privacy).to eq('public') + end + + context 'when documentation area already exists' do + before do + # Delete any existing documentation navigation items first (FK constraint) + doc_area = BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation') + if doc_area + doc_area.navigation_items.where.not(parent_id: nil).delete_all + doc_area.navigation_items.where(parent_id: nil).delete_all + doc_area.delete + end + end + + let!(:existing_area) do + BetterTogether::NavigationArea.create!( + name: 'Old Documentation', + slug: 'documentation', + visible: false, + protected: false + ) + end + + let!(:existing_item) do + existing_area.navigation_items.create!( + title: 'Old Item', + slug: 'old-item', + item_type: 'link', + position: 0, + visible: true, + protected: false + ) + end + + it 'updates the existing area' do + described_class.build + + area = BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation') + expect(area.id).to eq(existing_area.id) + expect(area.name).to eq('Documentation') + expect(area.visible).to be true + expect(area.protected).to be true + end + + it 'deletes old navigation items' do + described_class.build + + expect(BetterTogether::NavigationItem.find_by(id: existing_item.id)).to be_nil + end + + it 'creates new navigation items' do + described_class.build + + area = BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation') + expect(area.navigation_items.count).to be > 0 + expect(area.navigation_items.i18n.where(title: 'Old Item').count).to eq(0) + end + end + + context 'when docs directory is empty' do + let(:empty_docs_root) { Pathname.new(Dir.mktmpdir('empty-docs')) } + + before do + allow(described_class).to receive(:documentation_root).and_return(empty_docs_root) + end + + after do + FileUtils.remove_entry(empty_docs_root) + end + + it 'does not create a navigation area' do + initial_count = BetterTogether::NavigationArea.count + + described_class.build + + expect(BetterTogether::NavigationArea.count).to eq(initial_count) + end + end + + context 'when docs directory does not exist' do + before do + allow(described_class).to receive(:documentation_root).and_return(Pathname.new('/nonexistent/path')) + end + + it 'does not create a navigation area' do + initial_count = BetterTogether::NavigationArea.count + + described_class.build + + expect(BetterTogether::NavigationArea.count).to eq(initial_count) + end + end + end +end diff --git a/spec/builders/better_together/navigation_builder_spec.rb b/spec/builders/better_together/navigation_builder_spec.rb new file mode 100644 index 000000000..9eb49e027 --- /dev/null +++ b/spec/builders/better_together/navigation_builder_spec.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BetterTogether::NavigationBuilder, type: :model do + describe '.reset_navigation_areas' do + it 'deletes all navigation items' do + # Create some test navigation areas and items first + area = create(:better_together_navigation_area) + create(:better_together_navigation_item, navigation_area: area) + + described_class.reset_navigation_areas + + # After reset, should have new items from seed_data, but old ones should be gone + expect(BetterTogether::NavigationItem.where(navigation_area: area).count).to eq(0) + end + + it 'deletes all navigation areas' do + # Create a test navigation area + create(:better_together_navigation_area, name: 'Test Area', identifier: 'test-area') + + described_class.reset_navigation_areas + + # Should have exactly 4 areas (the seeded ones - documentation disabled), regardless of what was there before + expect(BetterTogether::NavigationArea.count).to eq(4) + # The test area should be gone + expect(BetterTogether::NavigationArea.find_by(identifier: 'test-area')).to be_nil + end + + it 'rebuilds all navigation areas' do + described_class.reset_navigation_areas + + expect(BetterTogether::NavigationArea.count).to eq(4) + + # Use identifier instead of slug + area_identifiers = BetterTogether::NavigationArea.pluck(:identifier) + expect(area_identifiers).to contain_exactly( + 'platform-header', + 'platform-host', + 'better-together', + 'platform-footer' + # 'documentation' - disabled for now + ) + end + + it 'recreates navigation items' do + described_class.reset_navigation_areas + + expect(BetterTogether::NavigationItem.count).to be > 0 + end + + it 'creates protected navigation areas' do + described_class.reset_navigation_areas + + BetterTogether::NavigationArea.find_each do |area| + expect(area.protected).to be true + end + end + + it 'creates protected navigation items' do + described_class.reset_navigation_areas + + # Most seeded items should be protected (but not all, some may be unprotected) + protected_items = BetterTogether::NavigationItem.where(protected: true) + expect(protected_items.count).to be > 0 + end + end + + describe '.reset_navigation_area' do + context 'with valid navigation area identifier' do + it 'works for platform-header' do + described_class.reset_navigation_area('platform-header') + + header = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-header') + expect(header).to be_present + expect(header.navigation_items.count).to be > 0 + end + + it 'works for platform-host' do + described_class.reset_navigation_area('platform-host') + + host = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-host') + expect(host).to be_present + expect(host.navigation_items.count).to be > 0 + end + + it 'works for better-together' do + described_class.reset_navigation_area('better-together') + + bt = BetterTogether::NavigationArea.i18n.find_by(slug: 'better-together') + expect(bt).to be_present + expect(bt.navigation_items.count).to be > 0 + end + + it 'works for platform-footer' do + described_class.reset_navigation_area('platform-footer') + + footer = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-footer') + expect(footer).to be_present + expect(footer.navigation_items.count).to be > 0 + end + + it 'works for documentation' do + skip 'Documentation builder is disabled from auto-seeding (WIP)' + + # Documentation builder available but not auto-seeded + described_class.reset_navigation_area('documentation') + + docs_area = BetterTogether::NavigationArea.i18n.find_by(slug: 'documentation') + expect(docs_area).to be_present + expect(docs_area.navigation_items.count).to be > 0 + end + + it 'deletes old navigation items for that area' do + # Create the footer area first + described_class.reset_navigation_area('platform-footer') + + # Reset it again - the area gets deleted and recreated + described_class.reset_navigation_area('platform-footer') + + # Should have the recreated footer with navigation items + footer_area = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-footer') + expect(footer_area).to be_present + expect(footer_area.navigation_items.count).to be > 0 + end + + it 'creates new navigation items for that area' do + described_class.reset_navigation_area('platform-footer') + + footer = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-footer') + expect(footer.navigation_items.count).to be > 0 + end + + it 'resets the specified area' do + # Setup initial state + described_class.reset_navigation_areas + + footer_area = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-footer') + initial_name = footer_area.name + + # Reset just the footer + described_class.reset_navigation_area('platform-footer') + + footer_area = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-footer') + expect(footer_area.name).to eq(initial_name) + end + + it 'preserves other navigation areas' do + # Setup initial state + described_class.reset_navigation_areas + + initial_identifiers = BetterTogether::NavigationArea.pluck(:identifier).sort + + # Reset just the footer + described_class.reset_navigation_area('platform-footer') + + # Should still have all 5 areas + final_identifiers = BetterTogether::NavigationArea.pluck(:identifier).sort + expect(final_identifiers).to eq(initial_identifiers) + end + end + + context 'with invalid navigation area identifier' do + it 'does not raise an error' do + expect do + described_class.reset_navigation_area('invalid-slug') + end.not_to raise_error + end + + it 'does not affect existing areas' do + described_class.reset_navigation_areas + initial_count = BetterTogether::NavigationArea.count + + described_class.reset_navigation_area('invalid-slug') + + expect(BetterTogether::NavigationArea.count).to eq(initial_count) + end + end + + context 'with nil identifier' do + it 'does not raise an error' do + expect do + described_class.reset_navigation_area(nil) + end.not_to raise_error + end + end + end + + describe 'navigation item relationships' do + before do + described_class.reset_navigation_areas + end + + it 'creates parent-child relationships for contributor agreements' do + footer = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-footer') + + # Find the "Contributor Agreements" parent item + contributor_agreements_item = footer.navigation_items.find_by(item_type: 'dropdown') + + expect(contributor_agreements_item).to be_present + expect(contributor_agreements_item.children.count).to eq(2) + end + + it 'includes both contributor agreement pages as children' do + footer = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-footer') + contributor_agreements_item = footer.navigation_items.find_by(item_type: 'dropdown') + + child_slugs = contributor_agreements_item.children.map { |child| child.linkable&.slug }.compact + # Check that both agreement types are present (slug may have FriendlyId suffix after reset) + expect(child_slugs.any? { |slug| slug.start_with?('code-contributor-agreement') }).to be true + expect(child_slugs.any? { |slug| slug.start_with?('content-contributor-agreement') }).to be true + end + + it 'preserves nested structure after reset' do + # Get initial structure + footer = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-footer') + initial_parent_count = footer.navigation_items.where(parent_id: nil).count + initial_child_count = footer.navigation_items.where.not(parent_id: nil).count + + # Reset the footer + described_class.reset_navigation_area('platform-footer') + + # Check structure is preserved + footer = BetterTogether::NavigationArea.i18n.find_by(slug: 'platform-footer') + expect(footer.navigation_items.where(parent_id: nil).count).to eq(initial_parent_count) + expect(footer.navigation_items.where.not(parent_id: nil).count).to eq(initial_child_count) + end + end +end diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index 1d04cb2cb..d3522e21c 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -27,6 +27,9 @@ class Application < Rails::Application # Use the latest cache format and remove deprecated Active Storage setting config.active_support.cache_format_version = 7.1 + # Opt in to Rails 8.1 behavior: preserve timezone when converting to Time + config.active_support.to_time_preserves_timezone = :zone + 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..c9a79d55f 100644 --- a/spec/dummy/config/i18n-tasks.yml +++ b/spec/dummy/config/i18n-tasks.yml @@ -4,6 +4,8 @@ locales: - en - es - fr + - uk + # Paths to scan for translations search: # Paths in the dummy app @@ -23,3 +25,21 @@ exclude: - 'spec/**' - 'tmp/**' - 'log/**' + +# Ignore keys (both missing and unused checks) +# Keys that are provided by external gems like i18n-timezones +ignore_missing: + - '{timezones,timezones.*}' + +ignore_unused: + - '{timezones,timezones.*}' + +# Alternative: use eq_base to ignore keys that are the same as base locale +# This tells i18n-tasks that these keys are intentionally the same in all locales +ignore_eq_base: + es: + - '{timezones,timezones.*}' + fr: + - '{timezones,timezones.*}' + uk: + - '{timezones,timezones.*}' diff --git a/spec/dummy/config/initializers/dartsass.rb b/spec/dummy/config/initializers/dartsass.rb new file mode 100644 index 000000000..35fc47e8e --- /dev/null +++ b/spec/dummy/config/initializers/dartsass.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Configure Dart Sass to silence Bootstrap deprecation warnings +# These warnings are from Bootstrap's internal implementation and will be +# fixed when Bootstrap releases a Dart Sass 3.0 compatible version. +# +# Warnings silenced: +# - import: Bootstrap still uses @import (will be removed in Bootstrap 6) +# - global-builtin: Bootstrap uses global functions (type-of, unit, map-has-key) +# - color-functions: Bootstrap uses deprecated color functions (red, green, blue) + +Rails.application.config.sass.quiet_deps = true +Rails.application.config.sass.silence_deprecations = %w[ + import + global-builtin + color-functions +] diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index 87b23511a..9678fa8e4 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_11_20_160356) do +ActiveRecord::Schema[7.2].define(version: 2025_11_25_142646) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -1007,6 +1007,8 @@ t.string "layout" t.string "template" t.uuid "sidebar_nav_id" + t.uuid "creator_id" + t.index ["creator_id"], name: "index_better_together_pages_on_creator_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" @@ -1535,6 +1537,7 @@ 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_pages", "better_together_people", column: "creator_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" diff --git a/spec/factories/better_together/calendar_entries.rb b/spec/factories/better_together/calendar_entries.rb index 50eb04517..78e21a2e4 100644 --- a/spec/factories/better_together/calendar_entries.rb +++ b/spec/factories/better_together/calendar_entries.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true FactoryBot.define do - factory :calendar_entry do # rubocop:todo Lint/EmptyBlock + factory :calendar_entry, class: 'BetterTogether::CalendarEntry', aliases: [:better_together_calendar_entry] do + association :calendar, factory: :calendar + association :event, factory: :event + starts_at { 1.week.from_now } + ends_at { 1.week.from_now + 2.hours } + duration_minutes { 120 } end end diff --git a/spec/factories/better_together/call_for_interests.rb b/spec/factories/better_together/call_for_interests.rb index ba8c6b9f1..b0d2c01cf 100644 --- a/spec/factories/better_together/call_for_interests.rb +++ b/spec/factories/better_together/call_for_interests.rb @@ -1,6 +1,36 @@ # frozen_string_literal: true FactoryBot.define do - factory :call_for_interest do # rubocop:todo Lint/EmptyBlock + factory :call_for_interest, class: 'BetterTogether::CallForInterest' do + sequence(:identifier) { |n| "call_for_interest_#{n}" } + name { Faker::Company.catch_phrase } + description { Faker::Lorem.paragraph(sentence_count: 3) } + privacy { 'public' } + association :creator, factory: :person + starts_at { 1.week.from_now } + ends_at { 2.weeks.from_now } + + trait :with_event do + association :interestable, factory: :event + end + + trait :draft do + starts_at { nil } + ends_at { nil } + end + + trait :past do + starts_at { 2.weeks.ago } + ends_at { 1.week.ago } + end + + trait :upcoming do + starts_at { 1.week.from_now } + ends_at { 2.weeks.from_now } + end + + trait :private do + privacy { 'private' } + end end end diff --git a/spec/factories/better_together/categories.rb b/spec/factories/better_together/categories.rb index da38d8342..7b8e820e1 100644 --- a/spec/factories/better_together/categories.rb +++ b/spec/factories/better_together/categories.rb @@ -1,6 +1,15 @@ # frozen_string_literal: true FactoryBot.define do - factory :category do # rubocop:todo Lint/EmptyBlock + factory :category, class: 'BetterTogether::Category' do + sequence(:identifier) { |n| "category_#{n}" } + name { Faker::Lorem.unique.words(number: 2).join(' ').titleize } + description { Faker::Lorem.paragraph } + type { 'BetterTogether::Category' } + icon { 'fas fa-folder' } + + trait :with_custom_icon do + icon { 'fas fa-star' } + end end end diff --git a/spec/factories/better_together/categorizations.rb b/spec/factories/better_together/categorizations.rb index 9c1e796a7..b90c75b09 100644 --- a/spec/factories/better_together/categorizations.rb +++ b/spec/factories/better_together/categorizations.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true FactoryBot.define do - factory :categorization do # rubocop:todo Lint/EmptyBlock + factory :categorization, class: 'BetterTogether::Categorization' do + association :category, factory: :event_category + association :categorizable, factory: :event end end diff --git a/spec/factories/better_together/contact_details.rb b/spec/factories/better_together/contact_details.rb index 4554e7780..27de89360 100644 --- a/spec/factories/better_together/contact_details.rb +++ b/spec/factories/better_together/contact_details.rb @@ -2,6 +2,6 @@ FactoryBot.define do factory :better_together_contact_detail, class: BetterTogether::ContactDetail, aliases: [:contact_detail] do - # contactable association should be set by the caller + association :contactable, factory: :person end end diff --git a/spec/factories/better_together/content/heroes.rb b/spec/factories/better_together/content/heroes.rb new file mode 100644 index 000000000..c4e70e714 --- /dev/null +++ b/spec/factories/better_together/content/heroes.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :better_together_content_hero, class: 'BetterTogether::Content::Hero' do + transient do + title { Faker::Lorem.sentence } + subtitle { Faker::Lorem.paragraph } + end + + heading { title } + content { subtitle } + cta_text { Faker::Lorem.words(number: 2).join(' ') } + cta_url { Faker::Internet.url } + cta_button_style { 'btn-primary' } + css_classes { 'text-white' } + container_class { '' } + overlay_color { '#000' } + overlay_opacity { 0.25 } + + trait :with_background_image do + after(:create) do |hero| + hero.background_image_file.attach( + io: StringIO.new('fake image content'), + filename: 'hero_background.jpg', + content_type: 'image/jpeg' + ) + end + end + + trait :primary_button do + cta_button_style { 'btn-primary' } + end + + trait :secondary_button do + cta_button_style { 'btn-secondary' } + end + + trait :dark_overlay do + overlay_color { '#000' } + overlay_opacity { 0.5 } + end + + trait :light_overlay do + overlay_color { '#fff' } + overlay_opacity { 0.3 } + end + end + + factory :content_hero, parent: :better_together_content_hero +end diff --git a/spec/factories/better_together/content/htmls.rb b/spec/factories/better_together/content/htmls.rb new file mode 100644 index 000000000..a64a29c39 --- /dev/null +++ b/spec/factories/better_together/content/htmls.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :better_together_content_html, class: 'BetterTogether::Content::Html' do + content { "
    #{Faker::Lorem.paragraph}
    " } + + trait :with_heading do + content { "

    #{Faker::Lorem.sentence}

    #{Faker::Lorem.paragraph}

    " } + end + + trait :with_link do + content { "#{Faker::Lorem.words(number: 2).join(' ')}" } + end + + trait :with_list do + content do + <<-HTML + + HTML + end + end + end + + factory :content_html, parent: :better_together_content_html +end diff --git a/spec/factories/better_together/content/images.rb b/spec/factories/better_together/content/images.rb new file mode 100644 index 000000000..48e497997 --- /dev/null +++ b/spec/factories/better_together/content/images.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :better_together_content_image, class: 'BetterTogether::Content::Image' do + association :creator, factory: :better_together_person + privacy { 'public' } + identifier { "image-block-#{SecureRandom.hex(4)}" } + attribution { Faker::Name.name } + alt_text { Faker::Lorem.sentence(word_count: 3) } + caption { Faker::Lorem.sentence } + attribution_url { Faker::Internet.url } + + after(:build) do |image| + next if image.media.attached? + + # Create a minimal valid 1x1 PNG image in memory + # rubocop:todo Layout/LineLength + png_data = "\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\b\x06\x00\x00\x00\x1F\x15\xC4\x89\x00\x00\x00\nIDATx\x9Cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xB4\x00\x00\x00\x00IEND\xAEB`\x82" + # rubocop:enable Layout/LineLength + image.media.attach( + io: StringIO.new(png_data), + filename: 'test-image.png', + content_type: 'image/png' + ) + end + + trait :with_jpg do + after(:build) do |image| + image.media.purge if image.media.attached? + image.media.attach( + io: StringIO.new("fake jpg content #{SecureRandom.hex}"), + filename: 'test-image.jpg', + content_type: 'image/jpeg' + ) + end + end + + trait :with_gif do + after(:build) do |image| + image.media.purge if image.media.attached? + image.media.attach( + io: StringIO.new("fake gif content #{SecureRandom.hex}"), + filename: 'animated.gif', + content_type: 'image/gif' + ) + end + end + + trait :with_webp do + after(:build) do |image| + image.media.purge if image.media.attached? + image.media.attach( + io: StringIO.new("fake webp content #{SecureRandom.hex}"), + filename: 'modern.webp', + content_type: 'image/webp' + ) + end + end + + trait :without_attribution do + attribution { nil } + attribution_url { '' } + end + + trait :with_long_caption do + caption { Faker::Lorem.paragraph(sentence_count: 3) } + end + end +end diff --git a/spec/factories/better_together/content/markdowns.rb b/spec/factories/better_together/content/markdowns.rb new file mode 100644 index 000000000..55de2f4e6 --- /dev/null +++ b/spec/factories/better_together/content/markdowns.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :content_markdown, class: 'BetterTogether::Content::Markdown', aliases: [:markdown_block] do + markdown_source { Faker::Markdown.random } + + trait :with_source do + markdown_source do + <<~MD + # #{Faker::Lorem.sentence} + + #{Faker::Lorem.paragraph} + + ## #{Faker::Lorem.words(number: 3).join(' ').capitalize} + + #{Faker::Lorem.paragraphs(number: 2).join("\n\n")} + + - #{Faker::Lorem.sentence} + - #{Faker::Lorem.sentence} + - #{Faker::Lorem.sentence} + + **#{Faker::Lorem.sentence}** + + *#{Faker::Lorem.sentence}* + MD + end + markdown_file_path { nil } + end + + trait :with_file do + markdown_source { nil } + markdown_file_path do + file_path = Rails.root.join("spec/fixtures/files/factory_markdown_#{SecureRandom.hex(4)}.md") + FileUtils.mkdir_p(File.dirname(file_path)) + File.write(file_path, Faker::Markdown.random) + file_path.to_s + end + + after(:create) do |markdown| + # Clean up the file after tests complete + FileUtils.rm_f(markdown.markdown_file_path) if markdown.markdown_file_path.present? + end + end + + trait :simple do + markdown_source { "# #{Faker::Lorem.sentence}\n\n#{Faker::Lorem.paragraph}" } + markdown_file_path { nil } + end + + trait :with_table do + markdown_source do + <<~MD + # Data Table + + | Column 1 | Column 2 | Column 3 | + |----------|----------|----------| + | Data 1 | Data 2 | Data 3 | + | Value A | Value B | Value C | + MD + end + markdown_file_path { nil } + end + + trait :with_code do + markdown_source do + <<~MD + # Code Example + + Here's some Ruby code: + + ```ruby + def hello(name) + puts "Hello, \#{name}!" + end + ``` + MD + end + markdown_file_path { nil } + end + + trait :with_links do + markdown_source do + <<~MD + # Links + + Check out [this external link](https://example.com) and [this internal link](/about). + MD + end + markdown_file_path { nil } + end + + trait :empty do + markdown_source { '' } + markdown_file_path { nil } + end + end +end diff --git a/spec/factories/better_together/content/rich_texts.rb b/spec/factories/better_together/content/rich_texts.rb index 6e4237540..a1aa5e4ba 100644 --- a/spec/factories/better_together/content/rich_texts.rb +++ b/spec/factories/better_together/content/rich_texts.rb @@ -1,6 +1,41 @@ # frozen_string_literal: true FactoryBot.define do + factory :better_together_content_rich_text, class: 'BetterTogether::Content::RichText' do + association :creator, factory: :better_together_person + privacy { 'public' } + identifier { "rich-text-block-#{SecureRandom.hex(4)}" } + + transient do + content_html { "

    #{Faker::Lorem.paragraph}

    " } + end + + after(:build) do |block, evaluator| + block.content = evaluator.content_html if evaluator.content_html.present? + end + + trait :with_heading do + content_html { "

    #{Faker::Lorem.sentence}

    #{Faker::Lorem.paragraph}

    " } + end + + trait :with_link do + content_html { "

    Visit our website for more info.

    " } + end + + trait :with_list do + content_html do + <<-HTML + + HTML + end + end + end + + # Legacy factory for ActionText::RichText records (kept for backward compatibility) factory :content_rich_text, class: 'ActionText::RichText' do association :record, factory: :platform name { 'body' } diff --git a/spec/factories/better_together/event_categories.rb b/spec/factories/better_together/event_categories.rb index 4957edc06..d8e399aeb 100644 --- a/spec/factories/better_together/event_categories.rb +++ b/spec/factories/better_together/event_categories.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true FactoryBot.define do - factory :event_category do # rubocop:todo Lint/EmptyBlock + factory :event_category, class: 'BetterTogether::EventCategory' do + sequence(:identifier) { |n| "event_category_#{n}" } + name { Faker::Lorem.unique.words(number: 2).join(' ').titleize } + description { Faker::Lorem.paragraph } + type { 'BetterTogether::EventCategory' } end end diff --git a/spec/factories/better_together/geography/continents.rb b/spec/factories/better_together/geography/continents.rb index 1016c0d0f..90e6e57e6 100644 --- a/spec/factories/better_together/geography/continents.rb +++ b/spec/factories/better_together/geography/continents.rb @@ -1,8 +1,27 @@ # frozen_string_literal: true FactoryBot.define do - factory :geography_continent, class: '::BetterTogether::Geography::Continent', aliases: %i[continent] do - name { Faker::Name.name } - description { Faker::Lorem.paragraphs(number: 3) } + factory :geography_continent, class: '::BetterTogether::Geography::Continent', + aliases: %i[continent better_together_geography_continent] do + transient do + sequence(:continent_number) { |n| n } + end + + name { "Continent #{continent_number}" } + description { Faker::Lorem.paragraphs(number: 3).join("\n\n") } + sequence(:identifier) { |n| "continent-#{n}" } + protected { false } + + association :community, factory: :better_together_community + + trait :protected do + protected { true } + end + + trait :with_countries do + after(:create) do |continent| + create_list(:geography_country, 2, continents: [continent]) + end + end end end diff --git a/spec/factories/better_together/geography/countries.rb b/spec/factories/better_together/geography/countries.rb index c01c9751f..7e949c148 100644 --- a/spec/factories/better_together/geography/countries.rb +++ b/spec/factories/better_together/geography/countries.rb @@ -1,10 +1,34 @@ # frozen_string_literal: true FactoryBot.define do - factory :geography_country, class: '::BetterTogether::Geography::Country', aliases: %i[country] do - name { Faker::Name.name } - description { Faker::Lorem.paragraphs(number: 3) } + factory :geography_country, class: '::BetterTogether::Geography::Country', + aliases: %i[country better_together_geography_country] do + transient do + sequence(:country_number) { |n| n } + end - iso_code { Faker::String.random(length: 2).to_s } + name { "Country #{country_number}" } + description { Faker::Lorem.paragraphs(number: 3).join("\n\n") } + sequence(:identifier) { |n| "country-#{n}" } + iso_code { Faker::Address.country_code } + protected { false } + + association :community, factory: :better_together_community + + trait :protected do + protected { true } + end + + trait :with_continents do + after(:create) do |country| + create_list(:geography_continent, 2, countries: [country]) + end + end + + trait :with_states do + after(:create) do |country| + create_list(:geography_state, 3, country:) + end + end end end diff --git a/spec/factories/better_together/geography/maps.rb b/spec/factories/better_together/geography/maps.rb index e45905a35..1af9c39bf 100644 --- a/spec/factories/better_together/geography/maps.rb +++ b/spec/factories/better_together/geography/maps.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true FactoryBot.define do - factory :geography_map, class: 'Geography::Map' do - name { 'test' } + factory :geography_map, class: 'BetterTogether::Geography::Map', + aliases: %i[map better_together_geography_map] do + transient do + sequence(:map_number) { |n| n } + end + + title { "Map #{map_number}" } + description { Faker::Lorem.paragraphs(number: 2).join("\n\n") } + sequence(:identifier) { |n| "map-#{n}" } + zoom { 10 } + privacy { 'public' } + protected { false } + + association :creator, factory: :better_together_person + + after(:build) do |map| + factory = RGeo::Geographic.spherical_factory(srid: 4326) + map.center ||= factory.point(-57.9474, 48.9517) # Corner Brook, NL + end + + trait :protected do + protected { true } + end + + trait :private do + privacy { 'private' } + end + + trait :with_mappable do + association :mappable, factory: :better_together_community + end end end diff --git a/spec/factories/better_together/geography/regions.rb b/spec/factories/better_together/geography/regions.rb index 68d52fa0e..bff286a92 100644 --- a/spec/factories/better_together/geography/regions.rb +++ b/spec/factories/better_together/geography/regions.rb @@ -1,8 +1,37 @@ # frozen_string_literal: true FactoryBot.define do - factory :geography_region, class: '::BetterTogether::Geography::Region', aliases: %i[region] do - name { Faker::Name.name } - description { Faker::Lorem.paragraphs(number: 3) } + factory :geography_region, class: '::BetterTogether::Geography::Region', + aliases: %i[region better_together_geography_region] do + transient do + sequence(:region_number) { |n| n } + end + + name { "Region #{region_number}" } + description { Faker::Lorem.paragraphs(number: 3).join("\n\n") } + sequence(:identifier) { |n| "region-#{n}" } + protected { false } + + association :community, factory: :better_together_community + association :country, factory: :geography_country + association :state, factory: :geography_state + + trait :protected do + protected { true } + end + + trait :without_country do + country { nil } + end + + trait :without_state do + state { nil } + end + + trait :with_settlements do + after(:create) do |region| + create_list(:geography_settlement, 2, regions: [region]) + end + end end end diff --git a/spec/factories/better_together/geography/settlements.rb b/spec/factories/better_together/geography/settlements.rb index f366b7eec..bfd086266 100644 --- a/spec/factories/better_together/geography/settlements.rb +++ b/spec/factories/better_together/geography/settlements.rb @@ -1,8 +1,37 @@ # frozen_string_literal: true FactoryBot.define do - factory :geography_settlement, class: '::BetterTogether::Geography::Settlement', aliases: %i[settlement] do - name { Faker::Name.name } - description { Faker::Lorem.paragraphs(number: 3) } + factory :geography_settlement, class: '::BetterTogether::Geography::Settlement', + aliases: %i[settlement better_together_geography_settlement] do + transient do + sequence(:settlement_number) { |n| n } + end + + name { "Settlement #{settlement_number}" } + description { Faker::Lorem.paragraphs(number: 3).join("\n\n") } + sequence(:identifier) { |n| "settlement-#{n}" } + protected { false } + + association :community, factory: :better_together_community + association :country, factory: :geography_country + association :state, factory: :geography_state + + trait :protected do + protected { true } + end + + trait :without_country do + country { nil } + end + + trait :without_state do + state { nil } + end + + trait :with_regions do + after(:create) do |settlement| + create_list(:geography_region, 2, settlements: [settlement]) + end + end end end diff --git a/spec/factories/better_together/geography/states.rb b/spec/factories/better_together/geography/states.rb index 7788d2d31..9cda65707 100644 --- a/spec/factories/better_together/geography/states.rb +++ b/spec/factories/better_together/geography/states.rb @@ -1,10 +1,35 @@ # frozen_string_literal: true FactoryBot.define do - factory :geography_state, class: '::BetterTogether::Geography::State', aliases: %i[state] do - name { Faker::Name.name } - description { Faker::Lorem.paragraphs(number: 3) } + factory :geography_state, class: '::BetterTogether::Geography::State', + aliases: %i[state better_together_geography_state] do + transient do + sequence(:state_number) { |n| n } + end - iso_code { "#{Faker::String.random(length: 2)}-#{Faker::String.random(length: 2)}" } + name { "State #{state_number}" } + description { Faker::Lorem.paragraphs(number: 3).join("\n\n") } + sequence(:identifier) { |n| "state-#{n}" } + iso_code { "#{Faker::Address.country_code}-#{Faker::Address.state_abbr}" } + protected { false } + + association :community, factory: :better_together_community + association :country, factory: :geography_country + + trait :protected do + protected { true } + end + + trait :with_regions do + after(:create) do |state| + create_list(:geography_region, 2, state:) + end + end + + trait :with_settlements do + after(:create) do |state| + create_list(:geography_settlement, 3, state:) + end + end end end diff --git a/spec/factories/better_together/jwt_denylists.rb b/spec/factories/better_together/jwt_denylists.rb index cb60a239b..be77fd3fc 100644 --- a/spec/factories/better_together/jwt_denylists.rb +++ b/spec/factories/better_together/jwt_denylists.rb @@ -1,8 +1,24 @@ # frozen_string_literal: true FactoryBot.define do - factory :jwt_denylist do - jti { 'MyString' } - exp { '2021-01-03 20:16:42' } + factory :jwt_denylist, class: 'BetterTogether::JwtDenylist' do + jti { SecureRandom.uuid } + exp { 1.hour.from_now } + + trait :expired do + exp { 1.hour.ago } + end + + trait :recently_expired do + exp { 5.minutes.ago } + end + + trait :expires_soon do + exp { 5.minutes.from_now } + end + + trait :long_lived do + exp { 1.week.from_now } + end end end diff --git a/spec/factories/better_together/metrics/downloads.rb b/spec/factories/better_together/metrics/downloads.rb index 2cd8a4e49..6f00fbcaf 100644 --- a/spec/factories/better_together/metrics/downloads.rb +++ b/spec/factories/better_together/metrics/downloads.rb @@ -1,6 +1,15 @@ # frozen_string_literal: true FactoryBot.define do - factory :metrics_download, class: 'Metrics::Download' do # rubocop:todo Lint/EmptyBlock + factory :metrics_download, class: 'BetterTogether::Metrics::Download', aliases: [:download] do + file_name { 'document.pdf' } + file_type { 'application/pdf' } + file_size { 1024 } + downloaded_at { Time.current } + locale { 'en' } + + trait :with_community do + association :downloadable, factory: :community + end end end diff --git a/spec/factories/better_together/metrics/link_click_reports.rb b/spec/factories/better_together/metrics/link_click_reports.rb index 0a7467e60..40dce6546 100644 --- a/spec/factories/better_together/metrics/link_click_reports.rb +++ b/spec/factories/better_together/metrics/link_click_reports.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true FactoryBot.define do - factory :metrics_link_click_report, class: 'Metrics::LinkClickReport' do # rubocop:todo Lint/EmptyBlock + factory :metrics_link_click_report, class: 'BetterTogether::Metrics::LinkClickReport', aliases: [:link_click_report] do + file_format { 'csv' } + filters { {} } end end diff --git a/spec/factories/better_together/metrics/link_clicks.rb b/spec/factories/better_together/metrics/link_clicks.rb index d79067479..023849a00 100644 --- a/spec/factories/better_together/metrics/link_clicks.rb +++ b/spec/factories/better_together/metrics/link_clicks.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true FactoryBot.define do - factory :metrics_link_click, class: 'Metrics::LinkClick' do # rubocop:todo Lint/EmptyBlock + factory :metrics_link_click, class: 'BetterTogether::Metrics::LinkClick' do + url { 'https://example.com' } + page_url { '/test-page' } + locale { I18n.default_locale.to_s } + clicked_at { Time.current } + internal { false } end end diff --git a/spec/factories/better_together/metrics/page_views.rb b/spec/factories/better_together/metrics/page_views.rb index b5ef01d43..90d0a7126 100644 --- a/spec/factories/better_together/metrics/page_views.rb +++ b/spec/factories/better_together/metrics/page_views.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true FactoryBot.define do - factory :metrics_page_view, class: 'Metrics::PageView' do # rubocop:todo Lint/EmptyBlock + factory :metrics_page_view, class: 'BetterTogether::Metrics::PageView' do + viewed_at { Time.current } + locale { I18n.default_locale.to_s } + page_url { '/test-page' } + + trait :with_pageable do + association :pageable, factory: :page + page_url { nil } # Let the model generate it from pageable + end end end diff --git a/spec/factories/better_together/metrics/shares.rb b/spec/factories/better_together/metrics/shares.rb index ab7955254..481d3501f 100644 --- a/spec/factories/better_together/metrics/shares.rb +++ b/spec/factories/better_together/metrics/shares.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true FactoryBot.define do - factory :metrics_share, class: 'Metrics::Share' do # rubocop:todo Lint/EmptyBlock + factory :metrics_share, class: 'BetterTogether::Metrics::Share', aliases: [:share] do + platform { 'facebook' } + url { 'https://facebook.com/share/12345' } + shared_at { Time.current } + locale { 'en' } + + trait :with_community do + association :shareable, factory: :community + end end end diff --git a/spec/factories/better_together/resource_permissions.rb b/spec/factories/better_together/resource_permissions.rb index 44e33d5d1..63a194835 100644 --- a/spec/factories/better_together/resource_permissions.rb +++ b/spec/factories/better_together/resource_permissions.rb @@ -10,5 +10,6 @@ resource_type { BetterTogether::Resourceful::RESOURCE_CLASSES.sample } # Derive target from the resource_type (e.g., 'BetterTogether::Community' => 'community') target { resource_type.demodulize.underscore } + # Position is automatically assigned by the Positioned concern based on resource_type scope end end diff --git a/spec/factories/better_together/social_media_accounts.rb b/spec/factories/better_together/social_media_accounts.rb index 95031929c..1e75a9dd7 100644 --- a/spec/factories/better_together/social_media_accounts.rb +++ b/spec/factories/better_together/social_media_accounts.rb @@ -1,6 +1,42 @@ # frozen_string_literal: true FactoryBot.define do - factory :social_media_account do # rubocop:todo Lint/EmptyBlock + factory :social_media_account, class: 'BetterTogether::SocialMediaAccount' do + contact_detail { association :contact_detail } + platform { 'Facebook' } + handle { Faker::Internet.username(specifier: 5..15, separators: %w[.]) } + privacy { 'public' } + + trait :instagram do + platform { 'Instagram' } + end + + trait :linkedin do + platform { 'LinkedIn' } + end + + trait :youtube do + platform { 'YouTube' } + end + + trait :tiktok do + platform { 'TikTok' } + end + + trait :reddit do + platform { 'Reddit' } + end + + trait :with_url do + url { Faker::Internet.url } + end + + trait :with_at_handle do + handle { "@#{Faker::Internet.username(specifier: 5..15, separators: %w[.])}" } + end + + trait :private do + privacy { 'private' } + end end end diff --git a/spec/factories/better_together/website_links.rb b/spec/factories/better_together/website_links.rb index 4aed9b74c..2cecd213e 100644 --- a/spec/factories/better_together/website_links.rb +++ b/spec/factories/better_together/website_links.rb @@ -1,6 +1,42 @@ # frozen_string_literal: true FactoryBot.define do - factory :website_link do # rubocop:todo Lint/EmptyBlock + factory :website_link, class: 'BetterTogether::WebsiteLink' do + contact_detail { association :contact_detail } + url { Faker::Internet.url } + label { 'personal_website' } + privacy { 'public' } + + trait :blog do + label { 'blog' } + end + + trait :portfolio do + label { 'portfolio' } + end + + trait :company_website do + label { 'company_website' } + end + + trait :community_page do + label { 'community_page' } + end + + trait :documentation do + label { 'documentation' } + end + + trait :private do + privacy { 'private' } + end + + trait :https do + url { "https://#{Faker::Internet.domain_name}" } + end + + trait :http do + url { "http://#{Faker::Internet.domain_name}" } + 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..03ffa957d 100644 --- a/spec/factories/better_together/wizard_step_definitions.rb +++ b/spec/factories/better_together/wizard_step_definitions.rb @@ -6,15 +6,15 @@ factory :better_together_wizard_step_definition, class: 'BetterTogether::WizardStepDefinition', aliases: %i[wizard_step_definition] do - id { SecureRandom.uuid } + sequence(:id) { |_n| SecureRandom.uuid } wizard { create(:wizard) } - name { Faker::Lorem.unique.sentence(word_count: 3) } + name { Faker::Lorem.sentence(word_count: 3) } description { Faker::Lorem.paragraph } - identifier { name.parameterize } + sequence(:identifier) { |n| "#{name.parameterize}-#{n}" } 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) } + sequence(:step_number) { |n| n } protected { Faker::Boolean.boolean } end end diff --git a/spec/helpers/better_together/hub_helper_spec.rb b/spec/helpers/better_together/hub_helper_spec.rb index ac1b3a97d..0ffc84184 100644 --- a/spec/helpers/better_together/hub_helper_spec.rb +++ b/spec/helpers/better_together/hub_helper_spec.rb @@ -2,20 +2,220 @@ require 'rails_helper' -# Specs in this file have access to a helper object that includes -# the HubHelper. For example: -# -# describe HubHelper 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 +# rubocop:disable Metrics/ModuleLength module BetterTogether RSpec.describe HubHelper do - it 'exists' do - expect(described_class).to be # rubocop:todo RSpec/Be + describe '#activities' do + let(:user) { create(:user) } + let(:page) { create(:page) } + + before do + allow(helper).to receive(:current_user).and_return(user) + # Skip - Activity factory not yet implemented + # create_list(:activity, 3, owner: user.person, trackable: page) + end + + it 'returns scoped activities based on policy' do + skip 'Activity factory not yet implemented' + activities = helper.activities + expect(activities).to be_a(ActiveRecord::Relation) + end + + it 'uses ActivityPolicy::Scope to filter activities' do + skip 'Activity factory not yet implemented' + expect(BetterTogether::ActivityPolicy::Scope).to receive(:new) + .with(user, PublicActivity::Activity) + .and_call_original + + helper.activities + end + end + + describe '#timeago' do + let(:test_time) { Time.zone.parse('2025-11-24 12:00:00 UTC') } + + context 'with valid time' do + it 'generates abbr tag with timeago class' do + result = helper.timeago(test_time) + expect(result).to have_css('abbr.timeago') + end + + it 'includes ISO8601 formatted time in title attribute' do + result = helper.timeago(test_time) + expect(result).to have_css("abbr[title='#{test_time.getutc.iso8601}']") + end + + it 'displays time string as content' do + result = helper.timeago(test_time) + expect(result).to include(test_time.to_s) + end + + it 'accepts custom CSS class' do + result = helper.timeago(test_time, class: 'custom-class') + expect(result).to have_css('abbr.custom-class') + end + + it 'merges custom options with defaults' do + result = helper.timeago(test_time, class: 'custom', id: 'my-time') + expect(result).to have_css('abbr#my-time.custom') + end + end + + context 'with nil time' do + it 'returns nil' do + expect(helper.timeago(nil)).to be_nil + end + end + + context 'with different time zones' do + it 'converts to UTC for title' do + tokyo_time = Time.zone.parse('2025-11-24 21:00:00 +0900') + result = helper.timeago(tokyo_time) + utc_time = tokyo_time.getutc.iso8601 + expect(result).to have_css("abbr[title='#{utc_time}']") + end + end + end + + describe '#whose?' do + let(:user) { create(:user) } + let(:other_user) { create(:user) } + let(:page) { create(:page, creator: user.person) } + let(:other_page) { create(:page, creator: other_user.person) } + + context 'when user owns the object' do + it 'returns "his"' do + result = helper.whose?(user, page) + expect(result).to eq('his') + end + end + + context 'when user does not own the object' do + it 'returns owner name with possessive' do + result = helper.whose?(user, other_page) + expect(result).to eq("#{other_user.person.name}'s") + end + end + + context 'when user is nil' do + it 'returns empty string' do + result = helper.whose?(nil, page) + expect(result).to eq('') + end + end + + context 'when object has no owner' do + let(:orphaned_page) { build(:page, creator: nil) } + + it 'returns empty string' do + result = helper.whose?(user, orphaned_page) + expect(result).to eq('') + end + end + + context 'when both user and owner are nil' do + it 'returns empty string' do + result = helper.whose?(nil, build(:page, creator: nil)) + expect(result).to eq('') + end + end + end + + describe '#link_to_trackable' do + context 'when object exists' do + let(:page) { create(:page, title: 'Test Page') } + + it 'returns link to the object' do + result = helper.link_to_trackable(page, 'Page') + expect(result).to include(page.title) + expect(result).to have_link(page.title) + end + + it 'includes model name as prefix' do + result = helper.link_to_trackable(page, 'Page') + expect(result).to include(page.class.model_name.human) + end + + it 'uses object.url if available' do + allow(page).to receive(:url).and_return('/custom-url') + result = helper.link_to_trackable(page, 'Page') + expect(result).to have_link(page.title, href: '/custom-url') + end + + it 'falls back to object itself for URL' do + skip 'Routing helper issues in engine context' + # Remove url method to test fallback + allow(page).to receive(:respond_to?).with(:url).and_return(false) + result = helper.link_to_trackable(page, 'Page') + expect(result).to be_present + end + + it 'adds text-decoration-none class to link' do + result = helper.link_to_trackable(page, 'Page') + expect(result).to have_css('a.text-decoration-none') + end + end + + context 'when object is nil' do + it 'returns message about deleted object' do + result = helper.link_to_trackable(nil, 'Post') + expect(result).to eq('a post which does not exist anymore') + end + + it 'downcases object type' do + result = helper.link_to_trackable(nil, 'ARTICLE') + expect(result).to eq('a article which does not exist anymore') + end + end + + context 'with different object types' do + it 'handles different model types' do + skip 'Routing helper issues in engine context' + community = create(:community, name: 'Test Community') + result = helper.link_to_trackable(community, 'Community') + expect(result).to include(community.class.model_name.human) + expect(result).to have_link(community.name) + end + end + end + + describe 'helper integration' do + it 'includes all expected methods' do + expect(helper).to respond_to(:activities) + expect(helper).to respond_to(:timeago) + expect(helper).to respond_to(:whose?) + expect(helper).to respond_to(:link_to_trackable) + end + end + + describe 'edge cases' do + describe '#timeago with edge times' do + it 'handles very old dates' do + old_time = 100.years.ago + result = helper.timeago(old_time) + expect(result).to be_present + expect(result).to have_css('abbr.timeago') + end + + it 'handles future dates' do + future_time = 10.years.from_now + result = helper.timeago(future_time) + expect(result).to be_present + expect(result).to have_css('abbr.timeago') + end + end + + describe '#whose? with complex ownership' do + it 'handles users with special characters in nicknames' do + user = create(:user) + special_user = create(:user, person: create(:person, name: "O'Brien")) + page = create(:page, creator: special_user.person) + + result = helper.whose?(user, page) + expect(result).to include("O'Brien") + end + end end end end +# rubocop:enable Metrics/ModuleLength diff --git a/spec/helpers/better_together/markdown_helper_spec.rb b/spec/helpers/better_together/markdown_helper_spec.rb new file mode 100644 index 000000000..d80f8a850 --- /dev/null +++ b/spec/helpers/better_together/markdown_helper_spec.rb @@ -0,0 +1,264 @@ +# frozen_string_literal: true + +require 'rails_helper' + +module BetterTogether # rubocop:todo Metrics/ModuleLength + RSpec.describe MarkdownHelper do + describe '#render_markdown' do + context 'with basic markdown' do + it 'renders markdown to HTML' do + html = helper.render_markdown('# Hello World') + + expect(html).to include('bold') + expect(html).to include('italic') + expect(html).to include('