Compare commits

..

No commits in common. "main" and "expenses-endpoint" have entirely different histories.

185 changed files with 1181 additions and 4830 deletions

View File

@ -1,58 +0,0 @@
---
:position: before
:position_in_additional_file_patterns: before
:position_in_class: before
:position_in_factory: before
:position_in_fixture: before
:position_in_routes: before
:position_in_serializer: before
:position_in_test: before
:classified_sort: true
:exclude_controllers: true
:exclude_factories: true
:exclude_fixtures: false
:exclude_helpers: true
:exclude_scaffolds: true
:exclude_serializers: false
:exclude_sti_subclasses: false
:exclude_tests: true
:force: false
:format_markdown: false
:format_rdoc: false
:format_yard: false
:frozen: false
:ignore_model_sub_dir: false
:ignore_unknown_models: false
:include_version: false
:show_check_constraints: false
:show_complete_foreign_keys: false
:show_foreign_keys: true
:show_indexes: true
:simple_indexes: false
:sort: false
:timestamp: false
:trace: false
:with_comment: true
:with_column_comments: true
:with_table_comments: true
:active_admin: false
:command:
:debug: false
:hide_default_column_types: ''
:hide_limit_column_types: ''
:ignore_columns:
:ignore_routes:
:models: true
:routes: false
:skip_on_db_migrate: false
:target_action: :do_annotations
:wrapper:
:wrapper_close:
:wrapper_open:
:classes_default_to_s: []
:additional_file_patterns: []
:model_dir:
- app/models
:require: []
:root_dir:
- ''

View File

