Compare commits
	
		
			No commits in common. "main" and "fix-prod-db-host" have entirely different histories.
		
	
	
		
			main
			...
			fix-prod-d
		
	
		
| @ -1,159 +0,0 @@ | |||||||
| 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@v5 |  | ||||||
|         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 |  | ||||||
|       - 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: |  | ||||||
|     if: github.event_name == 'pull_request' |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v5 |  | ||||||
|         with: |  | ||||||
|           token: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|       - uses: ruby/setup-ruby@v1 |  | ||||||
|       - run: bundle install |  | ||||||
|       - run: bundle exec rubocop --force-exclusion --parallel |  | ||||||
|   check-licenses: |  | ||||||
|     if: github.event_name == 'pull_request' |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|         - uses: actions/checkout@v5 |  | ||||||
|           with: |  | ||||||
|             token: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|         - uses: ruby/setup-ruby@v1 |  | ||||||
|         - name: Install project dependencies |  | ||||||
|           run: bundle install --jobs `getconf _NPROCESSORS_ONLN` |  | ||||||
|         - name: Run license finder |  | ||||||
|           run: license_finder |  | ||||||
|   copyright_notice: |  | ||||||
|     if: github.event_name == 'pull_request' |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v5 |  | ||||||
|         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 |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v5 |  | ||||||
|         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 |  | ||||||
							
								
								
									
										36
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | name: Build Nginx-based docker image | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  | concurrency: | ||||||
|  |   group: ${{ github.ref }} | ||||||
|  |   cancel-in-progress: true | ||||||
|  | jobs: | ||||||
|  |   build-static-assets: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     timeout-minutes: 30 | ||||||
|  |     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 | ||||||
|  |         uses: docker/build-push-action@v6 | ||||||
|  |         with: | ||||||
|  |           context: . | ||||||
|  |           push: ${{ github.event_name != 'pull_request' }} | ||||||
|  |           tags: | | ||||||
|  |             ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest | ||||||
|  |           cache-from: type=registry,ref=user/app:latest | ||||||
|  |           cache-to: type=inline | ||||||
							
								
								
									
										39
									
								
								.github/workflows/copyright_notice.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								.github/workflows/copyright_notice.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | name: Add copyright notice | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  | permissions: | ||||||
|  |   contents: write | ||||||
|  | concurrency: | ||||||
|  |   group: ${{ github.ref }} | ||||||
|  |   cancel-in-progress: true | ||||||
|  | jobs: | ||||||
|  |   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 Manuel Bustillo\n\n' | ||||||
|  |           FileType: '.rb' | ||||||
|  |           Path: 'app/, config/, db/, spec/' | ||||||
|  |       - uses: VinnyBabuManjaly/copyright-action@v1.0.0 | ||||||
|  |         with: | ||||||
|  |           CopyrightString: '<%# Copyright (C) 2024 Manuel Bustillo %>\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 | ||||||
							
								
								
									
										23
									
								
								.github/workflows/license_finder.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/license_finder.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | name: Check usage of free licenses | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |   pull_request: | ||||||
|  | concurrency: | ||||||
|  |   group: ${{ github.ref }} | ||||||
|  |   cancel-in-progress: true | ||||||
|  | jobs: | ||||||
|  |   check-licenses: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |         - uses: actions/checkout@v4 | ||||||
|  |           with: | ||||||
|  |             token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |         - uses: ruby/setup-ruby@v1.202.0 | ||||||
|  |           with: | ||||||
|  |             ruby-version: '3.3.6' | ||||||
|  |         - name: Install project dependencies | ||||||
|  |           run: bundle install --jobs `getconf _NPROCESSORS_ONLN` | ||||||
|  |         - name: Run license finder | ||||||
|  |           run: license_finder | ||||||
							
								
								
									
										43
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | 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: | ||||||
|  |       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.202.0 | ||||||
|  |       - 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 | ||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -36,4 +36,3 @@ | |||||||
| 
 | 
 | ||||||
| # Ignore swagger generated documentation | # Ignore swagger generated documentation | ||||||
| swagger/v1/swagger.yaml | swagger/v1/swagger.yaml | ||||||
| wedding-planner.code-workspace |  | ||||||
|  | |||||||
							
								
								
									
										29
									
								
								.rubocop.yml
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								.rubocop.yml
									
									
									
									
									
								
							| @ -1,29 +0,0 @@ | |||||||
| 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.4.3 | ruby-3.3.6 | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -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.4.3 | ARG RUBY_VERSION=3.3.6 | ||||||
| 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 | ||||||
| @ -13,14 +13,14 @@ ENV RAILS_ENV="production" \ | |||||||
|     BUNDLE_PATH="/usr/local/bundle" \ |     BUNDLE_PATH="/usr/local/bundle" \ | ||||||
|     BUNDLE_WITHOUT="development" |     BUNDLE_WITHOUT="development" | ||||||
| 
 | 
 | ||||||
| RUN apt-get update && apt-get install -y nodejs wkhtmltopdf | 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 libyaml-dev |     apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config | ||||||
| 
 | 
 | ||||||
| # Install application gems | # Install application gems | ||||||
| COPY Gemfile Gemfile.lock ./ | COPY Gemfile Gemfile.lock ./ | ||||||
|  | |||||||
| @ -1,19 +1,19 @@ | |||||||
| # 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.4.3 | ARG RUBY_VERSION=3.3.6 | ||||||
| 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 | ||||||
| 
 | 
 | ||||||
| RUN apt-get update && apt-get install -y nodejs wkhtmltopdf | RUN apt-get update && apt-get install -y nodejs | ||||||
| 
 | 
 | ||||||
| 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 libyaml-dev |     apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config | ||||||
| 
 | 
 | ||||||
| # Install application gems | # Install application gems | ||||||
| COPY Gemfile Gemfile.lock ./ | COPY Gemfile Gemfile.lock ./ | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								Gemfile
									
									
									
									
									
								
							| @ -1,8 +1,6 @@ | |||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| source 'https://rubygems.org' | source 'https://rubygems.org' | ||||||
| 
 | 
 | ||||||
| ruby '3.4.3' | ruby '3.3.6' | ||||||
| gem 'bootsnap', require: false | gem 'bootsnap', require: false | ||||||
| gem 'csv' | gem 'csv' | ||||||
| gem 'importmap-rails' | gem 'importmap-rails' | ||||||
| @ -17,15 +15,13 @@ 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 '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' | ||||||
|  | gem 'acts_as_tenant' | ||||||
|  | gem 'httparty' | ||||||
| 
 | 
 | ||||||
| group :development, :test do | group :development, :test do | ||||||
|   gem 'annotaterb' |   gem 'annotaterb' | ||||||
| @ -33,25 +29,18 @@ group :development, :test do | |||||||
|   gem 'factory_bot_rails' |   gem 'factory_bot_rails' | ||||||
|   gem 'license_finder' |   gem 'license_finder' | ||||||
|   gem 'pry' |   gem 'pry' | ||||||
|   gem 'rspec-rails', '~> 8.0.0' |   gem 'rspec-rails', '~> 7.1.0' | ||||||
|  |   gem 'rswag' | ||||||
|   gem 'shoulda-matchers', '~> 6.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' | ||||||
|  |   gem 'letter_opener_web' | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| gem 'chroma' | gem 'chroma' | ||||||
| gem 'solid_queue', '~> 1.0' | gem 'solid_queue', '~> 1.0' | ||||||
| 
 | 
 | ||||||
| gem 'devise', '~> 4.9' | gem "devise", "~> 4.9" | ||||||
| 
 |  | ||||||
| gem 'wicked_pdf', '~> 2.8' |  | ||||||
| 
 |  | ||||||
