WIP: Ractors #25
							
								
								
									
										58
									
								
								.annotaterb.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								.annotaterb.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					:position: before
 | 
				
			||||||
 | 
					:position_in_additional_file_patterns: before
 | 
				
			||||||
 | 
					:position_in_class: before
 | 
				
			||||||
 | 
					:position_in_factory: before
 | 
				
			||||||
 | 
					:position_in_fixture: before
 | 
				
			||||||
 | 
					:position_in_routes: before
 | 
				
			||||||
 | 
					:position_in_serializer: before
 | 
				
			||||||
 | 
					:position_in_test: before
 | 
				
			||||||
 | 
					:classified_sort: true
 | 
				
			||||||
 | 
					:exclude_controllers: true
 | 
				
			||||||
 | 
					:exclude_factories: true
 | 
				
			||||||
 | 
					:exclude_fixtures: false
 | 
				
			||||||
 | 
					:exclude_helpers: true
 | 
				
			||||||
 | 
					:exclude_scaffolds: true
 | 
				
			||||||
 | 
					:exclude_serializers: false
 | 
				
			||||||
 | 
					:exclude_sti_subclasses: false
 | 
				
			||||||
 | 
					:exclude_tests: true
 | 
				
			||||||
 | 
					:force: false
 | 
				
			||||||
 | 
					:format_markdown: false
 | 
				
			||||||
 | 
					:format_rdoc: false
 | 
				
			||||||
 | 
					:format_yard: false
 | 
				
			||||||
 | 
					:frozen: false
 | 
				
			||||||
 | 
					:ignore_model_sub_dir: false
 | 
				
			||||||
 | 
					:ignore_unknown_models: false
 | 
				
			||||||
 | 
					:include_version: false
 | 
				
			||||||
 | 
					:show_check_constraints: false
 | 
				
			||||||
 | 
					:show_complete_foreign_keys: false
 | 
				
			||||||
 | 
					:show_foreign_keys: true
 | 
				
			||||||
 | 
					:show_indexes: true
 | 
				
			||||||
 | 
					:simple_indexes: false
 | 
				
			||||||
 | 
					:sort: false
 | 
				
			||||||
 | 
					:timestamp: false
 | 
				
			||||||
 | 
					:trace: false
 | 
				
			||||||
 | 
					:with_comment: true
 | 
				
			||||||
 | 
					:with_column_comments: true
 | 
				
			||||||
 | 
					:with_table_comments: true
 | 
				
			||||||
 | 
					:active_admin: false
 | 
				
			||||||
 | 
					:command: 
 | 
				
			||||||
 | 
					:debug: false
 | 
				
			||||||
 | 
					:hide_default_column_types: ''
 | 
				
			||||||
 | 
					:hide_limit_column_types: ''
 | 
				
			||||||
 | 
					:ignore_columns: 
 | 
				
			||||||
 | 
					:ignore_routes: 
 | 
				
			||||||
 | 
					:models: true
 | 
				
			||||||
 | 
					:routes: false
 | 
				
			||||||
 | 
					:skip_on_db_migrate: false
 | 
				
			||||||
 | 
					:target_action: :do_annotations
 | 
				
			||||||
 | 
					:wrapper: 
 | 
				
			||||||
 | 
					:wrapper_close: 
 | 
				
			||||||
 | 
					:wrapper_open: 
 | 
				
			||||||
 | 
					:classes_default_to_s: []
 | 
				
			||||||
 | 
					:additional_file_patterns: []
 | 
				
			||||||
 | 
					:model_dir:
 | 
				
			||||||
 | 
					- app/models
 | 
				
			||||||
 | 
					:require: []
 | 
				
			||||||
 | 
					:root_dir:
 | 
				
			||||||
 | 
					- ''
 | 
				
			||||||
@ -35,3 +35,4 @@
 | 
				
			|||||||
/app/assets/builds/*
 | 
					/app/assets/builds/*
 | 
				
			||||||
!/app/assets/builds/.keep
 | 
					!/app/assets/builds/.keep
 | 
				
			||||||
/public/assets
 | 
					/public/assets
 | 
				
			||||||
 | 
					.docker-compose.yml
 | 
				
			||||||
							
								
								
									
										159
									
								
								.gitea/workflows/tests.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								.gitea/workflows/tests.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,159 @@
 | 
				
			|||||||
 | 
					name: Run unit tests
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - main
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					concurrency:
 | 
				
			||||||
 | 
					  group: ${{ github.ref }}
 | 
				
			||||||
 | 
					  cancel-in-progress: true
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  unit_tests:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    services: &services
 | 
				
			||||||
 | 
					      postgres:
 | 
				
			||||||
 | 
					        image: postgres
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          POSTGRES_USER: postgres
 | 
				
			||||||
 | 
					          POSTGRES_PASSWORD: postgres
 | 
				
			||||||
 | 
					        ports:
 | 
				
			||||||
 | 
					          - 5432
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          token: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					          ref: ${{ github.head_ref }} # Checkout the actual branch, not the result if merged into the base
 | 
				
			||||||
 | 
					      - uses: ruby/setup-ruby@v1.207.0
 | 
				
			||||||
 | 
					      - run: bundle install
 | 
				
			||||||
 | 
					      - &postgres_wait
 | 
				
			||||||
 | 
					        name: Wait until Postgres is ready to accept connections
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          apt-get update && apt-get install -f -y postgresql-client
 | 
				
			||||||
 | 
					          until pg_isready -h postgres -U postgres -d postgres
 | 
				
			||||||
 | 
					          do
 | 
				
			||||||
 | 
					            sleep 1
 | 
				
			||||||
 | 
					            echo "Trying again"
 | 
				
			||||||
 | 
					          done
 | 
				
			||||||
 | 
					      - name: Load schema and run unit tests
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          bundle exec rake db:schema:load
 | 
				
			||||||
 | 
					          bundle exec rspec
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          RAILS_ENV: test
 | 
				
			||||||
 | 
					          DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
 | 
				
			||||||
 | 
					      - name: Get all migrations added
 | 
				
			||||||
 | 
					        id: changed-migration-files
 | 
				
			||||||
 | 
					        uses: tj-actions/changed-files@v45
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          files: |
 | 
				
			||||||
 | 
					            db/migrate/**.rb
 | 
				
			||||||
 | 
					      - name: Redo all migrations and check there are no schema changes
 | 
				
			||||||
 | 
					        if: steps.changed-migration-files.outputs.any_changed == 'true'
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          ALL_CHANGED_FILES: ${{ steps.changed-migration-files.outputs.all_changed_files }}
 | 
				
			||||||
 | 
					          RAILS_ENV: test
 | 
				
			||||||
 | 
					          DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          echo ${#ALL_CHANGED_FILES[@]} migrations changed:
 | 
				
			||||||
 | 
					          for file in ${ALL_CHANGED_FILES}; do
 | 
				
			||||||
 | 
					            echo "$file"
 | 
				
			||||||
 | 
					          done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          bundle exec rake db:migrate:redo STEP=${#ALL_CHANGED_FILES[@]}
 | 
				
			||||||
 | 
					          git diff --exit-code db/schema.rb
 | 
				
			||||||
 | 
					      - name: Clean up containers generated by this flow
 | 
				
			||||||
 | 
					        if: failure()
 | 
				
			||||||
 | 
					        run: docker ps --filter network=$JOB_CONTAINER_NAME-$GITHUB_JOB-network --filter name=$JOB_CONTAINER_NAME-* --format "{{.ID}}" | xargs docker rm -f
 | 
				
			||||||
 | 
					  rubocop:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          token: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					      - uses: ruby/setup-ruby@v1.207.0
 | 
				
			||||||
 | 
					      - run: bundle install
 | 
				
			||||||
 | 
					      - run: bundle exec rubocop --force-exclusion --parallel
 | 
				
			||||||
 | 
					  check-licenses:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					        - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					          with:
 | 
				
			||||||
 | 
					            token: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					        - uses: ruby/setup-ruby@v1.207.0
 | 
				
			||||||
 | 
					        - name: Install project dependencies
 | 
				
			||||||
 | 
					          run: bundle install --jobs `getconf _NPROCESSORS_ONLN`
 | 
				
			||||||
 | 
					        - name: Run license finder
 | 
				
			||||||
 | 
					          run: license_finder
 | 
				
			||||||
 | 
					  copyright_notice:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          token: ${{ secrets.ACTIONS_TOKEN }}
 | 
				
			||||||
 | 
					          ref: ${{ github.head_ref }}
 | 
				
			||||||
 | 
					      - uses: VinnyBabuManjaly/copyright-action@v1.0.0
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          CopyrightString: '# Copyright (C) 2024-2025 LibreWeddingPlanner contributors\n\n'
 | 
				
			||||||
 | 
					          FileType: '.rb'
 | 
				
			||||||
 | 
					          Path: 'app/, config/, db/, spec/'
 | 
				
			||||||
 | 
					          IgnorePath: 'db'
 | 
				
			||||||
 | 
					      - uses: VinnyBabuManjaly/copyright-action@v1.0.0
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          CopyrightString: '<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>\n\n'
 | 
				
			||||||
 | 
					          FileType: '.erb'
 | 
				
			||||||
 | 
					          Path: 'app/'
 | 
				
			||||||
 | 
					      - name: Commit changes
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          git config --local user.email "bustikiller@bustikiller.com"
 | 
				
			||||||
 | 
					          git config --local user.name "Manuel Bustillo"
 | 
				
			||||||
 | 
					          git add .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if [ -n "$(git status --porcelain)" ]; then
 | 
				
			||||||
 | 
					            echo "there are changes";
 | 
				
			||||||
 | 
					            git commit -m "Add copyright notice"
 | 
				
			||||||
 | 
					            git push
 | 
				
			||||||
 | 
					          else
 | 
				
			||||||
 | 
					            echo "no changes";
 | 
				
			||||||
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  build-static-assets:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    timeout-minutes: 30
 | 
				
			||||||
 | 
					    needs:
 | 
				
			||||||
 | 
					      - unit_tests
 | 
				
			||||||
 | 
					      - rubocop
 | 
				
			||||||
 | 
					      - check-licenses
 | 
				
			||||||
 | 
					      - copyright_notice
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          token: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Set up Docker Buildx
 | 
				
			||||||
 | 
					        uses: docker/setup-buildx-action@v3
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      - name: Login to the private Docker registry
 | 
				
			||||||
 | 
					        uses: docker/login-action@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          registry: ${{ secrets.PRIVATE_REGISTRY_HOST }}
 | 
				
			||||||
 | 
					          username: ${{ secrets.PRIVATE_REGISTRY_USERNAME }}
 | 
				
			||||||
 | 
					          password: ${{ secrets.PRIVATE_REGISTRY_TOKEN }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Build and push intermediate stages (build)
 | 
				
			||||||
 | 
					        uses: docker/build-push-action@v6
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          context: .
 | 
				
			||||||
 | 
					          target: build
 | 
				
			||||||
 | 
					          push: ${{ github.ref == 'refs/heads/main' }}
 | 
				
			||||||
 | 
					          tags: ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:build
 | 
				
			||||||
 | 
					          cache-from: type=registry,ref=${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:build
 | 
				
			||||||
 | 
					          cache-to: type=inline
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Build and push (final)
 | 
				
			||||||
 | 
					        uses: docker/build-push-action@v6
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          context: .
 | 
				
			||||||
 | 
					          push: ${{ github.ref == 'refs/heads/main' }}
 | 
				
			||||||
 | 
					          tags: ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest
 | 
				
			||||||
 | 
					          cache-from: type=registry,ref=${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest
 | 
				
			||||||
 | 
					          cache-to: type=inline
 | 
				
			||||||
							
								
								
									
										31
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@ -1,31 +0,0 @@
 | 
				
			|||||||
name: Build docker image
 | 
					 | 
				
			||||||
on:
 | 
					 | 
				
			||||||
  push:
 | 
					 | 
				
			||||||
    branches:
 | 
					 | 
				
			||||||
      - main
 | 
					 | 
				
			||||||
  pull_request:
 | 
					 | 
				
			||||||
jobs:
 | 
					 | 
				
			||||||
  build-static-assets:
 | 
					 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					 | 
				
			||||||
    steps:
 | 
					 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          token: ${{ secrets.GITHUB_TOKEN }}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Set up Docker Buildx
 | 
					 | 
				
			||||||
        uses: docker/setup-buildx-action@v3
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      - name: Login to Docker Hub
 | 
					 | 
				
			||||||
        uses: docker/login-action@v3
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
					 | 
				
			||||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
      - name: Build and push
 | 
					 | 
				
			||||||
        uses: docker/build-push-action@v5
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          context: .
 | 
					 | 
				
			||||||
          push: ${{ github.event_name != 'pull_request' }}
 | 
					 | 
				
			||||||
          tags: bustikiller/wedding-planner:latest
 | 
					 | 
				
			||||||
          cache-from: type=registry,ref=user/app:latest
 | 
					 | 
				
			||||||
          cache-to: type=inline
 | 
					 | 
				
			||||||
							
								
								
									
										40
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										40
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
								
							@ -1,40 +0,0 @@
 | 
				
			|||||||
name: Run unit tests
 | 
					 | 
				
			||||||
on:
 | 
					 | 
				
			||||||
  push:
 | 
					 | 
				
			||||||
    branches:
 | 
					 | 
				
			||||||
      - main
 | 
					 | 
				
			||||||
  pull_request:
 | 
					 | 
				
			||||||
jobs:
 | 
					 | 
				
			||||||
  unit_tests:
 | 
					 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					 | 
				
			||||||
    services:
 | 
					 | 
				
			||||||
      postgres:
 | 
					 | 
				
			||||||
        image: postgres
 | 
					 | 
				
			||||||
        env:
 | 
					 | 
				
			||||||
          POSTGRES_USER: postgres
 | 
					 | 
				
			||||||
          POSTGRES_PASSWORD: postgres
 | 
					 | 
				
			||||||
        ports:
 | 
					 | 
				
			||||||
          - 5432
 | 
					 | 
				
			||||||
    steps:
 | 
					 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          token: ${{ secrets.GITHUB_TOKEN }}
 | 
					 | 
				
			||||||
      - uses: ruby/setup-ruby@v1
 | 
					 | 
				
			||||||
      - run: bundle install
 | 
					 | 
				
			||||||
      - name: Wait until Postgres is ready to accept connections
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          apt-get update && apt-get install -f -y postgresql-client
 | 
					 | 
				
			||||||
          until pg_isready -h postgres -U postgres -d postgres
 | 
					 | 
				
			||||||
          do
 | 
					 | 
				
			||||||
            sleep 1
 | 
					 | 
				
			||||||
            echo "Trying again"
 | 
					 | 
				
			||||||
          done
 | 
					 | 
				
			||||||
      - run: |
 | 
					 | 
				
			||||||
          bundle exec rake db:schema:load
 | 
					 | 
				
			||||||
          bundle exec rspec
 | 
					 | 
				
			||||||
        env:
 | 
					 | 
				
			||||||
          RAILS_ENV: test
 | 
					 | 
				
			||||||
          DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
 | 
					 | 
				
			||||||
      - name: Clean up containers generated by this flow
 | 
					 | 
				
			||||||
        if: failure()
 | 
					 | 
				
			||||||
        run: docker ps --filter network=$JOB_CONTAINER_NAME-$GITHUB_JOB-network --filter name=$JOB_CONTAINER_NAME-* --format "{{.ID}}" | xargs docker rm -f
 | 
					 | 
				
			||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -33,3 +33,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Ignore master key for decrypting credentials and more.
 | 
					# Ignore master key for decrypting credentials and more.
 | 
				
			||||||
/config/master.key
 | 
					/config/master.key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Ignore swagger generated documentation
 | 
				
			||||||
 | 
					swagger/v1/swagger.yaml
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										29
									
								
								.rubocop.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.rubocop.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					require:
 | 
				
			||||||
 | 
					  - rubocop-rails
 | 
				
			||||||
 | 
					  - rubocop-factory_bot
 | 
				
			||||||
 | 
					  - rubocop-rspec
 | 
				
			||||||
 | 
					  - rubocop-rspec_rails
 | 
				
			||||||
 | 
					AllCops:
 | 
				
			||||||
 | 
					  NewCops: enable
 | 
				
			||||||
 | 
					  Exclude:
 | 
				
			||||||
 | 
					    - 'db/**/*'
 | 
				
			||||||
 | 
					    - 'config/**/*'
 | 
				
			||||||
 | 
					    - 'script/**/*'
 | 
				
			||||||
 | 
					    - 'bin/*'
 | 
				
			||||||
 | 
					    - '*.yml'
 | 
				
			||||||
 | 
					Layout/LineLength:
 | 
				
			||||||
 | 
					  Max: 120
 | 
				
			||||||
 | 
					RSpec/ExampleLength:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
 | 
					Metrics/ModuleLength:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
 | 
					RSpec/MultipleMemoizedHelpers:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
 | 
					Style/Documentation:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
 | 
					Metrics/MethodLength:
 | 
				
			||||||
 | 
					  Max: 20
 | 
				
			||||||
 | 
					Rails/SkipsModelValidations:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
 | 
					Metrics/AbcSize:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
@ -1 +1 @@
 | 
				
			|||||||