@ -35,4 +35,3 @@
/app/assets/builds/* /app/assets/builds/*
!/app/assets/builds/.keep !/app/assets/builds/.keep
/public/assets /public/assets
.docker-compose.yml

View File

@ -1,46 +0,0 @@
name: Build Nginx-based docker image
on:
push:
branches:
- main
pull_request:
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 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

View File

@ -1,21 +0,0 @@
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.207.0
- name: Install project dependencies
run: bundle install --jobs `getconf _NPROCESSORS_ONLN`
- name: Run license finder
run: license_finder

33
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: Build Nginx-based docker image
on:
push:
branches:
- main
pull_request:
jobs:
build-static-assets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to 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

View File

@ -3,9 +3,6 @@ on:
pull_request: pull_request:
permissions: permissions:
contents: write contents: write
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
copyright_notice: copyright_notice:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -18,7 +15,7 @@ jobs:
with: with:
CopyrightString: '# Copyright (C) 2024 Manuel Bustillo\n\n' CopyrightString: '# Copyright (C) 2024 Manuel Bustillo\n\n'
FileType: '.rb' FileType: '.rb'
Path: 'app/, config/, db/, spec/' Path: 'app/, config/, db/'
- uses: VinnyBabuManjaly/copyright-action@v1.0.0 - uses: VinnyBabuManjaly/copyright-action@v1.0.0
with: with:
CopyrightString: '<%# Copyright (C) 2024 Manuel Bustillo %>\n\n' CopyrightString: '<%# Copyright (C) 2024 Manuel Bustillo %>\n\n'

View File

@ -4,9 +4,6 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
unit_tests: unit_tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -22,9 +19,8 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- uses: ruby/setup-ruby@v1.207.0 - uses: ruby/setup-ruby@v1
- run: bundle install - run: bundle install
- run: bundle exec rubocop --force-exclusion --parallel
- name: Wait until Postgres is ready to accept connections - name: Wait until Postgres is ready to accept connections
run: | run: |
apt-get update && apt-get install -f -y postgresql-client apt-get update && apt-get install -f -y postgresql-client

3
.gitignore vendored
View File

@ -33,6 +33,3 @@
# Ignore master key for decrypting credentials and more. # Ignore master key for decrypting credentials and more.
/config/master.key /config/master.key
# Ignore swagger generated documentation
swagger/v1/swagger.yaml

View File

@ -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

View File

@ -1 +1 @@
ruby-3.4.1 ruby-3.3.5

View File

@ -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.1 ARG RUBY_VERSION=3.3.5
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
# Rails app lives here # Rails app lives here
WORKDIR /rails WORKDIR /rails
@ -16,11 +16,11 @@ ENV RAILS_ENV="production" \
RUN apt-get update && apt-get install -y nodejs RUN apt-get update && apt-get install -y nodejs
# Throw-away build stage to reduce size of final image # Throw-away build stage to reduce size of final image
FROM base AS build FROM base as build
# Install packages needed to build gems # Install packages needed to build gems
RUN apt-get update -qq && \ RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config 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 ./

View File

@ -1,42 +0,0 @@
# syntax = docker/dockerfile:1
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.4.1
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
# Rails app lives here
WORKDIR /rails
RUN apt-get update && apt-get install -y nodejs
FROM base as build
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config libyaml-dev
# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install
# Copy application code
COPY . .
# Final stage for app image
FROM base
# Install packages needed for deployment
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libvips postgresql-client && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Copy built artifacts: gems, application
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD ["./bin/rails", "server", "--binding=0.0.0.0"]

28
Gemfile
View File

@ -1,8 +1,7 @@
# frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.4.1' ruby '3.3.5'
gem 'acts-as-taggable-on'
gem 'bootsnap', require: false gem 'bootsnap', require: false
gem 'csv' gem 'csv'
gem 'importmap-rails' gem 'importmap-rails'
@ -10,44 +9,27 @@ gem 'jbuilder'
gem 'money' gem 'money'
gem 'pg', '~> 1.1' gem 'pg', '~> 1.1'
gem 'puma', '>= 5.0' gem 'puma', '>= 5.0'
gem 'rails', '~> 8.0.0', '>= 8.0.0' gem 'rails', '~> 7.2.0', '>= 7.2.1'
gem 'redis', '>= 4.0.1' gem 'redis', '>= 4.0.1'
gem 'sprockets-rails' gem 'sprockets-rails'
gem 'stimulus-rails' gem 'stimulus-rails'
gem 'turbo-rails' gem 'turbo-rails'
gem 'tzinfo-data', platforms: %i[windows jruby] gem 'tzinfo-data', platforms: %i[windows jruby]
gem 'acts_as_tenant'
gem 'faker'
gem 'httparty'
gem 'jsonapi-rails' gem 'jsonapi-rails'
gem 'pluck_to_hash'
gem 'rack-cors' gem 'rack-cors'
gem 'react-rails' gem 'react-rails'
gem 'rswag'
gem 'rubytree' gem 'rubytree'
group :development, :test do group :development, :test do
gem 'annotaterb'
gem 'debug', platforms: %i[mri windows] gem 'debug', platforms: %i[mri windows]
gem 'factory_bot_rails' gem 'factory_bot_rails'
gem 'license_finder' gem 'faker'
gem 'pry' gem 'pry'
gem 'rspec-rails', '~> 7.1.0' gem 'rspec-rails', '~> 7.0.0'
gem 'shoulda-matchers', '~> 6.0'
end end
group :development do group :development do
gem 'letter_opener_web'
gem 'rubocop' gem 'rubocop'
gem 'rubocop-factory_bot', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
gem 'rubocop-rspec_rails', require: false
gem 'web-console' gem 'web-console'
end end
gem 'chroma'
gem 'solid_queue', '~> 1.0'
gem 'devise', '~> 4.9'

View File

@ -1,67 +1,67 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (8.0.1) actioncable (7.2.1.2)
actionpack (= 8.0.1) actionpack (= 7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
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.1) actionmailbox (7.2.1.2)
actionpack (= 8.0.1) actionpack (= 7.2.1.2)
activejob (= 8.0.1) activejob (= 7.2.1.2)
activerecord (= 8.0.1) activerecord (= 7.2.1.2)
activestorage (= 8.0.1) activestorage (= 7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.0.1) actionmailer (7.2.1.2)
actionpack (= 8.0.1) actionpack (= 7.2.1.2)
actionview (= 8.0.1) actionview (= 7.2.1.2)
activejob (= 8.0.1) activejob (= 7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.0.1) actionpack (7.2.1.2)
actionview (= 8.0.1) actionview (= 7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) racc
rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.0.1) actiontext (7.2.1.2)
actionpack (= 8.0.1) actionpack (= 7.2.1.2)
activerecord (= 8.0.1) activerecord (= 7.2.1.2)
activestorage (= 8.0.1) activestorage (= 7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.0.1) actionview (7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
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.1) activejob (7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.1) activemodel (7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
activerecord (8.0.1) activerecord (7.2.1.2)
activemodel (= 8.0.1) activemodel (= 7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.0.1) activestorage (7.2.1.2)
actionpack (= 8.0.1) actionpack (= 7.2.1.2)
activejob (= 8.0.1) activejob (= 7.2.1.2)
activerecord (= 8.0.1) activerecord (= 7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.1) activesupport (7.2.1.2)
base64 base64
benchmark (>= 0.3)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
@ -71,48 +71,32 @@ GEM
minitest (>= 5.1) minitest (>= 5.1)
securerandom (>= 0.3) securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1) acts-as-taggable-on (11.0.0)
acts_as_tenant (1.0.1) activerecord (>= 7.0, < 8.0)
rails (>= 6.0) zeitwerk (>= 2.4, < 3.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
annotaterb (4.13.0)
ast (2.4.2) ast (2.4.2)
babel-source (5.8.35) babel-source (5.8.35)
babel-transpiler (0.7.0) babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6) babel-source (>= 4.0, < 6)
execjs (~> 2.0) execjs (~> 2.0)
base64 (0.2.0) base64 (0.2.0)
bcrypt (3.1.20) bigdecimal (3.1.8)
benchmark (0.4.0)
bigdecimal (3.1.9)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.3.0) builder (3.3.0)
childprocess (5.1.0)
logger (~> 1.5)
chroma (0.2.0)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.3.4) concurrent-ruby (1.3.4)
connection_pool (2.4.1) connection_pool (2.4.1)
crass (1.0.6) crass (1.0.6)
csv (3.3.2) csv (3.3.0)
date (3.4.1) date (3.3.4)
debug (1.10.0) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
diff-lcs (1.5.1) diff-lcs (1.5.1)
drb (2.2.1) drb (2.2.1)
erubi (1.13.1) erubi (1.13.0)
et-orbi (1.2.11)
tzinfo
execjs (2.9.1) execjs (2.9.1)
factory_bot (6.4.6) factory_bot (6.4.6)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
@ -121,31 +105,22 @@ GEM
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.5.1) faker (3.5.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
httparty (0.22.0)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.6) i18n (1.14.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
importmap-rails (2.1.0) 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.0) io-console (0.7.2)
irb (1.14.3) irb (1.14.1)
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.9.1) json (2.7.2)
json-schema (5.0.1)
addressable (~> 2.8)
jsonapi-deserializable (0.2.0) jsonapi-deserializable (0.2.0)
jsonapi-parser (0.1.1) jsonapi-parser (0.1.1)
jsonapi-rails (0.4.1) jsonapi-rails (0.4.1)
@ -158,26 +133,8 @@ GEM
jsonapi-serializable (0.3.1) jsonapi-serializable (0.3.1)
jsonapi-renderer (~> 0.2.0) jsonapi-renderer (~> 0.2.0)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (3.0.1) logger (1.6.1)
addressable (~> 2.8) loofah (2.22.0)
childprocess (~> 5.0)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)
actionmailer (>= 6.1)
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
license_finder (7.2.1)
bundler
csv (~> 3.2)
rubyzip (>= 1, < 3)
thor (~> 1.2)
tomlrb (>= 1.3, < 2.1)
with_env (= 1.1.0)
xml-simple (~> 1.1.9)
logger (1.6.5)
loofah (2.23.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@ -186,16 +143,13 @@ GEM
net-pop net-pop
net-smtp net-smtp
marcel (1.0.4) marcel (1.0.4)
method_source (1.1.0) method_source (1.0.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.8) minitest (5.25.1)
minitest (5.25.4)
money (6.19.0) money (6.19.0)
i18n (>= 0.6.4, <= 2) i18n (>= 0.6.4, <= 2)
msgpack (1.7.2) msgpack (1.7.2)
multi_xml (0.7.1) net-imap (0.4.17)
bigdecimal (~> 3.1)
net-imap (0.5.2)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@ -204,39 +158,31 @@ GEM
timeout timeout
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.3)
nokogiri (1.18.1) nokogiri (1.16.7-aarch64-linux)
mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.1-aarch64-linux-gnu) nokogiri (1.16.7-arm-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.1-arm-linux-gnu) nokogiri (1.16.7-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.1-arm64-darwin) nokogiri (1.16.7-x86-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.1-x86_64-darwin) nokogiri (1.16.7-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.1-x86_64-linux-gnu) nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
orm_adapter (0.5.0)
parallel (1.26.3) parallel (1.26.3)
parser (3.3.7.0) parser (3.3.5.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.9) pg (1.5.9)
pluck_to_hash (1.0.2) pry (0.14.2)
activerecord (>= 4.0.2)
activesupport (>= 4.0.2)
pry (0.15.2)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
psych (5.2.2) psych (5.1.2)
date
stringio stringio
public_suffix (6.0.1) puma (6.4.3)
puma (6.5.0)
nio4r (~> 2.0) nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.8) rack (3.1.8)
rack-cors (2.0.2) rack-cors (2.0.2)
@ -245,32 +191,33 @@ GEM
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (2.1.0)
rack (>= 3) rack (>= 3)
rails (8.0.1) webrick (~> 1.8)
actioncable (= 8.0.1) rails (7.2.1.2)
actionmailbox (= 8.0.1) actioncable (= 7.2.1.2)
actionmailer (= 8.0.1) actionmailbox (= 7.2.1.2)
actionpack (= 8.0.1) actionmailer (= 7.2.1.2)
actiontext (= 8.0.1) actionpack (= 7.2.1.2)
actionview (= 8.0.1) actiontext (= 7.2.1.2)
activejob (= 8.0.1) actionview (= 7.2.1.2)
activemodel (= 8.0.1) activejob (= 7.2.1.2)
activerecord (= 8.0.1) activemodel (= 7.2.1.2)
activestorage (= 8.0.1) activerecord (= 7.2.1.2)
activesupport (= 8.0.1) activestorage (= 7.2.1.2)
activesupport (= 7.2.1.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.1) railties (= 7.2.1.2)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2) rails-html-sanitizer (1.6.0)
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.14)
railties (8.0.1) railties (7.2.1.2)
actionpack (= 8.0.1) actionpack (= 7.2.1.2)
activesupport (= 8.0.1) activesupport (= 7.2.1.2)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@ -278,7 +225,7 @@ GEM
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
rdoc (6.10.0) rdoc (6.7.0)
psych (>= 4.0.0) psych (>= 4.0.0)
react-rails (3.2.1) react-rails (3.2.1)
babel-transpiler (>= 0.7.0) babel-transpiler (>= 0.7.0)
@ -290,13 +237,9 @@ GEM
redis-client (>= 0.22.0) redis-client (>= 0.22.0)
redis-client (0.22.2) redis-client (0.22.2)
connection_pool connection_pool
regexp_parser (2.10.0) regexp_parser (2.9.2)
reline (0.6.0) reline (0.5.10)
io-console (~> 0.5) io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.3.9)
rspec-core (3.13.2) rspec-core (3.13.2)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3) rspec-expectations (3.13.3)
@ -305,7 +248,7 @@ GEM
rspec-mocks (3.13.2) 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 (7.1.0) rspec-rails (7.0.1)
actionpack (>= 7.0) actionpack (>= 7.0)
activesupport (>= 7.0) activesupport (>= 7.0)
railties (>= 7.0) railties (>= 7.0)
@ -314,59 +257,22 @@ GEM
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-support (3.13.1) rspec-support (3.13.1)
rswag (2.16.0) rubocop (1.67.0)
rswag-api (= 2.16.0)
rswag-specs (= 2.16.0)
rswag-ui (= 2.16.0)
rswag-api (2.16.0)
activesupport (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1)
rswag-specs (2.16.0)
activesupport (>= 5.2, < 8.1)
json-schema (>= 2.2, < 6.0)
railties (>= 5.2, < 8.1)
rspec-core (>= 2.14)
rswag-ui (2.16.0)
actionpack (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1)
rubocop (1.70.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0) rubocop-ast (>= 1.32.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, < 3.0)
rubocop-ast (1.37.0) rubocop-ast (1.32.3)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-factory_bot (2.26.1)
rubocop (~> 1.61)
rubocop-rails (2.29.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.4.0)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
rubocop-rspec (~> 3, >= 3.0.1)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
rubytree (2.1.1) rubytree (2.1.0)
json (~> 2.0, > 2.9) json (~> 2.0, > 2.3.1)
rubyzip (2.3.2) securerandom (0.3.1)
securerandom (0.4.1)
shoulda-matchers (6.4.0)
activesupport (>= 5.2.0)
solid_queue (1.1.2)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11.0)
railties (>= 7.1)
thor (~> 1.3.1)
sprockets (4.2.1) sprockets (4.2.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4) rack (>= 2.2.4, < 4)
@ -376,32 +282,26 @@ 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.2) stringio (3.1.1)
thor (1.3.2) thor (1.3.2)
tilt (2.4.0) tilt (2.4.0)
timeout (0.4.3) timeout (0.4.1)
tomlrb (2.0.3)
turbo-rails (2.0.11) turbo-rails (2.0.11)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (2.6.0) unicode-display_width (2.6.0)
uri (1.0.2) useragent (0.16.10)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
actionview (>= 6.0.0) actionview (>= 6.0.0)
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webrick (1.8.2)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
with_env (1.1.0)
xml-simple (1.1.9)
rexml
zeitwerk (2.7.1) zeitwerk (2.7.1)
PLATFORMS PLATFORMS
@ -413,206 +313,34 @@ PLATFORMS
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
acts_as_tenant acts-as-taggable-on
annotaterb
bootsnap bootsnap
chroma
csv csv
debug debug
devise (~> 4.9)
factory_bot_rails factory_bot_rails
faker faker
httparty
importmap-rails importmap-rails
jbuilder jbuilder
jsonapi-rails jsonapi-rails
letter_opener_web
license_finder
money money
pg (~> 1.1) pg (~> 1.1)
pluck_to_hash
pry pry
puma (>= 5.0) puma (>= 5.0)
rack-cors rack-cors
rails (~> 8.0.0, >= 8.0.0) rails (~> 7.2.0, >= 7.2.1)
react-rails react-rails
redis (>= 4.0.1) redis (>= 4.0.1)
rspec-rails (~> 7.1.0) rspec-rails (~> 7.0.0)
rswag
rubocop rubocop
rubocop-factory_bot
rubocop-rails
rubocop-rspec
rubocop-rspec_rails
rubytree rubytree
shoulda-matchers (~> 6.0)
solid_queue (~> 1.0)
sprockets-rails sprockets-rails
stimulus-rails stimulus-rails
turbo-rails turbo-rails
tzinfo-data tzinfo-data
web-console web-console
CHECKSUMS
actioncable (8.0.1) sha256=808bff2a4e3aba36f66f0cd65d7a1579ad52fb65e99304442c46051a79689d9b
actionmailbox (8.0.1) sha256=bbc7db779be857fb6eb5b53f313d3881cd8cda38a150c3aa25f89f2f9977b08c
actionmailer (8.0.1) sha256=7b074e9590e4ec5cebd2fc91d1f9ba4c61bbd4bbd4376f731527da187cd39952
actionpack (8.0.1) sha256=c764e4bfc0ad9d3505c09ef9b6fbf9eca4292793550c6b7e2ea93167181bfcba
actiontext (8.0.1) sha256=f232d303e854db2098f34d7331fe493a72dc2e53dfce80fbd517c7b93d4b05b2
actionview (8.0.1) sha256=3005e3de5ca49ea789bf1ad46002d63fe5aa543c61c341239d3c533757e64f8a
activejob (8.0.1) sha256=95acd9a32d498d3a458efbb317f6191fb678758cde0ebb6c68f0b25e0fe3477f
activemodel (8.0.1) sha256=f46292fd6dcc128e18d588854298a933fd9eb22544c412b414ec02821062dc78
activerecord (8.0.1) sha256=34a7f0610660bb704f0363025d4b8d35ffe8ddc8f5b8147e0809171f724b5306
activestorage (8.0.1) sha256=91a8f156638568fac971ff25962a617d9c58fdc0e44eb6bd0edff36aff7df205
activesupport (8.0.1) sha256=fd5bc74641c24ac3541055c2879789198ff42adee3e39c2933289ba008912e37
acts_as_tenant (1.0.1) sha256=6944e4d64533337938a8817a6b4ff9b11189c9dcc0b1333bb89f3821a4c14c53
addressable (2.8.7) sha256=462986537cf3735ab5f3c0f557f14155d778f4b43ea4f485a9deb9c8f7c58232
annotaterb (4.13.0) sha256=6f472912002fefa735665b4132de47d0134ebf1efb76a7ef05f579cc4a6b2ff1
ast (2.4.2) sha256=1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12
babel-source (5.8.35) sha256=79ef222a9dcb867ac2efa3b0da35b4bcb15a4bfa67b6b2dcbf1e9a29104498d9
babel-transpiler (0.7.0) sha256=4c06f4ad9e8e1cabe94f99e11df2f140bb72aca9ba067dbb49dc14d9b98d1570
base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507
bcrypt (3.1.20) sha256=8410f8c7b3ed54a3c00cd2456bf13917d695117f033218e2483b2e40b0784099
benchmark (0.4.0) sha256=0f12f8c495545e3710c3e4f0480f63f06b4c842cc94cec7f33a956f5180e874a
bigdecimal (3.1.9) sha256=2ffc742031521ad69c2dfc815a98e426a230a3d22aeac1995826a75dabfad8cc
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
bootsnap (1.18.4) sha256=ac4c42af397f7ee15521820198daeff545e4c360d2772c601fbdc2c07d92af55
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec
chroma (0.2.0) sha256=64bdcd36a4765fbcd45adc64960cc153101300b4918f90ffdd89f4e2eb954b54
coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
concurrent-ruby (1.3.4) sha256=d4aa926339b0a86b5b5054a0a8c580163e6f5dcbdfd0f4bb916b1a2570731c32
connection_pool (2.4.1) sha256=0f40cf997091f1f04ff66da67eabd61a9fe0d4928b9a3645228532512fab62f4
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
csv (3.3.2) sha256=6ff0c135e65e485d1864dde6c1703b60d34cc9e19bed8452834a0b28a519bd4e
date (3.4.1) sha256=bf268e14ef7158009bfeaec40b5fa3c7271906e88b196d958a89d4b408abe64f
debug (1.10.0) sha256=11e28ca74875979e612444104f3972bd5ffb9e79179907d7ad46dba44bd2e7a4
devise (4.9.4) sha256=920042fe5e704c548aa4eb65ebdd65980b83ffae67feb32c697206bfd975a7f8
diff-lcs (1.5.1) sha256=273223dfb40685548436d32b4733aa67351769c7dea621da7d9dd4813e63ddfe
drb (2.2.1) sha256=e9d472bf785f558b96b25358bae115646da0dbfd45107ad858b0bc0d935cb340
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
et-orbi (1.2.11) sha256=d26e868cc21db88280a9ec1a50aa3da5d267eb9b2037ba7b831d6c2731f5df64
execjs (2.9.1) sha256=e8fd066f6df60c8e8fbebc32c6fb356b5212c77374e8416a9019ca4bb154dcfb
factory_bot (6.4.6) sha256=1a9486ce98d318d740d8f5804b885a8265a28f326ecf2bcd4ce9fb27a71a6e04
factory_bot_rails (6.4.3) sha256=ea73ceac1c0ff3dc11fff390bf2ea8a2604066525ed8ecd3b3bc2c267226dcc8
faker (3.5.1) sha256=1ad1fbea279d882f486059c23fe3ddb816ccd1d7052c05a45014b4450d859bfc
fugit (1.11.1) sha256=e89485e7be22226d8e9c6da411664d0660284b4b1c08cacb540f505907869868
globalid (1.2.1) sha256=70bf76711871f843dbba72beb8613229a49429d1866828476f9c9d6ccc327ce9
httparty (0.22.0) sha256=78652a5c9471cf0093d3b2083c2295c9c8f12b44c65112f1846af2b71430fa6c
i18n (1.14.6) sha256=dc229a74f5d181f09942dd60ab5d6e667f7392c4ee826f35096db36d1fe3614c
importmap-rails (2.1.0) sha256=9f10c67d60651a547579f448100d033df311c5d5db578301374aeb774faae741
io-console (0.8.0) sha256=cd6a9facbc69871d69b2cb8b926fc6ea7ef06f06e505e81a64f14a470fddefa2
irb (1.14.3) sha256=c457f1f2f1438ae9ce5c5be3981ae2138dec7fb894c7d73777eeeb0a6c0d0752
jbuilder (2.13.0) sha256=7200a38a1c0081aa81b7a9757e7a299db75bc58cf1fd45ca7919a91627d227d6
json (2.9.1) sha256=d2bdef4644052fad91c1785d48263756fe32fcac08b96a20bb15840e96550d11
json-schema (5.0.1) sha256=bef71a82c600a42594911553522e143f7634affc198ed507ef3ded2f920a74a9
jsonapi-deserializable (0.2.0) sha256=5f0ca2d3f8404cce1584a314e8a3753be32a56054c942adfe997b87e92bce147
jsonapi-parser (0.1.1) sha256=9ee0dc031e88fc7548d56fab66f9716d1e1c06f972b529b8c4617bc42a097020
jsonapi-rails (0.4.1) sha256=fa68b927b58f194e8b81f578c0bf18e61575638f45a390f66c832de2e6d179ba
jsonapi-rb (0.5.0) sha256=7922a164278f506c43d56277f6bd0800a0b603cc985f7f63fe7241b2628bd105
jsonapi-renderer (0.2.2) sha256=b5c44b033d61b4abdb6500fa4ab84807ca0b36ea0e59e47a2c3ca7095a6e447b
jsonapi-serializable (0.3.1) sha256=221e657677659d798e268a33ec97a83ec5ea0e4233f931358db84e88056552e9
language_server-protocol (3.17.0.3) sha256=3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f
launchy (3.0.1) sha256=b7fa60bda0197cf57614e271a250a8ca1f6a34ab889a3c73f67ec5d57c8a7f2c
letter_opener (1.10.0) sha256=2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2
letter_opener_web (3.0.0) sha256=3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860
license_finder (7.2.1) sha256=179ead19b64b170638b72fd16024233813673ac9d20d5ba75ae0b4444887ef14
logger (1.6.5) sha256=c3cfe56d01656490ddd103d38b8993d73d86296adebc5f58cefc9ec03741e56b
loofah (2.23.1) sha256=d0a07422cb3b69272e124afa914ef6d517e30d5496b7f1c1fc5b95481f13f75e
mail (2.8.1) sha256=ec3b9fadcf2b3755c78785cb17bc9a0ca9ee9857108a64b6f5cfc9c0b5bfc9ad
marcel (1.0.4) sha256=0d5649feb64b8f19f3d3468b96c680bae9746335d02194270287868a661516a4
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
mini_portile2 (2.8.8) sha256=8e47136cdac04ce81750bb6c09733b37895bf06962554e4b4056d78168d70a75
minitest (5.25.4) sha256=9cf2cae25ac4dfc90c988ebc3b917f53c054978b673273da1bd20bcb0778f947
money (6.19.0) sha256=ec936fa1e42f2783719241ed9fd52725d0efa628f928feea1eb5c37d5de7daf3
msgpack (1.7.2) sha256=59ab62fd8a4d0dfbde45009f87eb6f158ab2628a7c48886b0256f175166baaa8
multi_xml (0.7.1) sha256=4fce100c68af588ff91b8ba90a0bb3f0466f06c909f21a32f4962059140ba61b
net-imap (0.5.2) sha256=e955b55e539712518bdb4eb747c6514f9c8d56ec4eb8eb573a82a6885a9effea
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
net-smtp (0.5.0) sha256=5fc0415e6ea1cc0b3dfea7270438ec22b278ca8d524986a3ae4e5ae8d087b42a
nio4r (2.7.4) sha256=d95dee68e0bb251b8ff90ac3423a511e3b784124e5db7ff5f4813a220ae73ca9
nokogiri (1.18.1) sha256=df18be7e96c34736b6abfdeda80c6e845134fb9afe2fe5d4fbc1cf1f89c68475
nokogiri (1.18.1-aarch64-linux-gnu) sha256=35837013800e34342fcbaca305f8c49231f6bd4f779bfa23fe7b4686ae82d5b8
nokogiri (1.18.1-arm-linux-gnu) sha256=3b873fd6b0cd1ad7c77e87af701075bdfd14c9a6b2f2965c5e00ed29a5627a37
nokogiri (1.18.1-arm64-darwin) sha256=d75193f284c899d225943a8944479faedd995a7573ddd5c8308ffbdf2ec55204
nokogiri (1.18.1-x86_64-darwin) sha256=d94e3aa6483577495fc8969d6b4b5c075840ce6b1ab09636a6d4177ad171051d
nokogiri (1.18.1-x86_64-linux-gnu) sha256=e516cf16ccde67ed4cc595a2621ca5ddd42562ecb24928914b0045a20a41620e
orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9
parallel (1.26.3) sha256=d86babb7a2b814be9f4b81587bf0b6ce2da7d45969fab24d8ae4bf2bb4d4c7ef
parser (3.3.7.0) sha256=7449011771e3e7881297859b849de26a6f4fccd515bece9520a87e7d2116119b
pg (1.5.9) sha256=761efbdf73b66516f0c26fcbe6515dc7500c3f0aa1a1b853feae245433c64fdc
pluck_to_hash (1.0.2) sha256=1599906239716f98262a41493dd7d4cb72e8d83ad3d76d666deacfc5de50a47e
pry (0.15.2) sha256=12d54b8640d3fa29c9211dd4ffb08f3fd8bf7a4fd9b5a73ce5b59c8709385b6b
psych (5.2.2) sha256=a4a9477c85d3e858086c38cf64e7096abe40d1b1eed248b01020dec0ff9906ab
public_suffix (6.0.1) sha256=61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f
puma (6.5.0) sha256=94d1b75cab7f356d52e4f1b17b9040a090889b341dbeee6ee3703f441dc189f2
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rack (3.1.8) sha256=d3fbcbca43dc2b43c9c6d7dfbac01667ae58643c42cea10013d0da970218a1b1
rack-cors (2.0.2) sha256=415d4e1599891760c5dc9ef0349c7fecdf94f7c6a03e75b2e7c2b54b82adda1b
rack-session (2.0.0) sha256=db04b2063e180369192a9046b4559af311990af38c6a93d4c600cee4eb6d4e81
rack-test (2.1.0) sha256=0c61fc61904049d691922ea4bb99e28004ed3f43aa5cfd495024cc345f125dfb
rackup (2.2.1) sha256=f737191fd5c5b348b7f0a4412a3b86383f88c43e13b8217b63d4c8d90b9e798d
rails (8.0.1) sha256=c86f4cd7834a67c1e5d04a77d35c88a5f56a20e2022ec416fa52c1af2cdc9491
rails-dom-testing (2.2.0) sha256=e515712e48df1f687a1d7c380fd7b07b8558faa26464474da64183a7426fa93b
rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560
railties (8.0.1) sha256=8f653c6b1b0721b553045bd0deda1f22074b9ddc2209526e6f7285fcf607ac51
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.2.1) sha256=46cb38dae65d7d74b6020a4ac9d48afed8eb8149c040eccf0523bec91907059d
rdoc (6.10.0) sha256=db665021883ac9df3ba29cdf71aece960749888db1bf9615b4a584cfa3fa3eda
react-rails (3.2.1) sha256=2235db0b240517596b1cb3e26177ab5bc64d3a56579b0415ee242b1691f81f64
redis (5.3.0) sha256=6bf810c5ae889187f0c45f77db503310980310afa57cf1640d57f419ccda72b1
redis-client (0.22.2) sha256=31fee4b7cf04109b227327fabeaaf1fc5b652cf48a186a03bc607e40767bacc0
regexp_parser (2.10.0) sha256=cb6f0ddde88772cd64bff1dbbf68df66d376043fe2e66a9ef77fcb1b0c548c61
reline (0.6.0) sha256=57620375dcbe56ec09bac7192bfb7460c716bbf0054dc94345ecaa5438e539d2
responders (3.1.1) sha256=92f2a87e09028347368639cfb468f5fefa745cb0dc2377ef060db1cdd79a341a
rexml (3.3.9) sha256=d71875b85299f341edf47d44df0212e7658cbdf35aeb69cefdb63f57af3137c9
rspec-core (3.13.2) sha256=94fbda6e4738e478f1c7532b7cc241272fcdc8b9eac03a97338b1122e4573300
rspec-expectations (3.13.3) sha256=0e6b5af59b900147698ea0ff80456c4f2e69cac4394fbd392fbd1ca561f66c58
rspec-mocks (3.13.2) sha256=2327335def0e1665325a9b617e3af9ae20272741d80ac550336309a7c59abdef
rspec-rails (7.1.0) sha256=94585b69c4086ca79afae5cc8d2c5e314f6ad32a88c927f9c065b99596e3ee47
rspec-support (3.13.1) sha256=48877d4f15b772b7538f3693c22225f2eda490ba65a0515c4e7cf6f2f17de70f
rswag (2.16.0) sha256=f07ce41548b9bb51464c38bc7b95af22fee84b90f2d1197a515a623906353086
rswag-api (2.16.0) sha256=b653f7bd92e98be18b01ab4525d88950d7b0960e293a99f856b9efcee3ae6074
rswag-specs (2.16.0) sha256=8ba26085c408b0bd2ed21dc8015c80f417c7d34c63720ab7133c2549b5bd2a91
rswag-ui (2.16.0) sha256=a1f49e927dceda92e6e6e7c1000f1e217ee66c565f69e28131dc98b33cd3a04f
rubocop (1.70.0) sha256=96751f8440b36a0ac6e9a8ab596900803118d83d6b83f2037bf8b3d7a5bc440e
rubocop-ast (1.37.0) sha256=9513ac88aaf113d04b52912533ffe46475de1362d4aa41141b51b2455827c080
rubocop-factory_bot (2.26.1) sha256=8de13cd4edcee5ca800f255188167ecef8dbfc3d1fae9f15734e9d2e755392aa
rubocop-rails (2.29.0) sha256=35bffd140c80671453aafac0e2d5ab5b3dd65736a3fc8f3936ccca226b89c234
rubocop-rspec (3.4.0) sha256=8721c13b6a8c9530a7ac481cea9423022f946fcf72428bda8289f8b57e4d4885
rubocop-rspec_rails (2.30.0) sha256=888112e83f9d7ef7ad2397e9d69a0b9614a4bae24f072c399804a180f80c4c46
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
rubytree (2.1.1) sha256=4925016356a81730e982f1f8c3b5f8da461f18906c77d238bad4c4ba896abd41
rubyzip (2.3.2) sha256=3f57e3935dc2255c414484fbf8d673b4909d8a6a57007ed754dde39342d2373f
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
shoulda-matchers (6.4.0) sha256=9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0
solid_queue (1.1.2) sha256=178c9396d1cf0dac595c7508da90ddb397d25848ca007b5c5ed48e6ac6fc360c
sprockets (4.2.1) sha256=951b13dd2f2fcae840a7184722689a803e0ff9d2702d902bd844b196da773f97
sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
stringio (3.1.2) sha256=204f1828f85cdb39d57cac4abc6dc44b04505a223f131587f2e20ae3729ba131
thor (1.3.2) sha256=eef0293b9e24158ccad7ab383ae83534b7ad4ed99c09f96f1a6b036550abbeda
tilt (2.4.0) sha256=df74f29a451daed26591a85e8e0cebb198892cb75b6573394303acda273fba4d
timeout (0.4.3) sha256=9509f079b2b55fe4236d79633bd75e34c1c1e7e3fb4b56cb5fda61f80a0fe30e
tomlrb (2.0.3) sha256=c2736acf24919f793334023a4ff396c0647d93fce702a73c9d348deaa815d4f7
turbo-rails (2.0.11) sha256=fc47674736372780abd2a4dc0d84bef242f5ca156a457cd7fa6308291e397fcf
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
uri (1.0.2) sha256=b303504ceb7e5905771fa7fa14b649652fa949df18b5880d69cfb12494791e27
useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0
web-console (4.2.1) sha256=e7bcf37a10ea2b4ec4281649d1cee461b32232d0a447e82c786e6841fd22fe20
websocket-driver (0.7.6) sha256=f69400be7bc197879726ad8e6f5869a61823147372fd8928836a53c2c741d0db
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
with_env (1.1.0) sha256=50b3e4f0a6cda8f90d8a6bd87a6261f6c381429abafb161c4c69ad4a0cd0b6e4
xml-simple (1.1.9) sha256=d21131e519c86f1a5bc2b6d2d57d46e6998e47f18ed249b25cad86433dbd695d
zeitwerk (2.7.1) sha256=0945986050e4907140895378e74df1fe882a2271ed087cc6c6d6b00d415a2756
RUBY VERSION RUBY VERSION
ruby 3.4.1p0 ruby 3.3.5p100
BUNDLED WITH BUNDLED WITH
2.6.1 2.5.17

View File

@ -1,96 +1,24 @@
# Libre Wedding Planner # README
Libre Wedding Planner is Free, Open Source Software that helps organize several aspects of a wedding. This README would normally document whatever steps are necessary to get the
application up and running.
The project is not production-ready yet. Things you may want to cover:
## Features * Ruby version
The follwing features are either developed or under active development: * System dependencies
- Guests management * Configuration
- Expense management
- Seating chart
* Database creation
## Next steps * Database initialization
Some ideas we would like to implement next: * How to run the test suite
- Authentication (required to make an instance public) * Services (job queues, cache servers, search engines, etc.)
- Website with wedding information
- Attendance confirmation forms
- Multitenancy
# Development setup * Deployment instructions
Libre Wedding Planner is made of two main pieces:
- The backend (this repo), built with Ruby (on Rails)
- The frontend (repo [here](https://gitea.bustikiller.com/bustikiller/wedding-planner-frontend/)), built with NextJS and React. You will need both to have the service fully working.
Both repositories are expected to live have a common parent directory:
```
projects <or anything else>
|-> wedding-planner
|-> wedding-planner-frontend
```
## Docker compose
Docker compose is the recommended way to run Libre Wedding Planner for development purposes. After downloading both repositories, `cd` to the root of `wedding-planner` and run:
```bash
docker compose up --build
```
Several containers will be started:
- backend: starts a Rails server that will act as an API.
- workers: starts a runner of [solid queue](https://github.com/rails/solid_queue/) that takes .care of async tasks.
- frontend: starts a NextJS application in charge of the frontend.
- nginx: A reverse proxy that the backend and frontend under the same domain, and routes all requests to the upstream services.
- db: A Postgres instance used by the backend service.
The backend service will seed the database with fake data. It's worth noting that the Postgres container does not have a volume, so the application will be seeded every time the container is created.
The backend, frontend and workers have hot-reloading enabled, so changes made to the codebase should be reflected in the application on the next request.
Once all containers have started, visit http://libre-wedding-planner.app.localhost/default/dashboard to load the application.
## Multitenancy
LibreWeddingPlanner is designed to manage multiple weddings in a single host. All URLs (in the API and the frontend) are scoped under a slug that is unique per wedding. The slug is made of lowercase letters, numbers, and dashes (-).
The development environment is seeded with a wedding whose slug is `default`.
## Email delivery
In the development environment, real emails will not be sent. You can visit http://libre-wedding-planner.app.localhost/letter_opener/ to get a list of emails generated by the application.
## Testing
Unit tests can be executed with
```
bundle exec rspec
```
## API documentation
Generate the OpenAPI documentation with the command:
```
rake rswag:specs:swaggerize
```
The documentation is available in Swagger UI in http://libre-wedding-planner.app.localhost/api/api-docs/index.html. If testing the API through the UI, you will need to select the second server (which includes the `/api` path), intended for development.
## Contributing
Contributions of all kinds (code, UX/UI, testing, translations, etc.) are welcome. The procedures to contribute are still being defined, but don't hesitate to reach out in case you want to participate.
# License
This project is licensed under the GNU Affero General Public License (AGPL). Check [COPYING.md](./COPYING.md) for additional information.
* ...

View File

@ -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

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module ApplicationCable module ApplicationCable
class Channel < ActionCable::Channel::Base class Channel < ActionCable::Channel::Base
end end

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module ApplicationCable module ApplicationCable
class Connection < ActionCable::Connection::Base class Connection < ActionCable::Connection::Base
end end

View File

@ -1,74 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# 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

View File

@ -1,70 +1,15 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
set_current_tenant_through_filter after_action :set_csrf_cookie
before_action :set_tenant
before_action :authenticate_user!
after_action :set_csrf_cookie
skip_before_action :verify_authenticity_token, if: :development_swagger? private
rescue_from ActiveRecord::RecordInvalid do |exception| def set_csrf_cookie
render json: { cookies["csrf-token"] = {
message: 'Record invalid', value: form_authenticity_token,
errors: exception.record.errors.full_messages secure: Rails.env.production?,
}, status: :unprocessable_entity same_site: :strict,
end }
end
rescue_from ActionController::ParameterMissing do |exception|
render json: {
message: 'Parameter missing',
errors: [exception.message]
}, status: :bad_request
end
rescue_from ActiveRecord::RecordNotFound do |exception|
render json: {
message: 'Record not found',
errors: [exception.message]
}, status: :not_found
end
private
def validate_captcha!
Rails.logger.info("Captcha params: #{captcha_params}")
return if LibreCaptcha.new.valid?(id: captcha_params[:id], answer: captcha_params[:answer])
render json: { error: 'Incorrect CAPTCHA solution' }, status: :unprocessable_entity
end
def captcha_params
params.expect(captcha: %i[id answer])
end
def default_url_options(options = {})
options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug })
end
def set_tenant
set_current_tenant(Wedding.find_by!(slug: params[:slug]))
end
def development_swagger?
Rails.env.test? ||
(Rails.env.development? && request.headers['referer']&.include?('/api-docs/index.html'))
end
def set_csrf_cookie
cookies['csrf-token'] = {
value: form_authenticity_token,
secure: false,
same_site: :strict
}
end
end end

View File

@ -1,17 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class CaptchaController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :set_tenant
def create
id = LibreCaptcha.new.id
render json: {
id:,
media_url: media_captcha_index_url(id:)
}, status: :created
end
end

View File

@ -1,36 +1,76 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class ExpensesController < ApplicationController class ExpensesController < ApplicationController
before_action :set_expense, only: %i[ show edit update destroy ]
# GET /expenses or /expenses.json
def index
@expenses = Expense.all
end
def summary def summary
render json: Expenses::TotalQuery.new.call render json: Expenses::TotalQuery.new.call
end end
def index # GET /expenses/1 or /expenses/1.json
render json: Expense.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type]) def show
end end
# GET /expenses/new
def new
@expense = Expense.new
end
# GET /expenses/1/edit
def edit
end
# POST /expenses or /expenses.json
def create def create
Expense.create!(expense_params) @expense = Expense.new(expense_params)
render json: {}, status: :created
respond_to do |format|
if @expense.save
format.html { redirect_to expense_url(@expense), notice: "Expense was successfully created." }
format.json { render :show, status: :created, location: @expense }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @expense.errors, status: :unprocessable_entity }
end
end
end end
# PATCH/PUT /expenses/1 or /expenses/1.json
def update def update
Expense.find(params[:id]).update!(expense_params) respond_to do |format|
render json: {}, status: :ok if @expense.update(expense_params)
format.html { redirect_to expense_url(@expense), notice: "Expense was successfully updated." }
format.json { render :show, status: :ok, location: @expense }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @expense.errors, status: :unprocessable_entity }
end
end
end end
# DELETE /expenses/1 or /expenses/1.json
def destroy def destroy
Expense.find(params[:id]).destroy! @expense.destroy!
render json: {}, status: :ok
respond_to do |format|
format.html { redirect_to expenses_url, notice: "Expense was successfully destroyed." }
format.json { head :no_content }
end
end end
private private
# Use callbacks to share common setup or constraints between actions.
def set_expense
@expense = Expense.find(params[:id])
end
def expense_params # Only allow a list of trusted parameters through.
params.expect(expense: %i[name amount pricing_type]) def expense_params
end params.require(:expense).permit(:name, :amount, :pricing_type)
end
end end

View File

@ -1,48 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# 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| roots = Group.where(parent_id: nil)
{ render jsonapi: roots, include: [children: [children: [:children]]]
id: group[:id],
name: group[:name],
icon: group[:icon],
color: group[:color],
parent_id: group[:parent_id],
attendance: group.slice(:total, :considered, :invited, :confirmed, :declined, :tentative)
}
end
render json: query_result
end
def create
group = Group.create!(**group_params, parent:)
render json: group.as_json(only: %i[id name icon color parent_id]), status: :created
end
def update
group = Group.find(params[:id])
group.update!(**group_params, parent:)
render json: group.as_json(only: %i[id name icon color parent_id]), status: :ok
end
def destroy
Group.find(params[:id]).destroy!
render json: {}, status: :ok
end
private
def parent
params[:group][:parent_id].present? ? Group.find(params[:group][:parent_id]) : nil
end
def group_params
params.expect(group: %i[name icon color])
end end
end end

View File

@ -1,37 +1,96 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
require 'csv' require 'csv'
class GuestsController < ApplicationController class GuestsController < ApplicationController
before_action :set_guest, only: %i[show edit update destroy]
# GET /guests or /guests.json
def index def index
render json: Guest.includes(:group) @guests = Guest.all
.left_joins(:group) .joins(:group)
.order('groups.name' => :asc, name: :asc) .order('groups.name' => :asc, first_name: :asc, last_name: :asc)
.as_json(only: %i[id name status], include: { group: { only: %i[id name] } })
render jsonapi: @guests
end end
# GET /guests/1 or /guests/1.json
def show; end
# GET /guests/new
def new
@guest = Guest.new
end
# GET /guests/1/edit
def edit; end
# POST /guests or /guests.json
def create def create
Guest.create!(guest_params) @guest = Guest.new(guest_params)
render json: {}, status: :created
respond_to do |format|
if @guest.save
format.html { redirect_to guest_url(@guest), notice: 'Guest was successfully created.' }
format.json { render :show, status: :created, location: @guest }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @guest.errors, status: :unprocessable_entity }
end
end
end end
# PATCH/PUT /guests/1 or /guests/1.json
def update def update
Guest.find(params[:id]).update!(guest_params) respond_to do |format|
render json: {}, status: :ok if @guest.update(guest_params)
format.html { redirect_to guest_url(@guest), notice: 'Guest was successfully updated.' }
format.json { render :show, status: :ok, location: @guest }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @guest.errors, status: :unprocessable_entity }
end
end
end end
# DELETE /guests/1 or /guests/1.json
def destroy def destroy
Guest.find(params[:id]).destroy! @guest.destroy!
respond_to do |format|
format.html { redirect_to guests_url, notice: 'Guest was successfully destroyed.' }
format.json { head :no_content }
end
end
def import
csv = CSV.parse(params[:file].read, headers: true)
ActiveRecord::Base.transaction do
csv.each do |row|
guest = Guest.create!(first_name: row['name'])
guest.affinity_group_list.add(row['affinity_group'])
guest.save!
end
end
redirect_to guests_url
end
def bulk_update
Guest.where(id: params[:guest_ids]).update!(params.require(:properties).permit(:status))
render json: {}, status: :ok render json: {}, status: :ok
end end
private private
# Use callbacks to share common setup or constraints between actions.
def set_guest
@guest = Guest.find(params[:id])
end
# Only allow a list of trusted parameters through.
def guest_params def guest_params
params.expect(guest: %i[name group_id status]) params.require(:guest).permit(:first_name, :last_name, :email, :phone)
end end
end end

View File

@ -1,46 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# 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

View File

@ -1,42 +1,14 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class TablesArrangementsController < ApplicationController class TablesArrangementsController < ApplicationController
def index def index
render json: TablesArrangement.order(discomfort: :asc).limit(3).as_json(only: %i[id name discomfort]) @tables_arrangements = TablesArrangement.all.order(discomfort: :asc).limit(10)
end end
def show def show
Guest.joins(:seats, :group) @tables_arrangement = TablesArrangement.find(params[:id])
.where(seats: { tables_arrangement_id: params[:id] }) @seats = @tables_arrangement.seats
.select('guests.*', 'groups.color', 'seats.table_number') .includes(guest: %i[affinity_groups unbreakable_bonds])
.group_by(&:table_number) .group_by(&:table_number)
.map { |number, guests| format(number:, guests:) }
.then { |result| render json: { id: params[:id], tables: result } }
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

View File

@ -1,14 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# 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

View File

@ -1,29 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module Users
class ConfirmationsController < Devise::ConfirmationsController
clear_respond_to
respond_to :json
def show
super do |resource|
if resource.errors.empty?
respond_to do |format|
format.json { render json: resource, status: :ok }
format.any { redirect_to root_path }
end
else
render json: {
message: 'Record invalid',
errors: resource.errors.full_messages
}, status: :unprocessable_entity
end
return
end
end
end
end

View File

@ -1,34 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module Users
class RegistrationsController < Devise::RegistrationsController
clear_respond_to
respond_to :json
before_action :validate_captcha!, only: :create
def create
wedding = Wedding.create(slug: params[:slug])
unless wedding.persisted?
render json: { errors: wedding.errors.full_messages }, status: :unprocessable_entity
return
end
ActsAsTenant.with_tenant(wedding) do
super do |user|
wedding.destroy unless user.persisted?
end
end
end
private
def set_tenant
set_current_tenant(nil)
end
end
end

View File

@ -1,12 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module Users
class SessionsController < Devise::SessionsController
clear_respond_to
respond_to :json
end
end

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module TreeNodeExtension module TreeNodeExtension
def distance_to_common_ancestor(another_node) def distance_to_common_ancestor(another_node)
return 0 if self == another_node return 0 if self == another_node

View File

@ -1,8 +1,4 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module ApplicationHelper module ApplicationHelper
end end

View File

@ -1,8 +1,4 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module ExpensesHelper module ExpensesHelper
end end

View File

@ -1,8 +1,4 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module GroupsHelper module GroupsHelper
end end

View File

@ -1,8 +1,4 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module GuestsHelper module GuestsHelper
end end

View File

@ -1,8 +1,4 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module TablesArrangementsHelper module TablesArrangementsHelper
end end

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class ApplicationJob < ActiveJob::Base class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock # Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked # retry_on ActiveRecord::Deadlocked

View File

@ -1,32 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class TableSimulatorJob < ApplicationJob
queue_as :default
MIN_PER_TABLE = 8
MAX_PER_TABLE = 10
def perform(wedding_id)
ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do
engine = VNS::Engine.new
engine.add_perturbation(Tables::Swap)
engine.add_perturbation(Tables::Shift)
initial_solution = Tables::Distribution.new(min_per_table: MIN_PER_TABLE, max_per_table: MAX_PER_TABLE)
initial_solution.random_distribution(Guest.potential.shuffle)
engine.initial_solution = initial_solution
engine.target_function(&:discomfort)
best_solution = engine.run
best_solution.save!
end
end
end

View File

@ -1,10 +1,6 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com' default from: "from@example.com"
layout 'mailer' layout "mailer"
end end

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base class ApplicationRecord < ActiveRecord::Base
primary_abstract_class primary_abstract_class
end end

View File

@ -1,36 +1,4 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
# == Schema Information
#
# Table name: expenses
#
# id :uuid not null, primary key
# amount :decimal(, )
# name :string
# pricing_type :enum default("fixed"), not null
# created_at :datetime not null
# updated_at :datetime not null
# wedding_id :uuid not null
#
# Indexes
#
# index_expenses_on_wedding_id (wedding_id)
#
# Foreign Keys
#
# fk_rails_... (wedding_id => weddings.id)
#
class Expense < ApplicationRecord class Expense < ApplicationRecord
acts_as_tenant :wedding
enum :pricing_type,
fixed: 'fixed',
per_person: 'per_person'
validates :name, presence: true
validates :amount, presence: true, numericality: { greater_than: 0 }
validates :pricing_type, presence: true
end end

View File

@ -1,79 +1,11 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
# == Schema Information
#
# Table name: groups
#
# id :uuid not null, primary key
# color :string
# icon :string
# name :string not null
# order :integer default(1), not null
# created_at :datetime not null
# updated_at :datetime not null
# parent_id :uuid
# wedding_id :uuid not null
#
# Indexes
#
# index_groups_on_name (name) UNIQUE
# index_groups_on_parent_id (parent_id)
# index_groups_on_wedding_id (wedding_id)
#
# Foreign Keys
#
# fk_rails_... (parent_id => groups.id)
# fk_rails_... (wedding_id => weddings.id)
#
class Group < ApplicationRecord class Group < ApplicationRecord
acts_as_tenant :wedding
validates :name, uniqueness: true validates :name, uniqueness: true
validates :name, :order, presence: true validates :name, :order, presence: true
has_many :children, class_name: 'Group', foreign_key: 'parent_id', 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 has_many :guests
scope :roots, -> { where(parent_id: nil) }
has_many :guests, dependent: :nullify
def colorize_children(generation = 1)
children.zip(palette(generation)) do |child, raw_color|
final_color = raw_color.paint
final_color.brighten(60) if final_color.dark?
child.update!(color: final_color)
child.colorize_children(generation + 1)
end
end
def affinities
GroupAffinity.where(group_a_id: id).or(GroupAffinity.where(group_b_id: id))
end
private
def palette(generation)
if generation == 1
color.paint.palette.analogous(size: children.count)
else
color.paint.palette.decreasing_saturation
end
end
def set_color
return if color.present?
new_color = "##{SecureRandom.hex(3)}".paint
new_color = new_color.lighten(30) if new_color.dark?
self.color = new_color
end
end end

View File

@ -1,45 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# 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

View File

@ -1,58 +1,18 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
# == Schema Information
#
# Table name: guests
#
# id :uuid not null, primary key
# name :string
# phone :string
# status :integer default("considered")
# created_at :datetime not null
# updated_at :datetime not null
# group_id :uuid
# wedding_id :uuid not null
#
# Indexes
#
# index_guests_on_group_id (group_id)
# index_guests_on_wedding_id (wedding_id)
#
# Foreign Keys
#
# fk_rails_... (group_id => groups.id)
# fk_rails_... (wedding_id => weddings.id)
#
class Guest < ApplicationRecord class Guest < ApplicationRecord
acts_as_tenant :wedding acts_as_taggable_on :affinity_groups, :unbreakable_bonds
belongs_to :group, optional: true belongs_to :group
enum :status, { enum status: {
considered: 0, considered: 0,
invited: 10, invited: 10,
confirmed: 20, confirmed: 20,
declined: 30, declined: 30,
tentative: 40 tentative: 40,
}, validate: true }
validates :name, presence: true def full_name
"#{first_name} #{last_name}"
scope :potential, -> { where.not(status: %i[declined considered]) }
after_destroy :recalculate_simulations
after_save :recalculate_simulations, if: :saved_change_to_status?
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 end

View File

@ -1,35 +1,6 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
# == Schema Information
#
# Table name: seats
#
# id :uuid not null, primary key
# table_number :integer
# created_at :datetime not null
# updated_at :datetime not null
# guest_id :uuid not null
# tables_arrangement_id :uuid not null
# wedding_id :uuid not null
#
# Indexes
#
# index_seats_on_guest_id (guest_id)
# index_seats_on_tables_arrangement_id (tables_arrangement_id)
# index_seats_on_wedding_id (wedding_id)
#
# Foreign Keys
#
# fk_rails_... (guest_id => guests.id)
# fk_rails_... (tables_arrangement_id => tables_arrangements.id) ON DELETE => cascade
# fk_rails_... (wedding_id => weddings.id)
#
class Seat < ApplicationRecord class Seat < ApplicationRecord
acts_as_tenant :wedding
belongs_to :guest belongs_to :guest
belongs_to :table_arrangement belongs_to :table_arrangement
end end

View File

@ -1,38 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
# == Schema Information
#
# Table name: tables_arrangements
#
# id :uuid not null, primary key
# discomfort :integer
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# wedding_id :uuid not null
#
# Indexes
#
# index_tables_arrangements_on_wedding_id (wedding_id)
#
# Foreign Keys
#
# fk_rails_... (wedding_id => weddings.id)
#
class TablesArrangement < ApplicationRecord class TablesArrangement < ApplicationRecord
acts_as_tenant :wedding has_many :seats
has_many :seats, dependent: :delete_all
has_many :guests, through: :seats
before_create :assign_name
private
def assign_name
self.name = "#{Faker::Adjective.positive} #{Faker::Creature::Animal.name}".capitalize
end
end end

View File

@ -1,44 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
# == Schema Information
#
# Table name: users
#
# id :uuid not null, primary key
# confirmation_sent_at :datetime
# confirmation_token :string
# confirmed_at :datetime
# email :string default(""), not null
# encrypted_password :string default(""), not null
# failed_attempts :integer default(0), not null
# locked_at :datetime
# reset_password_sent_at :datetime
# reset_password_token :string
# unconfirmed_email :string
# unlock_token :string
# created_at :datetime not null
# updated_at :datetime not null
# wedding_id :uuid not null
#
# Indexes
#
# index_users_on_confirmation_token (confirmation_token) UNIQUE
# index_users_on_email (email) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
# index_users_on_unlock_token (unlock_token) UNIQUE
# index_users_on_wedding_id (wedding_id)
#
# Foreign Keys
#
# fk_rails_... (wedding_id => weddings.id)
#
class User < ApplicationRecord
acts_as_tenant :wedding
devise :database_authenticatable, :registerable,
:recoverable, :validatable, :confirmable, :lockable
end

View File

@ -1,24 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
# == Schema Information
#
# Table name: weddings
#
# id :uuid not null, primary key
# slug :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_weddings_on_slug (slug) UNIQUE
#
class Wedding < ApplicationRecord
SLUG_REGEX = /[a-z\d-]+/
validates :slug, presence: true, uniqueness: true, format: { with: /\A#{SLUG_REGEX}\z/ }
end

View File

@ -1,52 +1,48 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# 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
end end

View File

@ -1,33 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module Groups
class SummaryQuery
def call
Group.left_joins(:guests).group(:id).pluck_to_hash(
:id,
:name,
:icon,
:parent_id,
:color,
*count_expressions
)
end
private
def count_expressions
[
Arel.sql('count(*) filter (where status IS NOT NULL) as total'),
Arel.sql('count(*) filter (where status = 0) as considered'),
Arel.sql('count(*) filter (where status = 10) as invited'),
Arel.sql('count(*) filter (where status = 20) as confirmed'),
Arel.sql('count(*) filter (where status = 30) as declined'),
Arel.sql('count(*) filter (where status = 40) as tentative')
]
end
end
end

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class SerializableGroup < JSONAPI::Serializable::Resource class SerializableGroup < JSONAPI::Serializable::Resource
type 'group' type 'group'

View File

@ -1,16 +1,12 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class SerializableGuest < JSONAPI::Serializable::Resource class SerializableGuest < JSONAPI::Serializable::Resource
type 'guest' type 'guest'
attributes :id, :group_id, :status attributes :id, :email, :group_id, :status
attribute :name do attribute :name do
@object.name "#{@object.first_name} #{@object.last_name}"
end end
attribute :group_name do attribute :group_name do

View File

@ -1,79 +1,29 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class AffinityGroupsHierarchy < Array class AffinityGroupsHierarchy < Array
DEFAULT_DISCOMFORT = 1 include Singleton
def initialize def initialize
super super
@references = {} @references = {}
Group.roots.each do |group|
self << group.id
hydrate(group)
end
load_discomforts
freeze
end end
def find(id) def find(name)
@references[id] @references[name]
end end
def <<(id) def <<(name)
new_node = Tree::TreeNode.new(id) new_node = Tree::TreeNode.new(name)
super(new_node).tap { @references[id] = new_node } super(new_node).tap { @references[name] = new_node }
end end
def register_child(parent_id, child_id) def register_child(parent_name, child_name)
@references[parent_id] << Tree::TreeNode.new(child_id).tap { |child_node| @references[child_id] = child_node } @references[parent_name] << Tree::TreeNode.new(child_name).tap { |child_node| @references[child_name] = child_node }
end end
def distance(id_a, id_b) def distance(name_a, name_b)
return nil if @references[id_a].nil? || @references[id_b].nil? return nil if @references[name_a].nil? || @references[name_b].nil?
@references[id_a].distance_to_common_ancestor(@references[id_b]) @references[name_a].distance_to_common_ancestor(@references[name_b])
end
def discomfort(id_a, id_b)
return 0 if id_a == id_b
@discomforts[uuid_to_int(id_a) + uuid_to_int(id_b)] || DEFAULT_DISCOMFORT
end
def default_discomfort(id_a, id_b)
return 0 if id_a == id_b
dist = distance(id_a, id_b)
return DEFAULT_DISCOMFORT if dist.nil?
Rational(dist, dist + 1)
end
private
def load_discomforts
@load_discomforts ||= GroupAffinity.pluck(:group_a_id, :group_b_id,
:discomfort).each_with_object({}) do |(id_a, id_b, discomfort), acc|
acc[uuid_to_int(id_a) + uuid_to_int(id_b)] = discomfort
end
end
def uuid_to_int(uuid)
uuid.gsub('-', '').hex
end
def hydrate(group)
group.children.each do |child|
register_child(group.id, child.id)
hydrate(child)
end
end end
end end

View File

@ -1,22 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
class LibreCaptcha
def id
HTTParty.post('http://libre-captcha:8888/v2/captcha',
body: {
input_type: 'text',
level: :hard,
media: 'image/png',
size: '350x100'
}.to_json).then { |raw| JSON.parse(raw)['id'] }
end
def valid?(id:, answer:)
HTTParty.post('http://libre-captcha:8888/v2/answer',
body: { id:, answer: }.to_json).then { |raw| JSON.parse(raw)['result'] == 'True' }
end
end

View File

@ -1,56 +1,26 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# 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 cohesion_penalty
end
def breakdown
@breakdown ||= { table_size_penalty:, cohesion_penalty: }
end end
private private
#
# Calculates the penalty associated with violating the table size constraints. The penalty is
# zero when the limits are honored, and it increases linearly as the number of guests deviates
# from the limits. Overcapacity is penalized more severely than undercapacity.
#
# @return [Number] The penalty associated with violating the table size constraints.
#
def table_size_penalty
case table.size
when 0...table.min_per_table then 5 * (table.min_per_table - table.size)
when table.min_per_table..table.max_per_table then 0
else 5 * (table.size - table.max_per_table)
end
end
def cohesion_penalty def cohesion_penalty
10 * (cohesion_discomfort * 1.0 / table.size) table.map { |guest| guest.affinity_group_list.first }.tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)|
end distance = AffinityGroupsHierarchy.instance.distance(a, b)
# next count_a * count_b if distance.nil?
# Calculates the discomfort of the table based on the cohesion of the guests. The total discomfort next 0 if distance.zero?
# is calculated as the sum of the discomfort of each pair of guests. The discomfort of a pair of
# guests is a rational number between 1 (unrelated groups) and 0 (same group). count_a * count_b * Rational(distance, distance + 1)
#
# @return [Number] Total discomfort of the table.
#
def cohesion_discomfort
table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)|
count_a * count_b * hierarchy.discomfort(a, b)
end end
end end
end end

View File

@ -1,29 +1,21 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
require_relative '../../extensions/tree_node_extension' require_relative '../../extensions/tree_node_extension'
module Tables module Tables
class Distribution class Distribution
attr_accessor :tables, :min_per_table, :max_per_table attr_accessor :tables
def initialize(min_per_table:, max_per_table:) def initialize(min_per_table:, max_per_table:)
@min_per_table = min_per_table @min_per_table = min_per_table
@max_per_table = max_per_table @max_per_table = max_per_table
@hierarchy = AffinityGroupsHierarchy.new
@tables = [] @tables = []
end end
def random_distribution(people) def random_distribution(people)
min_tables = (people.count * 1.0 / @max_per_table).ceil @tables = []
max_tables = (people.count * 1.0 / @min_per_table).ceil
@tables = people.in_groups(rand(min_tables..max_tables), false) @tables << Table.new(people.slice!(0..rand(@min_per_table..@max_per_table))) while people.any?
.map { |group| Table.new(group) }
.each { |table| table.min_per_table = @min_per_table }
.each { |table| table.max_per_table = @max_per_table }
end end
def discomfort def discomfort
@ -36,6 +28,12 @@ module Tables
"#{@tables.count} tables, discomfort: #{discomfort}" "#{@tables.count} tables, discomfort: #{discomfort}"
end end
def pretty_print
@tables.map.with_index do |table, i|
"Table #{i + 1} (#{table.count} ppl): (#{local_discomfort(table)}) #{table.map(&:full_name).join(', ')}"
end.join("\n")
end
def deep_dup def deep_dup
self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table).tap do |new_distribution| self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table).tap do |new_distribution|
new_distribution.tables = @tables.map(&:dup) new_distribution.tables = @tables.map(&:dup)
@ -63,7 +61,7 @@ module Tables
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

View File

@ -1,34 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module Tables
class Shift
private attr_reader :initial_solution
def initialize(initial_solution)
@initial_solution = initial_solution
end
def each
@initial_solution.tables.permutation(2) do |table_a, table_b|
table_a.dup.each do |person|
original_discomfort_a = table_a.reset
original_discomfort_b = table_b.reset
table_a.delete(person)
table_b << person
yield(@initial_solution)
ensure
table_b.delete(person)
table_a << person
table_a.discomfort = original_discomfort_a
table_b.discomfort = original_discomfort_b
end
end
end
end
end

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module Tables module Tables
class Swap class Swap
private attr_reader :initial_solution private attr_reader :initial_solution
@ -13,7 +9,7 @@ module Tables
def each def each
@initial_solution.tables.combination(2) do |table_a, table_b| @initial_solution.tables.combination(2) do |table_a, table_b|
table_a.to_a.product(table_b.to_a).each do |(person_a, person_b)| table_a.product(table_b).each do |(person_a, person_b)|
original_discomfort_a = table_a.reset original_discomfort_a = table_a.reset
original_discomfort_b = table_b.reset original_discomfort_b = table_b.reset

View File

@ -1,13 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module Tables module Tables
class Table < Set class Table < Array
attr_accessor :discomfort, :min_per_table, :max_per_table attr_accessor :discomfort
def initialize(*args) def initialize(*args)
super super
reset reset
@ -19,4 +14,4 @@ module Tables
original_discomfort original_discomfort
end end
end end
end end

View File

@ -1,18 +1,7 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
module VNS module VNS
class Engine class Engine
class << self
def sequence(elements)
elements = elements.to_a
(elements + elements.reverse).chunk(&:itself).map(&:first)
end
end
def target_function(&function) def target_function(&function)
@target_function = function @target_function = function
end end
@ -32,31 +21,29 @@ module VNS
@best_solution = @initial_solution @best_solution = @initial_solution
@best_score = @target_function.call(@best_solution) @best_score = @target_function.call(@best_solution)
self.class.sequence(@perturbations).each do |perturbation| puts "Initial score: #{@best_score.to_f}"
optimize(perturbation)
@perturbations.each do |perturbation|
puts "Running perturbation: #{perturbation.name}"
optimize(perturbation.new(@best_solution))
end end
@best_solution @best_solution
end end
private private
def optimize(perturbation_klass) def optimize(perturbation)
loop do perturbation.each do |alternative_solution|
optimized = false score = @target_function.call(alternative_solution)
next if score >= @best_score
perturbation_klass.new(@best_solution).each do |alternative_solution| @best_solution = alternative_solution.deep_dup
score = @target_function.call(alternative_solution) @best_score = score
next if score >= @best_score
@best_solution = alternative_solution.deep_dup puts "New lowest score: #{@best_score.to_f}"
@best_score = score
optimized = true
break return optimize(perturbation.class.new(@best_solution))
end
return unless optimized
end end
end end
end end

View File

@ -0,0 +1,19 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<div id="<%= dom_id expense %>">
<p>
<strong>Name:</strong>
<%= expense.name %>
</p>
<p>
<strong>Amount:</strong>
<%= expense.amount %>
</p>
<p>
<strong>Pricing type:</strong>
<%= expense.pricing_type %>
</p>
</div>

View File

@ -0,0 +1,34 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<%= form_with(model: expense) do |form| %>
<% if expense.errors.any? %>
<div style="color: red">
<h2><%= pluralize(expense.errors.count, "error") %> prohibited this expense from being saved:</h2>
<ul>
<% expense.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name %>
</div>
<div>
<%= form.label :amount, style: "display: block" %>
<%= form.text_field :amount %>
</div>
<div>
<%= form.label :pricing_type, style: "display: block" %>
<%= form.text_field :pricing_type %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>

View File

@ -0,0 +1,12 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<h1>Editing expense</h1>
<%= render "form", expense: @expense %>
<br>
<div>
<%= link_to "Show this expense", @expense %> |
<%= link_to "Back to expenses", expenses_path %>
</div>

View File

@ -0,0 +1,30 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<p style="color: green"><%= notice %></p>
<h1>Expenses</h1>
<div id="expenses">
<table>
<tr>
<th>Name</th>
<th>Amount</th>
<th colspan="2"></th>
</tr>
<% @expenses.each do |expense| %>
<tr>
<td><%= expense.name %></td>
<td><%= expense.amount.to_currency %></td>
<td><%= link_to "Show", expense %></td>
<td><%= link_to "Edit", edit_expense_path(expense) %></td>
</tr>
<% end %>
<tr>
<td>Total</td>
<td><%= @expenses.sum(&:amount).to_currency %></td>
<td colspan="2"></td>
</tr>
</table>
</div>
<%= link_to "New expense", new_expense_path %>

View File

@ -0,0 +1,11 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<h1>New expense</h1>
<%= render "form", expense: @expense %>
<br>
<div>
<%= link_to "Back to expenses", expenses_path %>
</div>

View File

@ -0,0 +1,12 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<p style="color: green"><%= notice %></p>
<%= render @expense %>
<div>
<%= link_to "Edit this expense", edit_expense_path(@expense) %> |
<%= link_to "Back to expenses", expenses_path %>
<%= button_to "Destroy this expense", @expense, method: :delete %>
</div>

View File

@ -0,0 +1,39 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<%= form_with(model: guest) do |form| %>
<% if guest.errors.any? %>
<div style="color: red">
<h2><%= pluralize(guest.errors.count, "error") %> prohibited this guest from being saved:</h2>
<ul>
<% guest.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :first_name, style: "display: block" %>
<%= form.text_field :first_name %>
</div>
<div>
<%= form.label :last_name, style: "display: block" %>
<%= form.text_field :last_name %>
</div>
<div>
<%= form.label :email, style: "display: block" %>
<%= form.text_field :email %>
</div>
<div>
<%= form.label :phone, style: "display: block" %>
<%= form.text_field :phone %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>

View File

@ -0,0 +1,24 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<div id="<%= dom_id guest %>">
<p>
<strong>First name:</strong>
<%= guest.first_name %>
</p>
<p>
<strong>Last name:</strong>
<%= guest.last_name %>
</p>
<p>
<strong>Email:</strong>
<%= guest.email %>
</p>
<p>
<strong>Phone:</strong>
<%= guest.phone %>
</p>
</div>

View File

@ -0,0 +1,12 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<h1>Editing guest</h1>
<%= render "form", guest: @guest %>
<br>
<div>
<%= link_to "Show this guest", @guest %> |
<%= link_to "Back to guests", guests_path %>
</div>

View File

@ -0,0 +1,39 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<p style="color: green"><%= notice %></p>
<h1>Guests</h1>
<div id="guests">
<table>
<tr>
<th>Row #</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Affinity groups</th>
<th>Unbreakable bonds</th>
<th colspan="2"></th>
</tr>
<% @guests.each_with_index do |guest, i| %>
<tr>
<td><%= i + 1 %></td>
<td><%= guest.full_name %></td>
<td><%= guest.email %></td>
<td><%= guest.phone %></td>
<td><%= guest.affinity_groups.pluck(:name).join(", ") %></td>
<td><%= guest.unbreakable_bonds.pluck(:name).join(", ") %></td>
<td><%= link_to "Show", guest %></td>
<td><%= link_to "Edit", edit_guest_path(guest) %></td>
</tr>
<% end %>
</table>
</div>
<%= link_to "New guest", new_guest_path %>
<%= form_with url: import_guests_path, method: :post do |form| %>
<%= form.label :file %>
<%= form.file_field :file %>
<%= form.submit "Import" %>
<% end %>

View File

@ -0,0 +1,11 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<h1>New guest</h1>
<%= render "form", guest: @guest %>
<br>
<div>
<%= link_to "Back to guests", guests_path %>
</div>

View File

@ -0,0 +1,12 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<p style="color: green"><%= notice %></p>
<%= render @guest %>
<div>
<%= link_to "Edit this guest", edit_guest_path(@guest) %> |
<%= link_to "Back to guests", guests_path %>
<%= button_to "Destroy this guest", @guest, method: :delete %>
</div>

View File

@ -0,0 +1,18 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<!DOCTYPE html>
<html>
<head>
<title>WeddingPlanner</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= yield %>
</body>
</html>

View File

@ -1,7 +1,5 @@
<%# Copyright (C) 2024 Manuel Bustillo %> <%# Copyright (C) 2024 Manuel Bustillo %>
<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>

View File

@ -1,5 +1,3 @@
<%# Copyright (C) 2024 Manuel Bustillo %> <%# Copyright (C) 2024 Manuel Bustillo %>
<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
<%= yield %> <%= yield %>

View File

@ -0,0 +1,11 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<h1>Tables arrangements</h1>
<ol>
<% @tables_arrangements.each_with_index do |tables_arrangement, i| %>
<li>
<p><%= link_to "Arrangement ##{i+1}", tables_arrangement_path(tables_arrangement) %> Discomfort: <%= tables_arrangement.discomfort %></p>
</li>
<% end %>
</ol>

View File

@ -0,0 +1,18 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<h1>ID: <%= @tables_arrangement.id %></h1>
<p>Discomfort: <%= @tables_arrangement.discomfort %></p>
<h2>Seats</h2>
<% @seats.each do |table_number, seats| %>
<h3>Table <%= table_number %></h3>
<ul>
<% seats.each do |seat| %>
<li><%= seat.guest.full_name %> (<%= seat.guest.affinity_groups.pluck(:name).join(", ") %>)</li>
<% end %>
</ul>
<% end %>

View File

@ -1,9 +0,0 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
<p>Welcome <%= @email %>!</p>
<p>You can confirm your account email through the link below:</p>
<p><%= link_to 'Confirm my account', confirmation_url(slug: ActsAsTenant.current_tenant&.slug, confirmation_token: @token) %></p>

View File

@ -1,11 +0,0 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
<p>Hello <%= @email %>!</p>
<% if @resource.try(:unconfirmed_email?) %>
<p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
<% else %>
<p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
<% end %>

View File

@ -1,7 +0,0 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
<p>Hello <%= @resource.email %>!</p>
<p>We're contacting you to notify you that your password has been changed.</p>

View File

@ -1,12 +0,0 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
<p>Hello <%= @resource.email %>!</p>
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
<p><%= link_to 'Change my password', edit_password_url(slug: ActsAsTenant.current_tenant&.slug, reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>

View File

@ -1,11 +0,0 @@
<%# Copyright (C) 2024 Manuel Bustillo %>
<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>
<p>Hello <%= @resource.email %>!</p>
<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
<p>Click the link below to unlock your account:</p>
<p><%= link_to 'Unlock my account', unlock_url(slug: ActsAsTenant.current_tenant&.slug, unlock_token: @token) %></p>

View File

@ -1,6 +0,0 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
require "solid_queue/cli"
SolidQueue::Cli.start(ARGV)

View File

@ -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

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
require_relative 'boot' require_relative 'boot'
require 'rails' require 'rails'
@ -32,9 +30,6 @@ module WeddingPlanner
# Common ones are `templates`, `generators`, or `middleware`, for example. # Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w[assets tasks]) config.autoload_lib(ignore: %w[assets tasks])
# Use a real queuing backend for Active Job (and separate queues per environment).
config.active_job.queue_adapter = :solid_queue
# Configuration for the application, engines, and railties goes here. # Configuration for the application, engines, and railties goes here.
# #
# These settings can be overridden in specific environments using the files # These settings can be overridden in specific environments using the files

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile. require "bundler/setup" # Set up gems listed in the Gemfile.

View File

@ -83,7 +83,6 @@ test:
# #
production: production:
<<: *default <<: *default
host: db
database: wedding_planner_production database: wedding_planner_production
username: wedding_planner username: wedding_planner
password: <%= ENV["WEDDING_PLANNER_DATABASE_PASSWORD"] %> password: <%= ENV["WEDDING_PLANNER_DATABASE_PASSWORD"] %>

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# Load the Rails application. # Load the Rails application.
require_relative "application" require_relative "application"

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
require "active_support/core_ext/integer/time" require "active_support/core_ext/integer/time"
Rails.application.configure do Rails.application.configure do
@ -42,10 +40,8 @@ Rails.application.configure do
# Don't care if the mailer can't send. # Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false
config.action_mailer.default_url_options = { host: 'libre-wedding-planner.app.localhost/api' }
config.action_mailer.delivery_method = :letter_opener_web
config.action_mailer.perform_deliveries = true
# Print deprecation notices to the Rails logger. # Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log config.active_support.deprecation = :log

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
require "active_support/core_ext/integer/time" require "active_support/core_ext/integer/time"
Rails.application.configure do Rails.application.configure do
@ -71,6 +69,8 @@ Rails.application.configure do
# Use a different cache store in production. # Use a different cache store in production.
# config.cache_store = :mem_cache_store # config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "wedding_planner_production" # config.active_job.queue_name_prefix = "wedding_planner_production"
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false
@ -94,9 +94,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"
# 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

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
require "active_support/core_ext/integer/time" require "active_support/core_ext/integer/time"
# The test environment is used exclusively to run your application's # The test environment is used exclusively to run your application's

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# Pin npm packages by running ./bin/importmap # Pin npm packages by running ./bin/importmap
pin "application" pin "application"

View File

@ -1,7 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
ActsAsTenant.configure do |config|
config.require_tenant = !Rails.env.test?
end

View File

@ -0,0 +1,41 @@
# Copyright (C) 2024 Manuel Bustillo
require_relative '../../app/services/affinity_groups_hierarchy'
hierarchy = AffinityGroupsHierarchy.instance
hierarchy << 'guests_a'
hierarchy << 'guests_b'
hierarchy << 'common_guests'
hierarchy.register_child('guests_a', 'family_a')
hierarchy.register_child('family_a', 'close_family_a')
hierarchy.register_child('family_a', 'cousins_a')
hierarchy.register_child('family_a', 'relatives_a')
hierarchy.register_child('guests_a', 'work_a')
hierarchy.register_child('work_a', 'besties_work_a')
hierarchy.register_child('guests_a', 'friends_a')
hierarchy.register_child('friends_a', 'college_friends_a')
hierarchy.register_child('friends_a', 'high_school_friends_a')
hierarchy.register_child('friends_a', 'childhood_friends_a')
hierarchy.register_child('guests_a', 'sports_a')
hierarchy.register_child('sports_a', 'basket_team_a')
hierarchy.register_child('sports_a', 'football_team_a')
hierarchy.register_child('guests_b', 'family_b')
hierarchy.register_child('family_b', 'close_family_b')
hierarchy.register_child('family_b', 'cousins_b')
hierarchy.register_child('family_b', 'relatives_b')
hierarchy.register_child('guests_b', 'work_b')
hierarchy.register_child('work_b', 'besties_work_b')
hierarchy.register_child('guests_b', 'friends_b')
hierarchy.register_child('friends_b', 'college_friends_b')
hierarchy.register_child('friends_b', 'high_school_friends_b')
hierarchy.register_child('friends_b', 'childhood_friends_b')
hierarchy.register_child('common_guests', 'dance_club')

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets. # Version of your assets, change this if you want to expire all your assets.

View File

@ -1,10 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
Chroma.define_palette :decreasing_saturation do
spin(20).desaturate(40)
spin(-20).desaturate(40)
spin(40).desaturate(40)
spin(-40).desaturate(40)
end

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
# Define an application-wide content security policy. # Define an application-wide content security policy.

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# config/initializers/cors.rb # config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do Rails.application.config.middleware.insert_before 0, Rack::Cors do

View File

@ -1,317 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# frozen_string_literal: true
# Assuming you have not yet modified this file, each configuration option below
# is set to its default value. Note that some are commented out while others
# are not: uncommented lines are intended to protect your configuration from
# breaking changes in upgrades (i.e., in the event that future versions of
# Devise change the default values for those options).
#
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
# The secret key used by Devise. Devise uses this key to generate
# random tokens. Changing this key will render invalid all existing
# confirmation, reset password and unlock tokens in the database.
# Devise will use the `secret_key_base` as its `secret_key`
# by default. You can change it below and use your own secret key.
# config.secret_key = '11353ae8c2bf66dd638d9edff9ec82856aecf74bba6c598273559a8750c902d3439da1b301e40c47578577a971f1058dbf37211c107fba5107c29baa654e9888'
# ==> Controller configuration
# Configure the parent class to the devise controllers.
# config.parent_controller = 'DeviseController'
# ==> Mailer Configuration
# Configure the e-mail address which will be shown in Devise::Mailer,
# note that it will be overwritten if you use your own mailer class
# with default "from" parameter.
config.mailer_sender = 'noreply@libreweddingplanner.org'
# Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer'
# Configure the parent class responsible to send e-mails.
# config.parent_mailer = 'ActionMailer::Base'
# ==> ORM configuration
# Load and configure the ORM. Supports :active_record (default) and
# :mongoid (bson_ext recommended) by default. Other ORMs may be
# available as additional gems.
require 'devise/orm/active_record'
# ==> Configuration for any authentication mechanism
# Configure which keys are used when authenticating a user. The default is
# just :email. You can configure it to use [:username, :subdomain], so for
# authenticating a user, both parameters are required. Remember that those
# parameters are used only when authenticating and not when retrieving from
# session. If you need permissions, you should implement that in a before filter.
# You can also supply a hash where the value is a boolean determining whether
# or not authentication should be aborted when the value is not present.
# config.authentication_keys = [:email]
# Configure parameters from the request object used for authentication. Each entry
# given should be a request method and it will automatically be passed to the
# find_for_authentication method and considered in your model lookup. For instance,
# if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
# The same considerations mentioned for authentication_keys also apply to request_keys.
# config.request_keys = []
# Configure which authentication keys should be case-insensitive.
# These keys will be downcased upon creating or modifying a user and when used
# to authenticate or find a user. Default is :email.
config.case_insensitive_keys = [:email]
# Configure which authentication keys should have whitespace stripped.
# These keys will have whitespace before and after removed upon creating or
# modifying a user and when used to authenticate or find a user. Default is :email.
config.strip_whitespace_keys = [:email]
# Tell if authentication through request.params is enabled. True by default.
# It can be set to an array that will enable params authentication only for the
# given strategies, for example, `config.params_authenticatable = [:database]` will
# enable it only for database (email + password) authentication.
# config.params_authenticatable = true
# Tell if authentication through HTTP Auth is enabled. False by default.
# It can be set to an array that will enable http authentication only for the
# given strategies, for example, `config.http_authenticatable = [:database]` will
# enable it only for database authentication.
# For API-only applications to support authentication "out-of-the-box", you will likely want to
# enable this with :database unless you are using a custom strategy.
# The supported strategies are:
# :database = Support basic authentication with authentication key + password
# config.http_authenticatable = false
# If 401 status code should be returned for AJAX requests. True by default.
# config.http_authenticatable_on_xhr = true
# The realm used in Http Basic Authentication. 'Application' by default.
# config.http_authentication_realm = 'Application'
# It will change confirmation, password recovery and other workflows
# to behave the same regardless if the e-mail provided was right or wrong.
# Does not affect registerable.
config.paranoid = true
# By default Devise will store the user in session. You can skip storage for
# particular strategies by setting this option.
# Notice that if you are skipping storage for all authentication paths, you
# may want to disable generating routes to Devise's sessions controller by
# passing skip: :sessions to `devise_for` in your config/routes.rb
config.skip_session_storage = [:http_auth]
# By default, Devise cleans up the CSRF token on authentication to
# avoid CSRF token fixation attacks. This means that, when using AJAX
# requests for sign in and sign up, you need to get a new CSRF token
# from the server. You can disable this option at your own risk.
# config.clean_up_csrf_token_on_authentication = true
# When false, Devise will not attempt to reload routes on eager load.
# This can reduce the time taken to boot the app but if your application
# requires the Devise mappings to be loaded during boot time the application
# won't boot properly.
# config.reload_routes = true
# ==> Configuration for :database_authenticatable
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
# using other algorithms, it sets how many times you want the password to be hashed.
# The number of stretches used for generating the hashed password are stored
# with the hashed password. This allows you to change the stretches without
# invalidating existing passwords.
#
# Limiting the stretches to just one in testing will increase the performance of
# your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
# a value less than 10 in other environments. Note that, for bcrypt (the default
# algorithm), the cost increases exponentially with the number of stretches (e.g.
# a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
config.stretches = Rails.env.test? ? 1 : 12
# Set up a pepper to generate the hashed password.
# config.pepper = '6f86425fd587f80f4a338a785a6abbbccf8de7322f70fcccf356118d982942c9421819445f9d236a296fa3c431ef5e509be20e6db03f90ec2b42aa78f3a7e526'
# Send a notification to the original email when the user's email is changed.
config.send_email_changed_notification = false
# Send a notification email when the user's password is changed.
config.send_password_change_notification = false
# ==> Configuration for :confirmable
# A period that the user is allowed to access the website even without
# confirming their account. For instance, if set to 2.days, the user will be
# able to access the website for two days without confirming their account,
# access will be blocked just in the third day.
# You can also set it to nil, which will allow the user to access the website
# without confirming their account.
# Default is 0.days, meaning the user cannot access the website without
# confirming their account.
# config.allow_unconfirmed_access_for = 2.days
# A period that the user is allowed to confirm their account before their
# token becomes invalid. For example, if set to 3.days, the user can confirm
# their account within 3 days after the mail was sent, but on the fourth day
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
config.confirm_within = 3.days
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email
# db field (see migrations). Until confirmed, new email is stored in
# unconfirmed_email column, and copied to email column on successful confirmation.
config.reconfirmable = true
# Defines which key will be used when confirming an account
# config.confirmation_keys = [:email]
# ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again.
# config.remember_for = 2.weeks
# Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true
# If true, extends the user's remember period when remembered via cookie.
# config.extend_remember_period = false
# Options to be passed to the created cookie. For instance, you can set
# secure: true in order to force SSL only cookies.
# config.rememberable_options = {}
# ==> Configuration for :validatable
# Range for password length.
config.password_length = 15..128
# Email regex used to validate email formats. It simply asserts that
# one (and only one) @ exists in the given string. This is mainly
# to give user feedback and not to assert the e-mail validity.
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
# ==> Configuration for :timeoutable
# The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again. Default is 30 minutes.
# config.timeout_in = 30.minutes
# ==> Configuration for :lockable
# Defines which strategy will be used to lock an account.
# :failed_attempts = Locks an account after a number of failed attempts to sign in.
# :none = No lock strategy. You should handle locking by yourself.
config.lock_strategy = :failed_attempts
# Defines which key will be used when locking and unlocking an account
# config.unlock_keys = [:email]
# Defines which strategy will be used to unlock an account.
# :email = Sends an unlock link to the user email
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
# :both = Enables both strategies
# :none = No unlock strategy. You should handle unlocking by yourself.
config.unlock_strategy = :both
# Number of authentication tries before locking an account if lock_strategy
# is failed attempts.
config.maximum_attempts = 10
# Time interval to unlock the account if :time is enabled as unlock_strategy.
config.unlock_in = 1.hour
# Warn on the last attempt before the account is locked.
# config.last_attempt_warning = true
# ==> Configuration for :recoverable
#
# Defines which key will be used when recovering the password for an account
# config.reset_password_keys = [:email]
# Time interval you can reset your password with a reset password key.
# Don't put a too small interval or your users won't have the time to
# change their passwords.
config.reset_password_within = 6.hours
# When set to false, does not sign a user in automatically after their password is
# reset. Defaults to true, so a user is signed in automatically after a reset.
config.sign_in_after_reset_password = true
# ==> Configuration for :encryptable
# Allow you to use another hashing or encryption algorithm besides bcrypt (default).
# You can use :sha1, :sha512 or algorithms from others authentication tools as
# :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20
# for default behavior) and :restful_authentication_sha1 (then you should set
# stretches to 10, and copy REST_AUTH_SITE_KEY to pepper).
#
# Require the `devise-encryptable` gem when using anything other than bcrypt
# config.encryptor = :sha512
# ==> Scopes configuration
# Turn scoped views on. Before rendering "sessions/new", it will first check for
# "users/sessions/new". It's turned off by default because it's slower if you
# are using only default views.
config.scoped_views = true
# Configure the default scope given to Warden. By default it's the first
# devise role declared in your routes (usually :user).
# config.default_scope = :user
# Set this configuration to false if you want /users/sign_out to sign out
# only the current scope. By default, Devise signs out all scopes.
# config.sign_out_all_scopes = true
# ==> Navigation configuration
# Lists the formats that should be treated as navigational. Formats like
# :html should redirect to the sign in page when the user does not have
# access, but formats like :xml or :json, should return 401.
#
# If you have any extra navigational formats, like :iphone or :mobile, you
# should add them to the navigational formats lists.
#
# The "*/*" below is required to match Internet Explorer requests.
# config.navigational_formats = ['*/*', :html, :turbo_stream]
# The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = :delete
# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
# config.warden do |manager|
# manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
# ==> Mountable engine configurations
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
# is mountable, there are some extra configurations to be taken into account.
# The following options are available, assuming the engine is mounted as:
#
# mount MyEngine, at: '/my_engine'
#
# The router that invoked `devise_for`, in the example above, would be:
# config.router_name = :my_engine
#
# When using OmniAuth, Devise cannot automatically set OmniAuth path,
# so you need to do it manually. For the users scope, it would be:
# config.omniauth_path_prefix = '/my_engine/users/auth'
# ==> Hotwire/Turbo configuration
# When using Devise with Hotwire/Turbo, the http status for error responses
# and some redirects must match the following. The default in Devise for existing
# apps is `200 OK` and `302 Found` respectively, but new apps are generated with
# these new defaults that match Hotwire/Turbo behavior.
# Note: These might become the new default in future versions of Devise.
config.responder.error_status = :unprocessable_entity
config.responder.redirect_status = :see_other
# ==> Configuration for :registerable
# When set to false, does not sign a user in automatically after their password is
# changed. Defaults to true, so a user is signed in automatically after changing a password.
# config.sign_in_after_change_password = true
end

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.

Some files were not shown because too many files have changed in this diff Show More