| gem 'rqrcode', '~> 3.1' |  | ||||||
|  | |||||||
							
								
								
									
										510
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										510
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @ -1,29 +1,29 @@ | |||||||
| GEM | GEM | ||||||
|   remote: https://rubygems.org/ |   remote: https://rubygems.org/ | ||||||
|   specs: |   specs: | ||||||
|     actioncable (8.0.2.1) |     actioncable (8.0.0) | ||||||
|       actionpack (= 8.0.2.1) |       actionpack (= 8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       nio4r (~> 2.0) |       nio4r (~> 2.0) | ||||||
|       websocket-driver (>= 0.6.1) |       websocket-driver (>= 0.6.1) | ||||||
|       zeitwerk (~> 2.6) |       zeitwerk (~> 2.6) | ||||||
|     actionmailbox (8.0.2.1) |     actionmailbox (8.0.0) | ||||||
|       actionpack (= 8.0.2.1) |       actionpack (= 8.0.0) | ||||||
|       activejob (= 8.0.2.1) |       activejob (= 8.0.0) | ||||||
|       activerecord (= 8.0.2.1) |       activerecord (= 8.0.0) | ||||||
|       activestorage (= 8.0.2.1) |       activestorage (= 8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       mail (>= 2.8.0) |       mail (>= 2.8.0) | ||||||
|     actionmailer (8.0.2.1) |     actionmailer (8.0.0) | ||||||
|       actionpack (= 8.0.2.1) |       actionpack (= 8.0.0) | ||||||
|       actionview (= 8.0.2.1) |       actionview (= 8.0.0) | ||||||
|       activejob (= 8.0.2.1) |       activejob (= 8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       mail (>= 2.8.0) |       mail (>= 2.8.0) | ||||||
|       rails-dom-testing (~> 2.2) |       rails-dom-testing (~> 2.2) | ||||||
|     actionpack (8.0.2.1) |     actionpack (8.0.0) | ||||||
|       actionview (= 8.0.2.1) |       actionview (= 8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       nokogiri (>= 1.8.5) |       nokogiri (>= 1.8.5) | ||||||
|       rack (>= 2.2.4) |       rack (>= 2.2.4) | ||||||
|       rack-session (>= 1.0.1) |       rack-session (>= 1.0.1) | ||||||
| @ -31,35 +31,35 @@ GEM | |||||||
|       rails-dom-testing (~> 2.2) |       rails-dom-testing (~> 2.2) | ||||||
|       rails-html-sanitizer (~> 1.6) |       rails-html-sanitizer (~> 1.6) | ||||||
|       useragent (~> 0.16) |       useragent (~> 0.16) | ||||||
|     actiontext (8.0.2.1) |     actiontext (8.0.0) | ||||||
|       actionpack (= 8.0.2.1) |       actionpack (= 8.0.0) | ||||||
|       activerecord (= 8.0.2.1) |       activerecord (= 8.0.0) | ||||||
|       activestorage (= 8.0.2.1) |       activestorage (= 8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       globalid (>= 0.6.0) |       globalid (>= 0.6.0) | ||||||
|       nokogiri (>= 1.8.5) |       nokogiri (>= 1.8.5) | ||||||
|     actionview (8.0.2.1) |     actionview (8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       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 (8.0.2.1) |     activejob (8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       globalid (>= 0.3.6) |       globalid (>= 0.3.6) | ||||||
|     activemodel (8.0.2.1) |     activemodel (8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|     activerecord (8.0.2.1) |     activerecord (8.0.0) | ||||||
|       activemodel (= 8.0.2.1) |       activemodel (= 8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       timeout (>= 0.4.0) |       timeout (>= 0.4.0) | ||||||
|     activestorage (8.0.2.1) |     activestorage (8.0.0) | ||||||
|       actionpack (= 8.0.2.1) |       actionpack (= 8.0.0) | ||||||
|       activejob (= 8.0.2.1) |       activejob (= 8.0.0) | ||||||
|       activerecord (= 8.0.2.1) |       activerecord (= 8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       marcel (~> 1.0) |       marcel (~> 1.0) | ||||||
|     activesupport (8.0.2.1) |     activesupport (8.0.0) | ||||||
|       base64 |       base64 | ||||||
|       benchmark (>= 0.3) |       benchmark (>= 0.3) | ||||||
|       bigdecimal |       bigdecimal | ||||||
| @ -76,33 +76,30 @@ GEM | |||||||
|       rails (>= 6.0) |       rails (>= 6.0) | ||||||
|     addressable (2.8.7) |     addressable (2.8.7) | ||||||
|       public_suffix (>= 2.0.2, < 7.0) |       public_suffix (>= 2.0.2, < 7.0) | ||||||
|     annotaterb (4.19.0) |     annotaterb (4.13.0) | ||||||
|       activerecord (>= 6.0.0) |     ast (2.4.2) | ||||||
|       activesupport (>= 6.0.0) |  | ||||||
|     ast (2.4.3) |  | ||||||
|     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.3.0) |     base64 (0.2.0) | ||||||
|     bcrypt (3.1.20) |     bcrypt (3.1.20) | ||||||
|     benchmark (0.4.1) |     benchmark (0.4.0) | ||||||
|     bigdecimal (3.2.3) |     bigdecimal (3.1.8) | ||||||
|     bindex (0.8.1) |     bindex (0.8.1) | ||||||
|     bootsnap (1.18.6) |     bootsnap (1.18.4) | ||||||
|       msgpack (~> 1.2) |       msgpack (~> 1.2) | ||||||
|     builder (3.3.0) |     builder (3.3.0) | ||||||
|     childprocess (5.1.0) |     childprocess (5.1.0) | ||||||
|       logger (~> 1.5) |       logger (~> 1.5) | ||||||
|     chroma (0.2.0) |     chroma (0.2.0) | ||||||
|     chunky_png (1.4.0) |  | ||||||
|     coderay (1.1.3) |     coderay (1.1.3) | ||||||
|     concurrent-ruby (1.3.5) |     concurrent-ruby (1.3.4) | ||||||
|     connection_pool (2.5.4) |     connection_pool (2.4.1) | ||||||
|     crass (1.0.6) |     crass (1.0.6) | ||||||
|     csv (3.3.5) |     csv (3.3.0) | ||||||
|     date (3.4.1) |     date (3.4.1) | ||||||
|     debug (1.11.0) |     debug (1.9.2) | ||||||
|       irb (~> 1.10) |       irb (~> 1.10) | ||||||
|       reline (>= 0.3.8) |       reline (>= 0.3.8) | ||||||
|     devise (4.9.4) |     devise (4.9.4) | ||||||
| @ -111,44 +108,42 @@ GEM | |||||||
|       railties (>= 4.1.0) |       railties (>= 4.1.0) | ||||||
|       responders |       responders | ||||||
|       warden (~> 1.2.3) |       warden (~> 1.2.3) | ||||||
|     diff-lcs (1.6.2) |     diff-lcs (1.5.1) | ||||||
|     drb (2.2.3) |     drb (2.2.1) | ||||||
|     erb (5.0.2) |     erubi (1.13.0) | ||||||
|     erubi (1.13.1) |  | ||||||
|     et-orbi (1.2.11) |     et-orbi (1.2.11) | ||||||
|       tzinfo |       tzinfo | ||||||
|     execjs (2.9.1) |     execjs (2.9.1) | ||||||
|     factory_bot (6.5.5) |     factory_bot (6.4.6) | ||||||
|       activesupport (>= 6.1.0) |       activesupport (>= 5.0.0) | ||||||
|     factory_bot_rails (6.5.1) |     factory_bot_rails (6.4.3) | ||||||
|       factory_bot (~> 6.5) |       factory_bot (~> 6.4) | ||||||
|       railties (>= 6.1.0) |       railties (>= 5.0.0) | ||||||
|     faker (3.5.2) |     faker (3.5.1) | ||||||
|       i18n (>= 1.8.11, < 2) |       i18n (>= 1.8.11, < 2) | ||||||
|     fugit (1.11.1) |     fugit (1.11.1) | ||||||
|       et-orbi (~> 1, >= 1.2.11) |       et-orbi (~> 1, >= 1.2.11) | ||||||
|       raabro (~> 1.4) |       raabro (~> 1.4) | ||||||
|     globalid (1.2.1) |     globalid (1.2.1) | ||||||
|       activesupport (>= 6.1) |       activesupport (>= 6.1) | ||||||
|     httparty (0.23.1) |     httparty (0.22.0) | ||||||
|       csv |       csv | ||||||
|       mini_mime (>= 1.0.0) |       mini_mime (>= 1.0.0) | ||||||
|       multi_xml (>= 0.5.2) |       multi_xml (>= 0.5.2) | ||||||
|     i18n (1.14.7) |     i18n (1.14.6) | ||||||
|       concurrent-ruby (~> 1.0) |       concurrent-ruby (~> 1.0) | ||||||
|     importmap-rails (2.2.2) |     importmap-rails (2.0.3) | ||||||
|       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.8.1) |     io-console (0.8.0) | ||||||
|     irb (1.15.2) |     irb (1.14.1) | ||||||
|       pp (>= 0.6.0) |  | ||||||
|       rdoc (>= 4.0.0) |       rdoc (>= 4.0.0) | ||||||
|       reline (>= 0.4.2) |       reline (>= 0.4.2) | ||||||
|     jbuilder (2.13.0) |     jbuilder (2.13.0) | ||||||
|       actionview (>= 5.0.0) |       actionview (>= 5.0.0) | ||||||
|       activesupport (>= 5.0.0) |       activesupport (>= 5.0.0) | ||||||
|     json (2.13.2) |     json (2.8.2) | ||||||
|     json-schema (5.0.1) |     json-schema (5.0.1) | ||||||
|       addressable (~> 2.8) |       addressable (~> 2.8) | ||||||
|     jsonapi-deserializable (0.2.0) |     jsonapi-deserializable (0.2.0) | ||||||
| @ -162,7 +157,7 @@ GEM | |||||||
|     jsonapi-renderer (0.2.2) |     jsonapi-renderer (0.2.2) | ||||||
|     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.5) |     language_server-protocol (3.17.0.3) | ||||||
|     launchy (3.0.1) |     launchy (3.0.1) | ||||||
|       addressable (~> 2.8) |       addressable (~> 2.8) | ||||||
|       childprocess (~> 5.0) |       childprocess (~> 5.0) | ||||||
| @ -181,9 +176,8 @@ GEM | |||||||
|       tomlrb (>= 1.3, < 2.1) |       tomlrb (>= 1.3, < 2.1) | ||||||
|       with_env (= 1.1.0) |       with_env (= 1.1.0) | ||||||
|       xml-simple (~> 1.1.9) |       xml-simple (~> 1.1.9) | ||||||
|     lint_roller (1.1.0) |     logger (1.6.2) | ||||||
|     logger (1.7.0) |     loofah (2.23.1) | ||||||
|     loofah (2.24.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) | ||||||
| @ -194,109 +188,92 @@ GEM | |||||||
|     marcel (1.0.4) |     marcel (1.0.4) | ||||||
|     method_source (1.1.0) |     method_source (1.1.0) | ||||||
|     mini_mime (1.1.5) |     mini_mime (1.1.5) | ||||||
|     mini_portile2 (2.8.9) |     minitest (5.25.4) | ||||||
|     minitest (5.25.5) |  | ||||||
|     money (6.19.0) |     money (6.19.0) | ||||||
|       i18n (>= 0.6.4, <= 2) |       i18n (>= 0.6.4, <= 2) | ||||||
|     msgpack (1.7.5) |     msgpack (1.7.2) | ||||||
|     multi_xml (0.7.1) |     multi_xml (0.7.1) | ||||||
|       bigdecimal (~> 3.1) |       bigdecimal (~> 3.1) | ||||||
|     net-imap (0.5.10) |     net-imap (0.5.1) | ||||||
|       date |       date | ||||||
|       net-protocol |       net-protocol | ||||||
|     net-pop (0.1.2) |     net-pop (0.1.2) | ||||||
|       net-protocol |       net-protocol | ||||||
|     net-protocol (0.2.2) |     net-protocol (0.2.2) | ||||||
|       timeout |       timeout | ||||||
|     net-smtp (0.5.1) |     net-smtp (0.5.0) | ||||||
|       net-protocol |       net-protocol | ||||||
|     nio4r (2.7.4) |     nio4r (2.7.4) | ||||||
|     nokogiri (1.18.10) |     nokogiri (1.16.8-aarch64-linux) | ||||||
|       mini_portile2 (~> 2.8.2) |  | ||||||
|       racc (~> 1.4) |       racc (~> 1.4) | ||||||
|     nokogiri (1.18.10-aarch64-linux-gnu) |     nokogiri (1.16.8-arm-linux) | ||||||
|       racc (~> 1.4) |       racc (~> 1.4) | ||||||
|     nokogiri (1.18.10-arm-linux-gnu) |     nokogiri (1.16.8-arm64-darwin) | ||||||
|       racc (~> 1.4) |       racc (~> 1.4) | ||||||
|     nokogiri (1.18.10-arm64-darwin) |     nokogiri (1.16.8-x86-linux) | ||||||
|       racc (~> 1.4) |       racc (~> 1.4) | ||||||
|     nokogiri (1.18.10-x86_64-darwin) |     nokogiri (1.16.8-x86_64-darwin) | ||||||
|       racc (~> 1.4) |       racc (~> 1.4) | ||||||
|     nokogiri (1.18.10-x86_64-linux-gnu) |     nokogiri (1.16.8-x86_64-linux) | ||||||
|       racc (~> 1.4) |       racc (~> 1.4) | ||||||
|     orm_adapter (0.5.0) |     orm_adapter (0.5.0) | ||||||
|     ostruct (0.6.2) |     parallel (1.26.3) | ||||||
|     parallel (1.27.0) |     parser (3.3.6.0) | ||||||
|     parser (3.3.9.0) |  | ||||||
|       ast (~> 2.4.1) |       ast (~> 2.4.1) | ||||||
|       racc |       racc | ||||||
|     pg (1.6.2) |     pg (1.5.9) | ||||||
|     pg (1.6.2-aarch64-linux) |     pry (0.15.0) | ||||||
|     pg (1.6.2-arm64-darwin) |  | ||||||
|     pg (1.6.2-x86_64-darwin) |  | ||||||
|     pg (1.6.2-x86_64-linux) |  | ||||||
|     pluck_to_hash (1.0.2) |  | ||||||
|       activerecord (>= 4.0.2) |  | ||||||
|       activesupport (>= 4.0.2) |  | ||||||
|     pp (0.6.2) |  | ||||||
|       prettyprint |  | ||||||
|     prettyprint (0.2.0) |  | ||||||
|     prism (1.5.1) |  | ||||||
|     pry (0.15.2) |  | ||||||
|       coderay (~> 1.1) |       coderay (~> 1.1) | ||||||
|       method_source (~> 1.0) |       method_source (~> 1.0) | ||||||
|     psych (5.2.6) |     psych (5.2.1) | ||||||
|       date |       date | ||||||
|       stringio |       stringio | ||||||
|     public_suffix (6.0.1) |     public_suffix (6.0.1) | ||||||
|     puma (6.6.1) |     puma (6.5.0) | ||||||
|       nio4r (~> 2.0) |       nio4r (~> 2.0) | ||||||
|     raabro (1.4.0) |     raabro (1.4.0) | ||||||
|     racc (1.8.1) |     racc (1.8.1) | ||||||
|     rack (3.2.1) |     rack (3.1.8) | ||||||
|     rack-cors (3.0.0) |     rack-cors (2.0.2) | ||||||
|       logger |       rack (>= 2.0.0) | ||||||
|       rack (>= 3.0.14) |     rack-session (2.0.0) | ||||||
|     rack-session (2.1.1) |  | ||||||
|       base64 (>= 0.1.0) |  | ||||||
|       rack (>= 3.0.0) |       rack (>= 3.0.0) | ||||||
|     rack-test (2.2.0) |     rack-test (2.1.0) | ||||||
|       rack (>= 1.3) |       rack (>= 1.3) | ||||||
|     rackup (2.2.1) |     rackup (2.2.1) | ||||||
|       rack (>= 3) |       rack (>= 3) | ||||||
|     rails (8.0.2.1) |     rails (8.0.0) | ||||||
|       actioncable (= 8.0.2.1) |       actioncable (= 8.0.0) | ||||||
|       actionmailbox (= 8.0.2.1) |       actionmailbox (= 8.0.0) | ||||||
|       actionmailer (= 8.0.2.1) |       actionmailer (= 8.0.0) | ||||||
|       actionpack (= 8.0.2.1) |       actionpack (= 8.0.0) | ||||||
|       actiontext (= 8.0.2.1) |       actiontext (= 8.0.0) | ||||||
|       actionview (= 8.0.2.1) |       actionview (= 8.0.0) | ||||||
|       activejob (= 8.0.2.1) |       activejob (= 8.0.0) | ||||||
|       activemodel (= 8.0.2.1) |       activemodel (= 8.0.0) | ||||||
|       activerecord (= 8.0.2.1) |       activerecord (= 8.0.0) | ||||||
|       activestorage (= 8.0.2.1) |       activestorage (= 8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       bundler (>= 1.15.0) |       bundler (>= 1.15.0) | ||||||
|       railties (= 8.0.2.1) |       railties (= 8.0.0) | ||||||
|     rails-dom-testing (2.3.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.2) |     rails-html-sanitizer (1.6.1) | ||||||
|       loofah (~> 2.21) |       loofah (~> 2.21) | ||||||
|       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) |       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 (8.0.2.1) |     railties (8.0.0) | ||||||
|       actionpack (= 8.0.2.1) |       actionpack (= 8.0.0) | ||||||
|       activesupport (= 8.0.2.1) |       activesupport (= 8.0.0) | ||||||
|       irb (~> 1.13) |       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.3.0) |     rake (13.2.1) | ||||||
|     rdoc (6.14.2) |     rdoc (6.8.1) | ||||||
|       erb |  | ||||||
|       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) | ||||||
| @ -304,38 +281,34 @@ GEM | |||||||
|       execjs |       execjs | ||||||
|       railties (>= 3.2) |       railties (>= 3.2) | ||||||
|       tilt |       tilt | ||||||
|     redis (5.4.1) |     redis (5.3.0) | ||||||
|       redis-client (>= 0.22.0) |       redis-client (>= 0.22.0) | ||||||
|     redis-client (0.23.2) |     redis-client (0.22.2) | ||||||
|       connection_pool |       connection_pool | ||||||
|     regexp_parser (2.11.3) |     regexp_parser (2.9.3) | ||||||
|     reline (0.6.2) |     reline (0.5.12) | ||||||
|       io-console (~> 0.5) |       io-console (~> 0.5) | ||||||
|     responders (3.1.1) |     responders (3.1.1) | ||||||
|       actionpack (>= 5.2) |       actionpack (>= 5.2) | ||||||
|       railties (>= 5.2) |       railties (>= 5.2) | ||||||
|     rexml (3.3.9) |     rexml (3.3.9) | ||||||
|     rqrcode (3.1.0) |     rspec-core (3.13.2) | ||||||
|       chunky_png (~> 1.0) |  | ||||||
|       rqrcode_core (~> 2.0) |  | ||||||
|     rqrcode_core (2.0.0) |  | ||||||
|     rspec-core (3.13.5) |  | ||||||
|       rspec-support (~> 3.13.0) |       rspec-support (~> 3.13.0) | ||||||
|     rspec-expectations (3.13.5) |     rspec-expectations (3.13.3) | ||||||
|       diff-lcs (>= 1.2.0, < 2.0) |       diff-lcs (>= 1.2.0, < 2.0) | ||||||
|       rspec-support (~> 3.13.0) |       rspec-support (~> 3.13.0) | ||||||
|     rspec-mocks (3.13.5) |     rspec-mocks (3.13.2) | ||||||
|       diff-lcs (>= 1.2.0, < 2.0) |       diff-lcs (>= 1.2.0, < 2.0) | ||||||
|       rspec-support (~> 3.13.0) |       rspec-support (~> 3.13.0) | ||||||
|     rspec-rails (8.0.2) |     rspec-rails (7.1.0) | ||||||
|       actionpack (>= 7.2) |       actionpack (>= 7.0) | ||||||
|       activesupport (>= 7.2) |       activesupport (>= 7.0) | ||||||
|       railties (>= 7.2) |       railties (>= 7.0) | ||||||
|       rspec-core (~> 3.13) |       rspec-core (~> 3.13) | ||||||
|       rspec-expectations (~> 3.13) |       rspec-expectations (~> 3.13) | ||||||
|       rspec-mocks (~> 3.13) |       rspec-mocks (~> 3.13) | ||||||
|       rspec-support (~> 3.13) |       rspec-support (~> 3.13) | ||||||
|     rspec-support (3.13.5) |     rspec-support (3.13.1) | ||||||
|     rswag (2.16.0) |     rswag (2.16.0) | ||||||
|       rswag-api (= 2.16.0) |       rswag-api (= 2.16.0) | ||||||
|       rswag-specs (= 2.16.0) |       rswag-specs (= 2.16.0) | ||||||
| @ -351,50 +324,32 @@ GEM | |||||||
|     rswag-ui (2.16.0) |     rswag-ui (2.16.0) | ||||||
|       actionpack (>= 5.2, < 8.1) |       actionpack (>= 5.2, < 8.1) | ||||||
|       railties (>= 5.2, < 8.1) |       railties (>= 5.2, < 8.1) | ||||||
|     rubocop (1.80.2) |     rubocop (1.69.1) | ||||||
|       json (~> 2.3) |       json (~> 2.3) | ||||||
|       language_server-protocol (~> 3.17.0.2) |       language_server-protocol (>= 3.17.0) | ||||||
|       lint_roller (~> 1.1.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.9.3, < 3.0) |       regexp_parser (>= 2.9.3, < 3.0) | ||||||
|       rubocop-ast (>= 1.46.0, < 2.0) |       rubocop-ast (>= 1.36.2, < 2.0) | ||||||
|       ruby-progressbar (~> 1.7) |       ruby-progressbar (~> 1.7) | ||||||
|       unicode-display_width (>= 2.4.0, < 4.0) |       unicode-display_width (>= 2.4.0, < 4.0) | ||||||
|     rubocop-ast (1.46.0) |     rubocop-ast (1.36.2) | ||||||
|       parser (>= 3.3.7.2) |       parser (>= 3.3.1.0) | ||||||
|       prism (~> 1.4) |  | ||||||
|     rubocop-factory_bot (2.27.1) |  | ||||||
|       lint_roller (~> 1.1) |  | ||||||
|       rubocop (~> 1.72, >= 1.72.1) |  | ||||||
|     rubocop-rails (2.33.3) |  | ||||||
|       activesupport (>= 4.2.0) |  | ||||||
|       lint_roller (~> 1.1) |  | ||||||
|       rack (>= 1.1) |  | ||||||
|       rubocop (>= 1.75.0, < 2.0) |  | ||||||
|       rubocop-ast (>= 1.44.0, < 2.0) |  | ||||||
|     rubocop-rspec (3.6.0) |  | ||||||
|       lint_roller (~> 1.1) |  | ||||||
|       rubocop (~> 1.72, >= 1.72.1) |  | ||||||
|     rubocop-rspec_rails (2.31.0) |  | ||||||
|       lint_roller (~> 1.1) |  | ||||||
|       rubocop (~> 1.72, >= 1.72.1) |  | ||||||
|       rubocop-rspec (~> 3.5) |  | ||||||
|     ruby-progressbar (1.13.0) |     ruby-progressbar (1.13.0) | ||||||
|     rubytree (2.1.1) |     rubytree (2.1.0) | ||||||
|       json (~> 2.0, > 2.9) |       json (~> 2.0, > 2.3.1) | ||||||
|     rubyzip (2.3.2) |     rubyzip (2.3.2) | ||||||
|     securerandom (0.4.1) |     securerandom (0.4.0) | ||||||
|     shoulda-matchers (6.5.0) |     shoulda-matchers (6.4.0) | ||||||
|       activesupport (>= 5.2.0) |       activesupport (>= 5.2.0) | ||||||
|     solid_queue (1.2.1) |     solid_queue (1.1.0) | ||||||
|       activejob (>= 7.1) |       activejob (>= 7.1) | ||||||
|       activerecord (>= 7.1) |       activerecord (>= 7.1) | ||||||
|       concurrent-ruby (>= 1.3.1) |       concurrent-ruby (>= 1.3.1) | ||||||
|       fugit (~> 1.11.0) |       fugit (~> 1.11.0) | ||||||
|       railties (>= 7.1) |       railties (>= 7.1) | ||||||
|       thor (>= 1.3.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) | ||||||
| @ -404,18 +359,18 @@ GEM | |||||||
|       sprockets (>= 3.0.0) |       sprockets (>= 3.0.0) | ||||||
|     stimulus-rails (1.3.4) |     stimulus-rails (1.3.4) | ||||||
|       railties (>= 6.0.0) |       railties (>= 6.0.0) | ||||||
|     stringio (3.1.7) |     stringio (3.1.2) | ||||||
|     thor (1.4.0) |     thor (1.3.2) | ||||||
|     tilt (2.4.0) |     tilt (2.4.0) | ||||||
|     timeout (0.4.3) |     timeout (0.4.2) | ||||||
|     tomlrb (2.0.3) |     tomlrb (2.0.3) | ||||||
|     turbo-rails (2.0.16) |     turbo-rails (2.0.11) | ||||||
|       actionpack (>= 7.1.0) |       actionpack (>= 6.0.0) | ||||||
|       railties (>= 7.1.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.6.0) |     unicode-display_width (2.6.0) | ||||||
|     uri (1.0.3) |     uri (1.0.2) | ||||||
|     useragent (0.16.11) |     useragent (0.16.11) | ||||||
|     warden (1.2.9) |     warden (1.2.9) | ||||||
|       rack (>= 2.0.9) |       rack (>= 2.0.9) | ||||||
| @ -424,17 +379,13 @@ GEM | |||||||
|       activemodel (>= 6.0.0) |       activemodel (>= 6.0.0) | ||||||
|       bindex (>= 0.4.0) |       bindex (>= 0.4.0) | ||||||
|       railties (>= 6.0.0) |       railties (>= 6.0.0) | ||||||
|     websocket-driver (0.7.7) |     websocket-driver (0.7.6) | ||||||
|       base64 |  | ||||||
|       websocket-extensions (>= 0.1.0) |       websocket-extensions (>= 0.1.0) | ||||||
|     websocket-extensions (0.1.5) |     websocket-extensions (0.1.5) | ||||||
|     wicked_pdf (2.8.2) |  | ||||||
|       activesupport |  | ||||||
|       ostruct |  | ||||||
|     with_env (1.1.0) |     with_env (1.1.0) | ||||||
|     xml-simple (1.1.9) |     xml-simple (1.1.9) | ||||||
|       rexml |       rexml | ||||||
|     zeitwerk (2.7.3) |     zeitwerk (2.7.1) | ||||||
| 
 | 
 | ||||||
| PLATFORMS | PLATFORMS | ||||||
|   aarch64-linux |   aarch64-linux | ||||||
| @ -462,21 +413,15 @@ DEPENDENCIES | |||||||
|   license_finder |   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 (~> 8.0.0, >= 8.0.0) |   rails (~> 8.0.0, >= 8.0.0) | ||||||
|   react-rails |   react-rails | ||||||
|   redis (>= 4.0.1) |   redis (>= 4.0.1) | ||||||
|   rqrcode (~> 3.1) |   rspec-rails (~> 7.1.0) | ||||||
|   rspec-rails (~> 8.0.0) |  | ||||||
|   rswag |   rswag | ||||||
|   rubocop |   rubocop | ||||||
|   rubocop-factory_bot |  | ||||||
|   rubocop-rails |  | ||||||
|   rubocop-rspec |  | ||||||
|   rubocop-rspec_rails |  | ||||||
|   rubytree |   rubytree | ||||||
|   shoulda-matchers (~> 6.0) |   shoulda-matchers (~> 6.0) | ||||||
|   solid_queue (~> 1.0) |   solid_queue (~> 1.0) | ||||||
| @ -485,182 +430,9 @@ DEPENDENCIES | |||||||
|   turbo-rails |   turbo-rails | ||||||
|   tzinfo-data |   tzinfo-data | ||||||
|   web-console |   web-console | ||||||
|   wicked_pdf (~> 2.8) |  | ||||||
| 
 |  | ||||||
| CHECKSUMS |  | ||||||
|   actioncable (8.0.2.1) sha256=6f1cb20db39fba28a93569e8d5dab42b2749d7ddd4baebb5bbecd4217e49d6a2 |  | ||||||
|   actionmailbox (8.0.2.1) sha256=8ea8c6e31e448961c06fc1d6282775b32aff1c009f232d4564e07e54850a6cad |  | ||||||
|   actionmailer (8.0.2.1) sha256=0de14d8d04541eab130858cb2f0697266be42de1afe1104bc43d7998137ddb9c |  | ||||||
|   actionpack (8.0.2.1) sha256=61e7e11a31dbe5152ca57221788bdca42ef302c4cc53b4c8993d68dce8982b0a |  | ||||||
|   actiontext (8.0.2.1) sha256=0cc4b3b5cfb9d915c6697b05b013dad7f4eaf074d9989700b6a0a55cf620d6b8 |  | ||||||
|   actionview (8.0.2.1) sha256=2ea6d20ccb0b7b84a221a940ac06853ce99235e4ecb4947815839c7c5ecbf347 |  | ||||||
|   activejob (8.0.2.1) sha256=d6e5f2da07ec8efac13a38af1752416770dc74e95783f7b252506d707aa32b89 |  | ||||||
|   activemodel (8.0.2.1) sha256=17bab6cdb86531844113df22f864480a89a276bf0318246e628f99e0ac077ec4 |  | ||||||
|   activerecord (8.0.2.1) sha256=a6556e7bdd53f3889d18d2aa3a7ff115fd6c5e1463dd06f97fb88d06b58c6df1 |  | ||||||
|   activestorage (8.0.2.1) sha256=43bb3d9e115471e201e6a66813810c1d15b607a321f29d62efdf9d90ffaf76f8 |  | ||||||
|   activesupport (8.0.2.1) sha256=0405a76fd1ca989975d9ae00d46a4d3979bdf3817482d846b63affa84bd561c6 |  | ||||||
|   acts_as_tenant (1.0.1) sha256=6944e4d64533337938a8817a6b4ff9b11189c9dcc0b1333bb89f3821a4c14c53 |  | ||||||
|   addressable (2.8.7) sha256=462986537cf3735ab5f3c0f557f14155d778f4b43ea4f485a9deb9c8f7c58232 |  | ||||||
|   annotaterb (4.19.0) sha256=c951df62059b3ac1ae383f4140bf935a140a15b6461f8d9a97d34b38ce2c7208 |  | ||||||
|   ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 |  | ||||||
|   babel-source (5.8.35) sha256=79ef222a9dcb867ac2efa3b0da35b4bcb15a4bfa67b6b2dcbf1e9a29104498d9 |  | ||||||
|   babel-transpiler (0.7.0) sha256=4c06f4ad9e8e1cabe94f99e11df2f140bb72aca9ba067dbb49dc14d9b98d1570 |  | ||||||
|   base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b |  | ||||||
|   bcrypt (3.1.20) sha256=8410f8c7b3ed54a3c00cd2456bf13917d695117f033218e2483b2e40b0784099 |  | ||||||
|   benchmark (0.4.1) sha256=d4ef40037bba27f03b28013e219b950b82bace296549ec15a78016552f8d2cce |  | ||||||
|   bigdecimal (3.2.3) sha256=ffd11d1ac67a0d3b2f44aec0a6487210b3f813f363dd11f1fcccf5ba00da4e1b |  | ||||||
|   bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e |  | ||||||
|   bootsnap (1.18.6) sha256=0ae2393c1e911e38be0f24e9173e7be570c3650128251bf06240046f84a07d00 |  | ||||||
|   builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f |  | ||||||
|   childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec |  | ||||||
|   chroma (0.2.0) sha256=64bdcd36a4765fbcd45adc64960cc153101300b4918f90ffdd89f4e2eb954b54 |  | ||||||
|   chunky_png (1.4.0) sha256=89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe |  | ||||||
|   coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b |  | ||||||
|   concurrent-ruby (1.3.5) sha256=813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6 |  | ||||||
|   connection_pool (2.5.4) sha256=e9e1922327416091f3f6542f5f4446c2a20745276b9aa796dd0bb2fd0ea1e70a |  | ||||||
|   crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d |  | ||||||
|   csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f |  | ||||||
|   date (3.4.1) sha256=bf268e14ef7158009bfeaec40b5fa3c7271906e88b196d958a89d4b408abe64f |  | ||||||
|   debug (1.11.0) sha256=1425db64cfa0130c952684e3dc974985be201dd62899bf4bbe3f8b5d6cf1aef2 |  | ||||||
|   devise (4.9.4) sha256=920042fe5e704c548aa4eb65ebdd65980b83ffae67feb32c697206bfd975a7f8 |  | ||||||
|   diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 |  | ||||||
|   drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 |  | ||||||
|   erb (5.0.2) sha256=d30f258143d4300fb4ecf430042ac12970c9bb4b33c974a545b8f58c1ec26c0f |  | ||||||
|   erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 |  | ||||||
|   et-orbi (1.2.11) sha256=d26e868cc21db88280a9ec1a50aa3da5d267eb9b2037ba7b831d6c2731f5df64 |  | ||||||
|   execjs (2.9.1) sha256=e8fd066f6df60c8e8fbebc32c6fb356b5212c77374e8416a9019ca4bb154dcfb |  | ||||||
|   factory_bot (6.5.5) sha256=ce59295daee1b4704dab8a2fee6816f513d467c6aa3bc587860767d74a66efbe |  | ||||||
|   factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 |  | ||||||
|   faker (3.5.2) sha256=f9a80291b2e3f259801d1dd552f0732fe04dce5d1f74e798365bc0413789c473 |  | ||||||
|   fugit (1.11.1) sha256=e89485e7be22226d8e9c6da411664d0660284b4b1c08cacb540f505907869868 |  | ||||||
|   globalid (1.2.1) sha256=70bf76711871f843dbba72beb8613229a49429d1866828476f9c9d6ccc327ce9 |  | ||||||
|   httparty (0.23.1) sha256=3ac1dd62f2010f6ece551716f5ceec2b2012011d89f1751917ab7f724e966b55 |  | ||||||
|   i18n (1.14.7) sha256=ceba573f8138ff2c0915427f1fc5bdf4aa3ab8ae88c8ce255eb3ecf0a11a5d0f |  | ||||||
|   importmap-rails (2.2.2) sha256=729f5b1092f832780829ade1d0b46c7e53d91c556f06da7254da2977e93fe614 |  | ||||||
|   io-console (0.8.1) sha256=1e15440a6b2f67b6ea496df7c474ed62c860ad11237f29b3bd187f054b925fcb |  | ||||||
|   irb (1.15.2) sha256=222f32952e278da34b58ffe45e8634bf4afc2dc7aa9da23fed67e581aa50fdba |  | ||||||
|   jbuilder (2.13.0) sha256=7200a38a1c0081aa81b7a9757e7a299db75bc58cf1fd45ca7919a91627d227d6 |  | ||||||
|   json (2.13.2) sha256=02e1f118d434c6b230a64ffa5c8dee07e3ec96244335c392eaed39e1199dbb68 |  | ||||||
|   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.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc |  | ||||||
|   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 |  | ||||||
|   lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 |  | ||||||
|   logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 |  | ||||||
|   loofah (2.24.1) sha256=655a30842b70ec476410b347ab1cd2a5b92da46a19044357bbd9f401b009a337 |  | ||||||
|   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.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289 |  | ||||||
|   minitest (5.25.5) sha256=391b6c6cb43a4802bfb7c93af1ebe2ac66a210293f4a3fb7db36f2fc7dc2c756 |  | ||||||
|   money (6.19.0) sha256=ec936fa1e42f2783719241ed9fd52725d0efa628f928feea1eb5c37d5de7daf3 |  | ||||||
|   msgpack (1.7.5) sha256=ffb04979f51e6406823c03abe50e1da2c825c55a37dee138518cdd09d9d3aea8 |  | ||||||
|   multi_xml (0.7.1) sha256=4fce100c68af588ff91b8ba90a0bb3f0466f06c909f21a32f4962059140ba61b |  | ||||||
|   net-imap (0.5.10) sha256=f84d206a296bff48a3a10507567fc38b050d2a40c92ea0d448164f64e60d6205 |  | ||||||
|   net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 |  | ||||||
|   net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 |  | ||||||
|   net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 |  | ||||||
|   nio4r (2.7.4) sha256=d95dee68e0bb251b8ff90ac3423a511e3b784124e5db7ff5f4813a220ae73ca9 |  | ||||||
|   nokogiri (1.18.10) sha256=d5cc0731008aa3b3a87b361203ea3d19b2069628cb55e46ac7d84a0445e69cc1 |  | ||||||
|   nokogiri (1.18.10-aarch64-linux-gnu) sha256=7fb87235d729c74a2be635376d82b1d459230cc17c50300f8e4fcaabc6195344 |  | ||||||
|   nokogiri (1.18.10-arm-linux-gnu) sha256=51f4f25ab5d5ba1012d6b16aad96b840a10b067b93f35af6a55a2c104a7ee322 |  | ||||||
|   nokogiri (1.18.10-arm64-darwin) sha256=c2b0de30770f50b92c9323fa34a4e1cf5a0af322afcacd239cd66ee1c1b22c85 |  | ||||||
|   nokogiri (1.18.10-x86_64-darwin) sha256=536e74bed6db2b5076769cab5e5f5af0cd1dccbbd75f1b3e1fa69d1f5c2d79e2 |  | ||||||
|   nokogiri (1.18.10-x86_64-linux-gnu) sha256=ff5ba26ba2dbce5c04b9ea200777fd225061d7a3930548806f31db907e500f72 |  | ||||||
|   orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9 |  | ||||||
|   ostruct (0.6.2) sha256=6d7302a299e400a2c248d6ce0dad18fc3a5714e8096facc25ffd0c54ee57cfc0 |  | ||||||
|   parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 |  | ||||||
|   parser (3.3.9.0) sha256=94d6929354b1a6e3e1f89d79d4d302cc8f5aa814431a6c9c7e0623335d7687f2 |  | ||||||
|   pg (1.6.2) sha256=58614afd405cc9c2c9e15bffe8432e0d6cfc58b722344ad4a47c73a85189c875 |  | ||||||
|   pg (1.6.2-aarch64-linux) sha256=0503c6be5b0ca5ca3aaf91f2ed638f90843313cb81e8e7d7b60ad4bb62c3d131 |  | ||||||
|   pg (1.6.2-arm64-darwin) sha256=4d44500b28d5193b26674583d199a6484f80f1f2ea9cf54f7d7d06a1b7e316b6 |  | ||||||
|   pg (1.6.2-x86_64-darwin) sha256=c441a55723584e2ae41749bf26024d7ffdfe1841b442308ed50cd6b7fda04115 |  | ||||||
|   pg (1.6.2-x86_64-linux) sha256=525f438137f2d1411a1ebcc4208ec35cb526b5a3b285a629355c73208506a8ea |  | ||||||
|   pluck_to_hash (1.0.2) sha256=1599906239716f98262a41493dd7d4cb72e8d83ad3d76d666deacfc5de50a47e |  | ||||||
|   pp (0.6.2) sha256=947ec3120c6f92195f8ee8aa25a7b2c5297bb106d83b41baa02983686577b6ff |  | ||||||
|   prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 |  | ||||||
|   prism (1.5.1) sha256=b40c1b76ccb9fcccc3d1553967cda6e79fa7274d8bfea0d98b15d27a6d187134 |  | ||||||
|   pry (0.15.2) sha256=12d54b8640d3fa29c9211dd4ffb08f3fd8bf7a4fd9b5a73ce5b59c8709385b6b |  | ||||||
|   psych (5.2.6) sha256=814328aa5dcb6d604d32126a20bc1cbcf05521a5b49dbb1a8b30a07e580f316e |  | ||||||
|   public_suffix (6.0.1) sha256=61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f |  | ||||||
|   puma (6.6.1) sha256=b9b56e4a4ea75d1bfa6d9e1972ee2c9f43d0883f011826d914e8e37b3694ea1e |  | ||||||
|   raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882 |  | ||||||
|   racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f |  | ||||||
|   rack (3.2.1) sha256=30af3f7e5ec21b0d14d822cf24446048dba5f651b617c7e97405b604f20a9e33 |  | ||||||
|   rack-cors (3.0.0) sha256=7b95be61db39606906b61b83bd7203fa802b0ceaaad8fcb2fef39e097bf53f68 |  | ||||||
|   rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9 |  | ||||||
|   rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 |  | ||||||
|   rackup (2.2.1) sha256=f737191fd5c5b348b7f0a4412a3b86383f88c43e13b8217b63d4c8d90b9e798d |  | ||||||
|   rails (8.0.2.1) sha256=13ab95615569e74e364384b346b1d83e4795dbde83d9edf584e8768e8049b3ac |  | ||||||
|   rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d |  | ||||||
|   rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560 |  | ||||||
|   railties (8.0.2.1) sha256=54e40e1771fc2878f572d5a4e076cddb057ba8d4d471f8b7d9bfc61bc1301d4c |  | ||||||
|   rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a |  | ||||||
|   rake (13.3.0) sha256=96f5092d786ff412c62fde76f793cc0541bd84d2eb579caa529aa8a059934493 |  | ||||||
|   rdoc (6.14.2) sha256=9fdd44df130f856ae70cc9a264dfd659b9b40de369b16581f4ab746e42439226 |  | ||||||
|   react-rails (3.2.1) sha256=2235db0b240517596b1cb3e26177ab5bc64d3a56579b0415ee242b1691f81f64 |  | ||||||
|   redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae |  | ||||||
|   redis-client (0.23.2) sha256=e33bab6682c8155cfef95e6dd296936bb9c2981a89fb578ace27a076fa2836fa |  | ||||||
|   regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4 |  | ||||||
|   reline (0.6.2) sha256=1dad26a6008872d59c8e05244b119347c9f2ddaf4a53dce97856cd5f30a02846 |  | ||||||
|   responders (3.1.1) sha256=92f2a87e09028347368639cfb468f5fefa745cb0dc2377ef060db1cdd79a341a |  | ||||||
|   rexml (3.3.9) sha256=d71875b85299f341edf47d44df0212e7658cbdf35aeb69cefdb63f57af3137c9 |  | ||||||
|   rqrcode (3.1.0) sha256=e2d5996375f6e9a013823c289ed575dbea678b8e0388574302c1fac563f098af |  | ||||||
|   rqrcode_core (2.0.0) sha256=1e40b823ab57a96482a417fff5dd5c33645a00cea6ef5d9e342fecc5ef91d9ab |  | ||||||
|   rspec-core (3.13.5) sha256=ab3f682897c6131c67f9a17cfee5022a597f283aebe654d329a565f9937a4fa3 |  | ||||||
|   rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 |  | ||||||
|   rspec-mocks (3.13.5) sha256=e4338a6f285ada9fe56f5893f5457783af8194f5d08884d17a87321d5195ea81 |  | ||||||
|   rspec-rails (8.0.2) sha256=113139a53f5d068d4f48d1c29ad5f982013ed9b0daa69d7f7b266eda5d433ace |  | ||||||
|   rspec-support (3.13.5) sha256=add745af535dd14b18f1209ab41ef987fdfad12786176b6a3b3619b9a7279fbf |  | ||||||
|   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.80.2) sha256=6485f30fefcf5c199db3b91e5e253b1ef43f7e564784e2315255809a3dd9abf4 |  | ||||||
|   rubocop-ast (1.46.0) sha256=0da7f6ad5b98614f89b74f11873c191059c823eae07d6ffd40a42a3338f2232b |  | ||||||
|   rubocop-factory_bot (2.27.1) sha256=9d744b5916778c1848e5fe6777cc69855bd96548853554ec239ba9961b8573fe |  | ||||||
|   rubocop-rails (2.33.3) sha256=848c011b58c1292f3066246c9eb18abf6ffcfbce28bc57c4ab888bbec79af74b |  | ||||||
|   rubocop-rspec (3.6.0) sha256=c0e4205871776727e54dee9cc91af5fd74578001551ba40e1fe1a1ab4b404479 |  | ||||||
|   rubocop-rspec_rails (2.31.0) sha256=775375e18a26a1184a812ef3054b79d218e85601b9ae897f38f8be24dddf1f45 |  | ||||||
|   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.5.0) sha256=ef6b572b2bed1ac4aba6ab2c5ff345a24b6d055a93a3d1c3bfc86d9d499e3f44 |  | ||||||
|   solid_queue (1.2.1) sha256=7976b3690a08080ef63d1b11281f0b77398f7697dbeda0e2c5532682639d4b15 |  | ||||||
|   sprockets (4.2.1) sha256=951b13dd2f2fcae840a7184722689a803e0ff9d2702d902bd844b196da773f97 |  | ||||||
|   sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e |  | ||||||
|   stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 |  | ||||||
|   stringio (3.1.7) sha256=5b78b7cb242a315fb4fca61a8255d62ec438f58da2b90be66048546ade4507fa |  | ||||||
|   thor (1.4.0) sha256=8763e822ccb0f1d7bee88cde131b19a65606657b847cc7b7b4b82e772bcd8a3d |  | ||||||
|   tilt (2.4.0) sha256=df74f29a451daed26591a85e8e0cebb198892cb75b6573394303acda273fba4d |  | ||||||
|   timeout (0.4.3) sha256=9509f079b2b55fe4236d79633bd75e34c1c1e7e3fb4b56cb5fda61f80a0fe30e |  | ||||||
|   tomlrb (2.0.3) sha256=c2736acf24919f793334023a4ff396c0647d93fce702a73c9d348deaa815d4f7 |  | ||||||
|   turbo-rails (2.0.16) sha256=d24e1b60f0c575b3549ecda967e5391027143f8220d837ed792c8d48ea0ea38d |  | ||||||
|   tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b |  | ||||||
|   unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a |  | ||||||
|   uri (1.0.3) sha256=e9f2244608eea2f7bc357d954c65c910ce0399ca5e18a7a29207ac22d8767011 |  | ||||||
|   useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 |  | ||||||
|   warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0 |  | ||||||
|   web-console (4.2.1) sha256=e7bcf37a10ea2b4ec4281649d1cee461b32232d0a447e82c786e6841fd22fe20 |  | ||||||
|   websocket-driver (0.7.7) sha256=056d99f2cd545712cfb1291650fde7478e4f2661dc1db6a0fa3b966231a146b4 |  | ||||||
|   websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 |  | ||||||
|   wicked_pdf (2.8.2) sha256=648d9b0cec5a34adbc9bbf809731052a78119e2d6d323b9e4aa1383e1d683824 |  | ||||||
|   with_env (1.1.0) sha256=50b3e4f0a6cda8f90d8a6bd87a6261f6c381429abafb161c4c69ad4a0cd0b6e4 |  | ||||||
|   xml-simple (1.1.9) sha256=d21131e519c86f1a5bc2b6d2d57d46e6998e47f18ed249b25cad86433dbd695d |  | ||||||
|   zeitwerk (2.7.3) sha256=b2e86b4a9b57d26ba68a15230dcc7fe6f040f06831ce64417b0621ad96ba3e85 |  | ||||||
| 
 | 
 | ||||||
| RUBY VERSION | RUBY VERSION | ||||||
|    ruby 3.4.3p32 |    ruby 3.3.6p108 | ||||||
| 
 | 
 | ||||||
| BUNDLED WITH | BUNDLED WITH | ||||||
|    2.6.1 |    2.5.17 | ||||||
|  | |||||||
| @ -57,12 +57,6 @@ The backend service will seed the database with fake data. It's worth noting tha | |||||||
| 
 | 
 | ||||||
| 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. | 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. | ||||||
| 
 | 
 | ||||||
| Please, include this in your `/etc/hosts` file: |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| 127.0.0.1 libre-wedding-planner.app.localhost |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Once all containers have started, visit http://libre-wedding-planner.app.localhost/default/dashboard to load the application. | Once all containers have started, visit http://libre-wedding-planner.app.localhost/default/dashboard to load the application. | ||||||
| 
 | 
 | ||||||
| ## Multitenancy | ## Multitenancy | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								Rakefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Rakefile
									
									
									
									
									
								
							| @ -1,8 +1,6 @@ | |||||||
| # 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,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module ApplicationCable | module ApplicationCable | ||||||
|   class Channel < ActionCable::Channel::Base |   class Channel < ActionCable::Channel::Base | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module ApplicationCable | module ApplicationCable | ||||||
|   class Connection < ActionCable::Connection::Base |   class Connection < ActionCable::Connection::Base | ||||||
|  | |||||||
| @ -1,72 +0,0 @@ | |||||||
| # 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,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class ApplicationController < ActionController::Base | class ApplicationController < ActionController::Base | ||||||
|   set_current_tenant_through_filter |   set_current_tenant_through_filter | ||||||
| @ -42,7 +40,7 @@ class ApplicationController < ActionController::Base | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def captcha_params |   def captcha_params | ||||||
|     params.expect(captcha: %i[id answer]) |     params.expect(captcha: [:id, :answer]) | ||||||
|   end |   end | ||||||
|    |    | ||||||
|   def default_url_options(options = {}) |   def default_url_options(options = {}) | ||||||
| @ -55,13 +53,13 @@ class ApplicationController < ActionController::Base | |||||||
| 
 | 
 | ||||||
|   def development_swagger? |   def development_swagger? | ||||||
|     Rails.env.test? || |     Rails.env.test? || | ||||||
|       (Rails.env.development? && request.headers['referer']&.include?('/api-docs/index.html')) |       Rails.env.development? && request.headers['referer']&.include?('/api-docs/index.html') | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def set_csrf_cookie |   def set_csrf_cookie | ||||||
|     cookies['csrf-token'] = { |     cookies['csrf-token'] = { | ||||||
|       value: form_authenticity_token, |       value: form_authenticity_token, | ||||||
|       secure: false, |       secure: Rails.env.production?, | ||||||
|       same_site: :strict |       same_site: :strict | ||||||
|     } |     } | ||||||
|   end |   end | ||||||
|  | |||||||
| @ -1,12 +1,10 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class CaptchaController < ApplicationController | class CaptchaController < ApplicationController | ||||||
|   skip_before_action :authenticate_user! |   skip_before_action :authenticate_user! | ||||||
|   skip_before_action :set_tenant | 
 | ||||||
|   def create |   def create | ||||||
|     id = LibreCaptcha.new.id |     id = LibreCaptcha.new.get_id | ||||||
|     render json: { |     render json: { | ||||||
|       id:, |       id:, | ||||||
|       media_url: media_captcha_index_url(id:) |       media_url: media_captcha_index_url(id:) | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class ExpensesController < ApplicationController | class ExpensesController < ApplicationController | ||||||
|   def summary |   def summary | ||||||
| @ -8,12 +6,7 @@ class ExpensesController < ApplicationController | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def index |   def index | ||||||
|     render json: Expense.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type]) |     render json: Expense.all.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type]) | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def create |  | ||||||
|     Expense.create!(expense_params) |  | ||||||
|     render json: {}, status: :created |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def update |   def update | ||||||
| @ -21,14 +14,9 @@ class ExpensesController < ApplicationController | |||||||
|     render json: {}, status: :ok |     render json: {}, status: :ok | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def destroy |  | ||||||
|     Expense.find(params[:id]).destroy! |  | ||||||
|     render json: {}, status: :ok |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def expense_params |   def expense_params | ||||||
|     params.expect(expense: %i[name amount pricing_type]) |     params.require(:expense).permit(:name, :amount, :pricing_type) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,46 +1,7 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class GroupsController < ApplicationController | class GroupsController < ApplicationController | ||||||
|   def index |   def index | ||||||
|     query_result = Groups::SummaryQuery.new.call.as_json.map(&:deep_symbolize_keys).map do |group| |     render json: Groups::SummaryQuery.new.call.as_json | ||||||
|       { |  | ||||||
|         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,35 +1,23 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| require 'csv' | require 'csv' | ||||||
| 
 | 
 | ||||||
| class GuestsController < ApplicationController | class GuestsController < ApplicationController | ||||||
|   GUEST_PARAMS = { only: %i[id name status], include: { group: { only: %i[id name] } } }.freeze |  | ||||||
| 
 |  | ||||||
|   skip_before_action :authenticate_user!, only: :update |  | ||||||
| 
 |  | ||||||
|   def index |   def index | ||||||
|     render json: Guest.includes(:group) |     render json: Guest.all.includes(:group) | ||||||
|                       .left_joins(:group) |                       .joins(:group) | ||||||
|                       .order('groups.name' => :asc, invitation_id: :asc, name: :asc) |                       .order('groups.name' => :asc, name: :asc) | ||||||
|                       .as_json(GUEST_PARAMS) |                       .as_json(only: %i[id name status], include: { group: { only: %i[id name] } }) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def create |   def create | ||||||
|     guest = Guest.create!(guest_params) |     Guest.create!(guest_params) | ||||||
|     render json: guest.as_json(GUEST_PARAMS), status: :created |     render json: {}, status: :created | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def update |   def update | ||||||
|     guest = Guest.find(params[:id]) |     Guest.find(params[:id]).update!(guest_params) | ||||||
|     guest.update!(guest_params) |     render json: {}, status: :ok | ||||||
| 
 |  | ||||||
|     if !user_signed_in? && guest.saved_change_to_status? |  | ||||||
|       AdminMailer.with(guest_id: guest.id).attendance_change_email.deliver_later |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     render json: guest.as_json(GUEST_PARAMS), status: :ok |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def destroy |   def destroy | ||||||
| @ -40,6 +28,6 @@ class GuestsController < ApplicationController | |||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def guest_params |   def guest_params | ||||||
|     user_signed_in? ? params.expect(guest: %i[name group_id status]) : params.expect(guest: %i[status]) |     params.require(:guest).permit(:name, :group_id, :status) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,76 +0,0 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors |  | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| class InvitationsController < ApplicationController |  | ||||||
|   skip_before_action :authenticate_user!, only: :show |  | ||||||
| 
 |  | ||||||
|   def index |  | ||||||
|     @invitations = Invitation.includes(:guests).all |  | ||||||
|     respond_to do |format| |  | ||||||
|       format.json do |  | ||||||
|         render json: @invitations.as_json( |  | ||||||
|           only: :id, |  | ||||||
|           include: { guests: { only: %i[id name] } } |  | ||||||
|         ) |  | ||||||
|       end |  | ||||||
|       format.pdf do |  | ||||||
|         pdf_html = ActionController::Base.new.render_to_string( |  | ||||||
|           template: 'invitations/sheet', |  | ||||||
|           layout: 'pdf', |  | ||||||
|           locals: { invitations: @invitations } |  | ||||||
|         ) |  | ||||||
|         pdf = WickedPdf.new.pdf_from_string(pdf_html) |  | ||||||
|         send_data pdf, filename: "invitations_#{Time.current.strftime('%Y%m%d_%H%M%S')}.pdf" |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def email |  | ||||||
|     AdminMailer.with(wedding_id: ActsAsTenant.current_tenant.id).invitations_pdf_email.deliver_later |  | ||||||
| 
 |  | ||||||
|     head :ok |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def sheet; end |  | ||||||
| 
 |  | ||||||
|   def show |  | ||||||
|     invitation = Invitation.includes(:guests).find(params[:id]) |  | ||||||
| 
 |  | ||||||
|     if invitation |  | ||||||
|       render json: invitation, only: :id, include: { guests: { only: %i[id name status] } }, status: :ok |  | ||||||
|     else |  | ||||||
|       render json: { error: 'Invitation not found' }, status: :not_found |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def create |  | ||||||
|     invitation = Invitation.create |  | ||||||
| 
 |  | ||||||
|     if invitation.persisted? |  | ||||||
|       render json: invitation, only: :id, status: :created |  | ||||||
|     else |  | ||||||
|       render json: { errors: invitation.errors.full_messages }, status: :unprocessable_entity |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def update |  | ||||||
|     invitation = Invitation.find(params[:id]) |  | ||||||
| 
 |  | ||||||
|     if invitation.update(guest_ids: params[:invitation][:guest_ids]) |  | ||||||
|       render json: invitation, only: :id, include: { guests: { only: %i[id name] } }, status: :ok |  | ||||||
|     else |  | ||||||
|       render json: { errors: invitation.errors.full_messages }, status: :unprocessable_entity |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def destroy |  | ||||||
|     invitation = Invitation.find(params[:id]) |  | ||||||
| 
 |  | ||||||
|     if invitation.destroy |  | ||||||
|       head :no_content |  | ||||||
|     else |  | ||||||
|       render json: { errors: invitation.errors.full_messages }, status: :unprocessable_entity |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
| @ -1,44 +0,0 @@ | |||||||
| # 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,57 +1,23 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class TablesArrangementsController < ApplicationController | class TablesArrangementsController < ApplicationController | ||||||
|   def index |   def index | ||||||
|     current_digest = Tables::Distribution.digest(current_tenant) |     render json: TablesArrangement.all.order(discomfort: :asc).limit(3).as_json(only: %i[id name discomfort]) | ||||||
| 
 |  | ||||||
|     render json: TablesArrangement |  | ||||||
|       .order(valid: :desc) |  | ||||||
|       .order(discomfort: :asc) |  | ||||||
|       .select(:id, :name, :discomfort, :status, :progress) |  | ||||||
|       .select("digest = '#{current_digest}'::uuid OR discomfort IS NULL as valid") |  | ||||||
|       .limit(20) |  | ||||||
|       .as_json(only: %i[id name discomfort valid status progress]) |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def show |   def show | ||||||
|     Guest.joins(:seats, :group) |     Seat.joins(guest: :group) | ||||||
|          .where(seats: { tables_arrangement_id: params[:id] }) |         .where(tables_arrangement_id: params[:id]) | ||||||
|          .select('guests.*', 'groups.color', 'seats.table_number') |         .order('guests.group_id') | ||||||
|          .group_by(&:table_number) |         .pluck( | ||||||
|          .map { |number, guests| format(number:, guests:) } |           :table_number, | ||||||
|          .then { |result| render json: { id: params[:id], tables: result } } |           'guests.name', | ||||||
|   end |           'guests.id', | ||||||
| 
 |           'groups.color' | ||||||
|   def create |         ) | ||||||
|     ActiveRecord::Base.transaction do |         .group_by(&:first) | ||||||
|       tables_arrangement = TablesArrangement.create!(status: :not_started) |         .transform_values { |table| table.map { |(_, name, id, color)| { id:, name:, color: } } } | ||||||
|       TableSimulatorJob.perform_later(current_tenant.id, tables_arrangement.id) |         .map { |number, guests| { number:, guests: } } | ||||||
|     end |         .then { |result| render json: result } | ||||||
| 
 |  | ||||||
|     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 | ||||||
|  | |||||||
| @ -1,12 +0,0 @@ | |||||||
| # 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 |  | ||||||
| @ -1,27 +1,23 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # frozen_string_literal: true | class Users::ConfirmationsController < Devise::ConfirmationsController | ||||||
|  |   clear_respond_to | ||||||
|  |   respond_to :json | ||||||
| 
 | 
 | ||||||
| module Users |   def show | ||||||
|   class ConfirmationsController < Devise::ConfirmationsController |     super do |resource| | ||||||
|     clear_respond_to |       if resource.errors.empty? | ||||||
|     respond_to :json |         respond_to do |format|   | ||||||
| 
 |           format.json { render json: resource, status: :ok } | ||||||
|     def show |           format.any { redirect_to root_path } | ||||||
|       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 |         end | ||||||
|         return |       else | ||||||
|  |         render json: { | ||||||
|  |           message: 'Record invalid', | ||||||
|  |           errors: resource.errors.full_messages | ||||||
|  |         }, status: :unprocessable_entity | ||||||
|       end |       end | ||||||
|  |       return | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
| @ -1,32 +1,28 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # frozen_string_literal: true | class Users::RegistrationsController < Devise::RegistrationsController | ||||||
|  |   clear_respond_to  | ||||||
|  |   respond_to :json | ||||||
| 
 | 
 | ||||||
| module Users |   before_action :validate_captcha!, only: :create | ||||||
|   class RegistrationsController < Devise::RegistrationsController |  | ||||||
|     clear_respond_to |  | ||||||
|     respond_to :json |  | ||||||
| 
 | 
 | ||||||
|     before_action :validate_captcha!, only: :create |   def create | ||||||
| 
 |     wedding = Wedding.create(wedding_params) | ||||||
|     def create |     unless wedding.persisted? | ||||||
|       wedding = Wedding.create(slug: params[:slug]) |       render json: { errors: wedding.errors.full_messages }, status: :unprocessable_entity | ||||||
|       unless wedding.persisted? |       return | ||||||
|         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 |     end | ||||||
| 
 | 
 | ||||||
|     private |     ActsAsTenant.with_tenant(wedding) do | ||||||
| 
 |       super do |user| | ||||||
|     def set_tenant |         wedding.destroy unless user.persisted? | ||||||
|       set_current_tenant(nil) |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def wedding_params | ||||||
|  |     { slug: params[:slug], **params.expect(wedding: :date) } | ||||||
|  |   end | ||||||
| end | end | ||||||
| @ -1,10 +1,6 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # frozen_string_literal: true | class Users::SessionsController < Devise::SessionsController | ||||||
| 
 |   clear_respond_to | ||||||
| module Users |   respond_to :json | ||||||
|   class SessionsController < Devise::SessionsController |  | ||||||
|     clear_respond_to |  | ||||||
|     respond_to :json |  | ||||||
|   end |  | ||||||
| end | end | ||||||
| @ -1,25 +0,0 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors |  | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| class WebsitesController < ApplicationController |  | ||||||
|   skip_before_action :authenticate_user!, only: :show |  | ||||||
| 
 |  | ||||||
|   def show |  | ||||||
|     render json: current_tenant.website.as_json(only: %i[content]) || {}, status: :ok |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def update |  | ||||||
|     ActiveRecord::Base.transaction do |  | ||||||
|       website = current_tenant.website || current_tenant.create_website |  | ||||||
|       website.update!(website_params) |  | ||||||
|       render json: website.as_json(only: %i[content]), status: :ok |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   private |  | ||||||
| 
 |  | ||||||
|   def website_params |  | ||||||
|     params.expect(website: [:content]) |  | ||||||
|   end |  | ||||||
| end |  | ||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module TreeNodeExtension | module TreeNodeExtension | ||||||
|   def distance_to_common_ancestor(another_node) |   def distance_to_common_ancestor(another_node) | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module ApplicationHelper | module ApplicationHelper | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module ExpensesHelper | module ExpensesHelper | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module GroupsHelper | module GroupsHelper | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module GuestsHelper | module GuestsHelper | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module TablesArrangementsHelper | module TablesArrangementsHelper | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # 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 | ||||||
|  | |||||||
| @ -1,42 +1,18 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class TableSimulatorJob < ApplicationJob | class TableSimulatorJob < ApplicationJob | ||||||
|   queue_as :default |   queue_as :default | ||||||
| 
 | 
 | ||||||
|   MIN_PER_TABLE = 8 |   def perform(wedding_id) | ||||||
|   MAX_PER_TABLE = 10 |  | ||||||
| 
 |  | ||||||
|   def perform(wedding_id, tables_arrangement_id) # rubocop:disable Metrics/MethodLength |  | ||||||
|     Rails.logger.info "Starting table simulation #{tables_arrangement_id} for wedding #{wedding_id}" |  | ||||||
|     ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do |     ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do | ||||||
|       engine = VNS::Engine.new |       engine = VNS::Engine.new | ||||||
| 
 | 
 | ||||||
|       engine.add_optimization(Tables::Swap) |       engine.add_perturbation(Tables::Swap) | ||||||
|       engine.add_optimization(Tables::Shift) |       engine.add_perturbation(Tables::Shift) | ||||||
| 
 |  | ||||||
|       tables_arrangement = TablesArrangement.find(tables_arrangement_id) |  | ||||||
| 
 |  | ||||||
|       initial_solution = Tables::Distribution.new( |  | ||||||
|         min_per_table: MIN_PER_TABLE, |  | ||||||
|         max_per_table: MAX_PER_TABLE, |  | ||||||
|         tables_arrangement_id: |  | ||||||
|       ) |  | ||||||
| 
 | 
 | ||||||
|  |       initial_solution = Tables::Distribution.new(min_per_table: 8, max_per_table: 10) | ||||||
|       initial_solution.random_distribution(Guest.potential.shuffle) |       initial_solution.random_distribution(Guest.potential.shuffle) | ||||||
| 
 | 
 | ||||||
|       initial_solution.save! |  | ||||||
| 
 |  | ||||||
|       engine.notify_progress do |current_progress| |  | ||||||
|         tables_arrangement.update_columns(status: :in_progress, progress: current_progress) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       engine.on_better_solution do |better_solution| |  | ||||||
|         better_solution.save! |  | ||||||
|         tables_arrangement.update_columns(discomfort: better_solution.discomfort) # TODO: remove? |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       engine.initial_solution = initial_solution |       engine.initial_solution = initial_solution | ||||||
| 
 | 
 | ||||||
|       engine.target_function(&:discomfort) |       engine.target_function(&:discomfort) | ||||||
| @ -44,8 +20,6 @@ class TableSimulatorJob < ApplicationJob | |||||||
|       best_solution = engine.run |       best_solution = engine.run | ||||||
| 
 | 
 | ||||||
|       best_solution.save! |       best_solution.save! | ||||||
| 
 |  | ||||||
|       tables_arrangement.update_columns(status: :completed) |  | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,45 +0,0 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors |  | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| class AdminMailer < ApplicationMailer |  | ||||||
|   def attendance_change_email |  | ||||||
|     @guest = Guest.find(params[:guest_id]) |  | ||||||
|     ActsAsTenant.with_tenant(@guest.wedding) do |  | ||||||
|       mail( |  | ||||||
|         to: recipients, |  | ||||||
|         subject: I18n.t( |  | ||||||
|           'admin_mailer.attendance_change_email.subject', |  | ||||||
|           name: @guest.name, |  | ||||||
|           status: I18n.t("active_record.attributes.guest/status.#{@guest.status}") |  | ||||||
|         ) |  | ||||||
|       ) |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def invitations_pdf_email |  | ||||||
|     ActsAsTenant.with_tenant(Wedding.find(params[:wedding_id])) do |  | ||||||
|       invitations = Invitation.includes(:guests).all |  | ||||||
| 
 |  | ||||||
|       pdf_html = ActionController::Base.new.render_to_string( |  | ||||||
|         template: 'invitations/sheet', |  | ||||||
|         layout: 'pdf', |  | ||||||
|         locals: { invitations: } |  | ||||||
|       ) |  | ||||||
|       pdf = WickedPdf.new.pdf_from_string(pdf_html) |  | ||||||
| 
 |  | ||||||
|       attachments["invitations_#{Time.current.strftime('%Y%m%d_%H%M%S')}.pdf"] = pdf |  | ||||||
| 
 |  | ||||||
|       mail( |  | ||||||
|         to: recipients, |  | ||||||
|         subject: I18n.t('admin_mailer.invitations_pdf_email.subject') |  | ||||||
|       ) |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   private |  | ||||||
| 
 |  | ||||||
|   def recipients |  | ||||||
|     ActsAsTenant.current_tenant.users.pluck(:email) |  | ||||||
|   end |  | ||||||
| end |  | ||||||
| @ -1,18 +1,6 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class ApplicationMailer < ActionMailer::Base | class ApplicationMailer < ActionMailer::Base | ||||||
|   class << self |   default from: "from@example.com" | ||||||
|     private |   layout "mailer" | ||||||
| 
 |  | ||||||
|     def default_from |  | ||||||
|       File.read('/run/secrets/smtp_user_name').strip |  | ||||||
|     rescue Errno::ENOENT |  | ||||||
|       'development@example.com' |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   default from: default_from |  | ||||||
|   layout 'mailer' |  | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class ApplicationRecord < ActiveRecord::Base | class ApplicationRecord < ActiveRecord::Base | ||||||
|   primary_abstract_class |   primary_abstract_class | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| # == Schema Information | # == Schema Information | ||||||
| # | # | ||||||
| @ -20,7 +18,7 @@ | |||||||
| # | # | ||||||
| # Foreign Keys | # Foreign Keys | ||||||
| # | # | ||||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | #  fk_rails_...  (wedding_id => weddings.id) | ||||||
| # | # | ||||||
| class Expense < ApplicationRecord | class Expense < ApplicationRecord | ||||||
|   acts_as_tenant :wedding |   acts_as_tenant :wedding | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| # == Schema Information | # == Schema Information | ||||||
| # | # | ||||||
| @ -25,7 +23,7 @@ | |||||||
| # Foreign Keys | # Foreign Keys | ||||||
| # | # | ||||||
| #  fk_rails_...  (parent_id => groups.id) | #  fk_rails_...  (parent_id => groups.id) | ||||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | #  fk_rails_...  (wedding_id => weddings.id) | ||||||
| # | # | ||||||
| class Group < ApplicationRecord | class Group < ApplicationRecord | ||||||
|   acts_as_tenant :wedding |   acts_as_tenant :wedding | ||||||
| @ -33,17 +31,20 @@ class Group < ApplicationRecord | |||||||
|   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', dependent: :nullify, inverse_of: :parent |   has_many :children, class_name: 'Group', foreign_key: 'parent_id' | ||||||
|   belongs_to :parent, class_name: 'Group', optional: true |   belongs_to :parent, class_name: 'Group', optional: true | ||||||
| 
 | 
 | ||||||
|   before_create :set_color |   before_create :set_color | ||||||
| 
 | 
 | ||||||
|   scope :roots, -> { where(parent_id: nil) } |   scope :roots, -> { where(parent_id: nil) } | ||||||
| 
 | 
 | ||||||
|   has_many :guests, dependent: :nullify |   has_many :guests | ||||||
| 
 | 
 | ||||||
|   def colorize_children(generation = 1) |   def colorize_children(generation = 1) | ||||||
|     children.zip(palette(generation)) do |child, raw_color| |     derived_colors = generation == 1 ? color.paint.palette.analogous(size: children.count) : color.paint.palette.decreasing_saturation | ||||||
|  | 
 | ||||||
|  |     children.zip(derived_colors) do |child, raw_color| | ||||||
|  | 
 | ||||||
|       final_color = raw_color.paint |       final_color = raw_color.paint | ||||||
|       final_color.brighten(60) if final_color.dark? |       final_color.brighten(60) if final_color.dark? | ||||||
| 
 | 
 | ||||||
| @ -53,20 +54,8 @@ class Group < ApplicationRecord | |||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def affinities |  | ||||||
|     GroupAffinity.where(group_a_id: id).or(GroupAffinity.where(group_b_id: id)) |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   private |   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 |   def set_color | ||||||
|     return if color.present? |     return if color.present? | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,43 +0,0 @@ | |||||||
| # 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,37 +1,31 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| # == Schema Information | # == Schema Information | ||||||
| # | # | ||||||
| # Table name: guests | # Table name: guests | ||||||
| # | # | ||||||
| #  id            :uuid             not null, primary key | #  id         :uuid             not null, primary key | ||||||
| #  name          :string | #  name       :string | ||||||
| #  phone         :string | #  phone      :string | ||||||
| #  status        :integer          default("considered") | #  status     :integer          default("considered") | ||||||
| #  created_at    :datetime         not null | #  created_at :datetime         not null | ||||||
| #  updated_at    :datetime         not null | #  updated_at :datetime         not null | ||||||
| #  group_id      :uuid | #  group_id   :uuid             not null | ||||||
| #  invitation_id :uuid | #  wedding_id :uuid             not null | ||||||
| #  wedding_id    :uuid             not null |  | ||||||
| # | # | ||||||
| # Indexes | # Indexes | ||||||
| # | # | ||||||
| #  index_guests_on_group_id       (group_id) | #  index_guests_on_group_id    (group_id) | ||||||
| #  index_guests_on_invitation_id  (invitation_id) | #  index_guests_on_wedding_id  (wedding_id) | ||||||
| #  index_guests_on_wedding_id     (wedding_id) |  | ||||||
| # | # | ||||||
| # Foreign Keys | # Foreign Keys | ||||||
| # | # | ||||||
| #  fk_rails_...  (group_id => groups.id) | #  fk_rails_...  (group_id => groups.id) | ||||||
| #  fk_rails_...  (invitation_id => invitations.id) | #  fk_rails_...  (wedding_id => weddings.id) | ||||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade |  | ||||||
| # | # | ||||||
| class Guest < ApplicationRecord | class Guest < ApplicationRecord | ||||||
|   acts_as_tenant :wedding |   acts_as_tenant :wedding | ||||||
|   belongs_to :group, optional: true |   belongs_to :group | ||||||
|   belongs_to :invitation, optional: true |  | ||||||
| 
 | 
 | ||||||
|   enum :status, { |   enum :status, { | ||||||
|     considered: 0, |     considered: 0, | ||||||
| @ -45,5 +39,16 @@ class Guest < ApplicationRecord | |||||||
| 
 | 
 | ||||||
|   scope :potential, -> { where.not(status: %i[declined considered]) } |   scope :potential, -> { where.not(status: %i[declined considered]) } | ||||||
| 
 | 
 | ||||||
|  |   after_save :recalculate_simulations, if: :saved_change_to_status? | ||||||
|  |   after_destroy :recalculate_simulations | ||||||
|  | 
 | ||||||
|   has_many :seats, dependent: :delete_all |   has_many :seats, dependent: :delete_all | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def recalculate_simulations | ||||||
|  |     TablesArrangement.delete_all | ||||||
|  | 
 | ||||||
|  |     ActiveJob.perform_all_later(50.times.map { TableSimulatorJob.new(wedding_id) }) | ||||||
|  |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,29 +0,0 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors |  | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| # == Schema Information |  | ||||||
| # |  | ||||||
| # Table name: invitations |  | ||||||
| # |  | ||||||
| #  id         :uuid             not null, primary key |  | ||||||
| #  created_at :datetime         not null |  | ||||||
| #  updated_at :datetime         not null |  | ||||||
| #  wedding_id :uuid             not null |  | ||||||
| # |  | ||||||
| # Indexes |  | ||||||
| # |  | ||||||
| #  index_invitations_on_wedding_id  (wedding_id) |  | ||||||
| # |  | ||||||
| # Foreign Keys |  | ||||||
| # |  | ||||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade |  | ||||||
| # |  | ||||||
| class Invitation < ApplicationRecord |  | ||||||
|   acts_as_tenant :wedding |  | ||||||
|   has_many :guests, dependent: :nullify |  | ||||||
| 
 |  | ||||||
|   def url |  | ||||||
|     "#{Rails.application.routes.url_helpers.root_url(slug: ActsAsTenant.current_tenant.slug)}/site/invitation/#{id}" |  | ||||||
|   end |  | ||||||
| end |  | ||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| # == Schema Information | # == Schema Information | ||||||
| # | # | ||||||
| @ -24,10 +22,10 @@ | |||||||
| # | # | ||||||
| #  fk_rails_...  (guest_id => guests.id) | #  fk_rails_...  (guest_id => guests.id) | ||||||
| #  fk_rails_...  (tables_arrangement_id => tables_arrangements.id) ON DELETE => cascade | #  fk_rails_...  (tables_arrangement_id => tables_arrangements.id) ON DELETE => cascade | ||||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | #  fk_rails_...  (wedding_id => weddings.id) | ||||||
| # | # | ||||||
| class Seat < ApplicationRecord | class Seat < ApplicationRecord | ||||||
|   acts_as_tenant :wedding |   acts_as_tenant :wedding | ||||||
|   belongs_to :guest |   belongs_to :guest | ||||||
|   belongs_to :tables_arrangement |   belongs_to :table_arrangement | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,17 +1,12 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| # == Schema Information | # == Schema Information | ||||||
| # | # | ||||||
| # Table name: tables_arrangements | # Table name: tables_arrangements | ||||||
| # | # | ||||||
| #  id         :uuid             not null, primary key | #  id         :uuid             not null, primary key | ||||||
| #  digest     :uuid             not null |  | ||||||
| #  discomfort :integer | #  discomfort :integer | ||||||
| #  name       :string           not null | #  name       :string           not null | ||||||
| #  progress   :float            default(0.0), not null |  | ||||||
| #  status     :string           default("complete"), not null |  | ||||||
| #  created_at :datetime         not null | #  created_at :datetime         not null | ||||||
| #  updated_at :datetime         not null | #  updated_at :datetime         not null | ||||||
| #  wedding_id :uuid             not null | #  wedding_id :uuid             not null | ||||||
| @ -22,11 +17,11 @@ | |||||||
| # | # | ||||||
| # Foreign Keys | # Foreign Keys | ||||||
| # | # | ||||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | #  fk_rails_...  (wedding_id => weddings.id) | ||||||
| # | # | ||||||
| class TablesArrangement < ApplicationRecord | class TablesArrangement < ApplicationRecord | ||||||
|   acts_as_tenant :wedding |   acts_as_tenant :wedding | ||||||
|   has_many :seats, dependent: :delete_all |   has_many :seats | ||||||
|   has_many :guests, through: :seats |   has_many :guests, through: :seats | ||||||
| 
 | 
 | ||||||
|   before_create :assign_name |   before_create :assign_name | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| # == Schema Information | # == Schema Information | ||||||
| # | # | ||||||
| @ -32,7 +30,7 @@ | |||||||
| # | # | ||||||
| # Foreign Keys | # Foreign Keys | ||||||
| # | # | ||||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | #  fk_rails_...  (wedding_id => weddings.id) | ||||||
| # | # | ||||||
| class User < ApplicationRecord | class User < ApplicationRecord | ||||||
|   acts_as_tenant :wedding |   acts_as_tenant :wedding | ||||||
|  | |||||||
| @ -1,25 +0,0 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors |  | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| # == Schema Information |  | ||||||
| # |  | ||||||
| # Table name: websites |  | ||||||
| # |  | ||||||
| #  id         :bigint           not null, primary key |  | ||||||
| #  content    :text |  | ||||||
| #  created_at :datetime         not null |  | ||||||
| #  updated_at :datetime         not null |  | ||||||
| #  wedding_id :uuid             not null |  | ||||||
| # |  | ||||||
| # Indexes |  | ||||||
| # |  | ||||||
| #  index_websites_on_wedding_id  (wedding_id) |  | ||||||
| # |  | ||||||
| # Foreign Keys |  | ||||||
| # |  | ||||||
| #  fk_rails_...  (wedding_id => weddings.id) |  | ||||||
| # |  | ||||||
| class Website < ApplicationRecord |  | ||||||
|   belongs_to :wedding |  | ||||||
| end |  | ||||||
| @ -1,12 +1,11 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| # == Schema Information | # == Schema Information | ||||||
| # | # | ||||||
| # Table name: weddings | # Table name: weddings | ||||||
| # | # | ||||||
| #  id         :uuid             not null, primary key | #  id         :uuid             not null, primary key | ||||||
|  | #  date       :date             not null | ||||||
| #  slug       :string           not null | #  slug       :string           not null | ||||||
| #  created_at :datetime         not null | #  created_at :datetime         not null | ||||||
| #  updated_at :datetime         not null | #  updated_at :datetime         not null | ||||||
| @ -18,11 +17,6 @@ | |||||||
| class Wedding < ApplicationRecord | class Wedding < ApplicationRecord | ||||||
|   SLUG_REGEX = /[a-z\d-]+/ |   SLUG_REGEX = /[a-z\d-]+/ | ||||||
| 
 | 
 | ||||||
|  |   validates :date, presence: true | ||||||
|   validates :slug, presence: true, uniqueness: true, format: { with: /\A#{SLUG_REGEX}\z/ } |   validates :slug, presence: true, uniqueness: true, format: { with: /\A#{SLUG_REGEX}\z/ } | ||||||
| 
 |  | ||||||
|   has_many :guests, dependent: :delete_all |  | ||||||
|   has_many :groups, dependent: :delete_all |  | ||||||
|   has_many :invitations, dependent: :delete_all |  | ||||||
|   has_many :users, dependent: :delete_all |  | ||||||
|   has_one :website, dependent: :destroy |  | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,49 +1,47 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module Expenses | module Expenses | ||||||
|   class TotalQuery |   class TotalQuery | ||||||
|     private attr_reader :wedding |  | ||||||
|     def initialize(wedding:) |  | ||||||
|       @wedding = wedding |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def call |     def call | ||||||
|       ActiveRecord::Base.connection.execute( |       ActiveRecord::Base.connection.execute(query).first | ||||||
|         ActiveRecord::Base.sanitize_sql_array([query, { wedding_id: wedding.id }]) |  | ||||||
|       ).first |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|     def query |     def query | ||||||
|       <<~SQL.squish |       <<~SQL | ||||||
|         WITH guest_count AS (#{guest_count_per_status}), |         WITH guest_count AS (#{guest_count_per_status}), | ||||||
|              expense_summary AS (#{expense_summary}) |              expense_summary AS (#{expense_summary}) | ||||||
|         SELECT guest_count.confirmed as confirmed_guests, |         SELECT expense_summary.fixed, | ||||||
|                guest_count.projected as projected_guests, |                expense_summary.fixed_count, | ||||||
|                expense_summary.fixed + expense_summary.variable * guest_count.confirmed as total_confirmed, |                expense_summary.variable, | ||||||
|                expense_summary.fixed + expense_summary.variable * guest_count.projected as total_projected |                expense_summary.variable_count, | ||||||
|  |                expense_summary.total_count, | ||||||
|  |                guest_count.confirmed as confirmed_guests, | ||||||
|  |                 guest_count.projected as projected_guests, | ||||||
|  |                expense_summary.fixed + expense_summary.variable * guest_count.confirmed as total, | ||||||
|  |                expense_summary.fixed + expense_summary.variable * guest_count.projected as max_projected, | ||||||
|  |                (expense_summary.fixed + expense_summary.variable * guest_count.confirmed) / guest_count.confirmed as per_person | ||||||
|         FROM guest_count, expense_summary; |         FROM guest_count, expense_summary; | ||||||
|       SQL |       SQL | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def expense_summary |     def expense_summary | ||||||
|       <<~SQL.squish |       <<~SQL | ||||||
|         SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed, |         SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed, | ||||||
|                coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable |                coalesce(count(amount) filter (where pricing_type = 'fixed'), 0) as fixed_count, | ||||||
|  |                coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable, | ||||||
|  |                coalesce(count(amount) filter (where pricing_type = 'per_person'), 0) as variable_count, | ||||||
|  |                count(*) as total_count | ||||||
|         FROM expenses |         FROM expenses | ||||||
|         WHERE wedding_id = :wedding_id |  | ||||||
|       SQL |       SQL | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def guest_count_per_status |     def guest_count_per_status | ||||||
|       <<~SQL.squish |       <<~SQL | ||||||
|         SELECT COALESCE(count(*) filter(where status = #{Guest.statuses['confirmed']}), 0) as confirmed, |         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 |                COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at("confirmed", "invited", "tentative").join(",")})), 0) as projected | ||||||
|         FROM guests |         FROM guests | ||||||
|         WHERE wedding_id = :wedding_id |  | ||||||
|       SQL |       SQL | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | |||||||
| @ -1,31 +1,31 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module Groups | module Groups | ||||||
|   class SummaryQuery |   class SummaryQuery | ||||||
|     def call |     def call | ||||||
|       Group.left_joins(:guests).group(:id).pluck_to_hash( |       ActiveRecord::Base.connection.execute(query).to_a | ||||||
|         :id, |  | ||||||
|         :name, |  | ||||||
|         :icon, |  | ||||||
|         :parent_id, |  | ||||||
|         :color, |  | ||||||
|         *count_expressions |  | ||||||
|       ) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|     def count_expressions |     def query | ||||||
|       [ |       <<~SQL.squish | ||||||
|         Arel.sql('count(*) filter (where status IS NOT NULL) as total'), |         SELECT | ||||||
|         Arel.sql('count(*) filter (where status = 0) as considered'), |           groups.id, | ||||||
|         Arel.sql('count(*) filter (where status = 10) as invited'), |           groups.name, | ||||||
|         Arel.sql('count(*) filter (where status = 20) as confirmed'), |           groups.icon, | ||||||
|         Arel.sql('count(*) filter (where status = 30) as declined'), |           groups.parent_id, | ||||||
|         Arel.sql('count(*) filter (where status = 40) as tentative') |           groups.color, | ||||||
|       ] |           count(*) filter (where status IS NOT NULL) as total, | ||||||
|  |           count(*) filter (where status = 0) as considered, | ||||||
|  |           count(*) filter (where status = 10) as invited, | ||||||
|  |           count(*) filter (where status = 20) as confirmed, | ||||||
|  |           count(*) filter (where status = 30) as declined, | ||||||
|  |           count(*) filter (where status = 40) as tentative | ||||||
|  |         FROM groups | ||||||
|  |         LEFT JOIN guests on groups.id = guests.group_id | ||||||
|  |         GROUP BY groups.id | ||||||
|  |       SQL | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class SerializableGroup < JSONAPI::Serializable::Resource | class SerializableGroup < JSONAPI::Serializable::Resource | ||||||
|   type 'group' |   type 'group' | ||||||
|  | |||||||
| @ -1,16 +1,18 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class SerializableGuest < JSONAPI::Serializable::Resource | class SerializableGuest < JSONAPI::Serializable::Resource | ||||||
|   type 'guest' |   type 'guest' | ||||||
| 
 | 
 | ||||||
|   attributes :id, :status |   attributes :id, :group_id, :status | ||||||
| 
 | 
 | ||||||
|   attribute :name do |   attribute :name do | ||||||
|     @object.name |     @object.name | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   attribute :group_name do | ||||||
|  |     @object.group.name | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   attribute :status do |   attribute :status do | ||||||
|     @object.status.capitalize |     @object.status.capitalize | ||||||
|   end |   end | ||||||
|  | |||||||
| @ -1,9 +1,7 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class AffinityGroupsHierarchy < Array | class AffinityGroupsHierarchy < Array | ||||||
|   DEFAULT_DISCOMFORT = 1 |   include Singleton | ||||||
| 
 | 
 | ||||||
|   def initialize |   def initialize | ||||||
|     super |     super | ||||||
| @ -14,10 +12,6 @@ class AffinityGroupsHierarchy < Array | |||||||
| 
 | 
 | ||||||
|       hydrate(group) |       hydrate(group) | ||||||
|     end |     end | ||||||
| 
 |  | ||||||
|     discomforts |  | ||||||
|     invitation_counts |  | ||||||
|     freeze |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def find(id) |   def find(id) | ||||||
| @ -39,43 +33,8 @@ class AffinityGroupsHierarchy < Array | |||||||
|     @references[id_a].distance_to_common_ancestor(@references[id_b]) |     @references[id_a].distance_to_common_ancestor(@references[id_b]) | ||||||
|   end |   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 |  | ||||||
| 
 |  | ||||||
|   def guest_count(invitation_id) |  | ||||||
|     @invitation_counts[invitation_id] || 0 |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def invitation_counts |  | ||||||
|     @invitation_counts = Guest.where.not(invitation_id: nil).group(:invitation_id).count |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   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) |   def hydrate(group) | ||||||
|     group.children.each do |child| |     group.children.each do |child| | ||||||
|       register_child(group.id, child.id) |       register_child(group.id, child.id) | ||||||
|  | |||||||
| @ -1,20 +1,20 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| class LibreCaptcha | class LibreCaptcha | ||||||
|   def id |   def get_id | ||||||
|     HTTParty.post('http://libre-captcha:8888/v2/captcha', |     HTTParty.post("http://libre-captcha:8888/v2/captcha",  | ||||||
|                   body: { |       body: { | ||||||
|                     input_type: 'text', |         input_type: "text", | ||||||
|                     level: :hard, |         level: :hard, | ||||||
|                     media: 'image/png', |         media: 'image/png', | ||||||
|                     size: '350x100' |         size: '350x100' | ||||||
|                   }.to_json).then { |raw| JSON.parse(raw)['id'] } |       }.to_json | ||||||
|  |     ).then { |raw| JSON.parse(raw)['id'] } | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def valid?(id:, answer:) |   def valid?(id:, answer:) | ||||||
|     HTTParty.post('http://libre-captcha:8888/v2/answer', |     HTTParty.post("http://libre-captcha:8888/v2/answer", | ||||||
|                   body: { id:, answer: }.to_json).then { |raw| JSON.parse(raw)['result'] == 'True' } |       body: { id:, answer: }.to_json | ||||||
|  |     ).then { |raw| JSON.parse(raw)['result'] == 'True' } | ||||||
|   end |   end | ||||||
| end | end | ||||||
| @ -1,21 +1,14 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module Tables | module Tables | ||||||
|   class DiscomfortCalculator |   class DiscomfortCalculator | ||||||
|     private attr_reader :table, :hierarchy |     private attr_reader :table | ||||||
|     def initialize(table:, hierarchy: AffinityGroupsHierarchy.new) |     def initialize(table:) | ||||||
|       @table = table |       @table = table | ||||||
|       @hierarchy = hierarchy |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def calculate |     def calculate | ||||||
|       breakdown.values.sum |       table_size_penalty + 10 * (cohesion_penalty * 1.0 / table.size) | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def breakdown |  | ||||||
|       @breakdown ||= { table_size_penalty:, cohesion_penalty:, invitations_penalty: } |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     private |     private | ||||||
| @ -35,16 +28,6 @@ module Tables | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def cohesion_penalty |  | ||||||
|       10 * (cohesion_discomfort * 1.0 / table.size) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def invitations_penalty |  | ||||||
|       2 * table.map(&:invitation_id) |  | ||||||
|                .tally |  | ||||||
|                .sum { |invitation_id, guests_in_table| hierarchy.guest_count(invitation_id) - guests_in_table } |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     # |     # | ||||||
|     # Calculates the discomfort of the table based on the cohesion of the guests. The total discomfort |     # 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 |     # is calculated as the sum of the discomfort of each pair of guests. The discomfort of a pair of | ||||||
| @ -52,9 +35,14 @@ module Tables | |||||||
|     # |     # | ||||||
|     # @return [Number] Total discomfort of the table. |     # @return [Number] Total discomfort of the table. | ||||||
|     # |     # | ||||||
|     def cohesion_discomfort |     def cohesion_penalty | ||||||
|       table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)| |       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) |         distance = AffinityGroupsHierarchy.instance.distance(a, b) | ||||||
|  | 
 | ||||||
|  |         next count_a * count_b if distance.nil? | ||||||
|  |         next 0 if distance.zero? | ||||||
|  | 
 | ||||||
|  |         count_a * count_b * Rational(distance, distance + 1) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | |||||||
| @ -1,32 +1,21 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| require_relative '../../extensions/tree_node_extension' | require_relative '../../extensions/tree_node_extension' | ||||||
| 
 | 
 | ||||||
| module Tables | module Tables | ||||||
|   class Distribution |   class Distribution | ||||||
|     class << self |     attr_accessor :tables, :min_per_table, :max_per_table | ||||||
|       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, :tables_arrangement_id |     def initialize(min_per_table:, max_per_table:) | ||||||
| 
 |  | ||||||
|     def initialize(min_per_table:, max_per_table:, tables_arrangement_id:, hierarchy: AffinityGroupsHierarchy.new) |  | ||||||
|       @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 = hierarchy |  | ||||||
|       @tables = [] |       @tables = [] | ||||||
|       @tables_arrangement_id = tables_arrangement_id |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def random_distribution(people, random: Random.new) |     def random_distribution(people) | ||||||
|       min_tables = (people.count * 1.0 / @max_per_table).ceil |       min_tables = (people.count * 1.0 / @max_per_table).ceil | ||||||
|       max_tables = (people.count * 1.0 / @min_per_table).ceil |       max_tables = (people.count * 1.0 / @min_per_table).ceil | ||||||
|       table_size = random.rand(min_tables..max_tables) |       @tables = people.in_groups(rand(min_tables..max_tables), false) | ||||||
|       @tables = people.in_groups(table_size, false) |  | ||||||
|                       .map { |group| Table.new(group) } |                       .map { |group| Table.new(group) } | ||||||
|                       .each { |table| table.min_per_table = @min_per_table } |                       .each { |table| table.min_per_table = @min_per_table } | ||||||
|                       .each { |table| table.max_per_table = @max_per_table } |                       .each { |table| table.max_per_table = @max_per_table } | ||||||
| @ -42,24 +31,21 @@ 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(&:name).join(', ')}" | ||||||
|  |       end.join("\n") | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     def deep_dup |     def deep_dup | ||||||
|       self.class.new( |       self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table).tap do |new_distribution| | ||||||
|         min_per_table: @min_per_table, |  | ||||||
|         max_per_table: @max_per_table, |  | ||||||
|         hierarchy: @hierarchy, |  | ||||||
|         tables_arrangement_id: @tables_arrangement_id |  | ||||||
|       ).tap do |new_distribution| |  | ||||||
|         new_distribution.tables = @tables.map(&:dup) |         new_distribution.tables = @tables.map(&:dup) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def save! |     def save! | ||||||
|       ActiveRecord::Base.transaction do |       ActiveRecord::Base.transaction do | ||||||
|         arrangement = TablesArrangement.find(tables_arrangement_id) |         arrangement = TablesArrangement.create! | ||||||
| 
 |  | ||||||
|         self.tables_arrangement_id = arrangement.id |  | ||||||
| 
 |  | ||||||
|         arrangement.seats.delete_all |  | ||||||
| 
 | 
 | ||||||
|         records_to_store = [] |         records_to_store = [] | ||||||
| 
 | 
 | ||||||
| @ -71,17 +57,14 @@ module Tables | |||||||
| 
 | 
 | ||||||
|         Seat.insert_all!(records_to_store) |         Seat.insert_all!(records_to_store) | ||||||
| 
 | 
 | ||||||
|         arrangement.update!( |         arrangement.update!(discomfort:) | ||||||
|           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:, hierarchy:).calculate |       table.discomfort ||= DiscomfortCalculator.new(table:).calculate | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module Tables | module Tables | ||||||
|   class Shift |   class Shift | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module Tables | module Tables | ||||||
|   class Swap |   class Swap | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module Tables | module Tables | ||||||
|   class Table < Set |   class Table < Set | ||||||
|  | |||||||
| @ -1,33 +0,0 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors |  | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| module Tables |  | ||||||
|   class WheelSwap |  | ||||||
|     private attr_reader :initial_solution |  | ||||||
|     def initialize(initial_solution) |  | ||||||
|       @initial_solution = initial_solution |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def call(size = 1) |  | ||||||
|       Rails.logger.debug { "WheelSwap with size: #{size}" } |  | ||||||
|       new_solution = @initial_solution.deep_dup |  | ||||||
| 
 |  | ||||||
|       selected_guests = [] |  | ||||||
| 
 |  | ||||||
|       size.times do |  | ||||||
|         selected_guests += new_solution.tables.map(&:pop) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       selected_guests.shuffle! |  | ||||||
| 
 |  | ||||||
|       tables = new_solution.tables.cycle |  | ||||||
| 
 |  | ||||||
|       tables.next << selected_guests.pop while selected_guests.any? |  | ||||||
| 
 |  | ||||||
|       new_solution.tables.each(&:reset) |  | ||||||
| 
 |  | ||||||
|       new_solution |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
| @ -1,11 +1,7 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 |  | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 | 
 | ||||||
| module VNS | module VNS | ||||||
|   class Engine |   class Engine | ||||||
|     PERTURBATION_SIZES = [1, 1, 1, 2, 2, 3].freeze |  | ||||||
|     ITERATIONS = 50 |  | ||||||
|     class << self |     class << self | ||||||
|       def sequence(elements) |       def sequence(elements) | ||||||
|         elements = elements.to_a |         elements = elements.to_a | ||||||
| @ -13,100 +9,43 @@ module VNS | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def initialize |  | ||||||
|       @perturbations = Set.new |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def target_function(&function) |     def target_function(&function) | ||||||
|       @target_function = function |       @target_function = function | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def add_optimization(klass) |  | ||||||
|       @optimizations ||= Set.new |  | ||||||
|       @optimizations << klass |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def add_perturbation(klass) |     def add_perturbation(klass) | ||||||
|  |       @perturbations ||= Set.new | ||||||
|       @perturbations << klass |       @perturbations << klass | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def notify_progress(&block) |  | ||||||
|       @progress_notifier = block |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def on_better_solution(&block) |  | ||||||
|       @better_solution_notifier = block |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     attr_writer :initial_solution |     attr_writer :initial_solution | ||||||
| 
 | 
 | ||||||
|     def run |     def run | ||||||
|       check_preconditions! |       raise 'No target function defined' unless @target_function | ||||||
|  |       raise 'No perturbations defined' unless @perturbations | ||||||
|  |       raise 'No initial solution defined' unless @initial_solution | ||||||
| 
 | 
 | ||||||
|       @current_solution = @initial_solution |       @best_solution = @initial_solution | ||||||
|       @best_score = @target_function.call(@current_solution) |       @best_score = @target_function.call(@best_solution) | ||||||
| 
 | 
 | ||||||
|       run_all_optimizations |       self.class.sequence(@perturbations).each do |perturbation| | ||||||
| 
 |         optimize(perturbation.new(@best_solution)) | ||||||
|       @progress_notifier&.call(Rational(1, ITERATIONS + 1)) |  | ||||||
| 
 |  | ||||||
|       best_solution = @current_solution |  | ||||||
| 
 |  | ||||||
|       (1..ITERATIONS).each do |iteration| |  | ||||||
|         @current_solution = Tables::WheelSwap.new(best_solution).call(PERTURBATION_SIZES.sample) |  | ||||||
|         @best_score = @target_function.call(@current_solution) |  | ||||||
|         Rails.logger.debug { "After perturbation: #{@best_score}" } |  | ||||||
| 
 |  | ||||||
|         run_all_optimizations |  | ||||||
| 
 |  | ||||||
|         @progress_notifier&.call(Rational(iteration + 1, ITERATIONS + 1)) |  | ||||||
| 
 |  | ||||||
|         next unless best_solution.discomfort > @current_solution.discomfort |  | ||||||
| 
 |  | ||||||
|         best_solution = @current_solution |  | ||||||
|         @better_solution_notifier&.call(best_solution) |  | ||||||
| 
 |  | ||||||
|         Rails.logger.debug do |  | ||||||
|           "Found better solution after perturbation optimization: #{@current_solution.discomfort}" |  | ||||||
|         end |  | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       best_solution |       @best_solution | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|     def check_preconditions! |     def optimize(perturbation) | ||||||
|       raise 'No target function defined' unless @target_function |       perturbation.each do |alternative_solution| | ||||||
|       raise 'No optimizations defined' unless @optimizations |         score = @target_function.call(alternative_solution) | ||||||
|       raise 'No initial solution defined' unless @initial_solution |         next if score >= @best_score | ||||||
|     end |  | ||||||
| 
 | 
 | ||||||
|     def run_all_optimizations |         @best_solution = alternative_solution.deep_dup | ||||||
|       self.class.sequence(@optimizations).each do |optimization| |         @best_score = score | ||||||
|         optimize(optimization) |  | ||||||
|         Rails.logger.debug { "Finished optimization phase: #{optimization}" } |  | ||||||
|       end |  | ||||||
|       Rails.logger.debug { 'Finished all optimization phases' } |  | ||||||
|     end |  | ||||||
| 
 | 
 | ||||||
|     def optimize(optimization_klass) |         return optimize(perturbation.class.new(@best_solution)) | ||||||
|       loop do |  | ||||||
|         optimized = false |  | ||||||
| 
 |  | ||||||
|         optimization_klass.new(@current_solution).each do |alternative_solution| |  | ||||||
|           score = @target_function.call(alternative_solution) |  | ||||||
|           next if score >= @best_score |  | ||||||
| 
 |  | ||||||
|           @current_solution = alternative_solution.deep_dup |  | ||||||
|           @best_score = score |  | ||||||
|           optimized = true |  | ||||||
|           Rails.logger.debug { "[#{optimization_klass}] Found better solution with score: #{score}" } |  | ||||||
| 
 |  | ||||||
|           break |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         return unless optimized |  | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | |||||||
| @ -1,17 +0,0 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> |  | ||||||
| 
 |  | ||||||
| <p><%= I18n.t('admin_mailer.greeting') %>,</p> |  | ||||||
| 
 |  | ||||||
| <p> |  | ||||||
|   <%= I18n.t('admin_mailer.attendance_change_email.paragraph_1', name: @guest.name) %> |  | ||||||
| </p> |  | ||||||
| 
 |  | ||||||
| <ul> |  | ||||||
|   <li> |  | ||||||
|     <strong><%= I18n.t("active_record.attributes.guest.status") %>:</strong> <%= I18n.t("active_record.attributes.guest/status.#{@guest.status}") %> |  | ||||||
|   </li> |  | ||||||
| </ul> |  | ||||||
| 
 |  | ||||||
| <p> |  | ||||||
|   <%= I18n.t("admin_mailer.attendance_change_email.notify_on_updates") %> |  | ||||||
| </p> |  | ||||||
| @ -1,9 +0,0 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> |  | ||||||
| 
 |  | ||||||
| <%= I18n.t('admin_mailer.greeting') %>, |  | ||||||
| 
 |  | ||||||
| <%= I18n.t('admin_mailer.attendance_change_email.paragraph_1', name: @guest.name) %> |  | ||||||
| 
 |  | ||||||
| - <%= I18n.t("active_record.attributes.guest.status") %>: <%= I18n.t("active_record.attributes.guest/status.#{@guest.status}") %> |  | ||||||
| 
 |  | ||||||
| <%= I18n.t("admin_mailer.attendance_change_email.notify_on_updates") %> |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> |  | ||||||
| 
 |  | ||||||
| <p><%= I18n.t('admin_mailer.greeting') %>,</p> |  | ||||||
| 
 |  | ||||||
| <p> |  | ||||||
|   <%= I18n.t('admin_mailer.invitations_pdf_email.paragraph_1') %> |  | ||||||
| </p> |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> |  | ||||||
| 
 |  | ||||||
| <%= I18n.t('admin_mailer.greeting') %>, |  | ||||||
| 
 |  | ||||||
| <%= I18n.t('admin_mailer.invitations_pdf_email.paragraph_1') %> |  | ||||||
| @ -1,33 +0,0 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> |  | ||||||
| 
 |  | ||||||
| <% invitations.each_slice(4) do |invitation_group| %> |  | ||||||
|   <table style="width: 100%; border-collapse: separate; border-spacing: 0 20px; margin-bottom: 40px;"> |  | ||||||
|     <% invitation_group.each do |invitation| %> |  | ||||||
|       <tr> |  | ||||||
|         <td style="width: 270px; height: 270px; text-align: center; vertical-align: middle; padding: 10px;"> |  | ||||||
|           <%= image_tag(RQRCode::QRCode.new(invitation.url).as_png( |  | ||||||
|                 bit_depth: 1, |  | ||||||
|                 border_modules: 4, |  | ||||||
|                 color_mode: ChunkyPNG::COLOR_GRAYSCALE, |  | ||||||
|                 color: "black", |  | ||||||
|                 file: nil, |  | ||||||
|                 fill: "white", |  | ||||||
|                 module_px_size: 6, |  | ||||||
|                 resize_exactly_to: false, |  | ||||||
|                 resize_gte_to: false, |  | ||||||
|                 size: 250 |  | ||||||
|               ).to_data_url) |  | ||||||
|           %> |  | ||||||
|         </td> |  | ||||||
|         <td style="vertical-align: middle; padding: 10px;"> |  | ||||||
|           <ul style="margin: 0; padding-left: 20px;"> |  | ||||||
|             <% invitation.guests.each do |guest| %> |  | ||||||
|               <%= content_tag(:li, guest.name) %> |  | ||||||
|             <% end %> |  | ||||||
|           </ul> |  | ||||||
|         </td> |  | ||||||
|       </tr> |  | ||||||
|     <% end %> |  | ||||||
|   </table> |  | ||||||
|   <div style="page-break-after: always;"></div> |  | ||||||
| <% end %> |  | ||||||
| @ -1,4 +1,4 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | <%# Copyright (C) 2024 Manuel Bustillo %> | ||||||
| 
 | 
 | ||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html> | <html> | ||||||
|  | |||||||
| @ -1,3 +1,3 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | <%# Copyright (C) 2024 Manuel Bustillo %> | ||||||
| 
 | 
 | ||||||
| <%= yield %> | <%= yield %> | ||||||
|  | |||||||
| @ -1,12 +0,0 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> |  | ||||||
| 
 |  | ||||||
| <!doctype html> |  | ||||||
| <html> |  | ||||||
|   <head> |  | ||||||
|     <meta charset='utf-8' /> |  | ||||||
|   </head> |  | ||||||
|     <div id="content"> |  | ||||||
|       <%= yield %> |  | ||||||
|     </div> |  | ||||||
|   </body> |  | ||||||
| </html> |  | ||||||
| @ -1,4 +1,4 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | <%# Copyright (C) 2024 Manuel Bustillo %> | ||||||
| 
 | 
 | ||||||
| <p>Welcome <%= @email %>!</p> | <p>Welcome <%= @email %>!</p> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | <%# Copyright (C) 2024 Manuel Bustillo %> | ||||||
| 
 | 
 | ||||||
| <p>Hello <%= @email %>!</p> | <p>Hello <%= @email %>!</p> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | <%# Copyright (C) 2024 Manuel Bustillo %> | ||||||
| 
 | 
 | ||||||
| <p>Hello <%= @resource.email %>!</p> | <p>Hello <%= @resource.email %>!</p> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | <%# Copyright (C) 2024 Manuel Bustillo %> | ||||||
| 
 | 
 | ||||||
| <p>Hello <%= @resource.email %>!</p> | <p>Hello <%= @resource.email %>!</p> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | <%# Copyright (C) 2024 Manuel Bustillo %> | ||||||
| 
 | 
 | ||||||
| <p>Hello <%= @resource.email %>!</p> | <p>Hello <%= @resource.email %>!</p> | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								bin/jobs
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								bin/jobs
									
									
									
									
									
								
							| @ -3,5 +3,4 @@ | |||||||
| require_relative "../config/environment" | require_relative "../config/environment" | ||||||
| require "solid_queue/cli" | require "solid_queue/cli" | ||||||
| 
 | 
 | ||||||
| SolidQueue.logger = ActiveSupport::Logger.new($stdout) |  | ||||||
| SolidQueue::Cli.start(ARGV) | SolidQueue::Cli.start(ARGV) | ||||||
|  | |||||||
| @ -1,8 +1,6 @@ | |||||||
| # 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,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| require_relative 'boot' | require_relative 'boot' | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # Load the Rails application. | # Load the Rails application. | ||||||
| require_relative "application" | require_relative "application" | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| require "active_support/core_ext/integer/time" | require "active_support/core_ext/integer/time" | ||||||
| 
 | 
 | ||||||
| @ -79,5 +79,4 @@ Rails.application.configure do | |||||||
|   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" |   config.hosts << "libre-wedding-planner.app.localhost" | ||||||
|   Rails.application.routes.default_url_options[:host] = "libre-wedding-planner.app.localhost" |  | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| require "active_support/core_ext/integer/time" | require "active_support/core_ext/integer/time" | ||||||
| 
 | 
 | ||||||
| @ -72,20 +72,6 @@ Rails.application.configure do | |||||||
|   # 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 | ||||||
|   config.action_mailer.delivery_method = :smtp |  | ||||||
|   config.action_mailer.smtp_settings = begin |  | ||||||
|     { |  | ||||||
|       address: File.read("/run/secrets/smtp_address").strip, |  | ||||||
|       port: File.read("/run/secrets/smtp_port").strip.to_i, |  | ||||||
|       user_name: File.read("/run/secrets/smtp_user_name").strip, |  | ||||||
|       password: File.read("/run/secrets/smtp_password").strip, |  | ||||||
|       authentication: File.read("/run/secrets/smtp_authentication").strip.to_sym, |  | ||||||
|       tls: true |  | ||||||
|     } |  | ||||||
|   rescue Errno::ENOENT |  | ||||||
|     {} |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|   # Ignore bad email addresses and do not raise email delivery errors. |   # Ignore bad email addresses and do not raise email delivery errors. | ||||||
|   # Set this to true and configure the email server for immediate delivery to raise delivery errors. |   # Set this to true and configure the email server for immediate delivery to raise delivery errors. | ||||||
| @ -106,10 +92,6 @@ 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" |  | ||||||
|   Rails.application.routes.default_url_options[:host] = "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,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| require "active_support/core_ext/integer/time" | require "active_support/core_ext/integer/time" | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # Pin npm packages by running ./bin/importmap | # Pin npm packages by running ./bin/importmap | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,9 +1,5 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| ActsAsTenant.configure do |config| | ActsAsTenant.configure do |config| | ||||||
|   config.require_tenant = !Rails.env.test? |   config.require_tenant = !Rails.env.test? | ||||||
| end | end | ||||||
| 
 |  | ||||||
| Rails.application.console do |  | ||||||
|   ActsAsTenant.current_tenant = Wedding.first |  | ||||||
| end |  | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # Be sure to restart your server when you modify this file. | # Be sure to restart your server when you modify this file. | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| Chroma.define_palette :decreasing_saturation do | Chroma.define_palette :decreasing_saturation do | ||||||
|   spin(20).desaturate(40) |   spin(20).desaturate(40) | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # Be sure to restart your server when you modify this file. | # Be sure to restart your server when you modify this file. | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # config/initializers/cors.rb | # config/initializers/cors.rb | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # Be sure to restart your server when you modify this file. | # Be sure to restart your server when you modify this file. | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # Be sure to restart your server when you modify this file. | # Be sure to restart your server when you modify this file. | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| # Be sure to restart your server when you modify this file. | # Be sure to restart your server when you modify this file. | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| Rswag::Api.configure do |c| | Rswag::Api.configure do |c| | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| Rswag::Ui.configure do |c| | Rswag::Ui.configure do |c| | ||||||
| 
 | 
 | ||||||
| @ -10,7 +10,7 @@ Rswag::Ui.configure do |c| | |||||||
|   # (under openapi_root) as JSON or YAML endpoints, then the list below should |   # (under openapi_root) as JSON or YAML endpoints, then the list below should | ||||||
|   # correspond to the relative paths for those endpoints. |   # correspond to the relative paths for those endpoints. | ||||||
| 
 | 
 | ||||||
|   c.openapi_endpoint '/api/api-docs/v1/swagger.yaml', 'API V1 Docs' |   c.swagger_endpoint '/api/api-docs/v1/swagger.yaml', 'API V1 Docs' | ||||||
| 
 | 
 | ||||||
|   # Add Basic Auth in case your API is private |   # Add Basic Auth in case your API is private | ||||||
|   # c.basic_auth_enabled = true |   # c.basic_auth_enabled = true | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | # Copyright (C) 2024 Manuel Bustillo | ||||||
| 
 | 
 | ||||||
| class Numeric | class Numeric | ||||||
|   def to_currency |   def to_currency | ||||||
| @ -10,10 +10,4 @@ class Set | |||||||
|   def to_table |   def to_table | ||||||
|     Tables::Table.new(self) |     Tables::Table.new(self) | ||||||
|   end |   end | ||||||
| 
 |  | ||||||
|   def pop |  | ||||||
|     element = self.to_a.sample |  | ||||||
|     self.delete(element) |  | ||||||
|     element |  | ||||||
|   end |  | ||||||
| end | end | ||||||
| @ -1,32 +0,0 @@ | |||||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors |  | ||||||
| 
 |  | ||||||
| # WickedPDF Global Configuration |  | ||||||
| # |  | ||||||
| # Use this to set up shared configuration options for your entire application. |  | ||||||
| # Any of the configuration options shown here can also be applied to single |  | ||||||
| # models by passing arguments to the `render :pdf` call. |  | ||||||
| # |  | ||||||
| # To learn more, check out the README: |  | ||||||
| # |  | ||||||
| # https://github.com/mileszs/wicked_pdf/blob/master/README.md |  | ||||||
| 
 |  | ||||||
| WickedPdf.configure do |config| |  | ||||||
|   # Path to the wkhtmltopdf executable: This usually isn't needed if using |  | ||||||
|   # one of the wkhtmltopdf-binary family of gems. |  | ||||||
|   # config.exe_path = '/usr/local/bin/wkhtmltopdf' |  | ||||||
|   #   or |  | ||||||
|   # config.exe_path = Gem.bin_path('wkhtmltopdf-binary', 'wkhtmltopdf') |  | ||||||
| 
 |  | ||||||
|   # Needed for wkhtmltopdf 0.12.6+ to use many wicked_pdf asset helpers |  | ||||||
|   # config.enable_local_file_access = true |  | ||||||
| 
 |  | ||||||
|   # Layout file to be used for all PDFs |  | ||||||
|   # (but can be overridden in `render :pdf` calls) |  | ||||||
|   # config.layout = 'pdf.html' |  | ||||||
| 
 |  | ||||||
|   # Using wkhtmltopdf without an X server can be achieved by enabling the |  | ||||||
|   # 'use_xvfb' flag. This will wrap all wkhtmltopdf commands around the |  | ||||||
|   # 'xvfb-run' command, in order to simulate an X server. |  | ||||||
|   # |  | ||||||
|   # config.use_xvfb = true |  | ||||||
| end |  | ||||||
| @ -1,22 +1,31 @@ | |||||||
|  | # Files in the config/locales directory are used for internationalization and | ||||||
|  | # are automatically loaded by Rails. If you want to use locales other than | ||||||
|  | # English, add the necessary files in this directory. | ||||||
|  | # | ||||||
|  | # To use the locales, use `I18n.t`: | ||||||
|  | # | ||||||
|  | #     I18n.t "hello" | ||||||
|  | # | ||||||
|  | # In views, this is aliased to just `t`: | ||||||
|  | # | ||||||
|  | #     <%= t("hello") %> | ||||||
|  | # | ||||||
|  | # To use a different locale, set it with `I18n.locale`: | ||||||
|  | # | ||||||
|  | #     I18n.locale = :es | ||||||
|  | # | ||||||
|  | # This would use the information in config/locales/es.yml. | ||||||
|  | # | ||||||
|  | # To learn more about the API, please read the Rails Internationalization guide | ||||||
|  | # at https://guides.rubyonrails.org/i18n.html. | ||||||
|  | # | ||||||
|  | # Be aware that YAML interprets the following case-insensitive strings as | ||||||
|  | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings | ||||||
|  | # must be quoted to be interpreted as strings. For example: | ||||||
|  | # | ||||||
|  | #     en: | ||||||
|  | #       "yes": yup | ||||||
|  | #       enabled: "ON" | ||||||
|  | 
 | ||||||
| en: | en: | ||||||
|   hello: "Hello world" |   hello: "Hello world" | ||||||
|   active_record: |  | ||||||
|     attributes: |  | ||||||
|       guest: |  | ||||||
|         status: Status |  | ||||||
|       guest/status: |  | ||||||
|         considered: Considered |  | ||||||
|         invited: Invited |  | ||||||
|         confirmed: Confirmed |  | ||||||
|         declined: Declined |  | ||||||
|         tentative: Tentative |  | ||||||
| 
 |  | ||||||
|   admin_mailer: |  | ||||||
|     greeting: "Dear user" |  | ||||||
|     attendance_change_email: |  | ||||||
|       subject: "%{name} has changed their attendance status: %{status}" |  | ||||||
|       paragraph_1: "The guest %{name} has changed their attendance for the wedding." |  | ||||||
|       notify_on_updates: "You will be notified of any further changes to their attendance status." |  | ||||||
|     invitations_pdf_email: |  | ||||||
|       subject: "Your wedding invitations are ready" |  | ||||||
|       paragraph_1: "Your wedding invitations are ready. Please, find them attached to this email." |  | ||||||
|  | |||||||
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