ruby-3.3.4
 | 
					ruby-3.4.1
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										660
									
								
								COPYING.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										660
									
								
								COPYING.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,660 @@
 | 
				
			|||||||
 | 
					# GNU AFFERO GENERAL PUBLIC LICENSE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Version 3, 19 November 2007
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright (C) 2007 Free Software Foundation, Inc.
 | 
				
			||||||
 | 
					<https://fsf.org/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Everyone is permitted to copy and distribute verbatim copies of this
 | 
				
			||||||
 | 
					license document, but changing it is not allowed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Preamble
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The GNU Affero General Public License is a free, copyleft license for
 | 
				
			||||||
 | 
					software and other kinds of works, specifically designed to ensure
 | 
				
			||||||
 | 
					cooperation with the community in the case of network server software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The licenses for most software and other practical works are designed
 | 
				
			||||||
 | 
					to take away your freedom to share and change the works. By contrast,
 | 
				
			||||||
 | 
					our General Public Licenses are intended to guarantee your freedom to
 | 
				
			||||||
 | 
					share and change all versions of a program--to make sure it remains
 | 
				
			||||||
 | 
					free software for all its users.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When we speak of free software, we are referring to freedom, not
 | 
				
			||||||
 | 
					price. Our General Public Licenses are designed to make sure that you
 | 
				
			||||||
 | 
					have the freedom to distribute copies of free software (and charge for
 | 
				
			||||||
 | 
					them if you wish), that you receive source code or can get it if you
 | 
				
			||||||
 | 
					want it, that you can change the software or use pieces of it in new
 | 
				
			||||||
 | 
					free programs, and that you know you can do these things.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Developers that use our General Public Licenses protect your rights
 | 
				
			||||||
 | 
					with two steps: (1) assert copyright on the software, and (2) offer
 | 
				
			||||||
 | 
					you this License which gives you legal permission to copy, distribute
 | 
				
			||||||
 | 
					and/or modify the software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A secondary benefit of defending all users' freedom is that
 | 
				
			||||||
 | 
					improvements made in alternate versions of the program, if they
 | 
				
			||||||
 | 
					receive widespread use, become available for other developers to
 | 
				
			||||||
 | 
					incorporate. Many developers of free software are heartened and
 | 
				
			||||||
 | 
					encouraged by the resulting cooperation. However, in the case of
 | 
				
			||||||
 | 
					software used on network servers, this result may fail to come about.
 | 
				
			||||||
 | 
					The GNU General Public License permits making a modified version and
 | 
				
			||||||
 | 
					letting the public access it on a server without ever releasing its
 | 
				
			||||||
 | 
					source code to the public.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The GNU Affero General Public License is designed specifically to
 | 
				
			||||||
 | 
					ensure that, in such cases, the modified source code becomes available
 | 
				
			||||||
 | 
					to the community. It requires the operator of a network server to
 | 
				
			||||||
 | 
					provide the source code of the modified version running there to the
 | 
				
			||||||
 | 
					users of that server. Therefore, public use of a modified version, on
 | 
				
			||||||
 | 
					a publicly accessible server, gives the public access to the source
 | 
				
			||||||
 | 
					code of the modified version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An older license, called the Affero General Public License and
 | 
				
			||||||
 | 
					published by Affero, was designed to accomplish similar goals. This is
 | 
				
			||||||
 | 
					a different license, not a version of the Affero GPL, but Affero has
 | 
				
			||||||
 | 
					released a new version of the Affero GPL which permits relicensing
 | 
				
			||||||
 | 
					under this license.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The precise terms and conditions for copying, distribution and
 | 
				
			||||||
 | 
					modification follow.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## TERMS AND CONDITIONS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 0. Definitions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"This License" refers to version 3 of the GNU Affero General Public
 | 
				
			||||||
 | 
					License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"Copyright" also means copyright-like laws that apply to other kinds
 | 
				
			||||||
 | 
					of works, such as semiconductor masks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"The Program" refers to any copyrightable work licensed under this
 | 
				
			||||||
 | 
					License. Each licensee is addressed as "you". "Licensees" and
 | 
				
			||||||
 | 
					"recipients" may be individuals or organizations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To "modify" a work means to copy from or adapt all or part of the work
 | 
				
			||||||
 | 
					in a fashion requiring copyright permission, other than the making of
 | 
				
			||||||
 | 
					an exact copy. The resulting work is called a "modified version" of
 | 
				
			||||||
 | 
					the earlier work or a work "based on" the earlier work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A "covered work" means either the unmodified Program or a work based
 | 
				
			||||||
 | 
					on the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To "propagate" a work means to do anything with it that, without
 | 
				
			||||||
 | 
					permission, would make you directly or secondarily liable for
 | 
				
			||||||
 | 
					infringement under applicable copyright law, except executing it on a
 | 
				
			||||||
 | 
					computer or modifying a private copy. Propagation includes copying,
 | 
				
			||||||
 | 
					distribution (with or without modification), making available to the
 | 
				
			||||||
 | 
					public, and in some countries other activities as well.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To "convey" a work means any kind of propagation that enables other
 | 
				
			||||||
 | 
					parties to make or receive copies. Mere interaction with a user
 | 
				
			||||||
 | 
					through a computer network, with no transfer of a copy, is not
 | 
				
			||||||
 | 
					conveying.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An interactive user interface displays "Appropriate Legal Notices" to
 | 
				
			||||||
 | 
					the extent that it includes a convenient and prominently visible
 | 
				
			||||||
 | 
					feature that (1) displays an appropriate copyright notice, and (2)
 | 
				
			||||||
 | 
					tells the user that there is no warranty for the work (except to the
 | 
				
			||||||
 | 
					extent that warranties are provided), that licensees may convey the
 | 
				
			||||||
 | 
					work under this License, and how to view a copy of this License. If
 | 
				
			||||||
 | 
					the interface presents a list of user commands or options, such as a
 | 
				
			||||||
 | 
					menu, a prominent item in the list meets this criterion.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1. Source Code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The "source code" for a work means the preferred form of the work for
 | 
				
			||||||
 | 
					making modifications to it. "Object code" means any non-source form of
 | 
				
			||||||
 | 
					a work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A "Standard Interface" means an interface that either is an official
 | 
				
			||||||
 | 
					standard defined by a recognized standards body, or, in the case of
 | 
				
			||||||
 | 
					interfaces specified for a particular programming language, one that
 | 
				
			||||||
 | 
					is widely used among developers working in that language.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The "System Libraries" of an executable work include anything, other
 | 
				
			||||||
 | 
					than the work as a whole, that (a) is included in the normal form of
 | 
				
			||||||
 | 
					packaging a Major Component, but which is not part of that Major
 | 
				
			||||||
 | 
					Component, and (b) serves only to enable use of the work with that
 | 
				
			||||||
 | 
					Major Component, or to implement a Standard Interface for which an
 | 
				
			||||||
 | 
					implementation is available to the public in source code form. A
 | 
				
			||||||
 | 
					"Major Component", in this context, means a major essential component
 | 
				
			||||||
 | 
					(kernel, window system, and so on) of the specific operating system
 | 
				
			||||||
 | 
					(if any) on which the executable work runs, or a compiler used to
 | 
				
			||||||
 | 
					produce the work, or an object code interpreter used to run it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The "Corresponding Source" for a work in object code form means all
 | 
				
			||||||
 | 
					the source code needed to generate, install, and (for an executable
 | 
				
			||||||
 | 
					work) run the object code and to modify the work, including scripts to
 | 
				
			||||||
 | 
					control those activities. However, it does not include the work's
 | 
				
			||||||
 | 
					System Libraries, or general-purpose tools or generally available free
 | 
				
			||||||
 | 
					programs which are used unmodified in performing those activities but
 | 
				
			||||||
 | 
					which are not part of the work. For example, Corresponding Source
 | 
				
			||||||
 | 
					includes interface definition files associated with source files for
 | 
				
			||||||
 | 
					the work, and the source code for shared libraries and dynamically
 | 
				
			||||||
 | 
					linked subprograms that the work is specifically designed to require,
 | 
				
			||||||
 | 
					such as by intimate data communication or control flow between those
 | 
				
			||||||
 | 
					subprograms and other parts of the work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Corresponding Source need not include anything that users can
 | 
				
			||||||
 | 
					regenerate automatically from other parts of the Corresponding Source.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Corresponding Source for a work in source code form is that same
 | 
				
			||||||
 | 
					work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2. Basic Permissions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All rights granted under this License are granted for the term of
 | 
				
			||||||
 | 
					copyright on the Program, and are irrevocable provided the stated
 | 
				
			||||||
 | 
					conditions are met. This License explicitly affirms your unlimited
 | 
				
			||||||
 | 
					permission to run the unmodified Program. The output from running a
 | 
				
			||||||
 | 
					covered work is covered by this License only if the output, given its
 | 
				
			||||||
 | 
					content, constitutes a covered work. This License acknowledges your
 | 
				
			||||||
 | 
					rights of fair use or other equivalent, as provided by copyright law.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may make, run and propagate covered works that you do not convey,
 | 
				
			||||||
 | 
					without conditions so long as your license otherwise remains in force.
 | 
				
			||||||
 | 
					You may convey covered works to others for the sole purpose of having
 | 
				
			||||||
 | 
					them make modifications exclusively for you, or provide you with
 | 
				
			||||||
 | 
					facilities for running those works, provided that you comply with the
 | 
				
			||||||
 | 
					terms of this License in conveying all material for which you do not
 | 
				
			||||||
 | 
					control copyright. Those thus making or running the covered works for
 | 
				
			||||||
 | 
					you must do so exclusively on your behalf, under your direction and
 | 
				
			||||||
 | 
					control, on terms that prohibit them from making any copies of your
 | 
				
			||||||
 | 
					copyrighted material outside their relationship with you.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Conveying under any other circumstances is permitted solely under the
 | 
				
			||||||
 | 
					conditions stated below. Sublicensing is not allowed; section 10 makes
 | 
				
			||||||
 | 
					it unnecessary.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					No covered work shall be deemed part of an effective technological
 | 
				
			||||||
 | 
					measure under any applicable law fulfilling obligations under article
 | 
				
			||||||
 | 
					11 of the WIPO copyright treaty adopted on 20 December 1996, or
 | 
				
			||||||
 | 
					similar laws prohibiting or restricting circumvention of such
 | 
				
			||||||
 | 
					measures.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When you convey a covered work, you waive any legal power to forbid
 | 
				
			||||||
 | 
					circumvention of technological measures to the extent such
 | 
				
			||||||
 | 
					circumvention is effected by exercising rights under this License with
 | 
				
			||||||
 | 
					respect to the covered work, and you disclaim any intention to limit
 | 
				
			||||||
 | 
					operation or modification of the work as a means of enforcing, against
 | 
				
			||||||
 | 
					the work's users, your or third parties' legal rights to forbid
 | 
				
			||||||
 | 
					circumvention of technological measures.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 4. Conveying Verbatim Copies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may convey verbatim copies of the Program's source code as you
 | 
				
			||||||
 | 
					receive it, in any medium, provided that you conspicuously and
 | 
				
			||||||
 | 
					appropriately publish on each copy an appropriate copyright notice;
 | 
				
			||||||
 | 
					keep intact all notices stating that this License and any
 | 
				
			||||||
 | 
					non-permissive terms added in accord with section 7 apply to the code;
 | 
				
			||||||
 | 
					keep intact all notices of the absence of any warranty; and give all
 | 
				
			||||||
 | 
					recipients a copy of this License along with the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may charge any price or no price for each copy that you convey,
 | 
				
			||||||
 | 
					and you may offer support or warranty protection for a fee.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 5. Conveying Modified Source Versions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may convey a work based on the Program, or the modifications to
 | 
				
			||||||
 | 
					produce it from the Program, in the form of source code under the
 | 
				
			||||||
 | 
					terms of section 4, provided that you also meet all of these
 | 
				
			||||||
 | 
					conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   a) The work must carry prominent notices stating that you modified
 | 
				
			||||||
 | 
					    it, and giving a relevant date.
 | 
				
			||||||
 | 
					-   b) The work must carry prominent notices stating that it is
 | 
				
			||||||
 | 
					    released under this License and any conditions added under
 | 
				
			||||||
 | 
					    section 7. This requirement modifies the requirement in section 4
 | 
				
			||||||
 | 
					    to "keep intact all notices".
 | 
				
			||||||
 | 
					-   c) You must license the entire work, as a whole, under this
 | 
				
			||||||
 | 
					    License to anyone who comes into possession of a copy. This
 | 
				
			||||||
 | 
					    License will therefore apply, along with any applicable section 7
 | 
				
			||||||
 | 
					    additional terms, to the whole of the work, and all its parts,
 | 
				
			||||||
 | 
					    regardless of how they are packaged. This License gives no
 | 
				
			||||||
 | 
					    permission to license the work in any other way, but it does not
 | 
				
			||||||
 | 
					    invalidate such permission if you have separately received it.
 | 
				
			||||||
 | 
					-   d) If the work has interactive user interfaces, each must display
 | 
				
			||||||
 | 
					    Appropriate Legal Notices; however, if the Program has interactive
 | 
				
			||||||
 | 
					    interfaces that do not display Appropriate Legal Notices, your
 | 
				
			||||||
 | 
					    work need not make them do so.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A compilation of a covered work with other separate and independent
 | 
				
			||||||
 | 
					works, which are not by their nature extensions of the covered work,
 | 
				
			||||||
 | 
					and which are not combined with it such as to form a larger program,
 | 
				
			||||||
 | 
					in or on a volume of a storage or distribution medium, is called an
 | 
				
			||||||
 | 
					"aggregate" if the compilation and its resulting copyright are not
 | 
				
			||||||
 | 
					used to limit the access or legal rights of the compilation's users
 | 
				
			||||||
 | 
					beyond what the individual works permit. Inclusion of a covered work
 | 
				
			||||||
 | 
					in an aggregate does not cause this License to apply to the other
 | 
				
			||||||
 | 
					parts of the aggregate.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 6. Conveying Non-Source Forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may convey a covered work in object code form under the terms of
 | 
				
			||||||
 | 
					sections 4 and 5, provided that you also convey the machine-readable
 | 
				
			||||||
 | 
					Corresponding Source under the terms of this License, in one of these
 | 
				
			||||||
 | 
					ways:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   a) Convey the object code in, or embodied in, a physical product
 | 
				
			||||||
 | 
					    (including a physical distribution medium), accompanied by the
 | 
				
			||||||
 | 
					    Corresponding Source fixed on a durable physical medium
 | 
				
			||||||
 | 
					    customarily used for software interchange.
 | 
				
			||||||
 | 
					-   b) Convey the object code in, or embodied in, a physical product
 | 
				
			||||||
 | 
					    (including a physical distribution medium), accompanied by a
 | 
				
			||||||
 | 
					    written offer, valid for at least three years and valid for as
 | 
				
			||||||
 | 
					    long as you offer spare parts or customer support for that product
 | 
				
			||||||
 | 
					    model, to give anyone who possesses the object code either (1) a
 | 
				
			||||||
 | 
					    copy of the Corresponding Source for all the software in the
 | 
				
			||||||
 | 
					    product that is covered by this License, on a durable physical
 | 
				
			||||||
 | 
					    medium customarily used for software interchange, for a price no
 | 
				
			||||||
 | 
					    more than your reasonable cost of physically performing this
 | 
				
			||||||
 | 
					    conveying of source, or (2) access to copy the Corresponding
 | 
				
			||||||
 | 
					    Source from a network server at no charge.
 | 
				
			||||||
 | 
					-   c) Convey individual copies of the object code with a copy of the
 | 
				
			||||||
 | 
					    written offer to provide the Corresponding Source. This
 | 
				
			||||||
 | 
					    alternative is allowed only occasionally and noncommercially, and
 | 
				
			||||||
 | 
					    only if you received the object code with such an offer, in accord
 | 
				
			||||||
 | 
					    with subsection 6b.
 | 
				
			||||||
 | 
					-   d) Convey the object code by offering access from a designated
 | 
				
			||||||
 | 
					    place (gratis or for a charge), and offer equivalent access to the
 | 
				
			||||||
 | 
					    Corresponding Source in the same way through the same place at no
 | 
				
			||||||
 | 
					    further charge. You need not require recipients to copy the
 | 
				
			||||||
 | 
					    Corresponding Source along with the object code. If the place to
 | 
				
			||||||
 | 
					    copy the object code is a network server, the Corresponding Source
 | 
				
			||||||
 | 
					    may be on a different server (operated by you or a third party)
 | 
				
			||||||
 | 
					    that supports equivalent copying facilities, provided you maintain
 | 
				
			||||||
 | 
					    clear directions next to the object code saying where to find the
 | 
				
			||||||
 | 
					    Corresponding Source. Regardless of what server hosts the
 | 
				
			||||||
 | 
					    Corresponding Source, you remain obligated to ensure that it is
 | 
				
			||||||
 | 
					    available for as long as needed to satisfy these requirements.
 | 
				
			||||||
 | 
					-   e) Convey the object code using peer-to-peer transmission,
 | 
				
			||||||
 | 
					    provided you inform other peers where the object code and
 | 
				
			||||||
 | 
					    Corresponding Source of the work are being offered to the general
 | 
				
			||||||
 | 
					    public at no charge under subsection 6d.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A separable portion of the object code, whose source code is excluded
 | 
				
			||||||
 | 
					from the Corresponding Source as a System Library, need not be
 | 
				
			||||||
 | 
					included in conveying the object code work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A "User Product" is either (1) a "consumer product", which means any
 | 
				
			||||||
 | 
					tangible personal property which is normally used for personal,
 | 
				
			||||||
 | 
					family, or household purposes, or (2) anything designed or sold for
 | 
				
			||||||
 | 
					incorporation into a dwelling. In determining whether a product is a
 | 
				
			||||||
 | 
					consumer product, doubtful cases shall be resolved in favor of
 | 
				
			||||||
 | 
					coverage. For a particular product received by a particular user,
 | 
				
			||||||
 | 
					"normally used" refers to a typical or common use of that class of
 | 
				
			||||||
 | 
					product, regardless of the status of the particular user or of the way
 | 
				
			||||||
 | 
					in which the particular user actually uses, or expects or is expected
 | 
				
			||||||
 | 
					to use, the product. A product is a consumer product regardless of
 | 
				
			||||||
 | 
					whether the product has substantial commercial, industrial or
 | 
				
			||||||
 | 
					non-consumer uses, unless such uses represent the only significant
 | 
				
			||||||
 | 
					mode of use of the product.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"Installation Information" for a User Product means any methods,
 | 
				
			||||||
 | 
					procedures, authorization keys, or other information required to
 | 
				
			||||||
 | 
					install and execute modified versions of a covered work in that User
 | 
				
			||||||
 | 
					Product from a modified version of its Corresponding Source. The
 | 
				
			||||||
 | 
					information must suffice to ensure that the continued functioning of
 | 
				
			||||||
 | 
					the modified object code is in no case prevented or interfered with
 | 
				
			||||||
 | 
					solely because modification has been made.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you convey an object code work under this section in, or with, or
 | 
				
			||||||
 | 
					specifically for use in, a User Product, and the conveying occurs as
 | 
				
			||||||
 | 
					part of a transaction in which the right of possession and use of the
 | 
				
			||||||
 | 
					User Product is transferred to the recipient in perpetuity or for a
 | 
				
			||||||
 | 
					fixed term (regardless of how the transaction is characterized), the
 | 
				
			||||||
 | 
					Corresponding Source conveyed under this section must be accompanied
 | 
				
			||||||
 | 
					by the Installation Information. But this requirement does not apply
 | 
				
			||||||
 | 
					if neither you nor any third party retains the ability to install
 | 
				
			||||||
 | 
					modified object code on the User Product (for example, the work has
 | 
				
			||||||
 | 
					been installed in ROM).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The requirement to provide Installation Information does not include a
 | 
				
			||||||
 | 
					requirement to continue to provide support service, warranty, or
 | 
				
			||||||
 | 
					updates for a work that has been modified or installed by the
 | 
				
			||||||
 | 
					recipient, or for the User Product in which it has been modified or
 | 
				
			||||||
 | 
					installed. Access to a network may be denied when the modification
 | 
				
			||||||
 | 
					itself materially and adversely affects the operation of the network
 | 
				
			||||||
 | 
					or violates the rules and protocols for communication across the
 | 
				
			||||||
 | 
					network.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Corresponding Source conveyed, and Installation Information provided,
 | 
				
			||||||
 | 
					in accord with this section must be in a format that is publicly
 | 
				
			||||||
 | 
					documented (and with an implementation available to the public in
 | 
				
			||||||
 | 
					source code form), and must require no special password or key for
 | 
				
			||||||
 | 
					unpacking, reading or copying.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 7. Additional Terms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"Additional permissions" are terms that supplement the terms of this
 | 
				
			||||||
 | 
					License by making exceptions from one or more of its conditions.
 | 
				
			||||||
 | 
					Additional permissions that are applicable to the entire Program shall
 | 
				
			||||||
 | 
					be treated as though they were included in this License, to the extent
 | 
				
			||||||
 | 
					that they are valid under applicable law. If additional permissions
 | 
				
			||||||
 | 
					apply only to part of the Program, that part may be used separately
 | 
				
			||||||
 | 
					under those permissions, but the entire Program remains governed by
 | 
				
			||||||
 | 
					this License without regard to the additional permissions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When you convey a copy of a covered work, you may at your option
 | 
				
			||||||
 | 
					remove any additional permissions from that copy, or from any part of
 | 
				
			||||||
 | 
					it. (Additional permissions may be written to require their own
 | 
				
			||||||
 | 
					removal in certain cases when you modify the work.) You may place
 | 
				
			||||||
 | 
					additional permissions on material, added by you to a covered work,
 | 
				
			||||||
 | 
					for which you have or can give appropriate copyright permission.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Notwithstanding any other provision of this License, for material you
 | 
				
			||||||
 | 
					add to a covered work, you may (if authorized by the copyright holders
 | 
				
			||||||
 | 
					of that material) supplement the terms of this License with terms:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   a) Disclaiming warranty or limiting liability differently from the
 | 
				
			||||||
 | 
					    terms of sections 15 and 16 of this License; or
 | 
				
			||||||
 | 
					-   b) Requiring preservation of specified reasonable legal notices or
 | 
				
			||||||
 | 
					    author attributions in that material or in the Appropriate Legal
 | 
				
			||||||
 | 
					    Notices displayed by works containing it; or
 | 
				
			||||||
 | 
					-   c) Prohibiting misrepresentation of the origin of that material,
 | 
				
			||||||
 | 
					    or requiring that modified versions of such material be marked in
 | 
				
			||||||
 | 
					    reasonable ways as different from the original version; or
 | 
				
			||||||
 | 
					-   d) Limiting the use for publicity purposes of names of licensors
 | 
				
			||||||
 | 
					    or authors of the material; or
 | 
				
			||||||
 | 
					-   e) Declining to grant rights under trademark law for use of some
 | 
				
			||||||
 | 
					    trade names, trademarks, or service marks; or
 | 
				
			||||||
 | 
					-   f) Requiring indemnification of licensors and authors of that
 | 
				
			||||||
 | 
					    material by anyone who conveys the material (or modified versions
 | 
				
			||||||
 | 
					    of it) with contractual assumptions of liability to the recipient,
 | 
				
			||||||
 | 
					    for any liability that these contractual assumptions directly
 | 
				
			||||||
 | 
					    impose on those licensors and authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All other non-permissive additional terms are considered "further
 | 
				
			||||||
 | 
					restrictions" within the meaning of section 10. If the Program as you
 | 
				
			||||||
 | 
					received it, or any part of it, contains a notice stating that it is
 | 
				
			||||||
 | 
					governed by this License along with a term that is a further
 | 
				
			||||||
 | 
					restriction, you may remove that term. If a license document contains
 | 
				
			||||||
 | 
					a further restriction but permits relicensing or conveying under this
 | 
				
			||||||
 | 
					License, you may add to a covered work material governed by the terms
 | 
				
			||||||
 | 
					of that license document, provided that the further restriction does
 | 
				
			||||||
 | 
					not survive such relicensing or conveying.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you add terms to a covered work in accord with this section, you
 | 
				
			||||||
 | 
					must place, in the relevant source files, a statement of the
 | 
				
			||||||
 | 
					additional terms that apply to those files, or a notice indicating
 | 
				
			||||||
 | 
					where to find the applicable terms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Additional terms, permissive or non-permissive, may be stated in the
 | 
				
			||||||
 | 
					form of a separately written license, or stated as exceptions; the
 | 
				
			||||||
 | 
					above requirements apply either way.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 8. Termination.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may not propagate or modify a covered work except as expressly
 | 
				
			||||||
 | 
					provided under this License. Any attempt otherwise to propagate or
 | 
				
			||||||
 | 
					modify it is void, and will automatically terminate your rights under
 | 
				
			||||||
 | 
					this License (including any patent licenses granted under the third
 | 
				
			||||||
 | 
					paragraph of section 11).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					However, if you cease all violation of this License, then your license
 | 
				
			||||||
 | 
					from a particular copyright holder is reinstated (a) provisionally,
 | 
				
			||||||
 | 
					unless and until the copyright holder explicitly and finally
 | 
				
			||||||
 | 
					terminates your license, and (b) permanently, if the copyright holder
 | 
				
			||||||
 | 
					fails to notify you of the violation by some reasonable means prior to
 | 
				
			||||||
 | 
					60 days after the cessation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Moreover, your license from a particular copyright holder is
 | 
				
			||||||
 | 
					reinstated permanently if the copyright holder notifies you of the
 | 
				
			||||||
 | 
					violation by some reasonable means, this is the first time you have
 | 
				
			||||||
 | 
					received notice of violation of this License (for any work) from that
 | 
				
			||||||
 | 
					copyright holder, and you cure the violation prior to 30 days after
 | 
				
			||||||
 | 
					your receipt of the notice.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Termination of your rights under this section does not terminate the
 | 
				
			||||||
 | 
					licenses of parties who have received copies or rights from you under
 | 
				
			||||||
 | 
					this License. If your rights have been terminated and not permanently
 | 
				
			||||||
 | 
					reinstated, you do not qualify to receive new licenses for the same
 | 
				
			||||||
 | 
					material under section 10.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 9. Acceptance Not Required for Having Copies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You are not required to accept this License in order to receive or run
 | 
				
			||||||
 | 
					a copy of the Program. Ancillary propagation of a covered work
 | 
				
			||||||
 | 
					occurring solely as a consequence of using peer-to-peer transmission
 | 
				
			||||||
 | 
					to receive a copy likewise does not require acceptance. However,
 | 
				
			||||||
 | 
					nothing other than this License grants you permission to propagate or
 | 
				
			||||||
 | 
					modify any covered work. These actions infringe copyright if you do
 | 
				
			||||||
 | 
					not accept this License. Therefore, by modifying or propagating a
 | 
				
			||||||
 | 
					covered work, you indicate your acceptance of this License to do so.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 10. Automatic Licensing of Downstream Recipients.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Each time you convey a covered work, the recipient automatically
 | 
				
			||||||
 | 
					receives a license from the original licensors, to run, modify and
 | 
				
			||||||
 | 
					propagate that work, subject to this License. You are not responsible
 | 
				
			||||||
 | 
					for enforcing compliance by third parties with this License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An "entity transaction" is a transaction transferring control of an
 | 
				
			||||||
 | 
					organization, or substantially all assets of one, or subdividing an
 | 
				
			||||||
 | 
					organization, or merging organizations. If propagation of a covered
 | 
				
			||||||
 | 
					work results from an entity transaction, each party to that
 | 
				
			||||||
 | 
					transaction who receives a copy of the work also receives whatever
 | 
				
			||||||
 | 
					licenses to the work the party's predecessor in interest had or could
 | 
				
			||||||
 | 
					give under the previous paragraph, plus a right to possession of the
 | 
				
			||||||
 | 
					Corresponding Source of the work from the predecessor in interest, if
 | 
				
			||||||
 | 
					the predecessor has it or can get it with reasonable efforts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may not impose any further restrictions on the exercise of the
 | 
				
			||||||
 | 
					rights granted or affirmed under this License. For example, you may
 | 
				
			||||||
 | 
					not impose a license fee, royalty, or other charge for exercise of
 | 
				
			||||||
 | 
					rights granted under this License, and you may not initiate litigation
 | 
				
			||||||
 | 
					(including a cross-claim or counterclaim in a lawsuit) alleging that
 | 
				
			||||||
 | 
					any patent claim is infringed by making, using, selling, offering for
 | 
				
			||||||
 | 
					sale, or importing the Program or any portion of it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 11. Patents.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A "contributor" is a copyright holder who authorizes use under this
 | 
				
			||||||
 | 
					License of the Program or a work on which the Program is based. The
 | 
				
			||||||
 | 
					work thus licensed is called the contributor's "contributor version".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A contributor's "essential patent claims" are all patent claims owned
 | 
				
			||||||
 | 
					or controlled by the contributor, whether already acquired or
 | 
				
			||||||
 | 
					hereafter acquired, that would be infringed by some manner, permitted
 | 
				
			||||||
 | 
					by this License, of making, using, or selling its contributor version,
 | 
				
			||||||
 | 
					but do not include claims that would be infringed only as a
 | 
				
			||||||
 | 
					consequence of further modification of the contributor version. For
 | 
				
			||||||
 | 
					purposes of this definition, "control" includes the right to grant
 | 
				
			||||||
 | 
					patent sublicenses in a manner consistent with the requirements of
 | 
				
			||||||
 | 
					this License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Each contributor grants you a non-exclusive, worldwide, royalty-free
 | 
				
			||||||
 | 
					patent license under the contributor's essential patent claims, to
 | 
				
			||||||
 | 
					make, use, sell, offer for sale, import and otherwise run, modify and
 | 
				
			||||||
 | 
					propagate the contents of its contributor version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					In the following three paragraphs, a "patent license" is any express
 | 
				
			||||||
 | 
					agreement or commitment, however denominated, not to enforce a patent
 | 
				
			||||||
 | 
					(such as an express permission to practice a patent or covenant not to
 | 
				
			||||||
 | 
					sue for patent infringement). To "grant" such a patent license to a
 | 
				
			||||||
 | 
					party means to make such an agreement or commitment not to enforce a
 | 
				
			||||||
 | 
					patent against the party.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you convey a covered work, knowingly relying on a patent license,
 | 
				
			||||||
 | 
					and the Corresponding Source of the work is not available for anyone
 | 
				
			||||||
 | 
					to copy, free of charge and under the terms of this License, through a
 | 
				
			||||||
 | 
					publicly available network server or other readily accessible means,
 | 
				
			||||||
 | 
					then you must either (1) cause the Corresponding Source to be so
 | 
				
			||||||
 | 
					available, or (2) arrange to deprive yourself of the benefit of the
 | 
				
			||||||
 | 
					patent license for this particular work, or (3) arrange, in a manner
 | 
				
			||||||
 | 
					consistent with the requirements of this License, to extend the patent
 | 
				
			||||||
 | 
					license to downstream recipients. "Knowingly relying" means you have
 | 
				
			||||||
 | 
					actual knowledge that, but for the patent license, your conveying the
 | 
				
			||||||
 | 
					covered work in a country, or your recipient's use of the covered work
 | 
				
			||||||
 | 
					in a country, would infringe one or more identifiable patents in that
 | 
				
			||||||
 | 
					country that you have reason to believe are valid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If, pursuant to or in connection with a single transaction or
 | 
				
			||||||
 | 
					arrangement, you convey, or propagate by procuring conveyance of, a
 | 
				
			||||||
 | 
					covered work, and grant a patent license to some of the parties
 | 
				
			||||||
 | 
					receiving the covered work authorizing them to use, propagate, modify
 | 
				
			||||||
 | 
					or convey a specific copy of the covered work, then the patent license
 | 
				
			||||||
 | 
					you grant is automatically extended to all recipients of the covered
 | 
				
			||||||
 | 
					work and works based on it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A patent license is "discriminatory" if it does not include within the
 | 
				
			||||||
 | 
					scope of its coverage, prohibits the exercise of, or is conditioned on
 | 
				
			||||||
 | 
					the non-exercise of one or more of the rights that are specifically
 | 
				
			||||||
 | 
					granted under this License. You may not convey a covered work if you
 | 
				
			||||||
 | 
					are a party to an arrangement with a third party that is in the
 | 
				
			||||||
 | 
					business of distributing software, under which you make payment to the
 | 
				
			||||||
 | 
					third party based on the extent of your activity of conveying the
 | 
				
			||||||
 | 
					work, and under which the third party grants, to any of the parties
 | 
				
			||||||
 | 
					who would receive the covered work from you, a discriminatory patent
 | 
				
			||||||
 | 
					license (a) in connection with copies of the covered work conveyed by
 | 
				
			||||||
 | 
					you (or copies made from those copies), or (b) primarily for and in
 | 
				
			||||||
 | 
					connection with specific products or compilations that contain the
 | 
				
			||||||
 | 
					covered work, unless you entered into that arrangement, or that patent
 | 
				
			||||||
 | 
					license was granted, prior to 28 March 2007.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Nothing in this License shall be construed as excluding or limiting
 | 
				
			||||||
 | 
					any implied license or other defenses to infringement that may
 | 
				
			||||||
 | 
					otherwise be available to you under applicable patent law.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 12. No Surrender of Others' Freedom.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If conditions are imposed on you (whether by court order, agreement or
 | 
				
			||||||
 | 
					otherwise) that contradict the conditions of this License, they do not
 | 
				
			||||||
 | 
					excuse you from the conditions of this License. If you cannot convey a
 | 
				
			||||||
 | 
					covered work so as to satisfy simultaneously your obligations under
 | 
				
			||||||
 | 
					this License and any other pertinent obligations, then as a
 | 
				
			||||||
 | 
					consequence you may not convey it at all. For example, if you agree to
 | 
				
			||||||
 | 
					terms that obligate you to collect a royalty for further conveying
 | 
				
			||||||
 | 
					from those to whom you convey the Program, the only way you could
 | 
				
			||||||
 | 
					satisfy both those terms and this License would be to refrain entirely
 | 
				
			||||||
 | 
					from conveying the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 13. Remote Network Interaction; Use with the GNU General Public License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Notwithstanding any other provision of this License, if you modify the
 | 
				
			||||||
 | 
					Program, your modified version must prominently offer all users
 | 
				
			||||||
 | 
					interacting with it remotely through a computer network (if your
 | 
				
			||||||
 | 
					version supports such interaction) an opportunity to receive the
 | 
				
			||||||
 | 
					Corresponding Source of your version by providing access to the
 | 
				
			||||||
 | 
					Corresponding Source from a network server at no charge, through some
 | 
				
			||||||
 | 
					standard or customary means of facilitating copying of software. This
 | 
				
			||||||
 | 
					Corresponding Source shall include the Corresponding Source for any
 | 
				
			||||||
 | 
					work covered by version 3 of the GNU General Public License that is
 | 
				
			||||||
 | 
					incorporated pursuant to the following paragraph.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Notwithstanding any other provision of this License, you have
 | 
				
			||||||
 | 
					permission to link or combine any covered work with a work licensed
 | 
				
			||||||
 | 
					under version 3 of the GNU General Public License into a single
 | 
				
			||||||
 | 
					combined work, and to convey the resulting work. The terms of this
 | 
				
			||||||
 | 
					License will continue to apply to the part which is the covered work,
 | 
				
			||||||
 | 
					but the work with which it is combined will remain governed by version
 | 
				
			||||||
 | 
					3 of the GNU General Public License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 14. Revised Versions of this License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Free Software Foundation may publish revised and/or new versions
 | 
				
			||||||
 | 
					of the GNU Affero General Public License from time to time. Such new
 | 
				
			||||||
 | 
					versions will be similar in spirit to the present version, but may
 | 
				
			||||||
 | 
					differ in detail to address new problems or concerns.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Each version is given a distinguishing version number. If the Program
 | 
				
			||||||
 | 
					specifies that a certain numbered version of the GNU Affero General
 | 
				
			||||||
 | 
					Public License "or any later version" applies to it, you have the
 | 
				
			||||||
 | 
					option of following the terms and conditions either of that numbered
 | 
				
			||||||
 | 
					version or of any later version published by the Free Software
 | 
				
			||||||
 | 
					Foundation. If the Program does not specify a version number of the
 | 
				
			||||||
 | 
					GNU Affero General Public License, you may choose any version ever
 | 
				
			||||||
 | 
					published by the Free Software Foundation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If the Program specifies that a proxy can decide which future versions
 | 
				
			||||||
 | 
					of the GNU Affero General Public License can be used, that proxy's
 | 
				
			||||||
 | 
					public statement of acceptance of a version permanently authorizes you
 | 
				
			||||||
 | 
					to choose that version for the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Later license versions may give you additional or different
 | 
				
			||||||
 | 
					permissions. However, no additional obligations are imposed on any
 | 
				
			||||||
 | 
					author or copyright holder as a result of your choosing to follow a
 | 
				
			||||||
 | 
					later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 15. Disclaimer of Warranty.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
 | 
				
			||||||
 | 
					APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
 | 
				
			||||||
 | 
					HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
 | 
				
			||||||
 | 
					WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
 | 
				
			||||||
 | 
					LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 | 
				
			||||||
 | 
					A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
 | 
				
			||||||
 | 
					PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
 | 
				
			||||||
 | 
					DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
 | 
				
			||||||
 | 
					CORRECTION.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 16. Limitation of Liability.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
 | 
				
			||||||
 | 
					WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
 | 
				
			||||||
 | 
					CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
 | 
				
			||||||
 | 
					INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
 | 
				
			||||||
 | 
					ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
 | 
				
			||||||
 | 
					NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
 | 
				
			||||||
 | 
					LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
 | 
				
			||||||
 | 
					TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
 | 
				
			||||||
 | 
					PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 17. Interpretation of Sections 15 and 16.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If the disclaimer of warranty and limitation of liability provided
 | 
				
			||||||
 | 
					above cannot be given local legal effect according to their terms,
 | 
				
			||||||
 | 
					reviewing courts shall apply local law that most closely approximates
 | 
				
			||||||
 | 
					an absolute waiver of all civil liability in connection with the
 | 
				
			||||||
 | 
					Program, unless a warranty or assumption of liability accompanies a
 | 
				
			||||||
 | 
					copy of the Program in return for a fee.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					END OF TERMS AND CONDITIONS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## How to Apply These Terms to Your New Programs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you develop a new program, and you want it to be of the greatest
 | 
				
			||||||
 | 
					possible use to the public, the best way to achieve this is to make it
 | 
				
			||||||
 | 
					free software which everyone can redistribute and change under these
 | 
				
			||||||
 | 
					terms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To do so, attach the following notices to the program. It is safest to
 | 
				
			||||||
 | 
					attach them to the start of each source file to most effectively state
 | 
				
			||||||
 | 
					the exclusion of warranty; and each file should have at least the
 | 
				
			||||||
 | 
					"copyright" line and a pointer to where the full notice is found.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <one line to give the program's name and a brief idea of what it does.>
 | 
				
			||||||
 | 
					        Copyright (C) <year>  <name of author>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This program is free software: you can redistribute it and/or modify
 | 
				
			||||||
 | 
					        it under the terms of the GNU Affero General Public License as
 | 
				
			||||||
 | 
					        published by the Free Software Foundation, either version 3 of the
 | 
				
			||||||
 | 
					        License, or (at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This program is distributed in the hope that it will be useful,
 | 
				
			||||||
 | 
					        but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
 | 
					        GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        You should have received a copy of the GNU Affero General Public License
 | 
				
			||||||
 | 
					        along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Also add information on how to contact you by electronic and paper
 | 
				
			||||||
 | 
					mail.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If your software can interact with users remotely through a computer
 | 
				
			||||||
 | 
					network, you should also make sure that it provides a way for users to
 | 
				
			||||||
 | 
					get its source. For example, if your program is a web application, its
 | 
				
			||||||
 | 
					interface could display a "Source" link that leads users to an archive
 | 
				
			||||||
 | 
					of the code. There are many ways you could offer source, and different
 | 
				
			||||||
 | 
					solutions will be better for different programs; see section 13 for
 | 
				
			||||||
 | 
					the specific requirements.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You should also get your employer (if you work as a programmer) or
 | 
				
			||||||
 | 
					school, if any, to sign a "copyright disclaimer" for the program, if
 | 
				
			||||||
 | 
					necessary. For more information on this, and how to apply and follow
 | 
				
			||||||
 | 
					the GNU AGPL, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
@ -1,8 +1,8 @@
 | 
				
			|||||||
# syntax = docker/dockerfile:1
 | 
					# syntax = docker/dockerfile:1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
 | 
					# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
 | 
				
			||||||
ARG RUBY_VERSION=3.3.4
 | 
					ARG RUBY_VERSION=3.4.1
 | 
				
			||||||
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
 | 
					FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Rails app lives here
 | 
					# Rails app lives here
 | 
				
			||||||
WORKDIR /rails
 | 
					WORKDIR /rails
 | 
				
			||||||
@ -16,11 +16,11 @@ ENV RAILS_ENV="production" \
 | 
				
			|||||||
RUN apt-get update && apt-get install -y nodejs
 | 
					RUN apt-get update && apt-get install -y nodejs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Throw-away build stage to reduce size of final image
 | 
					# Throw-away build stage to reduce size of final image
 | 
				
			||||||
FROM base as build
 | 
					FROM base AS build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Install packages needed to build gems
 | 
					# Install packages needed to build gems
 | 
				
			||||||
RUN apt-get update -qq && \
 | 
					RUN apt-get update -qq && \
 | 
				
			||||||
    apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config
 | 
					    apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config libyaml-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Install application gems
 | 
					# Install application gems
 | 
				
			||||||
COPY Gemfile Gemfile.lock ./
 | 
					COPY Gemfile Gemfile.lock ./
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										42
									
								
								Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					# syntax = docker/dockerfile:1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
 | 
				
			||||||
 | 
					ARG RUBY_VERSION=3.4.1
 | 
				
			||||||
 | 
					FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Rails app lives here
 | 
				
			||||||
 | 
					WORKDIR /rails
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN apt-get update && apt-get install -y nodejs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FROM base as build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install packages needed to build gems
 | 
				
			||||||
 | 
					RUN apt-get update -qq && \
 | 
				
			||||||
 | 
					    apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config libyaml-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install application gems
 | 
				
			||||||
 | 
					COPY Gemfile Gemfile.lock ./
 | 
				
			||||||
 | 
					RUN bundle install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy application code
 | 
				
			||||||
 | 
					COPY . .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Final stage for app image
 | 
				
			||||||
 | 
					FROM base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install packages needed for deployment
 | 
				
			||||||
 | 
					RUN apt-get update -qq && \
 | 
				
			||||||
 | 
					    apt-get install --no-install-recommends -y curl libvips postgresql-client && \
 | 
				
			||||||
 | 
					    rm -rf /var/lib/apt/lists /var/cache/apt/archives
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy built artifacts: gems, application
 | 
				
			||||||
 | 
					COPY --from=build /usr/local/bundle /usr/local/bundle
 | 
				
			||||||
 | 
					COPY --from=build /rails /rails
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Entrypoint prepares the database.
 | 
				
			||||||
 | 
					ENTRYPOINT ["/rails/bin/docker-entrypoint"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Start the server by default, this can be overwritten at runtime
 | 
				
			||||||
 | 
					EXPOSE 3000
 | 
				
			||||||
 | 
					CMD ["./bin/rails", "server", "--binding=0.0.0.0"]
 | 
				
			||||||
							
								
								
									
										28
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								Gemfile
									
									
									
									
									
								
							@ -1,7 +1,8 @@
 | 
				
			|||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
source 'https://rubygems.org'
 | 
					source 'https://rubygems.org'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ruby '3.3.4'
 | 
					ruby '3.4.1'
 | 
				
			||||||
gem 'acts-as-taggable-on'
 | 
					 | 
				
			||||||
gem 'bootsnap', require: false
 | 
					gem 'bootsnap', require: false
 | 
				
			||||||
gem 'csv'
 | 
					gem 'csv'
 | 
				
			||||||
gem 'importmap-rails'
 | 
					gem 'importmap-rails'
 | 
				
			||||||
@ -9,27 +10,44 @@ gem 'jbuilder'
 | 
				
			|||||||
gem 'money'
 | 
					gem 'money'
 | 
				
			||||||
gem 'pg', '~> 1.1'
 | 
					gem 'pg', '~> 1.1'
 | 
				
			||||||
gem 'puma', '>= 5.0'
 | 
					gem 'puma', '>= 5.0'
 | 
				
			||||||
gem 'rails', '~> 7.1.3', '>= 7.1.3.2'
 | 
					gem 'rails', '~> 8.0.0', '>= 8.0.0'
 | 
				
			||||||
gem 'redis', '>= 4.0.1'
 | 
					gem 'redis', '>= 4.0.1'
 | 
				
			||||||
gem 'sprockets-rails'
 | 
					gem 'sprockets-rails'
 | 
				
			||||||
gem 'stimulus-rails'
 | 
					gem 'stimulus-rails'
 | 
				
			||||||
gem 'turbo-rails'
 | 
					gem 'turbo-rails'
 | 
				
			||||||
gem 'tzinfo-data', platforms: %i[windows jruby]
 | 
					gem 'tzinfo-data', platforms: %i[windows jruby]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gem 'acts_as_tenant'
 | 
				
			||||||
 | 
					gem 'faker'
 | 
				
			||||||
 | 
					gem 'httparty'
 | 
				
			||||||
gem 'jsonapi-rails'
 | 
					gem 'jsonapi-rails'
 | 
				
			||||||
 | 
					gem 'pluck_to_hash'
 | 
				
			||||||
gem 'rack-cors'
 | 
					gem 'rack-cors'
 | 
				
			||||||
gem 'react-rails'
 | 
					gem 'react-rails'
 | 
				
			||||||
 | 
					gem 'rswag'
 | 
				
			||||||
gem 'rubytree'
 | 
					gem 'rubytree'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
group :development, :test do
 | 
					group :development, :test do
 | 
				
			||||||
 | 
					  gem 'annotaterb'
 | 
				
			||||||
  gem 'debug', platforms: %i[mri windows]
 | 
					  gem 'debug', platforms: %i[mri windows]
 | 
				
			||||||
  gem 'factory_bot_rails'
 | 
					  gem 'factory_bot_rails'
 | 
				
			||||||
  gem 'faker'
 | 
					  gem 'license_finder'
 | 
				
			||||||
  gem 'pry'
 | 
					  gem 'pry'
 | 
				
			||||||
  gem 'rspec-rails', '~> 6.1.0'
 | 
					  gem 'rspec-rails', '~> 7.1.0'
 | 
				
			||||||
 | 
					  gem 'shoulda-matchers', '~> 6.0'
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
group :development do
 | 
					group :development do
 | 
				
			||||||
 | 
					  gem 'letter_opener_web'
 | 
				
			||||||
  gem 'rubocop'
 | 
					  gem 'rubocop'
 | 
				
			||||||
 | 
					  gem 'rubocop-factory_bot', require: false
 | 
				
			||||||
 | 
					  gem 'rubocop-rails', require: false
 | 
				
			||||||
 | 
					  gem 'rubocop-rspec', require: false
 | 
				
			||||||
 | 
					  gem 'rubocop-rspec_rails', require: false
 | 
				
			||||||
  gem 'web-console'
 | 
					  gem 'web-console'
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gem 'chroma'
 | 
				
			||||||
 | 
					gem 'solid_queue', '~> 1.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gem 'devise', '~> 4.9'
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										560
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										560
									
								
								Gemfile.lock
									
									
									
									
									
								
							@ -1,129 +1,151 @@
 | 
				
			|||||||
GEM
 | 
					GEM
 | 
				
			||||||
  remote: https://rubygems.org/
 | 
					  remote: https://rubygems.org/
 | 
				
			||||||
  specs:
 | 
					  specs:
 | 
				
			||||||
    actioncable (7.1.3.4)
 | 
					    actioncable (8.0.1)
 | 
				
			||||||
      actionpack (= 7.1.3.4)
 | 
					      actionpack (= 8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      nio4r (~> 2.0)
 | 
					      nio4r (~> 2.0)
 | 
				
			||||||
      websocket-driver (>= 0.6.1)
 | 
					      websocket-driver (>= 0.6.1)
 | 
				
			||||||
      zeitwerk (~> 2.6)
 | 
					      zeitwerk (~> 2.6)
 | 
				
			||||||
    actionmailbox (7.1.3.4)
 | 
					    actionmailbox (8.0.1)
 | 
				
			||||||
      actionpack (= 7.1.3.4)
 | 
					      actionpack (= 8.0.1)
 | 
				
			||||||
      activejob (= 7.1.3.4)
 | 
					      activejob (= 8.0.1)
 | 
				
			||||||
      activerecord (= 7.1.3.4)
 | 
					      activerecord (= 8.0.1)
 | 
				
			||||||
      activestorage (= 7.1.3.4)
 | 
					      activestorage (= 8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      mail (>= 2.7.1)
 | 
					      mail (>= 2.8.0)
 | 
				
			||||||
      net-imap
 | 
					    actionmailer (8.0.1)
 | 
				
			||||||
      net-pop
 | 
					      actionpack (= 8.0.1)
 | 
				
			||||||
      net-smtp
 | 
					      actionview (= 8.0.1)
 | 
				
			||||||
    actionmailer (7.1.3.4)
 | 
					      activejob (= 8.0.1)
 | 
				
			||||||
      actionpack (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      actionview (= 7.1.3.4)
 | 
					      mail (>= 2.8.0)
 | 
				
			||||||
      activejob (= 7.1.3.4)
 | 
					 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					 | 
				
			||||||
      mail (~> 2.5, >= 2.5.4)
 | 
					 | 
				
			||||||
      net-imap
 | 
					 | 
				
			||||||
      net-pop
 | 
					 | 
				
			||||||
      net-smtp
 | 
					 | 
				
			||||||
      rails-dom-testing (~> 2.2)
 | 
					      rails-dom-testing (~> 2.2)
 | 
				
			||||||
    actionpack (7.1.3.4)
 | 
					    actionpack (8.0.1)
 | 
				
			||||||
      actionview (= 7.1.3.4)
 | 
					      actionview (= 8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      nokogiri (>= 1.8.5)
 | 
					      nokogiri (>= 1.8.5)
 | 
				
			||||||
      racc
 | 
					 | 
				
			||||||
      rack (>= 2.2.4)
 | 
					      rack (>= 2.2.4)
 | 
				
			||||||
      rack-session (>= 1.0.1)
 | 
					      rack-session (>= 1.0.1)
 | 
				
			||||||
      rack-test (>= 0.6.3)
 | 
					      rack-test (>= 0.6.3)
 | 
				
			||||||
      rails-dom-testing (~> 2.2)
 | 
					      rails-dom-testing (~> 2.2)
 | 
				
			||||||
      rails-html-sanitizer (~> 1.6)
 | 
					      rails-html-sanitizer (~> 1.6)
 | 
				
			||||||
    actiontext (7.1.3.4)
 | 
					      useragent (~> 0.16)
 | 
				
			||||||
      actionpack (= 7.1.3.4)
 | 
					    actiontext (8.0.1)
 | 
				
			||||||
      activerecord (= 7.1.3.4)
 | 
					      actionpack (= 8.0.1)
 | 
				
			||||||
      activestorage (= 7.1.3.4)
 | 
					      activerecord (= 8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activestorage (= 8.0.1)
 | 
				
			||||||
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      globalid (>= 0.6.0)
 | 
					      globalid (>= 0.6.0)
 | 
				
			||||||
      nokogiri (>= 1.8.5)
 | 
					      nokogiri (>= 1.8.5)
 | 
				
			||||||
    actionview (7.1.3.4)
 | 
					    actionview (8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      builder (~> 3.1)
 | 
					      builder (~> 3.1)
 | 
				
			||||||
      erubi (~> 1.11)
 | 
					      erubi (~> 1.11)
 | 
				
			||||||
      rails-dom-testing (~> 2.2)
 | 
					      rails-dom-testing (~> 2.2)
 | 
				
			||||||
      rails-html-sanitizer (~> 1.6)
 | 
					      rails-html-sanitizer (~> 1.6)
 | 
				
			||||||
    activejob (7.1.3.4)
 | 
					    activejob (8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      globalid (>= 0.3.6)
 | 
					      globalid (>= 0.3.6)
 | 
				
			||||||
    activemodel (7.1.3.4)
 | 
					    activemodel (8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
    activerecord (7.1.3.4)
 | 
					    activerecord (8.0.1)
 | 
				
			||||||
      activemodel (= 7.1.3.4)
 | 
					      activemodel (= 8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      timeout (>= 0.4.0)
 | 
					      timeout (>= 0.4.0)
 | 
				
			||||||
    activestorage (7.1.3.4)
 | 
					    activestorage (8.0.1)
 | 
				
			||||||
      actionpack (= 7.1.3.4)
 | 
					      actionpack (= 8.0.1)
 | 
				
			||||||
      activejob (= 7.1.3.4)
 | 
					      activejob (= 8.0.1)
 | 
				
			||||||
      activerecord (= 7.1.3.4)
 | 
					      activerecord (= 8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      marcel (~> 1.0)
 | 
					      marcel (~> 1.0)
 | 
				
			||||||
    activesupport (7.1.3.4)
 | 
					    activesupport (8.0.1)
 | 
				
			||||||
      base64
 | 
					      base64
 | 
				
			||||||
 | 
					      benchmark (>= 0.3)
 | 
				
			||||||
      bigdecimal
 | 
					      bigdecimal
 | 
				
			||||||
      concurrent-ruby (~> 1.0, >= 1.0.2)
 | 
					      concurrent-ruby (~> 1.0, >= 1.3.1)
 | 
				
			||||||
      connection_pool (>= 2.2.5)
 | 
					      connection_pool (>= 2.2.5)
 | 
				
			||||||
      drb
 | 
					      drb
 | 
				
			||||||
      i18n (>= 1.6, < 2)
 | 
					      i18n (>= 1.6, < 2)
 | 
				
			||||||
 | 
					      logger (>= 1.4.2)
 | 
				
			||||||
      minitest (>= 5.1)
 | 
					      minitest (>= 5.1)
 | 
				
			||||||
      mutex_m
 | 
					      securerandom (>= 0.3)
 | 
				
			||||||
      tzinfo (~> 2.0)
 | 
					      tzinfo (~> 2.0, >= 2.0.5)
 | 
				
			||||||
    acts-as-taggable-on (10.0.0)
 | 
					      uri (>= 0.13.1)
 | 
				
			||||||
      activerecord (>= 6.1, < 7.2)
 | 
					    acts_as_tenant (1.0.1)
 | 
				
			||||||
 | 
					      rails (>= 6.0)
 | 
				
			||||||
 | 
					    addressable (2.8.7)
 | 
				
			||||||
 | 
					      public_suffix (>= 2.0.2, < 7.0)
 | 
				
			||||||
 | 
					    annotaterb (4.13.0)
 | 
				
			||||||
    ast (2.4.2)
 | 
					    ast (2.4.2)
 | 
				
			||||||
    babel-source (5.8.35)
 | 
					    babel-source (5.8.35)
 | 
				
			||||||
    babel-transpiler (0.7.0)
 | 
					    babel-transpiler (0.7.0)
 | 
				
			||||||
      babel-source (>= 4.0, < 6)
 | 
					      babel-source (>= 4.0, < 6)
 | 
				
			||||||
      execjs (~> 2.0)
 | 
					      execjs (~> 2.0)
 | 
				
			||||||
    base64 (0.2.0)
 | 
					    base64 (0.2.0)
 | 
				
			||||||
    bigdecimal (3.1.8)
 | 
					    bcrypt (3.1.20)
 | 
				
			||||||
 | 
					    benchmark (0.4.0)
 | 
				
			||||||
 | 
					    bigdecimal (3.1.9)
 | 
				
			||||||
    bindex (0.8.1)
 | 
					    bindex (0.8.1)
 | 
				
			||||||
    bootsnap (1.18.4)
 | 
					    bootsnap (1.18.4)
 | 
				
			||||||
      msgpack (~> 1.2)
 | 
					      msgpack (~> 1.2)
 | 
				
			||||||
    builder (3.3.0)
 | 
					    builder (3.3.0)
 | 
				
			||||||
 | 
					    childprocess (5.1.0)
 | 
				
			||||||
 | 
					      logger (~> 1.5)
 | 
				
			||||||
 | 
					    chroma (0.2.0)
 | 
				
			||||||
    coderay (1.1.3)
 | 
					    coderay (1.1.3)
 | 
				
			||||||
    concurrent-ruby (1.3.4)
 | 
					    concurrent-ruby (1.3.5)
 | 
				
			||||||
    connection_pool (2.4.1)
 | 
					    connection_pool (2.4.1)
 | 
				
			||||||
    crass (1.0.6)
 | 
					    crass (1.0.6)
 | 
				
			||||||
    csv (3.3.0)
 | 
					    csv (3.3.2)
 | 
				
			||||||
    date (3.3.4)
 | 
					    date (3.4.1)
 | 
				
			||||||
    debug (1.9.2)
 | 
					    debug (1.10.0)
 | 
				
			||||||
      irb (~> 1.10)
 | 
					      irb (~> 1.10)
 | 
				
			||||||
      reline (>= 0.3.8)
 | 
					      reline (>= 0.3.8)
 | 
				
			||||||
 | 
					    devise (4.9.4)
 | 
				
			||||||
 | 
					      bcrypt (~> 3.0)
 | 
				
			||||||
 | 
					      orm_adapter (~> 0.1)
 | 
				
			||||||
 | 
					      railties (>= 4.1.0)
 | 
				
			||||||
 | 
					      responders
 | 
				
			||||||
 | 
					      warden (~> 1.2.3)
 | 
				
			||||||
    diff-lcs (1.5.1)
 | 
					    diff-lcs (1.5.1)
 | 
				
			||||||
    drb (2.2.1)
 | 
					    drb (2.2.1)
 | 
				
			||||||
    erubi (1.13.0)
 | 
					    erubi (1.13.1)
 | 
				
			||||||
 | 
					    et-orbi (1.2.11)
 | 
				
			||||||
 | 
					      tzinfo
 | 
				
			||||||
    execjs (2.9.1)
 | 
					    execjs (2.9.1)
 | 
				
			||||||
    factory_bot (6.4.6)
 | 
					    factory_bot (6.4.6)
 | 
				
			||||||
      activesupport (>= 5.0.0)
 | 
					      activesupport (>= 5.0.0)
 | 
				
			||||||
    factory_bot_rails (6.4.3)
 | 
					    factory_bot_rails (6.4.3)
 | 
				
			||||||
      factory_bot (~> 6.4)
 | 
					      factory_bot (~> 6.4)
 | 
				
			||||||
      railties (>= 5.0.0)
 | 
					      railties (>= 5.0.0)
 | 
				
			||||||
    faker (3.4.2)
 | 
					    faker (3.5.1)
 | 
				
			||||||
      i18n (>= 1.8.11, < 2)
 | 
					      i18n (>= 1.8.11, < 2)
 | 
				
			||||||
 | 
					    fugit (1.11.1)
 | 
				
			||||||
 | 
					      et-orbi (~> 1, >= 1.2.11)
 | 
				
			||||||
 | 
					      raabro (~> 1.4)
 | 
				
			||||||
    globalid (1.2.1)
 | 
					    globalid (1.2.1)
 | 
				
			||||||
      activesupport (>= 6.1)
 | 
					      activesupport (>= 6.1)
 | 
				
			||||||
    i18n (1.14.5)
 | 
					    httparty (0.22.0)
 | 
				
			||||||
 | 
					      csv
 | 
				
			||||||
 | 
					      mini_mime (>= 1.0.0)
 | 
				
			||||||
 | 
					      multi_xml (>= 0.5.2)
 | 
				
			||||||
 | 
					    i18n (1.14.7)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
    importmap-rails (2.0.1)
 | 
					    importmap-rails (2.1.0)
 | 
				
			||||||
      actionpack (>= 6.0.0)
 | 
					      actionpack (>= 6.0.0)
 | 
				
			||||||
      activesupport (>= 6.0.0)
 | 
					      activesupport (>= 6.0.0)
 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
    io-console (0.7.2)
 | 
					    io-console (0.8.0)
 | 
				
			||||||
    irb (1.14.0)
 | 
					    irb (1.14.3)
 | 
				
			||||||
      rdoc (>= 4.0.0)
 | 
					      rdoc (>= 4.0.0)
 | 
				
			||||||
      reline (>= 0.4.2)
 | 
					      reline (>= 0.4.2)
 | 
				
			||||||
    jbuilder (2.12.0)
 | 
					    jbuilder (2.13.0)
 | 
				
			||||||
      actionview (>= 5.0.0)
 | 
					      actionview (>= 5.0.0)
 | 
				
			||||||
      activesupport (>= 5.0.0)
 | 
					      activesupport (>= 5.0.0)
 | 
				
			||||||
    json (2.7.2)
 | 
					    json (2.9.1)
 | 
				
			||||||
 | 
					    json-schema (5.0.1)
 | 
				
			||||||
 | 
					      addressable (~> 2.8)
 | 
				
			||||||
    jsonapi-deserializable (0.2.0)
 | 
					    jsonapi-deserializable (0.2.0)
 | 
				
			||||||
    jsonapi-parser (0.1.1)
 | 
					    jsonapi-parser (0.1.1)
 | 
				
			||||||
    jsonapi-rails (0.4.1)
 | 
					    jsonapi-rails (0.4.1)
 | 
				
			||||||
@ -136,7 +158,26 @@ GEM
 | 
				
			|||||||
    jsonapi-serializable (0.3.1)
 | 
					    jsonapi-serializable (0.3.1)
 | 
				
			||||||
      jsonapi-renderer (~> 0.2.0)
 | 
					      jsonapi-renderer (~> 0.2.0)
 | 
				
			||||||
    language_server-protocol (3.17.0.3)
 | 
					    language_server-protocol (3.17.0.3)
 | 
				
			||||||
    loofah (2.22.0)
 | 
					    launchy (3.0.1)
 | 
				
			||||||
 | 
					      addressable (~> 2.8)
 | 
				
			||||||
 | 
					      childprocess (~> 5.0)
 | 
				
			||||||
 | 
					    letter_opener (1.10.0)
 | 
				
			||||||
 | 
					      launchy (>= 2.2, < 4)
 | 
				
			||||||
 | 
					    letter_opener_web (3.0.0)
 | 
				
			||||||
 | 
					      actionmailer (>= 6.1)
 | 
				
			||||||
 | 
					      letter_opener (~> 1.9)
 | 
				
			||||||
 | 
					      railties (>= 6.1)
 | 
				
			||||||
 | 
					      rexml
 | 
				
			||||||
 | 
					    license_finder (7.2.1)
 | 
				
			||||||
 | 
					      bundler
 | 
				
			||||||
 | 
					      csv (~> 3.2)
 | 
				
			||||||
 | 
					      rubyzip (>= 1, < 3)
 | 
				
			||||||
 | 
					      thor (~> 1.2)
 | 
				
			||||||
 | 
					      tomlrb (>= 1.3, < 2.1)
 | 
				
			||||||
 | 
					      with_env (= 1.1.0)
 | 
				
			||||||
 | 
					      xml-simple (~> 1.1.9)
 | 
				
			||||||
 | 
					    logger (1.6.5)
 | 
				
			||||||
 | 
					    loofah (2.23.1)
 | 
				
			||||||
      crass (~> 1.0.2)
 | 
					      crass (~> 1.0.2)
 | 
				
			||||||
      nokogiri (>= 1.12.0)
 | 
					      nokogiri (>= 1.12.0)
 | 
				
			||||||
    mail (2.8.1)
 | 
					    mail (2.8.1)
 | 
				
			||||||
@ -145,14 +186,16 @@ GEM
 | 
				
			|||||||
      net-pop
 | 
					      net-pop
 | 
				
			||||||
      net-smtp
 | 
					      net-smtp
 | 
				
			||||||
    marcel (1.0.4)
 | 
					    marcel (1.0.4)
 | 
				
			||||||
    method_source (1.0.0)
 | 
					    method_source (1.1.0)
 | 
				
			||||||
    mini_mime (1.1.5)
 | 
					    mini_mime (1.1.5)
 | 
				
			||||||
    minitest (5.24.1)
 | 
					    mini_portile2 (2.8.8)
 | 
				
			||||||
 | 
					    minitest (5.25.4)
 | 
				
			||||||
    money (6.19.0)
 | 
					    money (6.19.0)
 | 
				
			||||||
      i18n (>= 0.6.4, <= 2)
 | 
					      i18n (>= 0.6.4, <= 2)
 | 
				
			||||||
    msgpack (1.7.2)
 | 
					    msgpack (1.7.2)
 | 
				
			||||||
    mutex_m (0.2.0)
 | 
					    multi_xml (0.7.1)
 | 
				
			||||||
    net-imap (0.4.14)
 | 
					      bigdecimal (~> 3.1)
 | 
				
			||||||
 | 
					    net-imap (0.5.2)
 | 
				
			||||||
      date
 | 
					      date
 | 
				
			||||||
      net-protocol
 | 
					      net-protocol
 | 
				
			||||||
    net-pop (0.1.2)
 | 
					    net-pop (0.1.2)
 | 
				
			||||||
@ -161,74 +204,81 @@ GEM
 | 
				
			|||||||
      timeout
 | 
					      timeout
 | 
				
			||||||
    net-smtp (0.5.0)
 | 
					    net-smtp (0.5.0)
 | 
				
			||||||
      net-protocol
 | 
					      net-protocol
 | 
				
			||||||
    nio4r (2.7.3)
 | 
					    nio4r (2.7.4)
 | 
				
			||||||
    nokogiri (1.16.7-aarch64-linux)
 | 
					    nokogiri (1.18.1)
 | 
				
			||||||
 | 
					      mini_portile2 (~> 2.8.2)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.16.7-arm-linux)
 | 
					    nokogiri (1.18.1-aarch64-linux-gnu)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.16.7-arm64-darwin)
 | 
					    nokogiri (1.18.1-arm-linux-gnu)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.16.7-x86-linux)
 | 
					    nokogiri (1.18.1-arm64-darwin)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.16.7-x86_64-darwin)
 | 
					    nokogiri (1.18.1-x86_64-darwin)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nokogiri (1.16.7-x86_64-linux)
 | 
					    nokogiri (1.18.1-x86_64-linux-gnu)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    parallel (1.26.2)
 | 
					    orm_adapter (0.5.0)
 | 
				
			||||||
    parser (3.3.4.2)
 | 
					    parallel (1.26.3)
 | 
				
			||||||
 | 
					    parser (3.3.7.0)
 | 
				
			||||||
      ast (~> 2.4.1)
 | 
					      ast (~> 2.4.1)
 | 
				
			||||||
      racc
 | 
					      racc
 | 
				
			||||||
    pg (1.5.7)
 | 
					    pg (1.5.9)
 | 
				
			||||||
    pry (0.14.2)
 | 
					    pluck_to_hash (1.0.2)
 | 
				
			||||||
 | 
					      activerecord (>= 4.0.2)
 | 
				
			||||||
 | 
					      activesupport (>= 4.0.2)
 | 
				
			||||||
 | 
					    pry (0.15.2)
 | 
				
			||||||
      coderay (~> 1.1)
 | 
					      coderay (~> 1.1)
 | 
				
			||||||
      method_source (~> 1.0)
 | 
					      method_source (~> 1.0)
 | 
				
			||||||
    psych (5.1.2)
 | 
					    psych (5.2.2)
 | 
				
			||||||
 | 
					      date
 | 
				
			||||||
      stringio
 | 
					      stringio
 | 
				
			||||||
    puma (6.4.2)
 | 
					    public_suffix (6.0.1)
 | 
				
			||||||
 | 
					    puma (6.5.0)
 | 
				
			||||||
      nio4r (~> 2.0)
 | 
					      nio4r (~> 2.0)
 | 
				
			||||||
 | 
					    raabro (1.4.0)
 | 
				
			||||||
    racc (1.8.1)
 | 
					    racc (1.8.1)
 | 
				
			||||||
    rack (3.1.7)
 | 
					    rack (3.1.8)
 | 
				
			||||||
    rack-cors (2.0.2)
 | 
					    rack-cors (2.0.2)
 | 
				
			||||||
      rack (>= 2.0.0)
 | 
					      rack (>= 2.0.0)
 | 
				
			||||||
    rack-session (2.0.0)
 | 
					    rack-session (2.0.0)
 | 
				
			||||||
      rack (>= 3.0.0)
 | 
					      rack (>= 3.0.0)
 | 
				
			||||||
    rack-test (2.1.0)
 | 
					    rack-test (2.1.0)
 | 
				
			||||||
      rack (>= 1.3)
 | 
					      rack (>= 1.3)
 | 
				
			||||||
    rackup (2.1.0)
 | 
					    rackup (2.2.1)
 | 
				
			||||||
      rack (>= 3)
 | 
					      rack (>= 3)
 | 
				
			||||||
      webrick (~> 1.8)
 | 
					    rails (8.0.1)
 | 
				
			||||||
    rails (7.1.3.4)
 | 
					      actioncable (= 8.0.1)
 | 
				
			||||||
      actioncable (= 7.1.3.4)
 | 
					      actionmailbox (= 8.0.1)
 | 
				
			||||||
      actionmailbox (= 7.1.3.4)
 | 
					      actionmailer (= 8.0.1)
 | 
				
			||||||
      actionmailer (= 7.1.3.4)
 | 
					      actionpack (= 8.0.1)
 | 
				
			||||||
      actionpack (= 7.1.3.4)
 | 
					      actiontext (= 8.0.1)
 | 
				
			||||||
      actiontext (= 7.1.3.4)
 | 
					      actionview (= 8.0.1)
 | 
				
			||||||
      actionview (= 7.1.3.4)
 | 
					      activejob (= 8.0.1)
 | 
				
			||||||
      activejob (= 7.1.3.4)
 | 
					      activemodel (= 8.0.1)
 | 
				
			||||||
      activemodel (= 7.1.3.4)
 | 
					      activerecord (= 8.0.1)
 | 
				
			||||||
      activerecord (= 7.1.3.4)
 | 
					      activestorage (= 8.0.1)
 | 
				
			||||||
      activestorage (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					 | 
				
			||||||
      bundler (>= 1.15.0)
 | 
					      bundler (>= 1.15.0)
 | 
				
			||||||
      railties (= 7.1.3.4)
 | 
					      railties (= 8.0.1)
 | 
				
			||||||
    rails-dom-testing (2.2.0)
 | 
					    rails-dom-testing (2.2.0)
 | 
				
			||||||
      activesupport (>= 5.0.0)
 | 
					      activesupport (>= 5.0.0)
 | 
				
			||||||
      minitest
 | 
					      minitest
 | 
				
			||||||
      nokogiri (>= 1.6)
 | 
					      nokogiri (>= 1.6)
 | 
				
			||||||
    rails-html-sanitizer (1.6.0)
 | 
					    rails-html-sanitizer (1.6.2)
 | 
				
			||||||
      loofah (~> 2.21)
 | 
					      loofah (~> 2.21)
 | 
				
			||||||
      nokogiri (~> 1.14)
 | 
					      nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
 | 
				
			||||||
    railties (7.1.3.4)
 | 
					    railties (8.0.1)
 | 
				
			||||||
      actionpack (= 7.1.3.4)
 | 
					      actionpack (= 8.0.1)
 | 
				
			||||||
      activesupport (= 7.1.3.4)
 | 
					      activesupport (= 8.0.1)
 | 
				
			||||||
      irb
 | 
					      irb (~> 1.13)
 | 
				
			||||||
      rackup (>= 1.0.0)
 | 
					      rackup (>= 1.0.0)
 | 
				
			||||||
      rake (>= 12.2)
 | 
					      rake (>= 12.2)
 | 
				
			||||||
      thor (~> 1.0, >= 1.2.2)
 | 
					      thor (~> 1.0, >= 1.2.2)
 | 
				
			||||||
      zeitwerk (~> 2.6)
 | 
					      zeitwerk (~> 2.6)
 | 
				
			||||||
    rainbow (3.1.1)
 | 
					    rainbow (3.1.1)
 | 
				
			||||||
    rake (13.2.1)
 | 
					    rake (13.2.1)
 | 
				
			||||||
    rdoc (6.7.0)
 | 
					    rdoc (6.10.0)
 | 
				
			||||||
      psych (>= 4.0.0)
 | 
					      psych (>= 4.0.0)
 | 
				
			||||||
    react-rails (3.2.1)
 | 
					    react-rails (3.2.1)
 | 
				
			||||||
      babel-transpiler (>= 0.7.0)
 | 
					      babel-transpiler (>= 0.7.0)
 | 
				
			||||||
@ -236,47 +286,87 @@ GEM
 | 
				
			|||||||
      execjs
 | 
					      execjs
 | 
				
			||||||
      railties (>= 3.2)
 | 
					      railties (>= 3.2)
 | 
				
			||||||
      tilt
 | 
					      tilt
 | 
				
			||||||
    redis (5.2.0)
 | 
					    redis (5.3.0)
 | 
				
			||||||
      redis-client (>= 0.22.0)
 | 
					      redis-client (>= 0.22.0)
 | 
				
			||||||
    redis-client (0.22.2)
 | 
					    redis-client (0.22.2)
 | 
				
			||||||
      connection_pool
 | 
					      connection_pool
 | 
				
			||||||
    regexp_parser (2.9.2)
 | 
					    regexp_parser (2.10.0)
 | 
				
			||||||
    reline (0.5.9)
 | 
					    reline (0.6.0)
 | 
				
			||||||
      io-console (~> 0.5)
 | 
					      io-console (~> 0.5)
 | 
				
			||||||
    rexml (3.2.8)
 | 
					    responders (3.1.1)
 | 
				
			||||||
    rspec-core (3.12.3)
 | 
					      actionpack (>= 5.2)
 | 
				
			||||||
      rspec-support (~> 3.12.0)
 | 
					      railties (>= 5.2)
 | 
				
			||||||
    rspec-expectations (3.12.4)
 | 
					    rexml (3.3.9)
 | 
				
			||||||
 | 
					    rspec-core (3.13.2)
 | 
				
			||||||
 | 
					      rspec-support (~> 3.13.0)
 | 
				
			||||||
 | 
					    rspec-expectations (3.13.3)
 | 
				
			||||||
      diff-lcs (>= 1.2.0, < 2.0)
 | 
					      diff-lcs (>= 1.2.0, < 2.0)
 | 
				
			||||||
      rspec-support (~> 3.12.0)
 | 
					      rspec-support (~> 3.13.0)
 | 
				
			||||||
    rspec-mocks (3.12.7)
 | 
					    rspec-mocks (3.13.2)
 | 
				
			||||||
      diff-lcs (>= 1.2.0, < 2.0)
 | 
					      diff-lcs (>= 1.2.0, < 2.0)
 | 
				
			||||||
      rspec-support (~> 3.12.0)
 | 
					      rspec-support (~> 3.13.0)
 | 
				
			||||||
    rspec-rails (6.1.1)
 | 
					    rspec-rails (7.1.0)
 | 
				
			||||||
      actionpack (>= 6.1)
 | 
					      actionpack (>= 7.0)
 | 
				
			||||||
      activesupport (>= 6.1)
 | 
					      activesupport (>= 7.0)
 | 
				
			||||||
      railties (>= 6.1)
 | 
					      railties (>= 7.0)
 | 
				
			||||||
      rspec-core (~> 3.12)
 | 
					      rspec-core (~> 3.13)
 | 
				
			||||||
      rspec-expectations (~> 3.12)
 | 
					      rspec-expectations (~> 3.13)
 | 
				
			||||||
      rspec-mocks (~> 3.12)
 | 
					      rspec-mocks (~> 3.13)
 | 
				
			||||||
      rspec-support (~> 3.12)
 | 
					      rspec-support (~> 3.13)
 | 
				
			||||||
    rspec-support (3.12.2)
 | 
					    rspec-support (3.13.1)
 | 
				
			||||||
    rubocop (1.65.1)
 | 
					    rswag (2.16.0)
 | 
				
			||||||
 | 
					      rswag-api (= 2.16.0)
 | 
				
			||||||
 | 
					      rswag-specs (= 2.16.0)
 | 
				
			||||||
 | 
					      rswag-ui (= 2.16.0)
 | 
				
			||||||
 | 
					    rswag-api (2.16.0)
 | 
				
			||||||
 | 
					      activesupport (>= 5.2, < 8.1)
 | 
				
			||||||
 | 
					      railties (>= 5.2, < 8.1)
 | 
				
			||||||
 | 
					    rswag-specs (2.16.0)
 | 
				
			||||||
 | 
					      activesupport (>= 5.2, < 8.1)
 | 
				
			||||||
 | 
					      json-schema (>= 2.2, < 6.0)
 | 
				
			||||||
 | 
					      railties (>= 5.2, < 8.1)
 | 
				
			||||||
 | 
					      rspec-core (>= 2.14)
 | 
				
			||||||
 | 
					    rswag-ui (2.16.0)
 | 
				
			||||||
 | 
					      actionpack (>= 5.2, < 8.1)
 | 
				
			||||||
 | 
					      railties (>= 5.2, < 8.1)
 | 
				
			||||||
 | 
					    rubocop (1.71.0)
 | 
				
			||||||
      json (~> 2.3)
 | 
					      json (~> 2.3)
 | 
				
			||||||
      language_server-protocol (>= 3.17.0)
 | 
					      language_server-protocol (>= 3.17.0)
 | 
				
			||||||
      parallel (~> 1.10)
 | 
					      parallel (~> 1.10)
 | 
				
			||||||
      parser (>= 3.3.0.2)
 | 
					      parser (>= 3.3.0.2)
 | 
				
			||||||
      rainbow (>= 2.2.2, < 4.0)
 | 
					      rainbow (>= 2.2.2, < 4.0)
 | 
				
			||||||
      regexp_parser (>= 2.4, < 3.0)
 | 
					      regexp_parser (>= 2.9.3, < 3.0)
 | 
				
			||||||
      rexml (>= 3.2.5, < 4.0)
 | 
					      rubocop-ast (>= 1.36.2, < 2.0)
 | 
				
			||||||
      rubocop-ast (>= 1.31.1, < 2.0)
 | 
					 | 
				
			||||||
      ruby-progressbar (~> 1.7)
 | 
					      ruby-progressbar (~> 1.7)
 | 
				
			||||||
      unicode-display_width (>= 2.4.0, < 3.0)
 | 
					      unicode-display_width (>= 2.4.0, < 4.0)
 | 
				
			||||||
    rubocop-ast (1.32.0)
 | 
					    rubocop-ast (1.37.0)
 | 
				
			||||||
      parser (>= 3.3.1.0)
 | 
					      parser (>= 3.3.1.0)
 | 
				
			||||||
 | 
					    rubocop-factory_bot (2.26.1)
 | 
				
			||||||
 | 
					      rubocop (~> 1.61)
 | 
				
			||||||
 | 
					    rubocop-rails (2.29.1)
 | 
				
			||||||
 | 
					      activesupport (>= 4.2.0)
 | 
				
			||||||
 | 
					      rack (>= 1.1)
 | 
				
			||||||
 | 
					      rubocop (>= 1.52.0, < 2.0)
 | 
				
			||||||
 | 
					      rubocop-ast (>= 1.31.1, < 2.0)
 | 
				
			||||||
 | 
					    rubocop-rspec (3.4.0)
 | 
				
			||||||
 | 
					      rubocop (~> 1.61)
 | 
				
			||||||
 | 
					    rubocop-rspec_rails (2.30.0)
 | 
				
			||||||
 | 
					      rubocop (~> 1.61)
 | 
				
			||||||
 | 
					      rubocop-rspec (~> 3, >= 3.0.1)
 | 
				
			||||||
    ruby-progressbar (1.13.0)
 | 
					    ruby-progressbar (1.13.0)
 | 
				
			||||||
    rubytree (2.0.3)
 | 
					    rubytree (2.1.1)
 | 
				
			||||||
      json (~> 2.0, > 2.3.1)
 | 
					      json (~> 2.0, > 2.9)
 | 
				
			||||||
 | 
					    rubyzip (2.3.2)
 | 
				
			||||||
 | 
					    securerandom (0.4.1)
 | 
				
			||||||
 | 
					    shoulda-matchers (6.4.0)
 | 
				
			||||||
 | 
					      activesupport (>= 5.2.0)
 | 
				
			||||||
 | 
					    solid_queue (1.1.2)
 | 
				
			||||||
 | 
					      activejob (>= 7.1)
 | 
				
			||||||
 | 
					      activerecord (>= 7.1)
 | 
				
			||||||
 | 
					      concurrent-ruby (>= 1.3.1)
 | 
				
			||||||
 | 
					      fugit (~> 1.11.0)
 | 
				
			||||||
 | 
					      railties (>= 7.1)
 | 
				
			||||||
 | 
					      thor (~> 1.3.1)
 | 
				
			||||||
    sprockets (4.2.1)
 | 
					    sprockets (4.2.1)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
      rack (>= 2.2.4, < 4)
 | 
					      rack (>= 2.2.4, < 4)
 | 
				
			||||||
@ -284,29 +374,35 @@ GEM
 | 
				
			|||||||
      actionpack (>= 6.1)
 | 
					      actionpack (>= 6.1)
 | 
				
			||||||
      activesupport (>= 6.1)
 | 
					      activesupport (>= 6.1)
 | 
				
			||||||
      sprockets (>= 3.0.0)
 | 
					      sprockets (>= 3.0.0)
 | 
				
			||||||
    stimulus-rails (1.3.3)
 | 
					    stimulus-rails (1.3.4)
 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
    stringio (3.1.1)
 | 
					    stringio (3.1.2)
 | 
				
			||||||
    thor (1.3.1)
 | 
					    thor (1.3.2)
 | 
				
			||||||
    tilt (2.4.0)
 | 
					    tilt (2.4.0)
 | 
				
			||||||
    timeout (0.4.1)
 | 
					    timeout (0.4.3)
 | 
				
			||||||
    turbo-rails (2.0.6)
 | 
					    tomlrb (2.0.3)
 | 
				
			||||||
 | 
					    turbo-rails (2.0.11)
 | 
				
			||||||
      actionpack (>= 6.0.0)
 | 
					      actionpack (>= 6.0.0)
 | 
				
			||||||
      activejob (>= 6.0.0)
 | 
					 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
    tzinfo (2.0.6)
 | 
					    tzinfo (2.0.6)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
    unicode-display_width (2.5.0)
 | 
					    unicode-display_width (2.6.0)
 | 
				
			||||||
 | 
					    uri (1.0.2)
 | 
				
			||||||
 | 
					    useragent (0.16.11)
 | 
				
			||||||
 | 
					    warden (1.2.9)
 | 
				
			||||||
 | 
					      rack (>= 2.0.9)
 | 
				
			||||||
    web-console (4.2.1)
 | 
					    web-console (4.2.1)
 | 
				
			||||||
      actionview (>= 6.0.0)
 | 
					      actionview (>= 6.0.0)
 | 
				
			||||||
      activemodel (>= 6.0.0)
 | 
					      activemodel (>= 6.0.0)
 | 
				
			||||||
      bindex (>= 0.4.0)
 | 
					      bindex (>= 0.4.0)
 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
    webrick (1.8.1)
 | 
					 | 
				
			||||||
    websocket-driver (0.7.6)
 | 
					    websocket-driver (0.7.6)
 | 
				
			||||||
      websocket-extensions (>= 0.1.0)
 | 
					      websocket-extensions (>= 0.1.0)
 | 
				
			||||||
    websocket-extensions (0.1.5)
 | 
					    websocket-extensions (0.1.5)
 | 
				
			||||||
    zeitwerk (2.6.17)
 | 
					    with_env (1.1.0)
 | 
				
			||||||
 | 
					    xml-simple (1.1.9)
 | 
				
			||||||
 | 
					      rexml
 | 
				
			||||||
 | 
					    zeitwerk (2.7.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PLATFORMS
 | 
					PLATFORMS
 | 
				
			||||||
  aarch64-linux
 | 
					  aarch64-linux
 | 
				
			||||||
@ -317,34 +413,206 @@ PLATFORMS
 | 
				
			|||||||
  x86_64-linux
 | 
					  x86_64-linux
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DEPENDENCIES
 | 
					DEPENDENCIES
 | 
				
			||||||
  acts-as-taggable-on
 | 
					  acts_as_tenant
 | 
				
			||||||
 | 
					  annotaterb
 | 
				
			||||||
  bootsnap
 | 
					  bootsnap
 | 
				
			||||||
 | 
					  chroma
 | 
				
			||||||
  csv
 | 
					  csv
 | 
				
			||||||
  debug
 | 
					  debug
 | 
				
			||||||
 | 
					  devise (~> 4.9)
 | 
				
			||||||
  factory_bot_rails
 | 
					  factory_bot_rails
 | 
				
			||||||
  faker
 | 
					  faker
 | 
				
			||||||
 | 
					  httparty
 | 
				
			||||||
  importmap-rails
 | 
					  importmap-rails
 | 
				
			||||||
  jbuilder
 | 
					  jbuilder
 | 
				
			||||||
  jsonapi-rails
 | 
					  jsonapi-rails
 | 
				
			||||||
 | 
					  letter_opener_web
 | 
				
			||||||
 | 
					  license_finder
 | 
				
			||||||
  money
 | 
					  money
 | 
				
			||||||
  pg (~> 1.1)
 | 
					  pg (~> 1.1)
 | 
				
			||||||
 | 
					  pluck_to_hash
 | 
				
			||||||
  pry
 | 
					  pry
 | 
				
			||||||
  puma (>= 5.0)
 | 
					  puma (>= 5.0)
 | 
				
			||||||
  rack-cors
 | 
					  rack-cors
 | 
				
			||||||
  rails (~> 7.1.3, >= 7.1.3.2)
 | 
					  rails (~> 8.0.0, >= 8.0.0)
 | 
				
			||||||
  react-rails
 | 
					  react-rails
 | 
				
			||||||
  redis (>= 4.0.1)
 | 
					  redis (>= 4.0.1)
 | 
				
			||||||
  rspec-rails (~> 6.1.0)
 | 
					  rspec-rails (~> 7.1.0)
 | 
				
			||||||
 | 
					  rswag
 | 
				
			||||||
  rubocop
 | 
					  rubocop
 | 
				
			||||||
 | 
					  rubocop-factory_bot
 | 
				
			||||||
 | 
					  rubocop-rails
 | 
				
			||||||
 | 
					  rubocop-rspec
 | 
				
			||||||
 | 
					  rubocop-rspec_rails
 | 
				
			||||||
  rubytree
 | 
					  rubytree
 | 
				
			||||||
 | 
					  shoulda-matchers (~> 6.0)
 | 
				
			||||||
 | 
					  solid_queue (~> 1.0)
 | 
				
			||||||
  sprockets-rails
 | 
					  sprockets-rails
 | 
				
			||||||
  stimulus-rails
 | 
					  stimulus-rails
 | 
				
			||||||
  turbo-rails
 | 
					  turbo-rails
 | 
				
			||||||
  tzinfo-data
 | 
					  tzinfo-data
 | 
				
			||||||
  web-console
 | 
					  web-console
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CHECKSUMS
 | 
				
			||||||
 | 
					  actioncable (8.0.1) sha256=808bff2a4e3aba36f66f0cd65d7a1579ad52fb65e99304442c46051a79689d9b
 | 
				
			||||||
 | 
					  actionmailbox (8.0.1) sha256=bbc7db779be857fb6eb5b53f313d3881cd8cda38a150c3aa25f89f2f9977b08c
 | 
				
			||||||
 | 
					  actionmailer (8.0.1) sha256=7b074e9590e4ec5cebd2fc91d1f9ba4c61bbd4bbd4376f731527da187cd39952
 | 
				
			||||||
 | 
					  actionpack (8.0.1) sha256=c764e4bfc0ad9d3505c09ef9b6fbf9eca4292793550c6b7e2ea93167181bfcba
 | 
				
			||||||
 | 
					  actiontext (8.0.1) sha256=f232d303e854db2098f34d7331fe493a72dc2e53dfce80fbd517c7b93d4b05b2
 | 
				
			||||||
 | 
					  actionview (8.0.1) sha256=3005e3de5ca49ea789bf1ad46002d63fe5aa543c61c341239d3c533757e64f8a
 | 
				
			||||||
 | 
					  activejob (8.0.1) sha256=95acd9a32d498d3a458efbb317f6191fb678758cde0ebb6c68f0b25e0fe3477f
 | 
				
			||||||
 | 
					  activemodel (8.0.1) sha256=f46292fd6dcc128e18d588854298a933fd9eb22544c412b414ec02821062dc78
 | 
				
			||||||
 | 
					  activerecord (8.0.1) sha256=34a7f0610660bb704f0363025d4b8d35ffe8ddc8f5b8147e0809171f724b5306
 | 
				
			||||||
 | 
					  activestorage (8.0.1) sha256=91a8f156638568fac971ff25962a617d9c58fdc0e44eb6bd0edff36aff7df205
 | 
				
			||||||
 | 
					  activesupport (8.0.1) sha256=fd5bc74641c24ac3541055c2879789198ff42adee3e39c2933289ba008912e37
 | 
				
			||||||
 | 
					  acts_as_tenant (1.0.1) sha256=6944e4d64533337938a8817a6b4ff9b11189c9dcc0b1333bb89f3821a4c14c53
 | 
				
			||||||
 | 
					  addressable (2.8.7) sha256=462986537cf3735ab5f3c0f557f14155d778f4b43ea4f485a9deb9c8f7c58232
 | 
				
			||||||
 | 
					  annotaterb (4.13.0) sha256=6f472912002fefa735665b4132de47d0134ebf1efb76a7ef05f579cc4a6b2ff1
 | 
				
			||||||
 | 
					  ast (2.4.2) sha256=1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12
 | 
				
			||||||
 | 
					  babel-source (5.8.35) sha256=79ef222a9dcb867ac2efa3b0da35b4bcb15a4bfa67b6b2dcbf1e9a29104498d9
 | 
				
			||||||
 | 
					  babel-transpiler (0.7.0) sha256=4c06f4ad9e8e1cabe94f99e11df2f140bb72aca9ba067dbb49dc14d9b98d1570
 | 
				
			||||||
 | 
					  base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507
 | 
				
			||||||
 | 
					  bcrypt (3.1.20) sha256=8410f8c7b3ed54a3c00cd2456bf13917d695117f033218e2483b2e40b0784099
 | 
				
			||||||
 | 
					  benchmark (0.4.0) sha256=0f12f8c495545e3710c3e4f0480f63f06b4c842cc94cec7f33a956f5180e874a
 | 
				
			||||||
 | 
					  bigdecimal (3.1.9) sha256=2ffc742031521ad69c2dfc815a98e426a230a3d22aeac1995826a75dabfad8cc
 | 
				
			||||||
 | 
					  bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
 | 
				
			||||||
 | 
					  bootsnap (1.18.4) sha256=ac4c42af397f7ee15521820198daeff545e4c360d2772c601fbdc2c07d92af55
 | 
				
			||||||
 | 
					  builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
 | 
				
			||||||
 | 
					  childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec
 | 
				
			||||||
 | 
					  chroma (0.2.0) sha256=64bdcd36a4765fbcd45adc64960cc153101300b4918f90ffdd89f4e2eb954b54
 | 
				
			||||||
 | 
					  coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
 | 
				
			||||||
 | 
					  concurrent-ruby (1.3.5) sha256=813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6
 | 
				
			||||||
 | 
					  connection_pool (2.4.1) sha256=0f40cf997091f1f04ff66da67eabd61a9fe0d4928b9a3645228532512fab62f4
 | 
				
			||||||
 | 
					  crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
 | 
				
			||||||
 | 
					  csv (3.3.2) sha256=6ff0c135e65e485d1864dde6c1703b60d34cc9e19bed8452834a0b28a519bd4e
 | 
				
			||||||
 | 
					  date (3.4.1) sha256=bf268e14ef7158009bfeaec40b5fa3c7271906e88b196d958a89d4b408abe64f
 | 
				
			||||||
 | 
					  debug (1.10.0) sha256=11e28ca74875979e612444104f3972bd5ffb9e79179907d7ad46dba44bd2e7a4
 | 
				
			||||||
 | 
					  devise (4.9.4) sha256=920042fe5e704c548aa4eb65ebdd65980b83ffae67feb32c697206bfd975a7f8
 | 
				
			||||||
 | 
					  diff-lcs (1.5.1) sha256=273223dfb40685548436d32b4733aa67351769c7dea621da7d9dd4813e63ddfe
 | 
				
			||||||
 | 
					  drb (2.2.1) sha256=e9d472bf785f558b96b25358bae115646da0dbfd45107ad858b0bc0d935cb340
 | 
				
			||||||
 | 
					  erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
 | 
				
			||||||
 | 
					  et-orbi (1.2.11) sha256=d26e868cc21db88280a9ec1a50aa3da5d267eb9b2037ba7b831d6c2731f5df64
 | 
				
			||||||
 | 
					  execjs (2.9.1) sha256=e8fd066f6df60c8e8fbebc32c6fb356b5212c77374e8416a9019ca4bb154dcfb
 | 
				
			||||||
 | 
					  factory_bot (6.4.6) sha256=1a9486ce98d318d740d8f5804b885a8265a28f326ecf2bcd4ce9fb27a71a6e04
 | 
				
			||||||
 | 
					  factory_bot_rails (6.4.3) sha256=ea73ceac1c0ff3dc11fff390bf2ea8a2604066525ed8ecd3b3bc2c267226dcc8
 | 
				
			||||||
 | 
					  faker (3.5.1) sha256=1ad1fbea279d882f486059c23fe3ddb816ccd1d7052c05a45014b4450d859bfc
 | 
				
			||||||
 | 
					  fugit (1.11.1) sha256=e89485e7be22226d8e9c6da411664d0660284b4b1c08cacb540f505907869868
 | 
				
			||||||
 | 
					  globalid (1.2.1) sha256=70bf76711871f843dbba72beb8613229a49429d1866828476f9c9d6ccc327ce9
 | 
				
			||||||
 | 
					  httparty (0.22.0) sha256=78652a5c9471cf0093d3b2083c2295c9c8f12b44c65112f1846af2b71430fa6c
 | 
				
			||||||
 | 
					  i18n (1.14.7) sha256=ceba573f8138ff2c0915427f1fc5bdf4aa3ab8ae88c8ce255eb3ecf0a11a5d0f
 | 
				
			||||||
 | 
					  importmap-rails (2.1.0) sha256=9f10c67d60651a547579f448100d033df311c5d5db578301374aeb774faae741
 | 
				
			||||||
 | 
					  io-console (0.8.0) sha256=cd6a9facbc69871d69b2cb8b926fc6ea7ef06f06e505e81a64f14a470fddefa2
 | 
				
			||||||
 | 
					  irb (1.14.3) sha256=c457f1f2f1438ae9ce5c5be3981ae2138dec7fb894c7d73777eeeb0a6c0d0752
 | 
				
			||||||
 | 
					  jbuilder (2.13.0) sha256=7200a38a1c0081aa81b7a9757e7a299db75bc58cf1fd45ca7919a91627d227d6
 | 
				
			||||||
 | 
					  json (2.9.1) sha256=d2bdef4644052fad91c1785d48263756fe32fcac08b96a20bb15840e96550d11
 | 
				
			||||||
 | 
					  json-schema (5.0.1) sha256=bef71a82c600a42594911553522e143f7634affc198ed507ef3ded2f920a74a9
 | 
				
			||||||
 | 
					  jsonapi-deserializable (0.2.0) sha256=5f0ca2d3f8404cce1584a314e8a3753be32a56054c942adfe997b87e92bce147
 | 
				
			||||||
 | 
					  jsonapi-parser (0.1.1) sha256=9ee0dc031e88fc7548d56fab66f9716d1e1c06f972b529b8c4617bc42a097020
 | 
				
			||||||
 | 
					  jsonapi-rails (0.4.1) sha256=fa68b927b58f194e8b81f578c0bf18e61575638f45a390f66c832de2e6d179ba
 | 
				
			||||||
 | 
					  jsonapi-rb (0.5.0) sha256=7922a164278f506c43d56277f6bd0800a0b603cc985f7f63fe7241b2628bd105
 | 
				
			||||||
 | 
					  jsonapi-renderer (0.2.2) sha256=b5c44b033d61b4abdb6500fa4ab84807ca0b36ea0e59e47a2c3ca7095a6e447b
 | 
				
			||||||
 | 
					  jsonapi-serializable (0.3.1) sha256=221e657677659d798e268a33ec97a83ec5ea0e4233f931358db84e88056552e9
 | 
				
			||||||
 | 
					  language_server-protocol (3.17.0.3) sha256=3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f
 | 
				
			||||||
 | 
					  launchy (3.0.1) sha256=b7fa60bda0197cf57614e271a250a8ca1f6a34ab889a3c73f67ec5d57c8a7f2c
 | 
				
			||||||
 | 
					  letter_opener (1.10.0) sha256=2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2
 | 
				
			||||||
 | 
					  letter_opener_web (3.0.0) sha256=3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860
 | 
				
			||||||
 | 
					  license_finder (7.2.1) sha256=179ead19b64b170638b72fd16024233813673ac9d20d5ba75ae0b4444887ef14
 | 
				
			||||||
 | 
					  logger (1.6.5) sha256=c3cfe56d01656490ddd103d38b8993d73d86296adebc5f58cefc9ec03741e56b
 | 
				
			||||||
 | 
					  loofah (2.23.1) sha256=d0a07422cb3b69272e124afa914ef6d517e30d5496b7f1c1fc5b95481f13f75e
 | 
				
			||||||
 | 
					  mail (2.8.1) sha256=ec3b9fadcf2b3755c78785cb17bc9a0ca9ee9857108a64b6f5cfc9c0b5bfc9ad
 | 
				
			||||||
 | 
					  marcel (1.0.4) sha256=0d5649feb64b8f19f3d3468b96c680bae9746335d02194270287868a661516a4
 | 
				
			||||||
 | 
					  method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
 | 
				
			||||||
 | 
					  mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
 | 
				
			||||||
 | 
					  mini_portile2 (2.8.8) sha256=8e47136cdac04ce81750bb6c09733b37895bf06962554e4b4056d78168d70a75
 | 
				
			||||||
 | 
					  minitest (5.25.4) sha256=9cf2cae25ac4dfc90c988ebc3b917f53c054978b673273da1bd20bcb0778f947
 | 
				
			||||||
 | 
					  money (6.19.0) sha256=ec936fa1e42f2783719241ed9fd52725d0efa628f928feea1eb5c37d5de7daf3
 | 
				
			||||||
 | 
					  msgpack (1.7.2) sha256=59ab62fd8a4d0dfbde45009f87eb6f158ab2628a7c48886b0256f175166baaa8
 | 
				
			||||||
 | 
					  multi_xml (0.7.1) sha256=4fce100c68af588ff91b8ba90a0bb3f0466f06c909f21a32f4962059140ba61b
 | 
				
			||||||
 | 
					  net-imap (0.5.2) sha256=e955b55e539712518bdb4eb747c6514f9c8d56ec4eb8eb573a82a6885a9effea
 | 
				
			||||||
 | 
					  net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
 | 
				
			||||||
 | 
					  net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
 | 
				
			||||||
 | 
					  net-smtp (0.5.0) sha256=5fc0415e6ea1cc0b3dfea7270438ec22b278ca8d524986a3ae4e5ae8d087b42a
 | 
				
			||||||
 | 
					  nio4r (2.7.4) sha256=d95dee68e0bb251b8ff90ac3423a511e3b784124e5db7ff5f4813a220ae73ca9
 | 
				
			||||||
 | 
					  nokogiri (1.18.1) sha256=df18be7e96c34736b6abfdeda80c6e845134fb9afe2fe5d4fbc1cf1f89c68475
 | 
				
			||||||
 | 
					  nokogiri (1.18.1-aarch64-linux-gnu) sha256=35837013800e34342fcbaca305f8c49231f6bd4f779bfa23fe7b4686ae82d5b8
 | 
				
			||||||
 | 
					  nokogiri (1.18.1-arm-linux-gnu) sha256=3b873fd6b0cd1ad7c77e87af701075bdfd14c9a6b2f2965c5e00ed29a5627a37
 | 
				
			||||||
 | 
					  nokogiri (1.18.1-arm64-darwin) sha256=d75193f284c899d225943a8944479faedd995a7573ddd5c8308ffbdf2ec55204
 | 
				
			||||||
 | 
					  nokogiri (1.18.1-x86_64-darwin) sha256=d94e3aa6483577495fc8969d6b4b5c075840ce6b1ab09636a6d4177ad171051d
 | 
				
			||||||
 | 
					  nokogiri (1.18.1-x86_64-linux-gnu) sha256=e516cf16ccde67ed4cc595a2621ca5ddd42562ecb24928914b0045a20a41620e
 | 
				
			||||||
 | 
					  orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9
 | 
				
			||||||
 | 
					  parallel (1.26.3) sha256=d86babb7a2b814be9f4b81587bf0b6ce2da7d45969fab24d8ae4bf2bb4d4c7ef
 | 
				
			||||||
 | 
					  parser (3.3.7.0) sha256=7449011771e3e7881297859b849de26a6f4fccd515bece9520a87e7d2116119b
 | 
				
			||||||
 | 
					  pg (1.5.9) sha256=761efbdf73b66516f0c26fcbe6515dc7500c3f0aa1a1b853feae245433c64fdc
 | 
				
			||||||
 | 
					  pluck_to_hash (1.0.2) sha256=1599906239716f98262a41493dd7d4cb72e8d83ad3d76d666deacfc5de50a47e
 | 
				
			||||||
 | 
					  pry (0.15.2) sha256=12d54b8640d3fa29c9211dd4ffb08f3fd8bf7a4fd9b5a73ce5b59c8709385b6b
 | 
				
			||||||
 | 
					  psych (5.2.2) sha256=a4a9477c85d3e858086c38cf64e7096abe40d1b1eed248b01020dec0ff9906ab
 | 
				
			||||||
 | 
					  public_suffix (6.0.1) sha256=61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f
 | 
				
			||||||
 | 
					  puma (6.5.0) sha256=94d1b75cab7f356d52e4f1b17b9040a090889b341dbeee6ee3703f441dc189f2
 | 
				
			||||||
 | 
					  raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
 | 
				
			||||||
 | 
					  racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
 | 
				
			||||||
 | 
					  rack (3.1.8) sha256=d3fbcbca43dc2b43c9c6d7dfbac01667ae58643c42cea10013d0da970218a1b1
 | 
				
			||||||
 | 
					  rack-cors (2.0.2) sha256=415d4e1599891760c5dc9ef0349c7fecdf94f7c6a03e75b2e7c2b54b82adda1b
 | 
				
			||||||
 | 
					  rack-session (2.0.0) sha256=db04b2063e180369192a9046b4559af311990af38c6a93d4c600cee4eb6d4e81
 | 
				
			||||||
 | 
					  rack-test (2.1.0) sha256=0c61fc61904049d691922ea4bb99e28004ed3f43aa5cfd495024cc345f125dfb
 | 
				
			||||||
 | 
					  rackup (2.2.1) sha256=f737191fd5c5b348b7f0a4412a3b86383f88c43e13b8217b63d4c8d90b9e798d
 | 
				
			||||||
 | 
					  rails (8.0.1) sha256=c86f4cd7834a67c1e5d04a77d35c88a5f56a20e2022ec416fa52c1af2cdc9491
 | 
				
			||||||
 | 
					  rails-dom-testing (2.2.0) sha256=e515712e48df1f687a1d7c380fd7b07b8558faa26464474da64183a7426fa93b
 | 
				
			||||||
 | 
					  rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560
 | 
				
			||||||
 | 
					  railties (8.0.1) sha256=8f653c6b1b0721b553045bd0deda1f22074b9ddc2209526e6f7285fcf607ac51
 | 
				
			||||||
 | 
					  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
 | 
				
			||||||
 | 
					  rake (13.2.1) sha256=46cb38dae65d7d74b6020a4ac9d48afed8eb8149c040eccf0523bec91907059d
 | 
				
			||||||
 | 
					  rdoc (6.10.0) sha256=db665021883ac9df3ba29cdf71aece960749888db1bf9615b4a584cfa3fa3eda
 | 
				
			||||||
 | 
					  react-rails (3.2.1) sha256=2235db0b240517596b1cb3e26177ab5bc64d3a56579b0415ee242b1691f81f64
 | 
				
			||||||
 | 
					  redis (5.3.0) sha256=6bf810c5ae889187f0c45f77db503310980310afa57cf1640d57f419ccda72b1
 | 
				
			||||||
 | 
					  redis-client (0.22.2) sha256=31fee4b7cf04109b227327fabeaaf1fc5b652cf48a186a03bc607e40767bacc0
 | 
				
			||||||
 | 
					  regexp_parser (2.10.0) sha256=cb6f0ddde88772cd64bff1dbbf68df66d376043fe2e66a9ef77fcb1b0c548c61
 | 
				
			||||||
 | 
					  reline (0.6.0) sha256=57620375dcbe56ec09bac7192bfb7460c716bbf0054dc94345ecaa5438e539d2
 | 
				
			||||||
 | 
					  responders (3.1.1) sha256=92f2a87e09028347368639cfb468f5fefa745cb0dc2377ef060db1cdd79a341a
 | 
				
			||||||
 | 
					  rexml (3.3.9) sha256=d71875b85299f341edf47d44df0212e7658cbdf35aeb69cefdb63f57af3137c9
 | 
				
			||||||
 | 
					  rspec-core (3.13.2) sha256=94fbda6e4738e478f1c7532b7cc241272fcdc8b9eac03a97338b1122e4573300
 | 
				
			||||||
 | 
					  rspec-expectations (3.13.3) sha256=0e6b5af59b900147698ea0ff80456c4f2e69cac4394fbd392fbd1ca561f66c58
 | 
				
			||||||
 | 
					  rspec-mocks (3.13.2) sha256=2327335def0e1665325a9b617e3af9ae20272741d80ac550336309a7c59abdef
 | 
				
			||||||
 | 
					  rspec-rails (7.1.0) sha256=94585b69c4086ca79afae5cc8d2c5e314f6ad32a88c927f9c065b99596e3ee47
 | 
				
			||||||
 | 
					  rspec-support (3.13.1) sha256=48877d4f15b772b7538f3693c22225f2eda490ba65a0515c4e7cf6f2f17de70f
 | 
				
			||||||
 | 
					  rswag (2.16.0) sha256=f07ce41548b9bb51464c38bc7b95af22fee84b90f2d1197a515a623906353086
 | 
				
			||||||
 | 
					  rswag-api (2.16.0) sha256=b653f7bd92e98be18b01ab4525d88950d7b0960e293a99f856b9efcee3ae6074
 | 
				
			||||||
 | 
					  rswag-specs (2.16.0) sha256=8ba26085c408b0bd2ed21dc8015c80f417c7d34c63720ab7133c2549b5bd2a91
 | 
				
			||||||
 | 
					  rswag-ui (2.16.0) sha256=a1f49e927dceda92e6e6e7c1000f1e217ee66c565f69e28131dc98b33cd3a04f
 | 
				
			||||||
 | 
					  rubocop (1.71.0) sha256=e19679efd447346ac476122313d3788ae23c38214790bcf660e984c747608bf0
 | 
				
			||||||
 | 
					  rubocop-ast (1.37.0) sha256=9513ac88aaf113d04b52912533ffe46475de1362d4aa41141b51b2455827c080
 | 
				
			||||||
 | 
					  rubocop-factory_bot (2.26.1) sha256=8de13cd4edcee5ca800f255188167ecef8dbfc3d1fae9f15734e9d2e755392aa
 | 
				
			||||||
 | 
					  rubocop-rails (2.29.1) sha256=41c2fcf48d5d62f4a5f574d5f1c97bbaf4cba88ee367936c98b3422d047b17aa
 | 
				
			||||||
 | 
					  rubocop-rspec (3.4.0) sha256=8721c13b6a8c9530a7ac481cea9423022f946fcf72428bda8289f8b57e4d4885
 | 
				
			||||||
 | 
					  rubocop-rspec_rails (2.30.0) sha256=888112e83f9d7ef7ad2397e9d69a0b9614a4bae24f072c399804a180f80c4c46
 | 
				
			||||||
 | 
					  ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
 | 
				
			||||||
 | 
					  rubytree (2.1.1) sha256=4925016356a81730e982f1f8c3b5f8da461f18906c77d238bad4c4ba896abd41
 | 
				
			||||||
 | 
					  rubyzip (2.3.2) sha256=3f57e3935dc2255c414484fbf8d673b4909d8a6a57007ed754dde39342d2373f
 | 
				
			||||||
 | 
					  securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
 | 
				
			||||||
 | 
					  shoulda-matchers (6.4.0) sha256=9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0
 | 
				
			||||||
 | 
					  solid_queue (1.1.2) sha256=178c9396d1cf0dac595c7508da90ddb397d25848ca007b5c5ed48e6ac6fc360c
 | 
				
			||||||
 | 
					  sprockets (4.2.1) sha256=951b13dd2f2fcae840a7184722689a803e0ff9d2702d902bd844b196da773f97
 | 
				
			||||||
 | 
					  sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e
 | 
				
			||||||
 | 
					  stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
 | 
				
			||||||
 | 
					  stringio (3.1.2) sha256=204f1828f85cdb39d57cac4abc6dc44b04505a223f131587f2e20ae3729ba131
 | 
				
			||||||
 | 
					  thor (1.3.2) sha256=eef0293b9e24158ccad7ab383ae83534b7ad4ed99c09f96f1a6b036550abbeda
 | 
				
			||||||
 | 
					  tilt (2.4.0) sha256=df74f29a451daed26591a85e8e0cebb198892cb75b6573394303acda273fba4d
 | 
				
			||||||
 | 
					  timeout (0.4.3) sha256=9509f079b2b55fe4236d79633bd75e34c1c1e7e3fb4b56cb5fda61f80a0fe30e
 | 
				
			||||||
 | 
					  tomlrb (2.0.3) sha256=c2736acf24919f793334023a4ff396c0647d93fce702a73c9d348deaa815d4f7
 | 
				
			||||||
 | 
					  turbo-rails (2.0.11) sha256=fc47674736372780abd2a4dc0d84bef242f5ca156a457cd7fa6308291e397fcf
 | 
				
			||||||
 | 
					  tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
 | 
				
			||||||
 | 
					  unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
 | 
				
			||||||
 | 
					  uri (1.0.2) sha256=b303504ceb7e5905771fa7fa14b649652fa949df18b5880d69cfb12494791e27
 | 
				
			||||||
 | 
					  useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
 | 
				
			||||||
 | 
					  warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0
 | 
				
			||||||
 | 
					  web-console (4.2.1) sha256=e7bcf37a10ea2b4ec4281649d1cee461b32232d0a447e82c786e6841fd22fe20
 | 
				
			||||||
 | 
					  websocket-driver (0.7.6) sha256=f69400be7bc197879726ad8e6f5869a61823147372fd8928836a53c2c741d0db
 | 
				
			||||||
 | 
					  websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
 | 
				
			||||||
 | 
					  with_env (1.1.0) sha256=50b3e4f0a6cda8f90d8a6bd87a6261f6c381429abafb161c4c69ad4a0cd0b6e4
 | 
				
			||||||
 | 
					  xml-simple (1.1.9) sha256=d21131e519c86f1a5bc2b6d2d57d46e6998e47f18ed249b25cad86433dbd695d
 | 
				
			||||||
 | 
					  zeitwerk (2.7.1) sha256=0945986050e4907140895378e74df1fe882a2271ed087cc6c6d6b00d415a2756
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUBY VERSION
 | 
					RUBY VERSION
 | 
				
			||||||
   ruby 3.3.4p94
 | 
					   ruby 3.4.1p0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
BUNDLED WITH
 | 
					BUNDLED WITH
 | 
				
			||||||
   2.5.17
 | 
					   2.6.1
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										98
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										98
									
								
								README.md
									
									
									
									
									
								
							@ -1,24 +1,96 @@
 | 
				
			|||||||
# README
 | 
					# Libre Wedding Planner
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This README would normally document whatever steps are necessary to get the
 | 
					Libre Wedding Planner is Free, Open Source Software that helps organize several aspects of a wedding.
 | 
				
			||||||
application up and running.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Things you may want to cover:
 | 
					The project is not production-ready yet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Ruby version
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* System dependencies
 | 
					The follwing features are either developed or under active development:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Configuration
 | 
					- Guests management
 | 
				
			||||||
 | 
					- Expense management
 | 
				
			||||||
 | 
					- Seating chart
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Database creation
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Database initialization
 | 
					## Next steps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* How to run the test suite
 | 
					Some ideas we would like to implement next:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Services (job queues, cache servers, search engines, etc.)
 | 
					- Authentication (required to make an instance public)
 | 
				
			||||||
 | 
					- Website with wedding information
 | 
				
			||||||
 | 
					- Attendance confirmation forms
 | 
				
			||||||
 | 
					- Multitenancy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Deployment instructions
 | 
					# Development setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Libre Wedding Planner is made of two main pieces:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- The backend (this repo), built with Ruby (on Rails)
 | 
				
			||||||
 | 
					- The frontend (repo [here](https://gitea.bustikiller.com/bustikiller/wedding-planner-frontend/)), built with NextJS and React. You will need both to have the service fully working.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Both repositories are expected to live have a common parent directory:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					projects <or anything else>
 | 
				
			||||||
 | 
					  |-> wedding-planner 
 | 
				
			||||||
 | 
					  |-> wedding-planner-frontend 
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Docker compose
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Docker compose is the recommended way to run Libre Wedding Planner for development purposes. After downloading both repositories, `cd` to the root of `wedding-planner` and run:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					docker compose up --build
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Several containers will be started:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- backend: starts a Rails server that will act as an API.
 | 
				
			||||||
 | 
					- workers: starts a runner of [solid queue](https://github.com/rails/solid_queue/) that takes .care of async tasks.
 | 
				
			||||||
 | 
					- frontend: starts a NextJS application in charge of the frontend.
 | 
				
			||||||
 | 
					- nginx: A reverse proxy that the backend and frontend under the same domain, and routes all requests to the upstream services.
 | 
				
			||||||
 | 
					- db: A Postgres instance used by the backend service.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The backend service will seed the database with fake data. It's worth noting that the Postgres container does not have a volume, so the application will be seeded every time the container is created.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The backend, frontend and workers have hot-reloading enabled, so changes made to the codebase should be reflected in the application on the next request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Once all containers have started, visit http://libre-wedding-planner.app.localhost/default/dashboard to load the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Multitenancy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LibreWeddingPlanner is designed to manage multiple weddings in a single host. All URLs (in the API and the frontend) are scoped under a slug that is unique per wedding. The slug is made of lowercase letters, numbers, and dashes (-).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The development environment is seeded with a wedding whose slug is `default`.
 | 
				
			||||||
 | 
					## Email delivery
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					In the development environment, real emails will not be sent. You can visit http://libre-wedding-planner.app.localhost/letter_opener/ to get a list of emails generated by the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Testing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unit tests can be executed with
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					bundle exec rspec
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## API documentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Generate the OpenAPI documentation with the command:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					rake rswag:specs:swaggerize
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The documentation is available in Swagger UI in http://libre-wedding-planner.app.localhost/api/api-docs/index.html. If testing the API through the UI, you will need to select the second server (which includes the `/api` path), intended for development.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Contributing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Contributions of all kinds (code, UX/UI, testing, translations, etc.) are welcome. The procedures to contribute are still being defined, but don't hesitate to reach out in case you want to participate.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This project is licensed under the GNU Affero General Public License (AGPL). Check [COPYING.md](./COPYING.md) for additional information.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* ...
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								Rakefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Rakefile
									
									
									
									
									
								
							@ -1,6 +1,8 @@
 | 
				
			|||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Add your own tasks in files placed in lib/tasks ending in .rake,
 | 
					# Add your own tasks in files placed in lib/tasks ending in .rake,
 | 
				
			||||||
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
 | 
					# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require_relative "config/application"
 | 
					require_relative 'config/application'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Rails.application.load_tasks
 | 
					Rails.application.load_tasks
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,7 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module ApplicationCable
 | 
					module ApplicationCable
 | 
				
			||||||
  class Channel < ActionCable::Channel::Base
 | 
					  class Channel < ActionCable::Channel::Base
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,7 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module ApplicationCable
 | 
					module ApplicationCable
 | 
				
			||||||
  class Connection < ActionCable::Connection::Base
 | 
					  class Connection < ActionCable::Connection::Base
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										72
									
								
								app/controllers/affinities_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								app/controllers/affinities_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AffinitiesController < ApplicationController
 | 
				
			||||||
 | 
					  before_action :set_group, except: :reset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def index
 | 
				
			||||||
 | 
					    overridden = @group.affinities.each_with_object({}) do |affinity, acc|
 | 
				
			||||||
 | 
					      acc[affinity.another_group(@group).id] = affinity.discomfort
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for_each_group do |group_id|
 | 
				
			||||||
 | 
					      overridden[group_id] || GroupAffinity::NEUTRAL
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def bulk_update
 | 
				
			||||||
 | 
					    affinities = params.expect(affinities: [%i[group_id affinity]]).map(&:to_h).map do |affinity|
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        group_a_id: @group.id,
 | 
				
			||||||
 | 
					        group_b_id: affinity[:group_id],
 | 
				
			||||||
 | 
					        discomfort: GroupAffinity::MAX_DISCOMFORT - affinity[:affinity]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    GroupAffinity.upsert_all(affinities, unique_by: :uindex_group_pair)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render json: {}, status: :ok
 | 
				
			||||||
 | 
					  rescue ActiveRecord::InvalidForeignKey
 | 
				
			||||||
 | 
					    render json: { error: 'At least one of the group IDs provided does not exist.' }, status: :bad_request
 | 
				
			||||||
 | 
					  rescue ActiveRecord::StatementInvalid
 | 
				
			||||||
 | 
					    render json: { error: 'Invalid group ID or discomfort provided.' }, status: :bad_request
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def default
 | 
				
			||||||
 | 
					    hierarchy = AffinityGroupsHierarchy.new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for_each_group do |group_id|
 | 
				
			||||||
 | 
					      hierarchy.default_discomfort(@group.id, group_id).to_f
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def reset
 | 
				
			||||||
 | 
					    hierarchy = AffinityGroupsHierarchy.new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    affinities = Group.pluck(:id).combination(2).map do |(group_a_id, group_b_id)|
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        group_a_id:,
 | 
				
			||||||
 | 
					        group_b_id:,
 | 
				
			||||||
 | 
					        discomfort: hierarchy.default_discomfort(group_a_id, group_b_id).to_f
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    GroupAffinity.upsert_all(affinities, unique_by: :uindex_group_pair)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render json: {}, status: :ok
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def for_each_group
 | 
				
			||||||
 | 
					    Group.where.not(id: @group.id)
 | 
				
			||||||
 | 
					         .pluck(:id)
 | 
				
			||||||
 | 
					         .index_with { |group_id| GroupAffinity::MAX_DISCOMFORT - yield(group_id) }
 | 
				
			||||||
 | 
					         .then { |affinities| render json: affinities }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_group
 | 
				
			||||||
 | 
					    @group = Group.find(params[:group_id])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,2 +1,68 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApplicationController < ActionController::Base
 | 
					class ApplicationController < ActionController::Base
 | 
				
			||||||
 | 
					  set_current_tenant_through_filter
 | 
				
			||||||
 | 
					  before_action :set_tenant
 | 
				
			||||||
 | 
					  before_action :authenticate_user!
 | 
				
			||||||
 | 
					  after_action :set_csrf_cookie
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  skip_before_action :verify_authenticity_token, if: :development_swagger?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rescue_from ActiveRecord::RecordInvalid do |exception|
 | 
				
			||||||
 | 
					    render json: {
 | 
				
			||||||
 | 
					      message: 'Record invalid',
 | 
				
			||||||
 | 
					      errors: exception.record.errors.full_messages
 | 
				
			||||||
 | 
					    }, status: :unprocessable_entity
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rescue_from ActionController::ParameterMissing do |exception|
 | 
				
			||||||
 | 
					    render json: {
 | 
				
			||||||
 | 
					      message: 'Parameter missing',
 | 
				
			||||||
 | 
					      errors: [exception.message]
 | 
				
			||||||
 | 
					    }, status: :bad_request
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rescue_from ActiveRecord::RecordNotFound do |exception|
 | 
				
			||||||
 | 
					    render json: {
 | 
				
			||||||
 | 
					      message: 'Record not found',
 | 
				
			||||||
 | 
					      errors: [exception.message]
 | 
				
			||||||
 | 
					    }, status: :not_found
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def validate_captcha!
 | 
				
			||||||
 | 
					    Rails.logger.info("Captcha params: #{captcha_params}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return if LibreCaptcha.new.valid?(id: captcha_params[:id], answer: captcha_params[:answer])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render json: { error: 'Incorrect CAPTCHA solution' }, status: :unprocessable_entity
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def captcha_params
 | 
				
			||||||
 | 
					    params.expect(captcha: %i[id answer])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def default_url_options(options = {})
 | 
				
			||||||
 | 
					    options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug })
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_tenant
 | 
				
			||||||
 | 
					    set_current_tenant(Wedding.find_by!(slug: params[:slug]))
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def development_swagger?
 | 
				
			||||||
 | 
					    Rails.env.test? ||
 | 
				
			||||||
 | 
					      (Rails.env.development? && request.headers['referer']&.include?('/api-docs/index.html'))
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_csrf_cookie
 | 
				
			||||||
 | 
					    cookies['csrf-token'] = {
 | 
				
			||||||
 | 
					      value: form_authenticity_token,
 | 
				
			||||||
 | 
					      secure: false,
 | 
				
			||||||
 | 
					      same_site: :strict
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										15
									
								
								app/controllers/captcha_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/controllers/captcha_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CaptchaController < ApplicationController
 | 
				
			||||||
 | 
					  skip_before_action :authenticate_user!
 | 
				
			||||||
 | 
					  skip_before_action :set_tenant
 | 
				
			||||||
 | 
					  def create
 | 
				
			||||||
 | 
					    id = LibreCaptcha.new.id
 | 
				
			||||||
 | 
					    render json: {
 | 
				
			||||||
 | 
					      id:,
 | 
				
			||||||
 | 
					      media_url: media_captcha_index_url(id:)
 | 
				
			||||||
 | 
					    }, status: :created
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,70 +1,34 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExpensesController < ApplicationController
 | 
					class ExpensesController < ApplicationController
 | 
				
			||||||
  before_action :set_expense, only: %i[ show edit update destroy ]
 | 
					  def summary
 | 
				
			||||||
 | 
					    render json: Expenses::TotalQuery.new.call
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # GET /expenses or /expenses.json
 | 
					 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    @expenses = Expense.all
 | 
					    render json: Expense.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # GET /expenses/1 or /expenses/1.json
 | 
					 | 
				
			||||||
  def show
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # GET /expenses/new
 | 
					 | 
				
			||||||
  def new
 | 
					 | 
				
			||||||
    @expense = Expense.new
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # GET /expenses/1/edit
 | 
					 | 
				
			||||||
  def edit
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # POST /expenses or /expenses.json
 | 
					 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
    @expense = Expense.new(expense_params)
 | 
					    Expense.create!(expense_params)
 | 
				
			||||||
 | 
					    render json: {}, status: :created
 | 
				
			||||||
    respond_to do |format|
 | 
					 | 
				
			||||||
      if @expense.save
 | 
					 | 
				
			||||||
        format.html { redirect_to expense_url(@expense), notice: "Expense was successfully created." }
 | 
					 | 
				
			||||||
        format.json { render :show, status: :created, location: @expense }
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        format.html { render :new, status: :unprocessable_entity }
 | 
					 | 
				
			||||||
        format.json { render json: @expense.errors, status: :unprocessable_entity }
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # PATCH/PUT /expenses/1 or /expenses/1.json
 | 
					 | 
				
			||||||
  def update
 | 
					  def update
 | 
				
			||||||
    respond_to do |format|
 | 
					    Expense.find(params[:id]).update!(expense_params)
 | 
				
			||||||
      if @expense.update(expense_params)
 | 
					    render json: {}, status: :ok
 | 
				
			||||||
        format.html { redirect_to expense_url(@expense), notice: "Expense was successfully updated." }
 | 
					 | 
				
			||||||
        format.json { render :show, status: :ok, location: @expense }
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        format.html { render :edit, status: :unprocessable_entity }
 | 
					 | 
				
			||||||
        format.json { render json: @expense.errors, status: :unprocessable_entity }
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # DELETE /expenses/1 or /expenses/1.json
 | 
					 | 
				
			||||||
  def destroy
 | 
					  def destroy
 | 
				
			||||||
    @expense.destroy!
 | 
					    Expense.find(params[:id]).destroy!
 | 
				
			||||||
 | 
					    render json: {}, status: :ok
 | 
				
			||||||
    respond_to do |format|
 | 
					 | 
				
			||||||
      format.html { redirect_to expenses_url, notice: "Expense was successfully destroyed." }
 | 
					 | 
				
			||||||
      format.json { head :no_content }
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
    # Use callbacks to share common setup or constraints between actions.
 | 
					 | 
				
			||||||
    def set_expense
 | 
					 | 
				
			||||||
      @expense = Expense.find(params[:id])
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Only allow a list of trusted parameters through.
 | 
					 | 
				
			||||||
  def expense_params
 | 
					  def expense_params
 | 
				
			||||||
      params.require(:expense).permit(:name, :amount, :pricing_type)
 | 
					    params.expect(expense: %i[name amount pricing_type])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,46 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupsController < ApplicationController
 | 
					class GroupsController < ApplicationController
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    roots = Group.where(parent_id: nil)
 | 
					    query_result = Groups::SummaryQuery.new.call.as_json.map(&:deep_symbolize_keys).map do |group|
 | 
				
			||||||
    render jsonapi: roots, include: [children: [children: [:children]]]
 | 
					      {
 | 
				
			||||||
 | 
					        id: group[:id],
 | 
				
			||||||
 | 
					        name: group[:name],
 | 
				
			||||||
 | 
					        icon: group[:icon],
 | 
				
			||||||
 | 
					        color: group[:color],
 | 
				
			||||||
 | 
					        parent_id: group[:parent_id],
 | 
				
			||||||
 | 
					        attendance: group.slice(:total, :considered, :invited, :confirmed, :declined, :tentative)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render json: query_result
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create
 | 
				
			||||||
 | 
					    group = Group.create!(**group_params, parent:)
 | 
				
			||||||
 | 
					    render json: group.as_json(only: %i[id name icon color parent_id]), status: :created
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update
 | 
				
			||||||
 | 
					    group = Group.find(params[:id])
 | 
				
			||||||
 | 
					    group.update!(**group_params, parent:)
 | 
				
			||||||
 | 
					    render json: group.as_json(only: %i[id name icon color parent_id]), status: :ok
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def destroy
 | 
				
			||||||
 | 
					    Group.find(params[:id]).destroy!
 | 
				
			||||||
 | 
					    render json: {}, status: :ok
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def parent
 | 
				
			||||||
 | 
					    params[:group][:parent_id].present? ? Group.find(params[:group][:parent_id]) : nil
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def group_params
 | 
				
			||||||
 | 
					    params.expect(group: %i[name icon color])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,89 +1,35 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require 'csv'
 | 
					require 'csv'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GuestsController < ApplicationController
 | 
					class GuestsController < ApplicationController
 | 
				
			||||||
  before_action :set_guest, only: %i[show edit update destroy]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # GET /guests or /guests.json
 | 
					 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    @guests = Guest.all
 | 
					    render json: Guest.includes(:group)
 | 
				
			||||||
                   .joins(:group)
 | 
					                      .left_joins(:group)
 | 
				
			||||||
                   .order('groups.name' => :asc)
 | 
					                      .order('groups.name' => :asc, name: :asc)
 | 
				
			||||||
 | 
					                      .as_json(only: %i[id name status], include: { group: { only: %i[id name] } })
 | 
				
			||||||
      render jsonapi: @guests
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # GET /guests/1 or /guests/1.json
 | 
					 | 
				
			||||||
  def show; end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # GET /guests/new
 | 
					 | 
				
			||||||
  def new
 | 
					 | 
				
			||||||
    @guest = Guest.new
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # GET /guests/1/edit
 | 
					 | 
				
			||||||
  def edit; end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # POST /guests or /guests.json
 | 
					 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
    @guest = Guest.new(guest_params)
 | 
					    Guest.create!(guest_params)
 | 
				
			||||||
 | 
					    render json: {}, status: :created
 | 
				
			||||||
    respond_to do |format|
 | 
					 | 
				
			||||||
      if @guest.save
 | 
					 | 
				
			||||||
        format.html { redirect_to guest_url(@guest), notice: 'Guest was successfully created.' }
 | 
					 | 
				
			||||||
        format.json { render :show, status: :created, location: @guest }
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        format.html { render :new, status: :unprocessable_entity }
 | 
					 | 
				
			||||||
        format.json { render json: @guest.errors, status: :unprocessable_entity }
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # PATCH/PUT /guests/1 or /guests/1.json
 | 
					 | 
				
			||||||
  def update
 | 
					  def update
 | 
				
			||||||
    respond_to do |format|
 | 
					    Guest.find(params[:id]).update!(guest_params)
 | 
				
			||||||
      if @guest.update(guest_params)
 | 
					    render json: {}, status: :ok
 | 
				
			||||||
        format.html { redirect_to guest_url(@guest), notice: 'Guest was successfully updated.' }
 | 
					 | 
				
			||||||
        format.json { render :show, status: :ok, location: @guest }
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        format.html { render :edit, status: :unprocessable_entity }
 | 
					 | 
				
			||||||
        format.json { render json: @guest.errors, status: :unprocessable_entity }
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # DELETE /guests/1 or /guests/1.json
 | 
					 | 
				
			||||||
  def destroy
 | 
					  def destroy
 | 
				
			||||||
    @guest.destroy!
 | 
					    Guest.find(params[:id]).destroy!
 | 
				
			||||||
 | 
					    render json: {}, status: :ok
 | 
				
			||||||
    respond_to do |format|
 | 
					 | 
				
			||||||
      format.html { redirect_to guests_url, notice: 'Guest was successfully destroyed.' }
 | 
					 | 
				
			||||||
      format.json { head :no_content }
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def import
 | 
					 | 
				
			||||||
    csv = CSV.parse(params[:file].read, headers: true)
 | 
					 | 
				
			||||||
    ActiveRecord::Base.transaction do
 | 
					 | 
				
			||||||
      csv.each do |row|
 | 
					 | 
				
			||||||
        guest = Guest.create!(first_name: row['name'])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        guest.affinity_group_list.add(row['affinity_group'])
 | 
					 | 
				
			||||||
        guest.save!
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    redirect_to guests_url
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Use callbacks to share common setup or constraints between actions.
 | 
					 | 
				
			||||||
  def set_guest
 | 
					 | 
				
			||||||
    @guest = Guest.find(params[:id])
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # Only allow a list of trusted parameters through.
 | 
					 | 
				
			||||||
  def guest_params
 | 
					  def guest_params
 | 
				
			||||||
    params.require(:guest).permit(:first_name, :last_name, :email, :phone)
 | 
					    params.expect(guest: %i[name group_id status])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										44
									
								
								app/controllers/summary_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/controllers/summary_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SummaryController < ApplicationController
 | 
				
			||||||
 | 
					  def index
 | 
				
			||||||
 | 
					    render json: {
 | 
				
			||||||
 | 
					      expenses:,
 | 
				
			||||||
 | 
					      guests:
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def guests
 | 
				
			||||||
 | 
					    guest_summary = Guest.group(:status).count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      total: guest_summary.except('considered').values.sum,
 | 
				
			||||||
 | 
					      confirmed: guest_summary['confirmed'].to_i,
 | 
				
			||||||
 | 
					      declined: guest_summary['declined'].to_i,
 | 
				
			||||||
 | 
					      tentative: guest_summary['tentative'].to_i,
 | 
				
			||||||
 | 
					      invited: guest_summary['invited'].to_i
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def expenses
 | 
				
			||||||
 | 
					    expense_summary = Expenses::TotalQuery.new(wedding: ActsAsTenant.current_tenant).call
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      projected: {
 | 
				
			||||||
 | 
					        total: expense_summary['total_projected'],
 | 
				
			||||||
 | 
					        guests: expense_summary['projected_guests']
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      confirmed: {
 | 
				
			||||||
 | 
					        total: expense_summary['total_confirmed'],
 | 
				
			||||||
 | 
					        guests: expense_summary['confirmed_guests']
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      status: {
 | 
				
			||||||
 | 
					        paid: 0
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,12 +1,54 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TablesArrangementsController < ApplicationController
 | 
					class TablesArrangementsController < ApplicationController
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    @tables_arrangements = TablesArrangement.all.order(discomfort: :asc).limit(10)
 | 
					    current_digest = Tables::Distribution.digest(current_tenant)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render json: TablesArrangement
 | 
				
			||||||
 | 
					      .order(valid: :desc)
 | 
				
			||||||
 | 
					      .order(discomfort: :asc)
 | 
				
			||||||
 | 
					      .select(:id, :name, :discomfort)
 | 
				
			||||||
 | 
					      .select("digest = '#{current_digest}'::uuid as valid")
 | 
				
			||||||
 | 
					      .limit(20)
 | 
				
			||||||
 | 
					      .as_json(only: %i[id name discomfort valid])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show
 | 
					  def show
 | 
				
			||||||
    @tables_arrangement = TablesArrangement.find(params[:id])
 | 
					    Guest.joins(:seats, :group)
 | 
				
			||||||
    @seats = @tables_arrangement.seats
 | 
					         .where(seats: { tables_arrangement_id: params[:id] })
 | 
				
			||||||
                                .includes(guest: %i[affinity_groups unbreakable_bonds])
 | 
					         .select('guests.*', 'groups.color', 'seats.table_number')
 | 
				
			||||||
         .group_by(&:table_number)
 | 
					         .group_by(&:table_number)
 | 
				
			||||||
 | 
					         .map { |number, guests| format(number:, guests:) }
 | 
				
			||||||
 | 
					         .then { |result| render json: { id: params[:id], tables: result } }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create
 | 
				
			||||||
 | 
					    TableSimulatorJob.perform_later(current_tenant.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render json: {}, status: :created
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def format(number:, guests:)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      number: number,
 | 
				
			||||||
 | 
					      discomfort: discomfort(guests: guests),
 | 
				
			||||||
 | 
					      guests: guests.as_json(only: %i[id name color])
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def discomfort(guests:)
 | 
				
			||||||
 | 
					    table = Tables::Table.new(guests)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table.min_per_table = TableSimulatorJob::MIN_PER_TABLE
 | 
				
			||||||
 | 
					    table.max_per_table = TableSimulatorJob::MAX_PER_TABLE
 | 
				
			||||||
 | 
					    calculator = Tables::DiscomfortCalculator.new(table:)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      discomfort: calculator.calculate,
 | 
				
			||||||
 | 
					      breakdown: calculator.breakdown
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								app/controllers/tokens_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/controllers/tokens_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TokensController < ApplicationController
 | 
				
			||||||
 | 
					  skip_before_action :authenticate_user!
 | 
				
			||||||
 | 
					  skip_before_action :set_tenant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show
 | 
				
			||||||
 | 
					    head :ok
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										27
									
								
								app/controllers/users/confirmations_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/controllers/users/confirmations_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Users
 | 
				
			||||||
 | 
					  class ConfirmationsController < Devise::ConfirmationsController
 | 
				
			||||||
 | 
					    clear_respond_to
 | 
				
			||||||
 | 
					    respond_to :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def show
 | 
				
			||||||
 | 
					      super do |resource|
 | 
				
			||||||
 | 
					        if resource.errors.empty?
 | 
				
			||||||
 | 
					          respond_to do |format|
 | 
				
			||||||
 | 
					            format.json { render json: resource, status: :ok }
 | 
				
			||||||
 | 
					            format.any { redirect_to root_path }
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					          render json: {
 | 
				
			||||||
 | 
					            message: 'Record invalid',
 | 
				
			||||||
 | 
					            errors: resource.errors.full_messages
 | 
				
			||||||
 | 
					          }, status: :unprocessable_entity
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										32
									
								
								app/controllers/users/registrations_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/controllers/users/registrations_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Users
 | 
				
			||||||
 | 
					  class RegistrationsController < Devise::RegistrationsController
 | 
				
			||||||
 | 
					    clear_respond_to
 | 
				
			||||||
 | 
					    respond_to :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before_action :validate_captcha!, only: :create
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create
 | 
				
			||||||
 | 
					      wedding = Wedding.create(slug: params[:slug])
 | 
				
			||||||
 | 
					      unless wedding.persisted?
 | 
				
			||||||
 | 
					        render json: { errors: wedding.errors.full_messages }, status: :unprocessable_entity
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ActsAsTenant.with_tenant(wedding) do
 | 
				
			||||||
 | 
					        super do |user|
 | 
				
			||||||
 | 
					          wedding.destroy unless user.persisted?
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def set_tenant
 | 
				
			||||||
 | 
					      set_current_tenant(nil)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										10
									
								
								app/controllers/users/sessions_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/controllers/users/sessions_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Users
 | 
				
			||||||
 | 
					  class SessionsController < Devise::SessionsController
 | 
				
			||||||
 | 
					    clear_respond_to
 | 
				
			||||||
 | 
					    respond_to :json
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,3 +1,7 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module TreeNodeExtension
 | 
					module TreeNodeExtension
 | 
				
			||||||
  def distance_to_common_ancestor(another_node)
 | 
					  def distance_to_common_ancestor(another_node)
 | 
				
			||||||
    return 0 if self == another_node
 | 
					    return 0 if self == another_node
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,6 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module ApplicationHelper
 | 
					module ApplicationHelper
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,6 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module ExpensesHelper
 | 
					module ExpensesHelper
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,6 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module GroupsHelper
 | 
					module GroupsHelper
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,6 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module GuestsHelper
 | 
					module GuestsHelper
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,6 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module TablesArrangementsHelper
 | 
					module TablesArrangementsHelper
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,7 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApplicationJob < ActiveJob::Base
 | 
					class ApplicationJob < ActiveJob::Base
 | 
				
			||||||
  # Automatically retry jobs that encountered a deadlock
 | 
					  # Automatically retry jobs that encountered a deadlock
 | 
				
			||||||
  # retry_on ActiveRecord::Deadlocked
 | 
					  # retry_on ActiveRecord::Deadlocked
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										30
									
								
								app/jobs/table_simulator_job.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/jobs/table_simulator_job.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TableSimulatorJob < ApplicationJob
 | 
				
			||||||
 | 
					  queue_as :default
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MIN_PER_TABLE = 8
 | 
				
			||||||
 | 
					  MAX_PER_TABLE = 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def perform(wedding_id)
 | 
				
			||||||
 | 
					    ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do
 | 
				
			||||||
 | 
					      engine = VNS::Engine.new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      engine.add_perturbation(Tables::Swap)
 | 
				
			||||||
 | 
					      engine.add_perturbation(Tables::Shift)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      initial_solution = Tables::Distribution.new(min_per_table: MIN_PER_TABLE, max_per_table: MAX_PER_TABLE)
 | 
				
			||||||
 | 
					      initial_solution.random_distribution(Guest.potential.shuffle)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      engine.initial_solution = initial_solution
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      engine.target_function(&:discomfort)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      best_solution = engine.run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      best_solution.save!
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,4 +1,8 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApplicationMailer < ActionMailer::Base
 | 
					class ApplicationMailer < ActionMailer::Base
 | 
				
			||||||
  default from: "from@example.com"
 | 
					  default from: 'from@example.com'
 | 
				
			||||||
  layout "mailer"
 | 
					  layout 'mailer'
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,7 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApplicationRecord < ActiveRecord::Base
 | 
					class ApplicationRecord < ActiveRecord::Base
 | 
				
			||||||
  primary_abstract_class
 | 
					  primary_abstract_class
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,34 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: expenses
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id           :uuid             not null, primary key
 | 
				
			||||||
 | 
					#  amount       :decimal(, )
 | 
				
			||||||
 | 
					#  name         :string
 | 
				
			||||||
 | 
					#  pricing_type :enum             default("fixed"), not null
 | 
				
			||||||
 | 
					#  created_at   :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at   :datetime         not null
 | 
				
			||||||
 | 
					#  wedding_id   :uuid             not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_expenses_on_wedding_id  (wedding_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  fk_rails_...  (wedding_id => weddings.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
class Expense < ApplicationRecord
 | 
					class Expense < ApplicationRecord
 | 
				
			||||||
 | 
					  acts_as_tenant :wedding
 | 
				
			||||||
 | 
					  enum :pricing_type,
 | 
				
			||||||
 | 
					       fixed: 'fixed',
 | 
				
			||||||
 | 
					       per_person: 'per_person'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :name, presence: true
 | 
				
			||||||
 | 
					  validates :amount, presence: true, numericality: { greater_than: 0 }
 | 
				
			||||||
 | 
					  validates :pricing_type, presence: true
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,77 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: groups
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id         :uuid             not null, primary key
 | 
				
			||||||
 | 
					#  color      :string
 | 
				
			||||||
 | 
					#  icon       :string
 | 
				
			||||||
 | 
					#  name       :string           not null
 | 
				
			||||||
 | 
					#  order      :integer          default(1), not null
 | 
				
			||||||
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#  parent_id  :uuid
 | 
				
			||||||
 | 
					#  wedding_id :uuid             not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_groups_on_name        (name) UNIQUE
 | 
				
			||||||
 | 
					#  index_groups_on_parent_id   (parent_id)
 | 
				
			||||||
 | 
					#  index_groups_on_wedding_id  (wedding_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  fk_rails_...  (parent_id => groups.id)
 | 
				
			||||||
 | 
					#  fk_rails_...  (wedding_id => weddings.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
class Group < ApplicationRecord
 | 
					class Group < ApplicationRecord
 | 
				
			||||||
 | 
					  acts_as_tenant :wedding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validates :name, uniqueness: true
 | 
					  validates :name, uniqueness: true
 | 
				
			||||||
  validates :name, :order, presence: true
 | 
					  validates :name, :order, presence: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_many :children, class_name: 'Group', foreign_key: 'parent_id'
 | 
					  has_many :children, class_name: 'Group', foreign_key: 'parent_id', dependent: :nullify, inverse_of: :parent
 | 
				
			||||||
  belongs_to :parent, class_name: 'Group', optional: true
 | 
					  belongs_to :parent, class_name: 'Group', optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_many :guests
 | 
					  before_create :set_color
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  scope :roots, -> { where(parent_id: nil) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_many :guests, dependent: :nullify
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def colorize_children(generation = 1)
 | 
				
			||||||
 | 
					    children.zip(palette(generation)) do |child, raw_color|
 | 
				
			||||||
 | 
					      final_color = raw_color.paint
 | 
				
			||||||
 | 
					      final_color.brighten(60) if final_color.dark?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      child.update!(color: final_color)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      child.colorize_children(generation + 1)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def affinities
 | 
				
			||||||
 | 
					    GroupAffinity.where(group_a_id: id).or(GroupAffinity.where(group_b_id: id))
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def palette(generation)
 | 
				
			||||||
 | 
					    if generation == 1
 | 
				
			||||||
 | 
					      color.paint.palette.analogous(size: children.count)
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      color.paint.palette.decreasing_saturation
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_color
 | 
				
			||||||
 | 
					    return if color.present?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    new_color = "##{SecureRandom.hex(3)}".paint
 | 
				
			||||||
 | 
					    new_color = new_color.lighten(30) if new_color.dark?
 | 
				
			||||||
 | 
					    self.color = new_color
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										43
									
								
								app/models/group_affinity.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/models/group_affinity.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: group_affinities
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id         :bigint           not null, primary key
 | 
				
			||||||
 | 
					#  discomfort :float            not null
 | 
				
			||||||
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#  group_a_id :uuid             not null
 | 
				
			||||||
 | 
					#  group_b_id :uuid             not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_group_affinities_on_group_a_id  (group_a_id)
 | 
				
			||||||
 | 
					#  index_group_affinities_on_group_b_id  (group_b_id)
 | 
				
			||||||
 | 
					#  uindex_group_pair                     (LEAST(group_a_id, group_b_id), GREATEST(group_a_id, group_b_id)) UNIQUE
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  fk_rails_...  (group_a_id => groups.id)
 | 
				
			||||||
 | 
					#  fk_rails_...  (group_b_id => groups.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					class GroupAffinity < ApplicationRecord
 | 
				
			||||||
 | 
					  NEUTRAL = 1
 | 
				
			||||||
 | 
					  MIN_DISCOMFORT = 0
 | 
				
			||||||
 | 
					  MAX_DISCOMFORT = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  belongs_to :group_a, class_name: 'Group'
 | 
				
			||||||
 | 
					  belongs_to :group_b, class_name: 'Group'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :discomfort,
 | 
				
			||||||
 | 
					            numericality: { greater_than_or_equal_to: MIN_DISCOMFORT, less_than_or_equal_to: MAX_DISCOMFORT }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def another_group(group)
 | 
				
			||||||
 | 
					    return nil if group != group_a && group != group_b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    group == group_a ? group_b : group_a
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,15 +1,45 @@
 | 
				
			|||||||
class Guest < ApplicationRecord
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
  acts_as_taggable_on :affinity_groups, :unbreakable_bonds
 | 
					 | 
				
			||||||
  belongs_to :group
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  enum status: {
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: guests
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id         :uuid             not null, primary key
 | 
				
			||||||
 | 
					#  name       :string
 | 
				
			||||||
 | 
					#  phone      :string
 | 
				
			||||||
 | 
					#  status     :integer          default("considered")
 | 
				
			||||||
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#  group_id   :uuid
 | 
				
			||||||
 | 
					#  wedding_id :uuid             not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_guests_on_group_id    (group_id)
 | 
				
			||||||
 | 
					#  index_guests_on_wedding_id  (wedding_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  fk_rails_...  (group_id => groups.id)
 | 
				
			||||||
 | 
					#  fk_rails_...  (wedding_id => weddings.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					class Guest < ApplicationRecord
 | 
				
			||||||
 | 
					  acts_as_tenant :wedding
 | 
				
			||||||
 | 
					  belongs_to :group, optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  enum :status, {
 | 
				
			||||||
    considered: 0,
 | 
					    considered: 0,
 | 
				
			||||||
    invited: 10,
 | 
					    invited: 10,
 | 
				
			||||||
    confirmed: 20,
 | 
					    confirmed: 20,
 | 
				
			||||||
    declined: 30
 | 
					    declined: 30,
 | 
				
			||||||
  }
 | 
					    tentative: 40
 | 
				
			||||||
 | 
					  }, validate: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def full_name
 | 
					  validates :name, presence: true
 | 
				
			||||||
    "#{first_name} #{last_name}"
 | 
					
 | 
				
			||||||
  end
 | 
					  scope :potential, -> { where.not(status: %i[declined considered]) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_many :seats, dependent: :delete_all
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,33 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: seats
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id                    :uuid             not null, primary key
 | 
				
			||||||
 | 
					#  table_number          :integer
 | 
				
			||||||
 | 
					#  created_at            :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at            :datetime         not null
 | 
				
			||||||
 | 
					#  guest_id              :uuid             not null
 | 
				
			||||||
 | 
					#  tables_arrangement_id :uuid             not null
 | 
				
			||||||
 | 
					#  wedding_id            :uuid             not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_seats_on_guest_id               (guest_id)
 | 
				
			||||||
 | 
					#  index_seats_on_tables_arrangement_id  (tables_arrangement_id)
 | 
				
			||||||
 | 
					#  index_seats_on_wedding_id             (wedding_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  fk_rails_...  (guest_id => guests.id)
 | 
				
			||||||
 | 
					#  fk_rails_...  (tables_arrangement_id => tables_arrangements.id) ON DELETE => cascade
 | 
				
			||||||
 | 
					#  fk_rails_...  (wedding_id => weddings.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
class Seat < ApplicationRecord
 | 
					class Seat < ApplicationRecord
 | 
				
			||||||
 | 
					  acts_as_tenant :wedding
 | 
				
			||||||
  belongs_to :guest
 | 
					  belongs_to :guest
 | 
				
			||||||
  belongs_to :table_arrangement
 | 
					  belongs_to :table_arrangement
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,37 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: tables_arrangements
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id         :uuid             not null, primary key
 | 
				
			||||||
 | 
					#  digest     :uuid             not null
 | 
				
			||||||
 | 
					#  discomfort :integer
 | 
				
			||||||
 | 
					#  name       :string           not null
 | 
				
			||||||
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#  wedding_id :uuid             not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_tables_arrangements_on_wedding_id  (wedding_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  fk_rails_...  (wedding_id => weddings.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
class TablesArrangement < ApplicationRecord
 | 
					class TablesArrangement < ApplicationRecord
 | 
				
			||||||
  has_many :seats
 | 
					  acts_as_tenant :wedding
 | 
				
			||||||
 | 
					  has_many :seats, dependent: :delete_all
 | 
				
			||||||
 | 
					  has_many :guests, through: :seats
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_create :assign_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def assign_name
 | 
				
			||||||
 | 
					    self.name = "#{Faker::Adjective.positive} #{Faker::Creature::Animal.name}".capitalize
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										42
									
								
								app/models/user.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/models/user.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: users
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id                     :uuid             not null, primary key
 | 
				
			||||||
 | 
					#  confirmation_sent_at   :datetime
 | 
				
			||||||
 | 
					#  confirmation_token     :string
 | 
				
			||||||
 | 
					#  confirmed_at           :datetime
 | 
				
			||||||
 | 
					#  email                  :string           default(""), not null
 | 
				
			||||||
 | 
					#  encrypted_password     :string           default(""), not null
 | 
				
			||||||
 | 
					#  failed_attempts        :integer          default(0), not null
 | 
				
			||||||
 | 
					#  locked_at              :datetime
 | 
				
			||||||
 | 
					#  reset_password_sent_at :datetime
 | 
				
			||||||
 | 
					#  reset_password_token   :string
 | 
				
			||||||
 | 
					#  unconfirmed_email      :string
 | 
				
			||||||
 | 
					#  unlock_token           :string
 | 
				
			||||||
 | 
					#  created_at             :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at             :datetime         not null
 | 
				
			||||||
 | 
					#  wedding_id             :uuid             not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_users_on_confirmation_token    (confirmation_token) UNIQUE
 | 
				
			||||||
 | 
					#  index_users_on_email                 (email) UNIQUE
 | 
				
			||||||
 | 
					#  index_users_on_reset_password_token  (reset_password_token) UNIQUE
 | 
				
			||||||
 | 
					#  index_users_on_unlock_token          (unlock_token) UNIQUE
 | 
				
			||||||
 | 
					#  index_users_on_wedding_id            (wedding_id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Foreign Keys
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  fk_rails_...  (wedding_id => weddings.id)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					class User < ApplicationRecord
 | 
				
			||||||
 | 
					  acts_as_tenant :wedding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  devise :database_authenticatable, :registerable,
 | 
				
			||||||
 | 
					         :recoverable, :validatable, :confirmable, :lockable
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										24
									
								
								app/models/wedding.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/models/wedding.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: weddings
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id         :uuid             not null, primary key
 | 
				
			||||||
 | 
					#  slug       :string           not null
 | 
				
			||||||
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Indexes
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  index_weddings_on_slug  (slug) UNIQUE
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					class Wedding < ApplicationRecord
 | 
				
			||||||
 | 
					  SLUG_REGEX = /[a-z\d-]+/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :slug, presence: true, uniqueness: true, format: { with: /\A#{SLUG_REGEX}\z/ }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_many :guests, dependent: :delete_all
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										50
									
								
								app/queries/expenses/total_query.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/queries/expenses/total_query.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Expenses
 | 
				
			||||||
 | 
					  class TotalQuery
 | 
				
			||||||
 | 
					    private attr_reader :wedding
 | 
				
			||||||
 | 
					    def initialize(wedding:)
 | 
				
			||||||
 | 
					      @wedding = wedding
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def call
 | 
				
			||||||
 | 
					      ActiveRecord::Base.connection.execute(
 | 
				
			||||||
 | 
					        ActiveRecord::Base.sanitize_sql_array([query, { wedding_id: wedding.id }])
 | 
				
			||||||
 | 
					      ).first
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def query
 | 
				
			||||||
 | 
					      <<~SQL.squish
 | 
				
			||||||
 | 
					        WITH guest_count AS (#{guest_count_per_status}),
 | 
				
			||||||
 | 
					             expense_summary AS (#{expense_summary})
 | 
				
			||||||
 | 
					        SELECT guest_count.confirmed as confirmed_guests,
 | 
				
			||||||
 | 
					               guest_count.projected as projected_guests,
 | 
				
			||||||
 | 
					               expense_summary.fixed + expense_summary.variable * guest_count.confirmed as total_confirmed,
 | 
				
			||||||
 | 
					               expense_summary.fixed + expense_summary.variable * guest_count.projected as total_projected
 | 
				
			||||||
 | 
					        FROM guest_count, expense_summary;
 | 
				
			||||||
 | 
					      SQL
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def expense_summary
 | 
				
			||||||
 | 
					      <<~SQL.squish
 | 
				
			||||||
 | 
					        SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed,
 | 
				
			||||||
 | 
					               coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable
 | 
				
			||||||
 | 
					        FROM expenses
 | 
				
			||||||
 | 
					        WHERE wedding_id = :wedding_id
 | 
				
			||||||
 | 
					      SQL
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def guest_count_per_status
 | 
				
			||||||
 | 
					      <<~SQL.squish
 | 
				
			||||||
 | 
					        SELECT COALESCE(count(*) filter(where status = #{Guest.statuses['confirmed']}), 0) as confirmed,
 | 
				
			||||||
 | 
					               COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at('confirmed', 'invited', 'tentative').join(',')})), 0) as projected
 | 
				
			||||||
 | 
					        FROM guests
 | 
				
			||||||
 | 
					        WHERE wedding_id = :wedding_id
 | 
				
			||||||
 | 
					      SQL
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										31
									
								
								app/queries/groups/summary_query.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								app/queries/groups/summary_query.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Groups
 | 
				
			||||||
 | 
					  class SummaryQuery
 | 
				
			||||||
 | 
					    def call
 | 
				
			||||||
 | 
					      Group.left_joins(:guests).group(:id).pluck_to_hash(
 | 
				
			||||||
 | 
					        :id,
 | 
				
			||||||
 | 
					        :name,
 | 
				
			||||||
 | 
					        :icon,
 | 
				
			||||||
 | 
					        :parent_id,
 | 
				
			||||||
 | 
					        :color,
 | 
				
			||||||
 | 
					        *count_expressions
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def count_expressions
 | 
				
			||||||
 | 
					      [
 | 
				
			||||||
 | 
					        Arel.sql('count(*) filter (where status IS NOT NULL) as total'),
 | 
				
			||||||
 | 
					        Arel.sql('count(*) filter (where status = 0) as considered'),
 | 
				
			||||||
 | 
					        Arel.sql('count(*) filter (where status = 10) as invited'),
 | 
				
			||||||
 | 
					        Arel.sql('count(*) filter (where status = 20) as confirmed'),
 | 
				
			||||||
 | 
					        Arel.sql('count(*) filter (where status = 30) as declined'),
 | 
				
			||||||
 | 
					        Arel.sql('count(*) filter (where status = 40) as tentative')
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,3 +1,7 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SerializableGroup < JSONAPI::Serializable::Resource
 | 
					class SerializableGroup < JSONAPI::Serializable::Resource
 | 
				
			||||||
  type 'group'
 | 
					  type 'group'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,14 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SerializableGuest < JSONAPI::Serializable::Resource
 | 
					class SerializableGuest < JSONAPI::Serializable::Resource
 | 
				
			||||||
  type 'guest'
 | 
					  type 'guest'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attributes :id, :email, :group_id, :status
 | 
					  attributes :id, :group_id, :status
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attribute :name do
 | 
					  attribute :name do
 | 
				
			||||||
    "#{@object.first_name} #{@object.last_name}"
 | 
					    @object.name
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attribute :group_name do
 | 
					  attribute :group_name do
 | 
				
			||||||
 | 
				
			|||||||
@ -1,27 +1,77 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AffinityGroupsHierarchy < Array
 | 
					class AffinityGroupsHierarchy < Array
 | 
				
			||||||
  include Singleton
 | 
					  DEFAULT_DISCOMFORT = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def initialize
 | 
					  def initialize
 | 
				
			||||||
    super
 | 
					    super
 | 
				
			||||||
    @references = {}
 | 
					    @references = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Group.roots.each do |group|
 | 
				
			||||||
 | 
					      self << group.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      hydrate(group)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def find(name)
 | 
					    discomforts
 | 
				
			||||||
    @references[name]
 | 
					    freeze
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def <<(name)
 | 
					  def find(id)
 | 
				
			||||||
    new_node = Tree::TreeNode.new(name)
 | 
					    @references[id]
 | 
				
			||||||
    super(new_node).tap { @references[name] = new_node }
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def register_child(parent_name, child_name)
 | 
					  def <<(id)
 | 
				
			||||||
    @references[parent_name] << Tree::TreeNode.new(child_name).tap { |child_node| @references[child_name] = child_node }
 | 
					    new_node = Tree::TreeNode.new(id)
 | 
				
			||||||
 | 
					    super(new_node).tap { @references[id] = new_node }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def distance(name_a, name_b)
 | 
					  def register_child(parent_id, child_id)
 | 
				
			||||||
    return nil if @references[name_a].nil? || @references[name_b].nil?
 | 
					    @references[parent_id] << Tree::TreeNode.new(child_id).tap { |child_node| @references[child_id] = child_node }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @references[name_a].distance_to_common_ancestor(@references[name_b])
 | 
					  def distance(id_a, id_b)
 | 
				
			||||||
 | 
					    return nil if @references[id_a].nil? || @references[id_b].nil?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @references[id_a].distance_to_common_ancestor(@references[id_b])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def discomfort(id_a, id_b)
 | 
				
			||||||
 | 
					    return 0 if id_a == id_b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @discomforts[uuid_to_int(id_a) + uuid_to_int(id_b)] || DEFAULT_DISCOMFORT
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def default_discomfort(id_a, id_b)
 | 
				
			||||||
 | 
					    return 0 if id_a == id_b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dist = distance(id_a, id_b)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return DEFAULT_DISCOMFORT if dist.nil?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Rational(dist, dist + 1)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def discomforts
 | 
				
			||||||
 | 
					    @discomforts ||= GroupAffinity.pluck(:group_a_id, :group_b_id,
 | 
				
			||||||
 | 
					                                         :discomfort).each_with_object({}) do |(id_a, id_b, discomfort), acc|
 | 
				
			||||||
 | 
					      acc[uuid_to_int(id_a) + uuid_to_int(id_b)] = discomfort
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def uuid_to_int(uuid)
 | 
				
			||||||
 | 
					    uuid.gsub('-', '').hex
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def hydrate(group)
 | 
				
			||||||
 | 
					    group.children.each do |child|
 | 
				
			||||||
 | 
					      register_child(group.id, child.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      hydrate(child)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								app/services/libre_captcha.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/services/libre_captcha.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LibreCaptcha
 | 
				
			||||||
 | 
					  def id
 | 
				
			||||||
 | 
					    HTTParty.post('http://libre-captcha:8888/v2/captcha',
 | 
				
			||||||
 | 
					                  body: {
 | 
				
			||||||
 | 
					                    input_type: 'text',
 | 
				
			||||||
 | 
					                    level: :hard,
 | 
				
			||||||
 | 
					                    media: 'image/png',
 | 
				
			||||||
 | 
					                    size: '350x100'
 | 
				
			||||||
 | 
					                  }.to_json).then { |raw| JSON.parse(raw)['id'] }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def valid?(id:, answer:)
 | 
				
			||||||
 | 
					    HTTParty.post('http://libre-captcha:8888/v2/answer',
 | 
				
			||||||
 | 
					                  body: { id:, answer: }.to_json).then { |raw| JSON.parse(raw)['result'] == 'True' }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,24 +1,54 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module Tables
 | 
					module Tables
 | 
				
			||||||
  class DiscomfortCalculator
 | 
					  class DiscomfortCalculator
 | 
				
			||||||
    private attr_reader :table
 | 
					    private attr_reader :table, :hierarchy
 | 
				
			||||||
    def initialize(table)
 | 
					    def initialize(table:, hierarchy: AffinityGroupsHierarchy.new)
 | 
				
			||||||
      @table = table
 | 
					      @table = table
 | 
				
			||||||
 | 
					      @hierarchy = hierarchy
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def calculate
 | 
					    def calculate
 | 
				
			||||||
      cohesion_penalty
 | 
					      breakdown.values.sum
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def breakdown
 | 
				
			||||||
 | 
					      @breakdown ||= { table_size_penalty:, cohesion_penalty: }
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # Calculates the penalty associated with violating the table size constraints. The penalty is
 | 
				
			||||||
 | 
					    # zero when the limits are honored, and it increases linearly as the number of guests deviates
 | 
				
			||||||
 | 
					    # from the limits. Overcapacity is penalized more severely than undercapacity.
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # @return [Number] The penalty associated with violating the table size constraints.
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    def table_size_penalty
 | 
				
			||||||
 | 
					      case table.size
 | 
				
			||||||
 | 
					      when 0...table.min_per_table then 5 * (table.min_per_table - table.size)
 | 
				
			||||||
 | 
					      when table.min_per_table..table.max_per_table then 0
 | 
				
			||||||
 | 
					      else 5 * (table.size - table.max_per_table)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def cohesion_penalty
 | 
					    def cohesion_penalty
 | 
				
			||||||
      table.map { |guest| guest.affinity_group_list.first }.tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)|
 | 
					      10 * (cohesion_discomfort * 1.0 / table.size)
 | 
				
			||||||
        distance = AffinityGroupsHierarchy.instance.distance(a, b)
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        next count_a * count_b if distance.nil?
 | 
					    #
 | 
				
			||||||
        next 0 if distance.zero?
 | 
					    # Calculates the discomfort of the table based on the cohesion of the guests. The total discomfort
 | 
				
			||||||
 | 
					    # is calculated as the sum of the discomfort of each pair of guests. The discomfort of a pair of
 | 
				
			||||||
        count_a * count_b * Rational(distance, distance + 1)
 | 
					    # guests is a rational number between 1 (unrelated groups) and 0 (same group).
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # @return [Number] Total discomfort of the table.
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    def cohesion_discomfort
 | 
				
			||||||
 | 
					      table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)|
 | 
				
			||||||
 | 
					        count_a * count_b * hierarchy.discomfort(a, b)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,19 +1,33 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require_relative '../../extensions/tree_node_extension'
 | 
					require_relative '../../extensions/tree_node_extension'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module Tables
 | 
					module Tables
 | 
				
			||||||
  class Distribution
 | 
					  class Distribution
 | 
				
			||||||
    attr_accessor :tables
 | 
					    class << self
 | 
				
			||||||
 | 
					      def digest(wedding)
 | 
				
			||||||
 | 
					        Digest::UUID.uuid_v5(wedding.id, wedding.guests.potential.order(:id).pluck(:id).join)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    attr_accessor :tables, :min_per_table, :max_per_table, :hierarchy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def initialize(min_per_table:, max_per_table:)
 | 
					    def initialize(min_per_table:, max_per_table:)
 | 
				
			||||||
      @min_per_table = min_per_table
 | 
					      @min_per_table = min_per_table
 | 
				
			||||||
      @max_per_table = max_per_table
 | 
					      @max_per_table = max_per_table
 | 
				
			||||||
 | 
					      @hierarchy = AffinityGroupsHierarchy.new
 | 
				
			||||||
      @tables = []
 | 
					      @tables = []
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def random_distribution(people)
 | 
					    def random_distribution(people)
 | 
				
			||||||
      @tables = []
 | 
					      min_tables = (people.count * 1.0 / @max_per_table).ceil
 | 
				
			||||||
 | 
					      max_tables = (people.count * 1.0 / @min_per_table).ceil
 | 
				
			||||||
      @tables << Table.new(people.slice!(0..rand(@min_per_table..@max_per_table))) while people.any?
 | 
					      @tables = people.in_groups(rand(min_tables..max_tables), false)
 | 
				
			||||||
 | 
					                      .map { |group| Table.new(group) }
 | 
				
			||||||
 | 
					                      .each { |table| table.min_per_table = @min_per_table }
 | 
				
			||||||
 | 
					                      .each { |table| table.max_per_table = @max_per_table }
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def discomfort
 | 
					    def discomfort
 | 
				
			||||||
@ -26,12 +40,6 @@ module Tables
 | 
				
			|||||||
      "#{@tables.count} tables, discomfort: #{discomfort}"
 | 
					      "#{@tables.count} tables, discomfort: #{discomfort}"
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def pretty_print
 | 
					 | 
				
			||||||
      @tables.map.with_index do |table, i|
 | 
					 | 
				
			||||||
        "Table #{i + 1} (#{table.count} ppl): (#{local_discomfort(table)}) #{table.map(&:full_name).join(', ')}"
 | 
					 | 
				
			||||||
      end.join("\n")
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def deep_dup
 | 
					    def deep_dup
 | 
				
			||||||
      self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table).tap do |new_distribution|
 | 
					      self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table).tap do |new_distribution|
 | 
				
			||||||
        new_distribution.tables = @tables.map(&:dup)
 | 
					        new_distribution.tables = @tables.map(&:dup)
 | 
				
			||||||
@ -52,14 +60,17 @@ module Tables
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Seat.insert_all!(records_to_store)
 | 
					        Seat.insert_all!(records_to_store)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        arrangement.update!(discomfort:)
 | 
					        arrangement.update!(
 | 
				
			||||||
 | 
					          discomfort:,
 | 
				
			||||||
 | 
					          digest: self.class.digest(tables.first.first.wedding)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def local_discomfort(table)
 | 
					    def local_discomfort(table)
 | 
				
			||||||
      table.discomfort ||= DiscomfortCalculator.new(table).calculate
 | 
					      table.discomfort ||= DiscomfortCalculator.new(table:, hierarchy:).calculate
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										32
									
								
								app/services/tables/shift.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/services/tables/shift.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Tables
 | 
				
			||||||
 | 
					  class Shift
 | 
				
			||||||
 | 
					    private attr_reader :initial_solution
 | 
				
			||||||
 | 
					    def initialize(initial_solution)
 | 
				
			||||||
 | 
					      @initial_solution = initial_solution
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def each
 | 
				
			||||||
 | 
					      @initial_solution.tables.permutation(2) do |table_a, table_b|
 | 
				
			||||||
 | 
					        table_a.dup.each do |person|
 | 
				
			||||||
 | 
					          original_discomfort_a = table_a.reset
 | 
				
			||||||
 | 
					          original_discomfort_b = table_b.reset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          table_a.delete(person)
 | 
				
			||||||
 | 
					          table_b << person
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          yield(@initial_solution)
 | 
				
			||||||
 | 
					        ensure
 | 
				
			||||||
 | 
					          table_b.delete(person)
 | 
				
			||||||
 | 
					          table_a << person
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          table_a.discomfort = original_discomfort_a
 | 
				
			||||||
 | 
					          table_b.discomfort = original_discomfort_b
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,3 +1,7 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module Tables
 | 
					module Tables
 | 
				
			||||||
  class Swap
 | 
					  class Swap
 | 
				
			||||||
    private attr_reader :initial_solution
 | 
					    private attr_reader :initial_solution
 | 
				
			||||||
@ -7,7 +11,7 @@ module Tables
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def each
 | 
					    def each
 | 
				
			||||||
      @initial_solution.tables.combination(2) do |table_a, table_b|
 | 
					      @initial_solution.tables.combination(2) do |table_a, table_b|
 | 
				
			||||||
        table_a.product(table_b).each do |(person_a, person_b)|
 | 
					        table_a.to_a.product(table_b.to_a).each do |(person_a, person_b)|
 | 
				
			||||||
          original_discomfort_a = table_a.reset
 | 
					          original_discomfort_a = table_a.reset
 | 
				
			||||||
          original_discomfort_b = table_b.reset
 | 
					          original_discomfort_b = table_b.reset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,11 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module Tables
 | 
					module Tables
 | 
				
			||||||
  class Table < Array
 | 
					  class Table < Set
 | 
				
			||||||
    attr_accessor :discomfort
 | 
					    attr_accessor :discomfort, :min_per_table, :max_per_table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def initialize(*args)
 | 
					    def initialize(*args)
 | 
				
			||||||
      super
 | 
					      super
 | 
				
			||||||
      reset
 | 
					      reset
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,16 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module VNS
 | 
					module VNS
 | 
				
			||||||
  class Engine
 | 
					  class Engine
 | 
				
			||||||
 | 
					    class << self
 | 
				
			||||||
 | 
					      def sequence(elements)
 | 
				
			||||||
 | 
					        elements = elements.to_a
 | 
				
			||||||
 | 
					        (elements + elements.reverse).chunk(&:itself).map(&:first)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def target_function(&function)
 | 
					    def target_function(&function)
 | 
				
			||||||
      @target_function = function
 | 
					      @target_function = function
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
@ -19,11 +30,8 @@ module VNS
 | 
				
			|||||||
      @best_solution = @initial_solution
 | 
					      @best_solution = @initial_solution
 | 
				
			||||||
      @best_score = @target_function.call(@best_solution)
 | 
					      @best_score = @target_function.call(@best_solution)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      puts "Initial score: #{@best_score.to_f}"
 | 
					      self.class.sequence(@perturbations).each do |perturbation|
 | 
				
			||||||
 | 
					        optimize(perturbation)
 | 
				
			||||||
      @perturbations.each do |perturbation|
 | 
					 | 
				
			||||||
        puts "Running perturbation: #{perturbation.name}"
 | 
					 | 
				
			||||||
        optimize(perturbation.new(@best_solution))
 | 
					 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @best_solution
 | 
					      @best_solution
 | 
				
			||||||
@ -31,17 +39,22 @@ module VNS
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def optimize(perturbation)
 | 
					    def optimize(perturbation_klass)
 | 
				
			||||||
      perturbation.each do |alternative_solution|
 | 
					      loop do
 | 
				
			||||||
 | 
					        optimized = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        perturbation_klass.new(@best_solution).each do |alternative_solution|
 | 
				
			||||||
          score = @target_function.call(alternative_solution)
 | 
					          score = @target_function.call(alternative_solution)
 | 
				
			||||||
          next if score >= @best_score
 | 
					          next if score >= @best_score
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          @best_solution = alternative_solution.deep_dup
 | 
					          @best_solution = alternative_solution.deep_dup
 | 
				
			||||||
          @best_score = score
 | 
					          @best_score = score
 | 
				
			||||||
 | 
					          optimized = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        puts "New lowest score: #{@best_score.to_f}"
 | 
					          break
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return optimize(perturbation.class.new(@best_solution))
 | 
					        return unless optimized
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +0,0 @@
 | 
				
			|||||||
<div id="<%= dom_id expense %>">
 | 
					 | 
				
			||||||
  <p>
 | 
					 | 
				
			||||||
    <strong>Name:</strong>
 | 
					 | 
				
			||||||
    <%= expense.name %>
 | 
					 | 
				
			||||||
  </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <p>
 | 
					 | 
				
			||||||
    <strong>Amount:</strong>
 | 
					 | 
				
			||||||
    <%= expense.amount %>
 | 
					 | 
				
			||||||
  </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <p>
 | 
					 | 
				
			||||||
    <strong>Pricing type:</strong>
 | 
					 | 
				
			||||||
    <%= expense.pricing_type %>
 | 
					 | 
				
			||||||
  </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@ -1,32 +0,0 @@
 | 
				
			|||||||
<%= form_with(model: expense) do |form| %>
 | 
					 | 
				
			||||||
  <% if expense.errors.any? %>
 | 
					 | 
				
			||||||
    <div style="color: red">
 | 
					 | 
				
			||||||
      <h2><%= pluralize(expense.errors.count, "error") %> prohibited this expense from being saved:</h2>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <ul>
 | 
					 | 
				
			||||||
        <% expense.errors.each do |error| %>
 | 
					 | 
				
			||||||
          <li><%= error.full_message %></li>
 | 
					 | 
				
			||||||
        <% end %>
 | 
					 | 
				
			||||||
      </ul>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <%= form.label :name, style: "display: block" %>
 | 
					 | 
				
			||||||
    <%= form.text_field :name %>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <%= form.label :amount, style: "display: block" %>
 | 
					 | 
				
			||||||
    <%= form.text_field :amount %>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <%= form.label :pricing_type, style: "display: block" %>
 | 
					 | 
				
			||||||
    <%= form.text_field :pricing_type %>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <%= form.submit %>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
<% end %>
 | 
					 | 
				
			||||||
@ -1,10 +0,0 @@
 | 
				
			|||||||
<h1>Editing expense</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= render "form", expense: @expense %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div>
 | 
					 | 
				
			||||||
  <%= link_to "Show this expense", @expense %> |
 | 
					 | 
				
			||||||
  <%= link_to "Back to expenses", expenses_path %>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@ -1,28 +0,0 @@
 | 
				
			|||||||
<p style="color: green"><%= notice %></p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<h1>Expenses</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div id="expenses">
 | 
					 | 
				
			||||||
  <table>
 | 
					 | 
				
			||||||
    <tr>
 | 
					 | 
				
			||||||
      <th>Name</th>
 | 
					 | 
				
			||||||
      <th>Amount</th>
 | 
					 | 
				
			||||||
      <th colspan="2"></th>
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
  <% @expenses.each do |expense| %>
 | 
					 | 
				
			||||||
    <tr>
 | 
					 | 
				
			||||||
      <td><%= expense.name %></td>
 | 
					 | 
				
			||||||
      <td><%= expense.amount.to_currency %></td>
 | 
					 | 
				
			||||||
      <td><%= link_to "Show", expense %></td>
 | 
					 | 
				
			||||||
      <td><%= link_to "Edit", edit_expense_path(expense) %></td>
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
  <% end %>
 | 
					 | 
				
			||||||
  <tr>
 | 
					 | 
				
			||||||
    <td>Total</td>
 | 
					 | 
				
			||||||
    <td><%= @expenses.sum(&:amount).to_currency %></td>
 | 
					 | 
				
			||||||
    <td colspan="2"></td>
 | 
					 | 
				
			||||||
  </tr>
 | 
					 | 
				
			||||||
  </table>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= link_to "New expense", new_expense_path %>
 | 
					 | 
				
			||||||
@ -1,9 +0,0 @@
 | 
				
			|||||||
<h1>New expense</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= render "form", expense: @expense %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div>
 | 
					 | 
				
			||||||
  <%= link_to "Back to expenses", expenses_path %>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@ -1,10 +0,0 @@
 | 
				
			|||||||
<p style="color: green"><%= notice %></p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= render @expense %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div>
 | 
					 | 
				
			||||||
  <%= link_to "Edit this expense", edit_expense_path(@expense) %> |
 | 
					 | 
				
			||||||
  <%= link_to "Back to expenses", expenses_path %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <%= button_to "Destroy this expense", @expense, method: :delete %>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@ -1,37 +0,0 @@
 | 
				
			|||||||
<%= form_with(model: guest) do |form| %>
 | 
					 | 
				
			||||||
  <% if guest.errors.any? %>
 | 
					 | 
				
			||||||
    <div style="color: red">
 | 
					 | 
				
			||||||
      <h2><%= pluralize(guest.errors.count, "error") %> prohibited this guest from being saved:</h2>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <ul>
 | 
					 | 
				
			||||||
        <% guest.errors.each do |error| %>
 | 
					 | 
				
			||||||
          <li><%= error.full_message %></li>
 | 
					 | 
				
			||||||
        <% end %>
 | 
					 | 
				
			||||||
      </ul>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <%= form.label :first_name, style: "display: block" %>
 | 
					 | 
				
			||||||
    <%= form.text_field :first_name %>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <%= form.label :last_name, style: "display: block" %>
 | 
					 | 
				
			||||||
    <%= form.text_field :last_name %>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <%= form.label :email, style: "display: block" %>
 | 
					 | 
				
			||||||
    <%= form.text_field :email %>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <%= form.label :phone, style: "display: block" %>
 | 
					 | 
				
			||||||
    <%= form.text_field :phone %>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <%= form.submit %>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
<% end %>
 | 
					 | 
				
			||||||
@ -1,22 +0,0 @@
 | 
				
			|||||||
<div id="<%= dom_id guest %>">
 | 
					 | 
				
			||||||
  <p>
 | 
					 | 
				
			||||||
    <strong>First name:</strong>
 | 
					 | 
				
			||||||
    <%= guest.first_name %>
 | 
					 | 
				
			||||||
  </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <p>
 | 
					 | 
				
			||||||
    <strong>Last name:</strong>
 | 
					 | 
				
			||||||
    <%= guest.last_name %>
 | 
					 | 
				
			||||||
  </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <p>
 | 
					 | 
				
			||||||
    <strong>Email:</strong>
 | 
					 | 
				
			||||||
    <%= guest.email %>
 | 
					 | 
				
			||||||
  </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <p>
 | 
					 | 
				
			||||||
    <strong>Phone:</strong>
 | 
					 | 
				
			||||||
    <%= guest.phone %>
 | 
					 | 
				
			||||||
  </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@ -1,10 +0,0 @@
 | 
				
			|||||||
<h1>Editing guest</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= render "form", guest: @guest %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div>
 | 
					 | 
				
			||||||
  <%= link_to "Show this guest", @guest %> |
 | 
					 | 
				
			||||||
  <%= link_to "Back to guests", guests_path %>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@ -1,37 +0,0 @@
 | 
				
			|||||||
<p style="color: green"><%= notice %></p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<h1>Guests</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div id="guests">
 | 
					 | 
				
			||||||
  <table>
 | 
					 | 
				
			||||||
    <tr>
 | 
					 | 
				
			||||||
      <th>Row #</th>
 | 
					 | 
				
			||||||
      <th>Name</th>
 | 
					 | 
				
			||||||
      <th>Email</th>
 | 
					 | 
				
			||||||
      <th>Phone</th>
 | 
					 | 
				
			||||||
      <th>Affinity groups</th>
 | 
					 | 
				
			||||||
      <th>Unbreakable bonds</th>
 | 
					 | 
				
			||||||
      <th colspan="2"></th>
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
  <% @guests.each_with_index do |guest, i| %>
 | 
					 | 
				
			||||||
    <tr>
 | 
					 | 
				
			||||||
      <td><%= i + 1 %></td>
 | 
					 | 
				
			||||||
      <td><%= guest.full_name %></td>
 | 
					 | 
				
			||||||
      <td><%= guest.email %></td>
 | 
					 | 
				
			||||||
      <td><%= guest.phone %></td>
 | 
					 | 
				
			||||||
      <td><%= guest.affinity_groups.pluck(:name).join(", ") %></td>
 | 
					 | 
				
			||||||
      <td><%= guest.unbreakable_bonds.pluck(:name).join(", ") %></td>
 | 
					 | 
				
			||||||
      <td><%= link_to "Show", guest %></td>
 | 
					 | 
				
			||||||
      <td><%= link_to "Edit", edit_guest_path(guest) %></td>
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
  <% end %>
 | 
					 | 
				
			||||||
  </table>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= link_to "New guest", new_guest_path %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= form_with url: import_guests_path, method: :post do |form| %>
 | 
					 | 
				
			||||||
  <%= form.label :file %>
 | 
					 | 
				
			||||||
  <%= form.file_field :file %>
 | 
					 | 
				
			||||||
  <%= form.submit "Import" %>
 | 
					 | 
				
			||||||
<% end %>
 | 
					 | 
				
			||||||
@ -1,9 +0,0 @@
 | 
				
			|||||||
<h1>New guest</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= render "form", guest: @guest %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div>
 | 
					 | 
				
			||||||
  <%= link_to "Back to guests", guests_path %>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@ -1,10 +0,0 @@
 | 
				
			|||||||
<p style="color: green"><%= notice %></p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= render @guest %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div>
 | 
					 | 
				
			||||||
  <%= link_to "Edit this guest", edit_guest_path(@guest) %> |
 | 
					 | 
				
			||||||
  <%= link_to "Back to guests", guests_path %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <%= button_to "Destroy this guest", @guest, method: :delete %>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@ -1,16 +0,0 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					 | 
				
			||||||
<html>
 | 
					 | 
				
			||||||
  <head>
 | 
					 | 
				
			||||||
    <title>WeddingPlanner</title>
 | 
					 | 
				
			||||||
    <meta name="viewport" content="width=device-width,initial-scale=1">
 | 
					 | 
				
			||||||
    <%= csrf_meta_tags %>
 | 
					 | 
				
			||||||
    <%= csp_meta_tag %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
 | 
					 | 
				
			||||||
    <%= javascript_importmap_tags %>
 | 
					 | 
				
			||||||
  </head>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <body>
 | 
					 | 
				
			||||||
    <%= yield %>
 | 
					 | 
				
			||||||
  </body>
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html>
 | 
					<html>
 | 
				
			||||||
  <head>
 | 
					  <head>
 | 
				
			||||||
 | 
				
			|||||||
@ -1 +1,3 @@
 | 
				
			|||||||
 | 
					<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= yield %>
 | 
					<%= yield %>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +0,0 @@
 | 
				
			|||||||
<h1>Tables arrangements</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<ol>
 | 
					 | 
				
			||||||
    <% @tables_arrangements.each_with_index do |tables_arrangement, i| %>
 | 
					 | 
				
			||||||
        <li>
 | 
					 | 
				
			||||||
            <p><%= link_to "Arrangement ##{i+1}", tables_arrangement_path(tables_arrangement) %> Discomfort: <%= tables_arrangement.discomfort %></p>
 | 
					 | 
				
			||||||
        </li>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					 | 
				
			||||||
</ol>
 | 
					 | 
				
			||||||
@ -1,16 +0,0 @@
 | 
				
			|||||||
<h1>ID: <%= @tables_arrangement.id %></h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<p>Discomfort: <%= @tables_arrangement.discomfort %></p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<h2>Seats</h2>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<% @seats.each do |table_number, seats| %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <h3>Table <%= table_number %></h3>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <ul>
 | 
					 | 
				
			||||||
    <% seats.each do |seat| %>
 | 
					 | 
				
			||||||
      <li><%= seat.guest.full_name %> (<%= seat.guest.affinity_groups.pluck(:name).join(", ") %>)</li>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					 | 
				
			||||||
  </ul>
 | 
					 | 
				
			||||||
<% end %>
 | 
					 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Welcome <%= @email %>!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>You can confirm your account email through the link below:</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p><%= link_to 'Confirm my account', confirmation_url(slug: ActsAsTenant.current_tenant&.slug, confirmation_token: @token) %></p>
 | 
				
			||||||
							
								
								
									
										9
									
								
								app/views/users/mailer/email_changed.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/views/users/mailer/email_changed.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Hello <%= @email %>!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<% if @resource.try(:unconfirmed_email?) %>
 | 
				
			||||||
 | 
					  <p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
 | 
				
			||||||
 | 
					<% else %>
 | 
				
			||||||
 | 
					  <p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
 | 
				
			||||||
 | 
					<% end %>
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/views/users/mailer/password_change.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/views/users/mailer/password_change.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Hello <%= @resource.email %>!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>We're contacting you to notify you that your password has been changed.</p>
 | 
				
			||||||
							
								
								
									
										10
									
								
								app/views/users/mailer/reset_password_instructions.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/views/users/mailer/reset_password_instructions.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Hello <%= @resource.email %>!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Someone has requested a link to change your password. You can do this through the link below.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p><%= link_to 'Change my password', edit_password_url(slug: ActsAsTenant.current_tenant&.slug, reset_password_token: @token) %></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>If you didn't request this, please ignore this email.</p>
 | 
				
			||||||
 | 
					<p>Your password won't change until you access the link above and create a new one.</p>
 | 
				
			||||||
							
								
								
									
										9
									
								
								app/views/users/mailer/unlock_instructions.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/views/users/mailer/unlock_instructions.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Hello <%= @resource.email %>!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Click the link below to unlock your account:</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p><%= link_to 'Unlock my account', unlock_url(slug: ActsAsTenant.current_tenant&.slug, unlock_token: @token) %></p>
 | 
				
			||||||
							
								
								
									
										6
									
								
								bin/jobs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								bin/jobs
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env ruby
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require_relative "../config/environment"
 | 
				
			||||||
 | 
					require "solid_queue/cli"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SolidQueue::Cli.start(ARGV)
 | 
				
			||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This file is used by Rack-based servers to start the application.
 | 
					# This file is used by Rack-based servers to start the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require_relative "config/environment"
 | 
					require_relative 'config/environment'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
run Rails.application
 | 
					run Rails.application
 | 
				
			||||||
Rails.application.load_server
 | 
					Rails.application.load_server
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require_relative 'boot'
 | 
					require_relative 'boot'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require 'rails'
 | 
					require 'rails'
 | 
				
			||||||
@ -28,6 +30,9 @@ module WeddingPlanner
 | 
				
			|||||||
    # Common ones are `templates`, `generators`, or `middleware`, for example.
 | 
					    # Common ones are `templates`, `generators`, or `middleware`, for example.
 | 
				
			||||||
    config.autoload_lib(ignore: %w[assets tasks])
 | 
					    config.autoload_lib(ignore: %w[assets tasks])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Use a real queuing backend for Active Job (and separate queues per environment).
 | 
				
			||||||
 | 
					    config.active_job.queue_adapter = :solid_queue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Configuration for the application, engines, and railties goes here.
 | 
					    # Configuration for the application, engines, and railties goes here.
 | 
				
			||||||
    #
 | 
					    #
 | 
				
			||||||
    # These settings can be overridden in specific environments using the files
 | 
					    # These settings can be overridden in specific environments using the files
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
 | 
					ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require "bundler/setup" # Set up gems listed in the Gemfile.
 | 
					require "bundler/setup" # Set up gems listed in the Gemfile.
 | 
				
			||||||
 | 
				
			|||||||
@ -83,6 +83,7 @@ test:
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
production:
 | 
					production:
 | 
				
			||||||
  <<: *default
 | 
					  <<: *default
 | 
				
			||||||
 | 
					  host: db
 | 
				
			||||||
  database: wedding_planner_production
 | 
					  database: wedding_planner_production
 | 
				
			||||||
  username: wedding_planner
 | 
					  username: wedding_planner
 | 
				
			||||||
  password: <%= ENV["WEDDING_PLANNER_DATABASE_PASSWORD"] %>
 | 
					  password: <%= ENV["WEDDING_PLANNER_DATABASE_PASSWORD"] %>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Load the Rails application.
 | 
					# Load the Rails application.
 | 
				
			||||||
require_relative "application"
 | 
					require_relative "application"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require "active_support/core_ext/integer/time"
 | 
					require "active_support/core_ext/integer/time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Rails.application.configure do
 | 
					Rails.application.configure do
 | 
				
			||||||
@ -38,8 +40,10 @@ Rails.application.configure do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  # Don't care if the mailer can't send.
 | 
					  # Don't care if the mailer can't send.
 | 
				
			||||||
  config.action_mailer.raise_delivery_errors = false
 | 
					  config.action_mailer.raise_delivery_errors = false
 | 
				
			||||||
 | 
					 | 
				
			||||||
  config.action_mailer.perform_caching = false
 | 
					  config.action_mailer.perform_caching = false
 | 
				
			||||||
 | 
					  config.action_mailer.default_url_options = { host: 'libre-wedding-planner.app.localhost/api' }
 | 
				
			||||||
 | 
					  config.action_mailer.delivery_method = :letter_opener_web
 | 
				
			||||||
 | 
					  config.action_mailer.perform_deliveries = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Print deprecation notices to the Rails logger.
 | 
					  # Print deprecation notices to the Rails logger.
 | 
				
			||||||
  config.active_support.deprecation = :log
 | 
					  config.active_support.deprecation = :log
 | 
				
			||||||
@ -73,4 +77,6 @@ Rails.application.configure do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  # Raise error when a before_action's only/except options reference missing actions
 | 
					  # Raise error when a before_action's only/except options reference missing actions
 | 
				
			||||||
  config.action_controller.raise_on_missing_callback_actions = true
 | 
					  config.action_controller.raise_on_missing_callback_actions = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  config.hosts << "libre-wedding-planner.app.localhost"
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require "active_support/core_ext/integer/time"
 | 
					require "active_support/core_ext/integer/time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Rails.application.configure do
 | 
					Rails.application.configure do
 | 
				
			||||||
@ -67,8 +69,6 @@ Rails.application.configure do
 | 
				
			|||||||
  # Use a different cache store in production.
 | 
					  # Use a different cache store in production.
 | 
				
			||||||
  # config.cache_store = :mem_cache_store
 | 
					  # config.cache_store = :mem_cache_store
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Use a real queuing backend for Active Job (and separate queues per environment).
 | 
					 | 
				
			||||||
  # config.active_job.queue_adapter = :resque
 | 
					 | 
				
			||||||
  # config.active_job.queue_name_prefix = "wedding_planner_production"
 | 
					  # config.active_job.queue_name_prefix = "wedding_planner_production"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  config.action_mailer.perform_caching = false
 | 
					  config.action_mailer.perform_caching = false
 | 
				
			||||||
@ -92,6 +92,9 @@ Rails.application.configure do
 | 
				
			|||||||
  #   "example.com",     # Allow requests from example.com
 | 
					  #   "example.com",     # Allow requests from example.com
 | 
				
			||||||
  #   /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
 | 
					  #   /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
 | 
				
			||||||
  # ]
 | 
					  # ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  config.hosts << "app.libreweddingplanner.org"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Skip DNS rebinding protection for the default health check endpoint.
 | 
					  # Skip DNS rebinding protection for the default health check endpoint.
 | 
				
			||||||
  # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
 | 
					  config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require "active_support/core_ext/integer/time"
 | 
					require "active_support/core_ext/integer/time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# The test environment is used exclusively to run your application's
 | 
					# The test environment is used exclusively to run your application's
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Pin npm packages by running ./bin/importmap
 | 
					# Pin npm packages by running ./bin/importmap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pin "application"
 | 
					pin "application"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								config/initializers/acts_as_tenant.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								config/initializers/acts_as_tenant.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ActsAsTenant.configure do |config|
 | 
				
			||||||
 | 
					  config.require_tenant = !Rails.env.test?
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,39 +0,0 @@
 | 
				
			|||||||
require_relative '../../app/services/affinity_groups_hierarchy'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy = AffinityGroupsHierarchy.instance
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy << 'guests_a'
 | 
					 | 
				
			||||||
hierarchy << 'guests_b'
 | 
					 | 
				
			||||||
hierarchy << 'common_guests'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy.register_child('guests_a', 'family_a')
 | 
					 | 
				
			||||||
hierarchy.register_child('family_a', 'close_family_a')
 | 
					 | 
				
			||||||
hierarchy.register_child('family_a', 'cousins_a')
 | 
					 | 
				
			||||||
hierarchy.register_child('family_a', 'relatives_a')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy.register_child('guests_a', 'work_a')
 | 
					 | 
				
			||||||
hierarchy.register_child('work_a', 'besties_work_a')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy.register_child('guests_a', 'friends_a')
 | 
					 | 
				
			||||||
hierarchy.register_child('friends_a', 'college_friends_a')
 | 
					 | 
				
			||||||
hierarchy.register_child('friends_a', 'high_school_friends_a')
 | 
					 | 
				
			||||||
hierarchy.register_child('friends_a', 'childhood_friends_a')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy.register_child('guests_a', 'sports_a')
 | 
					 | 
				
			||||||
hierarchy.register_child('sports_a', 'basket_team_a')
 | 
					 | 
				
			||||||
hierarchy.register_child('sports_a', 'football_team_a')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy.register_child('guests_b', 'family_b')
 | 
					 | 
				
			||||||
hierarchy.register_child('family_b', 'close_family_b')
 | 
					 | 
				
			||||||
hierarchy.register_child('family_b', 'cousins_b')
 | 
					 | 
				
			||||||
hierarchy.register_child('family_b', 'relatives_b')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy.register_child('guests_b', 'work_b')
 | 
					 | 
				
			||||||
hierarchy.register_child('work_b', 'besties_work_b')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy.register_child('guests_b', 'friends_b')
 | 
					 | 
				
			||||||
hierarchy.register_child('friends_b', 'college_friends_b')
 | 
					 | 
				
			||||||
hierarchy.register_child('friends_b', 'high_school_friends_b')
 | 
					 | 
				
			||||||
hierarchy.register_child('friends_b', 'childhood_friends_b')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
hierarchy.register_child('common_guests', 'dance_club')
 | 
					 | 
				
			||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Be sure to restart your server when you modify this file.
 | 
					# Be sure to restart your server when you modify this file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Version of your assets, change this if you want to expire all your assets.
 | 
					# Version of your assets, change this if you want to expire all your assets.
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								config/initializers/colors.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								config/initializers/colors.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Chroma.define_palette :decreasing_saturation do
 | 
				
			||||||
 | 
					  spin(20).desaturate(40)
 | 
				
			||||||
 | 
					  spin(-20).desaturate(40)
 | 
				
			||||||
 | 
					  spin(40).desaturate(40)
 | 
				
			||||||
 | 
					  spin(-40).desaturate(40)
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Be sure to restart your server when you modify this file.
 | 
					# Be sure to restart your server when you modify this file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Define an application-wide content security policy.
 | 
					# Define an application-wide content security policy.
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# config/initializers/cors.rb
 | 
					# config/initializers/cors.rb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Rails.application.config.middleware.insert_before 0, Rack::Cors do
 | 
					Rails.application.config.middleware.insert_before 0, Rack::Cors do
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										315
									
								
								config/initializers/devise.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								config/initializers/devise.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,315 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Assuming you have not yet modified this file, each configuration option below
 | 
				
			||||||
 | 
					# is set to its default value. Note that some are commented out while others
 | 
				
			||||||
 | 
					# are not: uncommented lines are intended to protect your configuration from
 | 
				
			||||||
 | 
					# breaking changes in upgrades (i.e., in the event that future versions of
 | 
				
			||||||
 | 
					# Devise change the default values for those options).
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Use this hook to configure devise mailer, warden hooks and so forth.
 | 
				
			||||||
 | 
					# Many of these configuration options can be set straight in your model.
 | 
				
			||||||
 | 
					Devise.setup do |config|
 | 
				
			||||||
 | 
					  # The secret key used by Devise. Devise uses this key to generate
 | 
				
			||||||
 | 
					  # random tokens. Changing this key will render invalid all existing
 | 
				
			||||||
 | 
					  # confirmation, reset password and unlock tokens in the database.
 | 
				
			||||||
 | 
					  # Devise will use the `secret_key_base` as its `secret_key`
 | 
				
			||||||
 | 
					  # by default. You can change it below and use your own secret key.
 | 
				
			||||||
 | 
					  # config.secret_key = '11353ae8c2bf66dd638d9edff9ec82856aecf74bba6c598273559a8750c902d3439da1b301e40c47578577a971f1058dbf37211c107fba5107c29baa654e9888'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Controller configuration
 | 
				
			||||||
 | 
					  # Configure the parent class to the devise controllers.
 | 
				
			||||||
 | 
					  # config.parent_controller = 'DeviseController'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Mailer Configuration
 | 
				
			||||||
 | 
					  # Configure the e-mail address which will be shown in Devise::Mailer,
 | 
				
			||||||
 | 
					  # note that it will be overwritten if you use your own mailer class
 | 
				
			||||||
 | 
					  # with default "from" parameter.
 | 
				
			||||||
 | 
					  config.mailer_sender = 'noreply@libreweddingplanner.org'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Configure the class responsible to send e-mails.
 | 
				
			||||||
 | 
					  # config.mailer = 'Devise::Mailer'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Configure the parent class responsible to send e-mails.
 | 
				
			||||||
 | 
					  # config.parent_mailer = 'ActionMailer::Base'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> ORM configuration
 | 
				
			||||||
 | 
					  # Load and configure the ORM. Supports :active_record (default) and
 | 
				
			||||||
 | 
					  # :mongoid (bson_ext recommended) by default. Other ORMs may be
 | 
				
			||||||
 | 
					  # available as additional gems.
 | 
				
			||||||
 | 
					  require 'devise/orm/active_record'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for any authentication mechanism
 | 
				
			||||||
 | 
					  # Configure which keys are used when authenticating a user. The default is
 | 
				
			||||||
 | 
					  # just :email. You can configure it to use [:username, :subdomain], so for
 | 
				
			||||||
 | 
					  # authenticating a user, both parameters are required. Remember that those
 | 
				
			||||||
 | 
					  # parameters are used only when authenticating and not when retrieving from
 | 
				
			||||||
 | 
					  # session. If you need permissions, you should implement that in a before filter.
 | 
				
			||||||
 | 
					  # You can also supply a hash where the value is a boolean determining whether
 | 
				
			||||||
 | 
					  # or not authentication should be aborted when the value is not present.
 | 
				
			||||||
 | 
					  # config.authentication_keys = [:email]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Configure parameters from the request object used for authentication. Each entry
 | 
				
			||||||
 | 
					  # given should be a request method and it will automatically be passed to the
 | 
				
			||||||
 | 
					  # find_for_authentication method and considered in your model lookup. For instance,
 | 
				
			||||||
 | 
					  # if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
 | 
				
			||||||
 | 
					  # The same considerations mentioned for authentication_keys also apply to request_keys.
 | 
				
			||||||
 | 
					  # config.request_keys = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Configure which authentication keys should be case-insensitive.
 | 
				
			||||||
 | 
					  # These keys will be downcased upon creating or modifying a user and when used
 | 
				
			||||||
 | 
					  # to authenticate or find a user. Default is :email.
 | 
				
			||||||
 | 
					  config.case_insensitive_keys = [:email]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Configure which authentication keys should have whitespace stripped.
 | 
				
			||||||
 | 
					  # These keys will have whitespace before and after removed upon creating or
 | 
				
			||||||
 | 
					  # modifying a user and when used to authenticate or find a user. Default is :email.
 | 
				
			||||||
 | 
					  config.strip_whitespace_keys = [:email]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Tell if authentication through request.params is enabled. True by default.
 | 
				
			||||||
 | 
					  # It can be set to an array that will enable params authentication only for the
 | 
				
			||||||
 | 
					  # given strategies, for example, `config.params_authenticatable = [:database]` will
 | 
				
			||||||
 | 
					  # enable it only for database (email + password) authentication.
 | 
				
			||||||
 | 
					  # config.params_authenticatable = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Tell if authentication through HTTP Auth is enabled. False by default.
 | 
				
			||||||
 | 
					  # It can be set to an array that will enable http authentication only for the
 | 
				
			||||||
 | 
					  # given strategies, for example, `config.http_authenticatable = [:database]` will
 | 
				
			||||||
 | 
					  # enable it only for database authentication.
 | 
				
			||||||
 | 
					  # For API-only applications to support authentication "out-of-the-box", you will likely want to
 | 
				
			||||||
 | 
					  # enable this with :database unless you are using a custom strategy.
 | 
				
			||||||
 | 
					  # The supported strategies are:
 | 
				
			||||||
 | 
					  # :database      = Support basic authentication with authentication key + password
 | 
				
			||||||
 | 
					  # config.http_authenticatable = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # If 401 status code should be returned for AJAX requests. True by default.
 | 
				
			||||||
 | 
					  # config.http_authenticatable_on_xhr = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # The realm used in Http Basic Authentication. 'Application' by default.
 | 
				
			||||||
 | 
					  # config.http_authentication_realm = 'Application'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # It will change confirmation, password recovery and other workflows
 | 
				
			||||||
 | 
					  # to behave the same regardless if the e-mail provided was right or wrong.
 | 
				
			||||||
 | 
					  # Does not affect registerable.
 | 
				
			||||||
 | 
					  config.paranoid = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # By default Devise will store the user in session. You can skip storage for
 | 
				
			||||||
 | 
					  # particular strategies by setting this option.
 | 
				
			||||||
 | 
					  # Notice that if you are skipping storage for all authentication paths, you
 | 
				
			||||||
 | 
					  # may want to disable generating routes to Devise's sessions controller by
 | 
				
			||||||
 | 
					  # passing skip: :sessions to `devise_for` in your config/routes.rb
 | 
				
			||||||
 | 
					  config.skip_session_storage = [:http_auth]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # By default, Devise cleans up the CSRF token on authentication to
 | 
				
			||||||
 | 
					  # avoid CSRF token fixation attacks. This means that, when using AJAX
 | 
				
			||||||
 | 
					  # requests for sign in and sign up, you need to get a new CSRF token
 | 
				
			||||||
 | 
					  # from the server. You can disable this option at your own risk.
 | 
				
			||||||
 | 
					  # config.clean_up_csrf_token_on_authentication = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # When false, Devise will not attempt to reload routes on eager load.
 | 
				
			||||||
 | 
					  # This can reduce the time taken to boot the app but if your application
 | 
				
			||||||
 | 
					  # requires the Devise mappings to be loaded during boot time the application
 | 
				
			||||||
 | 
					  # won't boot properly.
 | 
				
			||||||
 | 
					  # config.reload_routes = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for :database_authenticatable
 | 
				
			||||||
 | 
					  # For bcrypt, this is the cost for hashing the password and defaults to 12. If
 | 
				
			||||||
 | 
					  # using other algorithms, it sets how many times you want the password to be hashed.
 | 
				
			||||||
 | 
					  # The number of stretches used for generating the hashed password are stored
 | 
				
			||||||
 | 
					  # with the hashed password. This allows you to change the stretches without
 | 
				
			||||||
 | 
					  # invalidating existing passwords.
 | 
				
			||||||
 | 
					  #
 | 
				
			||||||
 | 
					  # Limiting the stretches to just one in testing will increase the performance of
 | 
				
			||||||
 | 
					  # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
 | 
				
			||||||
 | 
					  # a value less than 10 in other environments. Note that, for bcrypt (the default
 | 
				
			||||||
 | 
					  # algorithm), the cost increases exponentially with the number of stretches (e.g.
 | 
				
			||||||
 | 
					  # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
 | 
				
			||||||
 | 
					  config.stretches = Rails.env.test? ? 1 : 12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Set up a pepper to generate the hashed password.
 | 
				
			||||||
 | 
					  # config.pepper = '6f86425fd587f80f4a338a785a6abbbccf8de7322f70fcccf356118d982942c9421819445f9d236a296fa3c431ef5e509be20e6db03f90ec2b42aa78f3a7e526'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Send a notification to the original email when the user's email is changed.
 | 
				
			||||||
 | 
					  config.send_email_changed_notification = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Send a notification email when the user's password is changed.
 | 
				
			||||||
 | 
					  config.send_password_change_notification = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for :confirmable
 | 
				
			||||||
 | 
					  # A period that the user is allowed to access the website even without
 | 
				
			||||||
 | 
					  # confirming their account. For instance, if set to 2.days, the user will be
 | 
				
			||||||
 | 
					  # able to access the website for two days without confirming their account,
 | 
				
			||||||
 | 
					  # access will be blocked just in the third day.
 | 
				
			||||||
 | 
					  # You can also set it to nil, which will allow the user to access the website
 | 
				
			||||||
 | 
					  # without confirming their account.
 | 
				
			||||||
 | 
					  # Default is 0.days, meaning the user cannot access the website without
 | 
				
			||||||
 | 
					  # confirming their account.
 | 
				
			||||||
 | 
					  # config.allow_unconfirmed_access_for = 2.days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # A period that the user is allowed to confirm their account before their
 | 
				
			||||||
 | 
					  # token becomes invalid. For example, if set to 3.days, the user can confirm
 | 
				
			||||||
 | 
					  # their account within 3 days after the mail was sent, but on the fourth day
 | 
				
			||||||
 | 
					  # their account can't be confirmed with the token any more.
 | 
				
			||||||
 | 
					  # Default is nil, meaning there is no restriction on how long a user can take
 | 
				
			||||||
 | 
					  # before confirming their account.
 | 
				
			||||||
 | 
					  config.confirm_within = 3.days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # If true, requires any email changes to be confirmed (exactly the same way as
 | 
				
			||||||
 | 
					  # initial account confirmation) to be applied. Requires additional unconfirmed_email
 | 
				
			||||||
 | 
					  # db field (see migrations). Until confirmed, new email is stored in
 | 
				
			||||||
 | 
					  # unconfirmed_email column, and copied to email column on successful confirmation.
 | 
				
			||||||
 | 
					  config.reconfirmable = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Defines which key will be used when confirming an account
 | 
				
			||||||
 | 
					  # config.confirmation_keys = [:email]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for :rememberable
 | 
				
			||||||
 | 
					  # The time the user will be remembered without asking for credentials again.
 | 
				
			||||||
 | 
					  # config.remember_for = 2.weeks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Invalidates all the remember me tokens when the user signs out.
 | 
				
			||||||
 | 
					  config.expire_all_remember_me_on_sign_out = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # If true, extends the user's remember period when remembered via cookie.
 | 
				
			||||||
 | 
					  # config.extend_remember_period = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Options to be passed to the created cookie. For instance, you can set
 | 
				
			||||||
 | 
					  # secure: true in order to force SSL only cookies.
 | 
				
			||||||
 | 
					  # config.rememberable_options = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for :validatable
 | 
				
			||||||
 | 
					  # Range for password length.
 | 
				
			||||||
 | 
					  config.password_length = 15..128
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Email regex used to validate email formats. It simply asserts that
 | 
				
			||||||
 | 
					  # one (and only one) @ exists in the given string. This is mainly
 | 
				
			||||||
 | 
					  # to give user feedback and not to assert the e-mail validity.
 | 
				
			||||||
 | 
					  config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for :timeoutable
 | 
				
			||||||
 | 
					  # The time you want to timeout the user session without activity. After this
 | 
				
			||||||
 | 
					  # time the user will be asked for credentials again. Default is 30 minutes.
 | 
				
			||||||
 | 
					  # config.timeout_in = 30.minutes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for :lockable
 | 
				
			||||||
 | 
					  # Defines which strategy will be used to lock an account.
 | 
				
			||||||
 | 
					  # :failed_attempts = Locks an account after a number of failed attempts to sign in.
 | 
				
			||||||
 | 
					  # :none            = No lock strategy. You should handle locking by yourself.
 | 
				
			||||||
 | 
					  config.lock_strategy = :failed_attempts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Defines which key will be used when locking and unlocking an account
 | 
				
			||||||
 | 
					  # config.unlock_keys = [:email]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Defines which strategy will be used to unlock an account.
 | 
				
			||||||
 | 
					  # :email = Sends an unlock link to the user email
 | 
				
			||||||
 | 
					  # :time  = Re-enables login after a certain amount of time (see :unlock_in below)
 | 
				
			||||||
 | 
					  # :both  = Enables both strategies
 | 
				
			||||||
 | 
					  # :none  = No unlock strategy. You should handle unlocking by yourself.
 | 
				
			||||||
 | 
					  config.unlock_strategy = :both
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Number of authentication tries before locking an account if lock_strategy
 | 
				
			||||||
 | 
					  # is failed attempts.
 | 
				
			||||||
 | 
					  config.maximum_attempts = 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Time interval to unlock the account if :time is enabled as unlock_strategy.
 | 
				
			||||||
 | 
					  config.unlock_in = 1.hour
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Warn on the last attempt before the account is locked.
 | 
				
			||||||
 | 
					  # config.last_attempt_warning = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for :recoverable
 | 
				
			||||||
 | 
					  #
 | 
				
			||||||
 | 
					  # Defines which key will be used when recovering the password for an account
 | 
				
			||||||
 | 
					  # config.reset_password_keys = [:email]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Time interval you can reset your password with a reset password key.
 | 
				
			||||||
 | 
					  # Don't put a too small interval or your users won't have the time to
 | 
				
			||||||
 | 
					  # change their passwords.
 | 
				
			||||||
 | 
					  config.reset_password_within = 6.hours
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # When set to false, does not sign a user in automatically after their password is
 | 
				
			||||||
 | 
					  # reset. Defaults to true, so a user is signed in automatically after a reset.
 | 
				
			||||||
 | 
					  config.sign_in_after_reset_password = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for :encryptable
 | 
				
			||||||
 | 
					  # Allow you to use another hashing or encryption algorithm besides bcrypt (default).
 | 
				
			||||||
 | 
					  # You can use :sha1, :sha512 or algorithms from others authentication tools as
 | 
				
			||||||
 | 
					  # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20
 | 
				
			||||||
 | 
					  # for default behavior) and :restful_authentication_sha1 (then you should set
 | 
				
			||||||
 | 
					  # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper).
 | 
				
			||||||
 | 
					  #
 | 
				
			||||||
 | 
					  # Require the `devise-encryptable` gem when using anything other than bcrypt
 | 
				
			||||||
 | 
					  # config.encryptor = :sha512
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Scopes configuration
 | 
				
			||||||
 | 
					  # Turn scoped views on. Before rendering "sessions/new", it will first check for
 | 
				
			||||||
 | 
					  # "users/sessions/new". It's turned off by default because it's slower if you
 | 
				
			||||||
 | 
					  # are using only default views.
 | 
				
			||||||
 | 
					  config.scoped_views = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Configure the default scope given to Warden. By default it's the first
 | 
				
			||||||
 | 
					  # devise role declared in your routes (usually :user).
 | 
				
			||||||
 | 
					  # config.default_scope = :user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Set this configuration to false if you want /users/sign_out to sign out
 | 
				
			||||||
 | 
					  # only the current scope. By default, Devise signs out all scopes.
 | 
				
			||||||
 | 
					  # config.sign_out_all_scopes = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Navigation configuration
 | 
				
			||||||
 | 
					  # Lists the formats that should be treated as navigational. Formats like
 | 
				
			||||||
 | 
					  # :html should redirect to the sign in page when the user does not have
 | 
				
			||||||
 | 
					  # access, but formats like :xml or :json, should return 401.
 | 
				
			||||||
 | 
					  #
 | 
				
			||||||
 | 
					  # If you have any extra navigational formats, like :iphone or :mobile, you
 | 
				
			||||||
 | 
					  # should add them to the navigational formats lists.
 | 
				
			||||||
 | 
					  #
 | 
				
			||||||
 | 
					  # The "*/*" below is required to match Internet Explorer requests.
 | 
				
			||||||
 | 
					  # config.navigational_formats = ['*/*', :html, :turbo_stream]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # The default HTTP method used to sign out a resource. Default is :delete.
 | 
				
			||||||
 | 
					  config.sign_out_via = :delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> OmniAuth
 | 
				
			||||||
 | 
					  # Add a new OmniAuth provider. Check the wiki for more information on setting
 | 
				
			||||||
 | 
					  # up on your models and hooks.
 | 
				
			||||||
 | 
					  # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Warden configuration
 | 
				
			||||||
 | 
					  # If you want to use other strategies, that are not supported by Devise, or
 | 
				
			||||||
 | 
					  # change the failure app, you can configure them inside the config.warden block.
 | 
				
			||||||
 | 
					  #
 | 
				
			||||||
 | 
					  # config.warden do |manager|
 | 
				
			||||||
 | 
					  #   manager.intercept_401 = false
 | 
				
			||||||
 | 
					  #   manager.default_strategies(scope: :user).unshift :some_external_strategy
 | 
				
			||||||
 | 
					  # end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Mountable engine configurations
 | 
				
			||||||
 | 
					  # When using Devise inside an engine, let's call it `MyEngine`, and this engine
 | 
				
			||||||
 | 
					  # is mountable, there are some extra configurations to be taken into account.
 | 
				
			||||||
 | 
					  # The following options are available, assuming the engine is mounted as:
 | 
				
			||||||
 | 
					  #
 | 
				
			||||||
 | 
					  #     mount MyEngine, at: '/my_engine'
 | 
				
			||||||
 | 
					  #
 | 
				
			||||||
 | 
					  # The router that invoked `devise_for`, in the example above, would be:
 | 
				
			||||||
 | 
					  # config.router_name = :my_engine
 | 
				
			||||||
 | 
					  #
 | 
				
			||||||
 | 
					  # When using OmniAuth, Devise cannot automatically set OmniAuth path,
 | 
				
			||||||
 | 
					  # so you need to do it manually. For the users scope, it would be:
 | 
				
			||||||
 | 
					  # config.omniauth_path_prefix = '/my_engine/users/auth'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Hotwire/Turbo configuration
 | 
				
			||||||
 | 
					  # When using Devise with Hotwire/Turbo, the http status for error responses
 | 
				
			||||||
 | 
					  # and some redirects must match the following. The default in Devise for existing
 | 
				
			||||||
 | 
					  # apps is `200 OK` and `302 Found` respectively, but new apps are generated with
 | 
				
			||||||
 | 
					  # these new defaults that match Hotwire/Turbo behavior.
 | 
				
			||||||
 | 
					  # Note: These might become the new default in future versions of Devise.
 | 
				
			||||||
 | 
					  config.responder.error_status = :unprocessable_entity
 | 
				
			||||||
 | 
					  config.responder.redirect_status = :see_other
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # ==> Configuration for :registerable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # When set to false, does not sign a user in automatically after their password is
 | 
				
			||||||
 | 
					  # changed. Defaults to true, so a user is signed in automatically after changing a password.
 | 
				
			||||||
 | 
					  # config.sign_in_after_change_password = true
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Be sure to restart your server when you modify this file.
 | 
					# Be sure to restart your server when you modify this file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
 | 
					# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Be sure to restart your server when you modify this file.
 | 
					# Be sure to restart your server when you modify this file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Add new inflection rules using the following format. Inflections
 | 
					# Add new inflection rules using the following format. Inflections
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user