Compare commits

..

2 Commits

Author SHA1 Message Date
d2b5f92636 Add copyright notice
All checks were successful
Add copyright notice / copyright_notice (pull_request) Successful in 1m18s
Run unit tests / unit_tests (pull_request) Successful in 1m44s
Build Nginx-based docker image / build-static-assets (pull_request) Successful in 44m9s
2024-10-31 23:37:06 +00:00
Renovate Bot
423bda7a86 Update dependency rubocop to v1.68.0
All checks were successful
Add copyright notice / copyright_notice (pull_request) Successful in 3m33s
Run unit tests / unit_tests (pull_request) Successful in 7m59s
Build Nginx-based docker image / build-static-assets (pull_request) Successful in 1h53m17s
2024-10-31 23:08:34 +00:00
88 changed files with 741 additions and 1889 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

@ -3,13 +3,10 @@ on:
push: push:
branches: branches:
- main - main
concurrency: pull_request:
group: ${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build-static-assets: build-static-assets:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:

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

View File

@ -1,23 +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.202.0
with:
ruby-version: '3.3.6'
- name: Install project dependencies
run: bundle install --jobs `getconf _NPROCESSORS_ONLN`
- name: Run license finder
run: license_finder

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,7 +19,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- uses: ruby/setup-ruby@v1.202.0 - uses: ruby/setup-ruby@v1
- run: bundle install - run: bundle install
- name: Wait until Postgres is ready to accept connections - name: Wait until Postgres is ready to accept connections
run: | run: |

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 +1 @@
ruby-3.3.6 ruby-3.3.5

View File

@ -1,7 +1,7 @@
# syntax = docker/dockerfile:1 # syntax = docker/dockerfile:1
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.3.6 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

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.3.6
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
# 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"]

View File

@ -1,29 +1,29 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.2.2) actioncable (7.2.1.2)
actionpack (= 7.2.2) actionpack (= 7.2.1.2)
activesupport (= 7.2.2) 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 (7.2.2) actionmailbox (7.2.1.2)
actionpack (= 7.2.2) actionpack (= 7.2.1.2)
activejob (= 7.2.2) activejob (= 7.2.1.2)
activerecord (= 7.2.2) activerecord (= 7.2.1.2)
activestorage (= 7.2.2) activestorage (= 7.2.1.2)
activesupport (= 7.2.2) activesupport (= 7.2.1.2)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (7.2.2) actionmailer (7.2.1.2)
actionpack (= 7.2.2) actionpack (= 7.2.1.2)
actionview (= 7.2.2) actionview (= 7.2.1.2)
activejob (= 7.2.2) activejob (= 7.2.1.2)
activesupport (= 7.2.2) activesupport (= 7.2.1.2)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.2.2) actionpack (7.2.1.2)
actionview (= 7.2.2) actionview (= 7.2.1.2)
activesupport (= 7.2.2) activesupport (= 7.2.1.2)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4, < 3.2) rack (>= 2.2.4, < 3.2)
@ -32,37 +32,36 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (7.2.2) actiontext (7.2.1.2)
actionpack (= 7.2.2) actionpack (= 7.2.1.2)
activerecord (= 7.2.2) activerecord (= 7.2.1.2)
activestorage (= 7.2.2) activestorage (= 7.2.1.2)
activesupport (= 7.2.2) activesupport (= 7.2.1.2)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.2.2) actionview (7.2.1.2)
activesupport (= 7.2.2) 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 (7.2.2) activejob (7.2.1.2)
activesupport (= 7.2.2) activesupport (= 7.2.1.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.2.2) activemodel (7.2.1.2)
activesupport (= 7.2.2) activesupport (= 7.2.1.2)
activerecord (7.2.2) activerecord (7.2.1.2)
activemodel (= 7.2.2) activemodel (= 7.2.1.2)
activesupport (= 7.2.2) activesupport (= 7.2.1.2)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.2.2) activestorage (7.2.1.2)
actionpack (= 7.2.2) actionpack (= 7.2.1.2)
activejob (= 7.2.2) activejob (= 7.2.1.2)
activerecord (= 7.2.2) activerecord (= 7.2.1.2)
activesupport (= 7.2.2) activesupport (= 7.2.1.2)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.2.2) 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)
@ -72,8 +71,8 @@ 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)
acts-as-taggable-on (12.0.0) acts-as-taggable-on (11.0.0)
activerecord (>= 7.1, < 8.1) activerecord (>= 7.0, < 8.0)
zeitwerk (>= 2.4, < 3.0) zeitwerk (>= 2.4, < 3.0)
ast (2.4.2) ast (2.4.2)
babel-source (5.8.35) babel-source (5.8.35)
@ -81,7 +80,6 @@ GEM
babel-source (>= 4.0, < 6) babel-source (>= 4.0, < 6)
execjs (~> 2.0) execjs (~> 2.0)
base64 (0.2.0) base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.8) bigdecimal (3.1.8)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.18.4)
@ -92,7 +90,7 @@ GEM
connection_pool (2.4.1) connection_pool (2.4.1)
crass (1.0.6) crass (1.0.6)
csv (3.3.0) csv (3.3.0)
date (3.4.0) date (3.3.4)
debug (1.9.2) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
@ -136,7 +134,7 @@ GEM
jsonapi-renderer (~> 0.2.0) jsonapi-renderer (~> 0.2.0)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
logger (1.6.1) logger (1.6.1)
loofah (2.23.1) loofah (2.22.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@ -145,13 +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)
minitest (5.25.2) minitest (5.25.1)
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)
net-imap (0.5.1) net-imap (0.4.17)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@ -160,7 +158,7 @@ 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.16.7-aarch64-linux) nokogiri (1.16.7-aarch64-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-arm-linux) nokogiri (1.16.7-arm-linux)
@ -178,12 +176,12 @@ GEM
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.9) pg (1.5.9)
pry (0.15.0) pry (0.14.2)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
psych (5.2.0) psych (5.1.2)
stringio stringio
puma (6.5.0) puma (6.4.3)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.8) rack (3.1.8)
@ -193,22 +191,23 @@ 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 (7.2.2) webrick (~> 1.8)
actioncable (= 7.2.2) rails (7.2.1.2)
actionmailbox (= 7.2.2) actioncable (= 7.2.1.2)
actionmailer (= 7.2.2) actionmailbox (= 7.2.1.2)
actionpack (= 7.2.2) actionmailer (= 7.2.1.2)
actiontext (= 7.2.2) actionpack (= 7.2.1.2)
actionview (= 7.2.2) actiontext (= 7.2.1.2)
activejob (= 7.2.2) actionview (= 7.2.1.2)
activemodel (= 7.2.2) activejob (= 7.2.1.2)
activerecord (= 7.2.2) activemodel (= 7.2.1.2)
activestorage (= 7.2.2) activerecord (= 7.2.1.2)
activesupport (= 7.2.2) activestorage (= 7.2.1.2)
activesupport (= 7.2.1.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.2.2) 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
@ -216,9 +215,9 @@ GEM
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (~> 1.14)
railties (7.2.2) railties (7.2.1.2)
actionpack (= 7.2.2) actionpack (= 7.2.1.2)
activesupport (= 7.2.2) 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)
@ -239,7 +238,7 @@ GEM
redis-client (0.22.2) redis-client (0.22.2)
connection_pool connection_pool
regexp_parser (2.9.2) regexp_parser (2.9.2)
reline (0.5.11) reline (0.5.10)
io-console (~> 0.5) io-console (~> 0.5)
rspec-core (3.13.2) rspec-core (3.13.2)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
@ -249,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.0.2) rspec-rails (7.0.1)
actionpack (>= 7.0) actionpack (>= 7.0)
activesupport (>= 7.0) activesupport (>= 7.0)
railties (>= 7.0) railties (>= 7.0)
@ -273,7 +272,7 @@ GEM
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
rubytree (2.1.0) rubytree (2.1.0)
json (~> 2.0, > 2.3.1) json (~> 2.0, > 2.3.1)
securerandom (0.3.2) securerandom (0.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)
@ -283,10 +282,10 @@ 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.2) timeout (0.4.1)
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)
@ -299,6 +298,7 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
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)

View File

@ -1,87 +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/dashboard to load 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,43 +1,15 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
after_action :set_csrf_cookie 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 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: Rails.env.production?,
same_site: :strict
}
end
end end

View File

@ -1,22 +1,76 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
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.all.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
@expense = Expense.new(expense_params)
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
# 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
# DELETE /expenses/1 or /expenses/1.json
def destroy
@expense.destroy!
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.require(:expense).permit(:name, :amount, :pricing_type) def expense_params
end params.require(:expense).permit(:name, :amount, :pricing_type)
end
end end

View File

@ -2,6 +2,7 @@
class GroupsController < ApplicationController class GroupsController < ApplicationController
def index def index
render json: Groups::SummaryQuery.new.call.as_json roots = Group.where(parent_id: nil)
render jsonapi: roots, include: [children: [children: [:children]]]
end end
end end

View File

@ -3,31 +3,94 @@
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.all.includes(:group) @guests = Guest.all
.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.require(:guest).permit(:name, :group_id, :status) params.require(:guest).permit(:first_name, :last_name, :email, :phone)
end end
end end

View File

@ -2,22 +2,13 @@
class TablesArrangementsController < ApplicationController class TablesArrangementsController < ApplicationController
def index def index
render json: TablesArrangement.all.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
Seat.joins(guest: :group) @tables_arrangement = TablesArrangement.find(params[:id])
.where(tables_arrangement_id: params[:id]) @seats = @tables_arrangement.seats
.order('guests.group_id') .includes(guest: %i[affinity_groups unbreakable_bonds])
.pluck( .group_by(&:table_number)
:table_number,
'guests.name',
'guests.id',
'groups.color'
)
.group_by(&:first)
.transform_values { |table| table.map { |(_, name, id, color)| { id:, name:, color: } } }
.map { |number, guests| { number:, guests: } }
.then { |result| render json: result }
end end
end end

View File

@ -1,22 +1,4 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == 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
#
class Expense < ApplicationRecord class Expense < ApplicationRecord
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,27 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == 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
#
# Indexes
#
# index_groups_on_name (name) UNIQUE
# index_groups_on_parent_id (parent_id)
#
# Foreign Keys
#
# fk_rails_... (parent_id => groups.id)
#
class Group < ApplicationRecord class Group < ApplicationRecord
validates :name, uniqueness: true validates :name, uniqueness: true
validates :name, :order, presence: true validates :name, :order, presence: true
@ -29,33 +7,5 @@ class Group < ApplicationRecord
has_many :children, class_name: 'Group', foreign_key: 'parent_id' 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
scope :roots, -> { where(parent_id: nil) }
has_many :guests has_many :guests
def colorize_children(generation = 1)
derived_colors = generation == 1 ? color.paint.palette.analogous(size: children.count) : color.paint.palette.decreasing_saturation
children.zip(derived_colors) do |child, raw_color|
final_color = raw_color.paint
final_color.brighten(60) if final_color.dark?
child.update!(color: final_color)
child.colorize_children(generation + 1)
end
end
private
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,50 +1,18 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == 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 not null
#
# Indexes
#
# index_guests_on_group_id (group_id)
#
# Foreign Keys
#
# fk_rails_... (group_id => groups.id)
#
class Guest < ApplicationRecord class Guest < ApplicationRecord
acts_as_taggable_on :affinity_groups, :unbreakable_bonds
belongs_to :group 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_save :recalculate_simulations, if: :saved_change_to_status?
after_destroy :recalculate_simulations
has_many :seats, dependent: :delete_all
private
def recalculate_simulations
TablesArrangement.delete_all
ActiveJob.perform_all_later(50.times.map { TableSimulatorJob.new })
end end
end end

View File

@ -1,26 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == 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
#
# Indexes
#
# index_seats_on_guest_id (guest_id)
# index_seats_on_tables_arrangement_id (tables_arrangement_id)
#
# Foreign Keys
#
# fk_rails_... (guest_id => guests.id)
# fk_rails_... (tables_arrangement_id => tables_arrangements.id) ON DELETE => cascade
#
class Seat < ApplicationRecord class Seat < ApplicationRecord
belongs_to :guest belongs_to :guest
belongs_to :table_arrangement belongs_to :table_arrangement

View File

@ -1,24 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# == 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
#
class TablesArrangement < ApplicationRecord class TablesArrangement < ApplicationRecord
has_many :seats has_many :seats
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,31 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
module Groups
class SummaryQuery
def call
ActiveRecord::Base.connection.execute(query).to_a
end
private
def query
<<~SQL.squish
SELECT
groups.id,
groups.name,
groups.icon,
groups.parent_id,
groups.color,
count(*) filter (where status IS NOT NULL) as total,
count(*) filter (where status = 0) as considered,
count(*) filter (where status = 10) as invited,
count(*) filter (where status = 20) as confirmed,
count(*) filter (where status = 30) as declined,
count(*) filter (where status = 40) as tentative
FROM groups
LEFT JOIN guests on groups.id = guests.group_id
GROUP BY groups.id
SQL
end
end
end

View File

@ -3,10 +3,10 @@
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

@ -6,40 +6,24 @@ class AffinityGroupsHierarchy < Array
def initialize def initialize
super super
@references = {} @references = {}
Group.roots.each do |group|
self << group.id
hydrate(group)
end
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
private
def hydrate(group)
group.children.each do |child|
register_child(group.id, child.id)
hydrate(child)
end
end end
end end

View File

@ -3,40 +3,18 @@
module Tables module Tables
class DiscomfortCalculator class DiscomfortCalculator
private attr_reader :table private attr_reader :table
def initialize(table:) def initialize(table)
@table = table @table = table
end end
def calculate def calculate
table_size_penalty + 10 * (cohesion_penalty * 1.0 / table.size) 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
#
# Calculates the discomfort of the table based on the cohesion of the guests. The total discomfort
# is calculated as the sum of the discomfort of each pair of guests. The discomfort of a pair of
# guests is a rational number between 1 (unrelated groups) and 0 (same group).
#
# @return [Number] Total discomfort of the table.
#
def cohesion_penalty def cohesion_penalty
table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)| table.map { |guest| guest.affinity_group_list.first }.tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)|
distance = AffinityGroupsHierarchy.instance.distance(a, b) distance = AffinityGroupsHierarchy.instance.distance(a, b)
next count_a * count_b if distance.nil? next count_a * count_b if distance.nil?

View File

@ -4,7 +4,7 @@ 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
@ -13,12 +13,9 @@ module 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
@ -33,7 +30,7 @@ module Tables
def pretty_print def pretty_print
@tables.map.with_index do |table, i| @tables.map.with_index do |table, i|
"Table #{i + 1} (#{table.count} ppl): (#{local_discomfort(table)}) #{table.map(&:name).join(', ')}" "Table #{i + 1} (#{table.count} ppl): (#{local_discomfort(table)}) #{table.map(&:full_name).join(', ')}"
end.join("\n") end.join("\n")
end end
@ -64,7 +61,7 @@ module Tables
private private
def local_discomfort(table) def local_discomfort(table)
table.discomfort ||= DiscomfortCalculator.new(table:).calculate table.discomfort ||= DiscomfortCalculator.new(table).calculate
end end
end end
end end

View File

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

@ -9,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,9 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
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
@ -15,4 +14,4 @@ module Tables
original_discomfort original_discomfort
end end
end end
end end

View File

@ -2,13 +2,6 @@
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
@ -30,12 +23,12 @@ module VNS
puts "Initial score: #{@best_score.to_f}" puts "Initial score: #{@best_score.to_f}"
self.class.sequence(@perturbations).each do |perturbation| @perturbations.each do |perturbation|
puts "Running perturbation: #{perturbation.name}" puts "Running perturbation: #{perturbation.name}"
optimize(perturbation.new(@best_solution)) optimize(perturbation.new(@best_solution))
end end
@best_solution @best_solution
end end
private private

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

@ -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,6 +0,0 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
require "solid_queue/cli"
SolidQueue::Cli.start(ARGV)

View File

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

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

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,8 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
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,16 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
Rswag::Api.configure do |c|
# Specify a root folder where Swagger JSON files are located
# This is used by the Swagger middleware to serve requests for API descriptions
# NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure
# that it's configured to generate files in the same folder
c.openapi_root = Rails.root.to_s + '/swagger'
# Inject a lambda function to alter the returned Swagger prior to serialization
# The function will have access to the rack env for the current request
# For example, you could leverage this to dynamically assign the "host" property
#
#c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
end

View File

@ -1,18 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
Rswag::Ui.configure do |c|
# List the Swagger endpoints that you want to be documented through the
# swagger-ui. The first parameter is the path (absolute or relative to the UI
# host) to the corresponding endpoint and the second is a title that will be
# displayed in the document selector.
# NOTE: If you're using rspec-api to expose Swagger files
# (under openapi_root) as JSON or YAML endpoints, then the list below should
# correspond to the relative paths for those endpoints.
c.swagger_endpoint '/api/api-docs/v1/swagger.yaml', 'API V1 Docs'
# Add Basic Auth in case your API is private
# c.basic_auth_enabled = true
# c.basic_auth_credentials 'username', 'password'
end

View File

@ -6,7 +6,7 @@ class Numeric
end end
end end
class Set class Array
def to_table def to_table
Tables::Table.new(self) Tables::Table.new(self)
end end

View File

@ -1,18 +0,0 @@
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "*"
threads: 3
processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
polling_interval: 0.1
development:
<<: *default
test:
<<: *default
production:
<<: *default

View File

@ -1,10 +0,0 @@
# production:
# periodic_cleanup:
# class: CleanSoftDeletedRecordsJob
# queue: background
# args: [ 1000, { batch_size: 500 } ]
# schedule: every hour
# periodic_command:
# command: "SoftDeletedRecord.due.delete_all"
# priority: 2
# schedule: at 5am every day

View File

@ -1,16 +1,15 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
Rails.application.routes.draw do Rails.application.routes.draw do
mount Rswag::Ui::Engine => '/api-docs'
mount Rswag::Api::Engine => '/api-docs'
resources :groups, only: :index resources :groups, only: :index
resources :guests, only: %i[index create update destroy] do resources :guests do
post :import, on: :collection
post :bulk_update, on: :collection post :bulk_update, on: :collection
end end
resources :expenses, only: %i[index update] do resources :expenses do
get :summary, on: :collection get :summary, on: :collection
end end
resources :tables_arrangements, only: %i[index show] resources :tables_arrangements, only: [:index, :show]
get 'up' => 'rails/health#show', as: :rails_health_check get 'up' => 'rails/health#show', as: :rails_health_check
end end

View File

@ -1,37 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
class DropTaggableTables < ActiveRecord::Migration[7.2]
def change
drop_table 'taggings', force: :cascade do |t|
t.bigint 'tag_id'
t.string 'taggable_type'
t.uuid 'taggable_id'
t.string 'tagger_type'
t.bigint 'tagger_id'
t.string 'context', limit: 128
t.datetime 'created_at', precision: nil
t.string 'tenant', limit: 128
t.index ['context'], name: 'index_taggings_on_context'
t.index %w[tag_id taggable_id taggable_type context tagger_id tagger_type], name: 'taggings_idx',
unique: true
t.index ['tag_id'], name: 'index_taggings_on_tag_id'
t.index %w[taggable_id taggable_type context], name: 'taggings_taggable_context_idx'
t.index %w[taggable_id taggable_type tagger_id context], name: 'taggings_idy'
t.index ['taggable_id'], name: 'index_taggings_on_taggable_id'
t.index %w[taggable_type taggable_id], name: 'index_taggings_on_taggable_type_and_taggable_id'
t.index ['taggable_type'], name: 'index_taggings_on_taggable_type'
t.index %w[tagger_id tagger_type], name: 'index_taggings_on_tagger_id_and_tagger_type'
t.index ['tagger_id'], name: 'index_taggings_on_tagger_id'
t.index %w[tagger_type tagger_id], name: 'index_taggings_on_tagger_type_and_tagger_id'
t.index ['tenant'], name: 'index_taggings_on_tenant'
end
drop_table "tags", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "taggings_count", default: 0
t.index ["name"], name: "index_tags_on_name", unique: true
end
end
end

View File

@ -1,7 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
class AddNameToTablesArrangements < ActiveRecord::Migration[7.2]
def change
add_column :tables_arrangements, :name, :string, null: false
end
end

View File

@ -1,134 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
class SolidQueueInstall < ActiveRecord::Migration[7.2]
def change
create_table 'solid_queue_blocked_executions', force: :cascade do |t|
t.bigint 'job_id', null: false
t.string 'queue_name', null: false
t.integer 'priority', default: 0, null: false
t.string 'concurrency_key', null: false
t.datetime 'expires_at', null: false
t.datetime 'created_at', null: false
t.index %w[concurrency_key priority job_id], name: 'index_solid_queue_blocked_executions_for_release'
t.index %w[expires_at concurrency_key], name: 'index_solid_queue_blocked_executions_for_maintenance'
t.index ['job_id'], name: 'index_solid_queue_blocked_executions_on_job_id', unique: true
end
create_table 'solid_queue_claimed_executions', force: :cascade do |t|
t.bigint 'job_id', null: false
t.bigint 'process_id'
t.datetime 'created_at', null: false
t.index ['job_id'], name: 'index_solid_queue_claimed_executions_on_job_id', unique: true
t.index %w[process_id job_id], name: 'index_solid_queue_claimed_executions_on_process_id_and_job_id'
end
create_table 'solid_queue_failed_executions', force: :cascade do |t|
t.bigint 'job_id', null: false
t.text 'error'
t.datetime 'created_at', null: false
t.index ['job_id'], name: 'index_solid_queue_failed_executions_on_job_id', unique: true
end
create_table 'solid_queue_jobs', force: :cascade do |t|
t.string 'queue_name', null: false
t.string 'class_name', null: false
t.text 'arguments'
t.integer 'priority', default: 0, null: false
t.string 'active_job_id'
t.datetime 'scheduled_at'
t.datetime 'finished_at'
t.string 'concurrency_key'
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.index ['active_job_id'], name: 'index_solid_queue_jobs_on_active_job_id'
t.index ['class_name'], name: 'index_solid_queue_jobs_on_class_name'
t.index ['finished_at'], name: 'index_solid_queue_jobs_on_finished_at'
t.index %w[queue_name finished_at], name: 'index_solid_queue_jobs_for_filtering'
t.index %w[scheduled_at finished_at], name: 'index_solid_queue_jobs_for_alerting'
end
create_table 'solid_queue_pauses', force: :cascade do |t|
t.string 'queue_name', null: false
t.datetime 'created_at', null: false
t.index ['queue_name'], name: 'index_solid_queue_pauses_on_queue_name', unique: true
end
create_table 'solid_queue_processes', force: :cascade do |t|
t.string 'kind', null: false
t.datetime 'last_heartbeat_at', null: false
t.bigint 'supervisor_id'
t.integer 'pid', null: false
t.string 'hostname'
t.text 'metadata'
t.datetime 'created_at', null: false
t.string 'name', null: false
t.index ['last_heartbeat_at'], name: 'index_solid_queue_processes_on_last_heartbeat_at'
t.index %w[name supervisor_id], name: 'index_solid_queue_processes_on_name_and_supervisor_id', unique: true
t.index ['supervisor_id'], name: 'index_solid_queue_processes_on_supervisor_id'
end
create_table 'solid_queue_ready_executions', force: :cascade do |t|
t.bigint 'job_id', null: false
t.string 'queue_name', null: false
t.integer 'priority', default: 0, null: false
t.datetime 'created_at', null: false
t.index ['job_id'], name: 'index_solid_queue_ready_executions_on_job_id', unique: true
t.index %w[priority job_id], name: 'index_solid_queue_poll_all'
t.index %w[queue_name priority job_id], name: 'index_solid_queue_poll_by_queue'
end
create_table 'solid_queue_recurring_executions', force: :cascade do |t|
t.bigint 'job_id', null: false
t.string 'task_key', null: false
t.datetime 'run_at', null: false
t.datetime 'created_at', null: false
t.index ['job_id'], name: 'index_solid_queue_recurring_executions_on_job_id', unique: true
t.index %w[task_key run_at], name: 'index_solid_queue_recurring_executions_on_task_key_and_run_at',
unique: true
end
create_table 'solid_queue_recurring_tasks', force: :cascade do |t|
t.string 'key', null: false
t.string 'schedule', null: false
t.string 'command', limit: 2048
t.string 'class_name'
t.text 'arguments'
t.string 'queue_name'
t.integer 'priority', default: 0
t.boolean 'static', default: true, null: false
t.text 'description'
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.index ['key'], name: 'index_solid_queue_recurring_tasks_on_key', unique: true
t.index ['static'], name: 'index_solid_queue_recurring_tasks_on_static'
end
create_table 'solid_queue_scheduled_executions', force: :cascade do |t|
t.bigint 'job_id', null: false
t.string 'queue_name', null: false
t.integer 'priority', default: 0, null: false
t.datetime 'scheduled_at', null: false
t.datetime 'created_at', null: false
t.index ['job_id'], name: 'index_solid_queue_scheduled_executions_on_job_id', unique: true
t.index %w[scheduled_at priority job_id], name: 'index_solid_queue_dispatch_all'
end
create_table 'solid_queue_semaphores', force: :cascade do |t|
t.string 'key', null: false
t.integer 'value', default: 1, null: false
t.datetime 'expires_at', null: false
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.index ['expires_at'], name: 'index_solid_queue_semaphores_on_expires_at'
t.index %w[key value], name: 'index_solid_queue_semaphores_on_key_and_value'
t.index ['key'], name: 'index_solid_queue_semaphores_on_key', unique: true
end
add_foreign_key 'solid_queue_blocked_executions', 'solid_queue_jobs', column: 'job_id', on_delete: :cascade
add_foreign_key 'solid_queue_claimed_executions', 'solid_queue_jobs', column: 'job_id', on_delete: :cascade
add_foreign_key 'solid_queue_failed_executions', 'solid_queue_jobs', column: 'job_id', on_delete: :cascade
add_foreign_key 'solid_queue_ready_executions', 'solid_queue_jobs', column: 'job_id', on_delete: :cascade
add_foreign_key 'solid_queue_recurring_executions', 'solid_queue_jobs', column: 'job_id', on_delete: :cascade
add_foreign_key 'solid_queue_scheduled_executions', 'solid_queue_jobs', column: 'job_id', on_delete: :cascade
end
end

View File

@ -1,7 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
class RemoveEmailFromGuests < ActiveRecord::Migration[7.2]
def change
remove_column :guests, :email, :string
end
end

View File

@ -1,7 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
class AddColorToGroup < ActiveRecord::Migration[7.2]
def change
add_column :groups, :color, :string
end
end

View File

@ -1,19 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
class MergeGuestNames < ActiveRecord::Migration[8.0]
def change
add_column :guests, :name, :string
reversible do |dir|
dir.up do
execute <<~SQL
UPDATE guests
SET name = CONCAT(first_name, ' ', last_name)
SQL
end
end
remove_column :guests, :first_name, :string
remove_column :guests, :last_name, :string
end
end

View File

@ -1,4 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
ActiveRecord::Schema[7.1].define(version: 1) do
end

169
db/schema.rb generated
View File

@ -12,9 +12,9 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2024_11_11_063741) do ActiveRecord::Schema[7.1].define(version: 2024_08_11_170021) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql" enable_extension "plpgsql"
# Custom types defined in this database. # Custom types defined in this database.
# Note that some types may not work with other database engines. Be careful if changing database. # Note that some types may not work with other database engines. Be careful if changing database.
@ -35,18 +35,19 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_11_063741) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.uuid "parent_id" t.uuid "parent_id"
t.string "color"
t.index ["name"], name: "index_groups_on_name", unique: true t.index ["name"], name: "index_groups_on_name", unique: true
t.index ["parent_id"], name: "index_groups_on_parent_id" t.index ["parent_id"], name: "index_groups_on_parent_id"
end end
create_table "guests", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| create_table "guests", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.string "email"
t.string "phone" t.string "phone"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.uuid "group_id", null: false t.uuid "group_id", null: false
t.integer "status", default: 0 t.integer "status", default: 0
t.string "name"
t.index ["group_id"], name: "index_guests_on_group_id" t.index ["group_id"], name: "index_guests_on_group_id"
end end
@ -60,142 +61,46 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_11_063741) do
t.index ["tables_arrangement_id"], name: "index_seats_on_tables_arrangement_id" t.index ["tables_arrangement_id"], name: "index_seats_on_tables_arrangement_id"
end end
create_table "solid_queue_blocked_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.string "concurrency_key", null: false
t.datetime "expires_at", null: false
t.datetime "created_at", null: false
t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release"
t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance"
t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
end
create_table "solid_queue_claimed_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.bigint "process_id"
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
end
create_table "solid_queue_failed_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.text "error"
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true
end
create_table "solid_queue_jobs", force: :cascade do |t|
t.string "queue_name", null: false
t.string "class_name", null: false
t.text "arguments"
t.integer "priority", default: 0, null: false
t.string "active_job_id"
t.datetime "scheduled_at"
t.datetime "finished_at"
t.string "concurrency_key"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id"
t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name"
t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at"
t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering"
t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting"
end
create_table "solid_queue_pauses", force: :cascade do |t|
t.string "queue_name", null: false
t.datetime "created_at", null: false
t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true
end
create_table "solid_queue_processes", force: :cascade do |t|
t.string "kind", null: false
t.datetime "last_heartbeat_at", null: false
t.bigint "supervisor_id"
t.integer "pid", null: false
t.string "hostname"
t.text "metadata"
t.datetime "created_at", null: false
t.string "name", null: false
t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at"
t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id"
end
create_table "solid_queue_ready_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true
t.index ["priority", "job_id"], name: "index_solid_queue_poll_all"
t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue"
end
create_table "solid_queue_recurring_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "task_key", null: false
t.datetime "run_at", null: false
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
end
create_table "solid_queue_recurring_tasks", force: :cascade do |t|
t.string "key", null: false
t.string "schedule", null: false
t.string "command", limit: 2048
t.string "class_name"
t.text "arguments"
t.string "queue_name"
t.integer "priority", default: 0
t.boolean "static", default: true, null: false
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true
t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static"
end
create_table "solid_queue_scheduled_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "scheduled_at", null: false
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all"
end
create_table "solid_queue_semaphores", force: :cascade do |t|
t.string "key", null: false
t.integer "value", default: 1, null: false
t.datetime "expires_at", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at"
t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value"
t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
end
create_table "tables_arrangements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| create_table "tables_arrangements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.integer "discomfort" t.integer "discomfort"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "name", null: false end
create_table "taggings", force: :cascade do |t|
t.bigint "tag_id"
t.string "taggable_type"
t.uuid "taggable_id"
t.string "tagger_type"
t.bigint "tagger_id"
t.string "context", limit: 128
t.datetime "created_at", precision: nil
t.string "tenant", limit: 128
t.index ["context"], name: "index_taggings_on_context"
t.index ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true
t.index ["tag_id"], name: "index_taggings_on_tag_id"
t.index ["taggable_id", "taggable_type", "context"], name: "taggings_taggable_context_idx"
t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy"
t.index ["taggable_id"], name: "index_taggings_on_taggable_id"
t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable_type_and_taggable_id"
t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type"
t.index ["tagger_id"], name: "index_taggings_on_tagger_id"
t.index ["tagger_type", "tagger_id"], name: "index_taggings_on_tagger_type_and_tagger_id"
t.index ["tenant"], name: "index_taggings_on_tenant"
end
create_table "tags", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "taggings_count", default: 0
t.index ["name"], name: "index_tags_on_name", unique: true
end end
add_foreign_key "groups", "groups", column: "parent_id" add_foreign_key "groups", "groups", column: "parent_id"
add_foreign_key "guests", "groups" add_foreign_key "guests", "groups"
add_foreign_key "seats", "guests" add_foreign_key "seats", "guests"
add_foreign_key "seats", "tables_arrangements", on_delete: :cascade add_foreign_key "seats", "tables_arrangements", on_delete: :cascade
add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "taggings", "tags"
add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
end end

View File

@ -5,6 +5,8 @@ NUMBER_OF_GUESTS = 50
TablesArrangement.delete_all TablesArrangement.delete_all
Expense.delete_all Expense.delete_all
Guest.delete_all Guest.delete_all
ActsAsTaggableOn::Tagging.delete_all
ActsAsTaggableOn::Tag.delete_all
Group.delete_all Group.delete_all
Expense.create!(name: 'Photographer', amount: 3000, pricing_type: 'fixed') Expense.create!(name: 'Photographer', amount: 3000, pricing_type: 'fixed')
@ -56,15 +58,11 @@ groups = Group.all
NUMBER_OF_GUESTS.times do NUMBER_OF_GUESTS.times do
Guest.create!( Guest.create!(
name: Faker::Name.name, first_name: Faker::Name.first_name,
last_name: Faker::Name.last_name,
email: Faker::Internet.email,
phone: Faker::PhoneNumber.cell_phone, phone: Faker::PhoneNumber.cell_phone,
group: groups.sample, group: groups.sample,
status: Guest.statuses.keys.sample status: Guest.statuses.keys.sample
) )
end end
ActiveJob.perform_all_later(3.times.map { TableSimulatorJob.new })
'red'.paint.palette.triad(as: :hex).zip(Group.roots).each { |(color, group)| group.update!(color: color.paint.desaturate(40)) }
Group.roots.each(&:colorize_children)

View File

@ -1,91 +0,0 @@
---
- - :permit
- MIT
- :who:
:why:
:versions: []
:when: 2024-10-25 17:45:36.831184284 Z
- - :permit
- ISC
- :who:
:why:
:versions: []
:when: 2024-10-25 17:48:14.527140943 Z
- - :permit
- Apache 2.0
- :who:
:why:
:versions: []
:when: 2024-10-25 17:48:23.863998708 Z
- - :permit
- Simplified BSD
- :who:
:why:
:versions: []
:when: 2024-10-25 17:49:01.330574375 Z
- - :permit
- New BSD
- :who:
:why:
:versions: []
:when: 2024-10-25 17:49:53.995999923 Z
- - :permit
- LGPL-3.0-or-later
- :who:
:why:
:versions: []
:when: 2024-10-25 17:51:16.274818102 Z
- - :permit
- Python-2.0
- :who:
:why:
:versions: []
:when: 2024-10-25 17:51:32.610018037 Z
- - :permit
- BlueOak-1.0.0
- :who:
:why:
:versions: []
:when: 2024-10-25 17:52:28.568966565 Z
- - :permit
- BSD
- :who:
:why:
:versions: []
:when: 2024-10-25 17:52:37.235297087 Z
- - :permit
- The Unlicense
- :who:
:why:
:versions: []
:when: 2024-10-25 17:52:49.646463302 Z
- - :permit
- CC-BY-4.0
- :who:
:why:
:versions: []
:when: 2024-10-25 17:54:29.363007852 Z
- - :permit
- "(MIT AND Zlib)"
- :who:
:why:
:versions: []
:when: 2024-10-25 17:54:49.936741134 Z
- - :permit
- BSD Zero Clause License
- :who:
:why:
:versions: []
:when: 2024-10-25 17:55:31.968339009 Z
- - :permit
- Artistic-2.0
- :who:
:why:
:versions: []
:when: 2024-10-25 17:55:52.371898047 Z
- - :permit
- ruby
- :who:
:why:
:versions: []
:when: 2024-11-03 10:58:35.358938407 Z

View File

@ -1,62 +0,0 @@
services:
backend:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- 3000
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/postgres
RAILS_ENV: development
volumes:
- .:/rails
workers:
build:
context: .
dockerfile: Dockerfile.dev
entrypoint: bin/jobs
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/postgres
RAILS_ENV: development
volumes:
- .:/rails
frontend:
build:
context: ../wedding-planner-frontend
dockerfile: Dockerfile.dev
ports:
- 3000
depends_on:
- backend
volumes:
- ../wedding-planner-frontend/:/app
nginx:
image: nginx:latest
ports:
- 80:80
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- frontend
- backend
db:
image: postgres:17
ports:
- 5432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5

View File

@ -1,8 +0,0 @@
# This rake task was added by annotate_rb gem.
# Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this
if Rails.env.development? && ENV["ANNOTATERB_SKIP_ON_DB_TASKS"].nil?
require "annotate_rb"
AnnotateRb::Core.load_rake_tasks
end

View File

@ -1,16 +1,11 @@
# Copyright (C) 2024 Manuel Bustillo namespace :vns do
task distribute_tables: :environment do
class TableSimulatorJob < ApplicationJob
queue_as :default
def perform(*_args)
engine = VNS::Engine.new engine = VNS::Engine.new
engine.add_perturbation(Tables::Swap) engine.add_perturbation(Tables::Swap)
engine.add_perturbation(Tables::Shift)
initial_solution = Tables::Distribution.new(min_per_table: 8, max_per_table: 10) initial_solution = Tables::Distribution.new(min_per_table: 8, max_per_table: 10)
initial_solution.random_distribution(Guest.potential.shuffle) initial_solution.random_distribution(Guest.all.shuffle)
engine.initial_solution = initial_solution engine.initial_solution = initial_solution

View File

@ -3,12 +3,12 @@ server {
server_name libre-wedding-planner.app.localhost; server_name libre-wedding-planner.app.localhost;
location /api/ { location /api/ {
proxy_pass http://backend:3000/; proxy_pass http://localhost:3001/;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
} }
location / { location / {
proxy_pass http://frontend:3000; proxy_pass http://localhost:3000;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
} }
} }

View File

@ -4,7 +4,9 @@ FactoryBot.define do
factory :guest do factory :guest do
association :group association :group
name { Faker::Name.name } first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
email { Faker::Internet.email }
phone { Faker::PhoneNumber.cell_phone } phone { Faker::PhoneNumber.cell_phone }
end end
end end

View File

@ -3,10 +3,5 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Expense, type: :model do RSpec.describe Expense, type: :model do
describe 'validations' do pending "add some examples to (or delete) #{__FILE__}"
it { should validate_presence_of(:name) }
it { should validate_presence_of(:amount) }
it { should validate_numericality_of(:amount).is_greater_than(0) }
it { should validate_presence_of(:pricing_type) }
end
end end

View File

@ -3,9 +3,5 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Group, type: :model do RSpec.describe Group, type: :model do
describe 'callbacks' do pending "add some examples to (or delete) #{__FILE__}"
it 'should set color before create' do
expect(create(:group).color).to be_present
end
end
end end

View File

@ -3,32 +3,5 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Guest, type: :model do RSpec.describe Guest, type: :model do
describe 'validations' do pending "add some examples to (or delete) #{__FILE__}"
it { should validate_presence_of(:name) }
it do
should define_enum_for(:status).with_values(
considered: 0,
invited: 10,
confirmed: 20,
declined: 30,
tentative: 40
)
end
end
it { should belong_to(:group) }
describe 'scopes' do
describe '.potential' do
it 'returns guests that are not declined or considered' do
_declined_guest = create(:guest, status: :declined)
_considered_guest = create(:guest, status: :considered)
invited_guest = create(:guest, status: :invited)
confirmed_guest = create(:guest, status: :confirmed)
tentative_guest = create(:guest, status: :tentative)
expect(Guest.potential).to match_array([invited_guest, confirmed_guest, tentative_guest])
end
end
end
end end

View File

@ -3,9 +3,5 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe TablesArrangement, type: :model do RSpec.describe TablesArrangement, type: :model do
describe 'callbacks' do pending "add some examples to (or delete) #{__FILE__}"
it 'assigns a name before creation' do
expect(described_class.create!.name).to be_present
end
end
end end

View File

@ -1,98 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'rails_helper'
module Groups
RSpec.describe SummaryQuery do
describe '#call' do
subject { described_class.new.call }
context 'when there are no groups' do
it { is_expected.to eq([]) }
end
context 'when groups are defined' do
let!(:parent) { create(:group, name: 'Friends', icon: 'icon-1', color: '#FF0000') }
let!(:child) { create(:group, name: 'Family', icon: 'icon-2', color: '#00FF00', parent:) }
context 'when there are no guests' do
it 'returns the summary of groups' do
is_expected.to contain_exactly(
{ 'id' => parent.id,
'name' => 'Friends',
'icon' => 'icon-1',
'parent_id' => nil,
'color' => '#FF0000',
'total' => 0,
'considered' => 0,
'invited' => 0,
'confirmed' => 0,
'declined' => 0,
'tentative' => 0 },
{ 'id' => child.id,
'name' => 'Family',
'icon' => 'icon-2',
'parent_id' => parent.id,
'color' => '#00FF00',
'total' => 0,
'considered' => 0,
'invited' => 0,
'confirmed' => 0,
'declined' => 0,
'tentative' => 0 }
)
end
end
context 'when there are guests' do
before do
# Parent group
create_list(:guest, 2, group: parent, status: :considered)
create_list(:guest, 3, group: parent, status: :invited)
create_list(:guest, 4, group: parent, status: :confirmed)
create_list(:guest, 5, group: parent, status: :declined)
create_list(:guest, 6, group: parent, status: :tentative)
# Child group
create_list(:guest, 7, group: child, status: :considered)
create_list(:guest, 8, group: child, status: :invited)
create_list(:guest, 9, group: child, status: :confirmed)
create_list(:guest, 10, group: child, status: :declined)
create_list(:guest, 11, group: child, status: :tentative)
end
it 'returns the summary of groups' do
is_expected.to contain_exactly(
{
'id' => parent.id,
'name' => 'Friends',
'icon' => 'icon-1',
'parent_id' => nil,
'color' => '#FF0000',
'total' => 20,
'considered' => 2,
'invited' => 3,
'confirmed' => 4,
'declined' => 5,
'tentative' => 6
},
{
'id' => child.id,
'name' => 'Family',
'icon' => 'icon-2',
'parent_id' => parent.id,
'color' => '#00FF00',
'total' => 45,
'considered' => 7,
'invited' => 8,
'confirmed' => 9,
'declined' => 10,
'tentative' => 11
}
)
end
end
end
end
end
end

View File

@ -66,10 +66,3 @@ RSpec.configure do |config|
# config.filter_gems_from_backtrace("gem name") # config.filter_gems_from_backtrace("gem name")
config.include FactoryBot::Syntax::Methods config.include FactoryBot::Syntax::Methods
end end
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end

View File

@ -1,49 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper'
RSpec.describe 'expenses', type: :request do
path '/expenses' do
get('list expenses') do
tags 'Expenses'
produces 'application/json'
response(200, 'successful') do
schema type: :array,
items: {
type: :object,
required: %i[id name amount pricing_type],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string },
amount: { type: :number },
pricing_type: { type: :string, enum: Expense.pricing_types.keys }
}
}
xit
end
end
end
path '/expenses/{id}' do
patch('update expense') do
tags 'Expenses'
consumes 'application/json'
produces 'application/json'
parameter name: 'id', in: :path, type: :string, format: :uuid, description: 'id'
parameter name: :body, in: :body, schema: {
type: :object,
properties: {
name: { type: :string },
amount: { type: :number, minimum: 0 },
pricing_type: { type: :string, enum: Expense.pricing_types.keys }
}
}
response_empty_200
response_422
response_404
end
end
end

View File

@ -1,33 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper'
RSpec.describe 'groups', type: :request do
path '/groups' do
get('list groups') do
tags 'Groups'
produces 'application/json'
response(200, 'successful') do
schema type: :array,
items: {
type: :object,
required: %i[id name icon parent_id color total considered invited confirmed declined tentative],
properties: {
id: { type: :string, format: :uuid, required: true },
name: { type: :string },
icon: { type: :string, example: 'pi pi-crown', description: 'The CSS classes used by the icon' },
parent_id: { type: :string, format: :uuid },
color: { type: :string, pattern: '^#(?:[0-9a-fA-F]{3}){1,2}$' },
total: { type: :integer, minimum: 0, description: 'Total number of guests in any status' },
considered: { type: :integer, minimum: 0 },
invited: { type: :integer, minimum: 0 },
confirmed: { type: :integer, minimum: 0 },
declined: { type: :integer, minimum: 0 },
tentative: { type: :integer, minimum: 0 }
}
}
xit
end
end
end
end

View File

@ -1,91 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper'
RSpec.describe 'guests', type: :request do
path '/guests' do
get('list guests') do
tags 'Guests'
produces 'application/json'
response(200, 'successful') do
schema type: :array,
items: {
type: :object,
required: %i[id name status group],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string },
status: { type: :string, enum: Guest.statuses.keys },
group: { type: :object,
required: %i[id name],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string }
} }
}
}
xit
end
end
post('create guest') do
tags 'Guests'
consumes 'application/json'
produces 'application/json'
parameter name: :body, in: :body, schema: {
type: :object,
required: %i[guest],
properties: {
guest: {
type: :object,
required: %i[name group_id status],
properties: {
name: { type: :string },
group_id: { type: :string, format: :uuid },
status: { type: :string, enum: Guest.statuses.keys }
}
}
}
}
response_empty_201
response_422
end
end
path '/guests/{id}' do
patch('update guest') do
tags 'Guests'
consumes 'application/json'
produces 'application/json'
parameter name: 'id', in: :path, type: :string, format: :uuid
parameter name: :body, in: :body, schema: {
type: :object,
required: %i[guest],
properties: {
guest: {
type: :object,
properties: {
name: { type: :string },
group_id: { type: :string, format: :uuid },
status: { type: :string, enum: Guest.statuses.keys }
}
}
}
}
response_empty_200
response_422
response_404
end
delete('delete guest') do
tags 'Guests'
produces 'application/json'
parameter name: 'id', in: :path, type: :string, format: :uuid
response_empty_200
response_404
end
end
end

View File

@ -3,73 +3,7 @@
require 'rails_helper' require 'rails_helper'
module Tables module Tables
RSpec.describe DiscomfortCalculator do RSpec.describe DiscomfortCalculator do
let(:calculator) { described_class.new(table:) } let(:calculator) { described_class.new(table) }
let(:family) { create(:group, name: 'family') }
let(:friends) { create(:group, name: 'friends') }
let(:work) { create(:group, name: 'work') }
let(:school) { create(:group, name: 'school') }
describe '#calculate' do
before do
allow(calculator).to receive(:table_size_penalty).and_return(2)
allow(calculator).to receive(:cohesion_penalty).and_return(3)
end
let(:table) { Table.new(create_list(:guest, 6)) }
it 'returns the sum of the table size penalty and the average cohesion penalty' do
expect(calculator.calculate).to eq(2 + 10 * 3 / 6.0)
end
end
describe '#table_size_penalty' do
before do
table.min_per_table = 5
table.max_per_table = 7
end
context 'when the number of guests is in the lower bound' do
let(:table) { Table.new(create_list(:guest, 5)) }
it { expect(calculator.send(:table_size_penalty)).to eq(0) }
end
context 'when the number of guests is within the table size limits' do
let(:table) { Table.new(create_list(:guest, 6)) }
it { expect(calculator.send(:table_size_penalty)).to eq(0) }
end
context 'when the number of guests is in the upper bound' do
let(:table) { Table.new(create_list(:guest, 7)) }
it { expect(calculator.send(:table_size_penalty)).to eq(0) }
end
context 'when the number of guests is one unit below the lower bound' do
let(:table) { Table.new(create_list(:guest, 4)) }
it { expect(calculator.send(:table_size_penalty)).to eq(5) }
end
context 'when the number of guests is two units below the lower bound' do
let(:table) { Table.new(create_list(:guest, 3)) }
it { expect(calculator.send(:table_size_penalty)).to eq(10) }
end
context 'when the number of guests is one unit above the upper bound' do
let(:table) { Table.new(create_list(:guest, 8)) }
it { expect(calculator.send(:table_size_penalty)).to eq(5) }
end
context 'when the number of guests is two units above the upper bound' do
let(:table) { Table.new(create_list(:guest, 9)) }
it { expect(calculator.send(:table_size_penalty)).to eq(10) }
end
end
describe '#cohesion_penalty' do describe '#cohesion_penalty' do
before do before do
@ -80,16 +14,16 @@ module Tables
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(group, group).and_return(0) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(group, group).and_return(0)
end end
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(family.id, friends.id).and_return(nil) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('family', 'friends').and_return(nil)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, work.id).and_return(1) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('friends', 'work').and_return(1)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(family.id, work.id).and_return(2) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('family', 'work').and_return(2)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(family.id, school.id).and_return(3) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('family', 'school').and_return(3)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, school.id).and_return(4) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('friends', 'school').and_return(4)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(work.id, school.id).and_return(5) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('work', 'school').and_return(5)
end end
context 'when the table contains just two guests' do context 'when the table contains just two guests' do
context 'when they belong to the same group' do context 'when they belong to the same group' do
let(:table) { create_list(:guest, 2, group: family) } let(:table) { create_list(:guest, 2, affinity_group_list: ['family']) }
it { expect(calculator.send(:cohesion_penalty)).to eq(0) } it { expect(calculator.send(:cohesion_penalty)).to eq(0) }
end end
@ -97,8 +31,8 @@ module Tables
context 'when they belong to completely unrelated groups' do context 'when they belong to completely unrelated groups' do
let(:table) do let(:table) do
[ [
create(:guest, group: family), create(:guest, affinity_group_list: ['family']),
create(:guest, group: friends) create(:guest, affinity_group_list: ['friends'])
] ]
end end
it { expect(calculator.send(:cohesion_penalty)).to eq(1) } it { expect(calculator.send(:cohesion_penalty)).to eq(1) }
@ -107,8 +41,8 @@ module Tables
context 'when they belong to groups at a distance of 1' do context 'when they belong to groups at a distance of 1' do
let(:table) do let(:table) do
[ [
create(:guest, group: friends), create(:guest, affinity_group_list: ['friends']),
create(:guest, group: work) create(:guest, affinity_group_list: ['work'])
] ]
end end
@ -118,8 +52,8 @@ module Tables
context 'when they belong to groups at a distance of 2' do context 'when they belong to groups at a distance of 2' do
let(:table) do let(:table) do
[ [
create(:guest, group: family), create(:guest, affinity_group_list: ['family']),
create(:guest, group: work) create(:guest, affinity_group_list: ['work'])
] ]
end end
@ -129,8 +63,8 @@ module Tables
context 'when they belong to groups at a distance of 3' do context 'when they belong to groups at a distance of 3' do
let(:table) do let(:table) do
[ [
create(:guest, group: family), create(:guest, affinity_group_list: ['family']),
create(:guest, group: school) create(:guest, affinity_group_list: ['school'])
] ]
end end
@ -141,9 +75,9 @@ module Tables
context 'when the table contains three guests' do context 'when the table contains three guests' do
let(:table) do let(:table) do
[ [
create(:guest, group: family), create(:guest, affinity_group_list: ['family']),
create(:guest, group: friends), create(:guest, affinity_group_list: ['friends']),
create(:guest, group: work) create(:guest, affinity_group_list: ['work'])
] ]
end end
@ -155,10 +89,10 @@ module Tables
context 'when the table contains four guests of different groups' do context 'when the table contains four guests of different groups' do
let(:table) do let(:table) do
[ [
create(:guest, group: family), create(:guest, affinity_group_list: ['family']),
create(:guest, group: friends), create(:guest, affinity_group_list: ['friends']),
create(:guest, group: work), create(:guest, affinity_group_list: ['work']),
create(:guest, group: school) create(:guest, affinity_group_list: ['school'])
] ]
end end
@ -171,8 +105,8 @@ module Tables
context 'when the table contains four guests of two evenly split groups' do context 'when the table contains four guests of two evenly split groups' do
let(:table) do let(:table) do
[ [
create_list(:guest, 2, group: family), create_list(:guest, 2, affinity_group_list: ['family']),
create_list(:guest, 2, group: friends) create_list(:guest, 2, affinity_group_list: ['friends'])
].flatten ].flatten
end end
@ -184,8 +118,8 @@ module Tables
context 'when the table contains six guests of two unevenly split groups' do context 'when the table contains six guests of two unevenly split groups' do
let(:table) do let(:table) do
[ [
create_list(:guest, 2, group: family), create_list(:guest, 2, affinity_group_list: ['family']),
create_list(:guest, 4, group: friends) create_list(:guest, 4, affinity_group_list: ['friends'])
].flatten ].flatten
end end
@ -197,9 +131,9 @@ module Tables
context 'when the table contains six guests of three evenly split groups' do context 'when the table contains six guests of three evenly split groups' do
let(:table) do let(:table) do
[ [
create_list(:guest, 2, group: family), create_list(:guest, 2, affinity_group_list: ['family']),
create_list(:guest, 2, group: friends), create_list(:guest, 2, affinity_group_list: ['friends']),
create_list(:guest, 2, group: work) create_list(:guest, 2, affinity_group_list: ['work'])
].flatten ].flatten
end end
@ -211,9 +145,9 @@ module Tables
context 'when the table contains six guests of three unevenly split groups' do context 'when the table contains six guests of three unevenly split groups' do
let(:table) do let(:table) do
[ [
create_list(:guest, 3, group: family), create_list(:guest, 3, affinity_group_list: ['family']),
create_list(:guest, 2, group: friends), create_list(:guest, 2, affinity_group_list: ['friends']),
create_list(:guest, 1, group: work) create_list(:guest, 1, affinity_group_list: ['work'])
].flatten ].flatten
end end

View File

@ -1,25 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'rails_helper'
module Tables
RSpec.describe Distribution do
describe '#random_distribution' do
let(:subject) { described_class.new(min_per_table: 5, max_per_table: 10) }
context 'when there are fewer people than the minimum per table' do
it 'creates one table' do
subject.random_distribution([1, 2, 3, 4])
expect(subject.tables.count).to eq(1)
end
end
context 'when there are more people than the maximum per table' do
it 'creates multiple tables' do
subject.random_distribution([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
expect(subject.tables.count).to be > 1
end
end
end
end
end

View File

@ -1,55 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'rails_helper'
module Tables
RSpec.describe Shift do
describe '#each' do
let(:shifts) do
acc = []
described_class.new(initial_solution).each do |solution|
acc << solution.tables.map(&:dup)
end
acc
end
context 'when there are two tables with two people each' do
let(:initial_solution) do
Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution|
distribution.tables << Set[:a, :b].to_table
distribution.tables << Set[:c, :d].to_table
end
end
it 'yields all possible shifts between the tables' do
expect(shifts).to contain_exactly(
[Set[:b], Set[:c, :d, :a]],
[Set[:a], Set[:c, :d, :b]],
[Set[:b, :a, :d], Set[:c]],
[Set[:b, :a, :c], Set[:d]]
)
end
end
context 'when there are two tables with three people each' do
let(:initial_solution) do
Distribution.new(min_per_table: 3, max_per_table: 3).tap do |distribution|
distribution.tables << Set[:a, :b, :c].to_table
distribution.tables << Set[:d, :e, :f].to_table
end
end
it 'yields all possible shifts between the tables' do
expect(shifts).to contain_exactly(
[Set[:b, :c], Set[:d, :e, :f, :a]],
[Set[:c, :a], Set[:d, :e, :f, :b]],
[Set[:a, :b], Set[:d, :e, :f, :c]],
[Set[:a, :b, :c, :d], Set[:e, :f]],
[Set[:a, :b, :c, :e], Set[:d, :f]],
[Set[:a, :b, :c, :f], Set[:d, :e]]
)
end
end
end
end
end

View File

@ -16,17 +16,17 @@ module Tables
context 'when there are two tables with two people each' do context 'when there are two tables with two people each' do
let(:initial_solution) do let(:initial_solution) do
Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution| Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution|
distribution.tables << Set[:a, :b].to_table distribution.tables << %i[a b].to_table
distribution.tables << Set[:c, :d].to_table distribution.tables << %i[c d].to_table
end end
end end
it 'yields all possible swaps between the tables' do it 'yields all possible swaps between the tables' do
expect(swaps).to contain_exactly( expect(swaps).to contain_exactly(
[Set[:a, :d], Set[:c, :b]], [%i[a d], %i[c b]],
[Set[:b, :c], Set[:d, :a]], [%i[b c], %i[d a]],
[Set[:a, :c], Set[:d, :b]], [%i[a c], %i[d b]],
[Set[:b, :d], Set[:c, :a]] [%i[b d], %i[c a]]
) )
end end
end end
@ -34,22 +34,22 @@ module Tables
context 'when there are two tables with three people each' do context 'when there are two tables with three people each' do
let(:initial_solution) do let(:initial_solution) do
Distribution.new(min_per_table: 3, max_per_table: 3).tap do |distribution| Distribution.new(min_per_table: 3, max_per_table: 3).tap do |distribution|
distribution.tables << Set[:a, :b, :c].to_table distribution.tables << %i[a b c].to_table
distribution.tables << Set[:d, :e, :f].to_table distribution.tables << %i[d e f].to_table
end end
end end
it 'yields all possible swaps between the tables' do it 'yields all possible swaps between the tables' do
expect(swaps).to contain_exactly( expect(swaps).to contain_exactly(
[Set[:b, :c, :d], Set[:e, :f, :a]], [%i[b c d], %i[e f a]],
[Set[:b, :c, :e], Set[:f, :d, :a]], [%i[b c e], %i[f d a]],
[Set[:b, :c, :f], Set[:d, :e, :a]], [%i[b c f], %i[d e a]],
[Set[:c, :a, :d], Set[:e, :f, :b]], [%i[c a d], %i[e f b]],
[Set[:c, :a, :e], Set[:f, :d, :b]], [%i[c a e], %i[f d b]],
[Set[:c, :a, :f], Set[:d, :e, :b]], [%i[c a f], %i[d e b]],
[Set[:a, :b, :d], Set[:e, :f, :c]], [%i[a b d], %i[e f c]],
[Set[:a, :b, :e], Set[:f, :d, :c]], [%i[a b e], %i[f d c]],
[Set[:a, :b, :f], Set[:d, :e, :c]] [%i[a b f], %i[d e c]]
) )
end end
end end
@ -57,26 +57,26 @@ module Tables
context 'when there are three tables with two people each' do context 'when there are three tables with two people each' do
let(:initial_solution) do let(:initial_solution) do
Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution| Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution|
distribution.tables << Set[:a, :b].to_table distribution.tables << %i[a b].to_table
distribution.tables << Set[:c, :d].to_table distribution.tables << %i[c d].to_table
distribution.tables << Set[:e, :f].to_table distribution.tables << %i[e f].to_table
end end
end end
it 'yields all possible swaps between the tables' do it 'yields all possible swaps between the tables' do
expect(swaps).to contain_exactly( expect(swaps).to contain_exactly(
[Set[:b, :c], Set[:d, :a], Set[:e, :f]], [%i[b c], %i[d a], %i[e f]],
[Set[:b, :d], Set[:c, :a], Set[:e, :f]], [%i[b d], %i[c a], %i[e f]],
[Set[:a, :c], Set[:d, :b], Set[:e, :f]], [%i[a c], %i[d b], %i[e f]],
[Set[:a, :d], Set[:c, :b], Set[:e, :f]], [%i[a d], %i[c b], %i[e f]],
[Set[:b, :e], Set[:c, :d], Set[:f, :a]], [%i[b e], %i[c d], %i[f a]],
[Set[:b, :f], Set[:c, :d], Set[:e, :a]], [%i[b f], %i[c d], %i[e a]],
[Set[:a, :e], Set[:c, :d], Set[:f, :b]], [%i[a e], %i[c d], %i[f b]],
[Set[:a, :f], Set[:c, :d], Set[:e, :b]], [%i[a f], %i[c d], %i[e b]],
[Set[:a, :b], Set[:d, :e], Set[:f, :c]], [%i[a b], %i[d e], %i[f c]],
[Set[:a, :b], Set[:d, :f], Set[:e, :c]], [%i[a b], %i[d f], %i[e c]],
[Set[:a, :b], Set[:c, :e], Set[:f, :d]], [%i[a b], %i[c e], %i[f d]],
[Set[:a, :b], Set[:c, :f], Set[:e, :d]] [%i[a b], %i[c f], %i[e d]]
) )
end end
end end

View File

@ -1,15 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'rails_helper'
module VNS
RSpec.describe Engine do
describe '.sequence' do
it { expect(described_class.sequence([])).to eq([]) }
it { expect(described_class.sequence([1])).to eq([1]) }
it { expect(described_class.sequence([1, 2])).to eq([1, 2, 1]) }
it { expect(described_class.sequence([1, 2, 3])).to eq([1, 2, 3, 2, 1]) }
it { expect(described_class.sequence([1, 2, 3, 4])).to eq([1, 2, 3, 4, 3, 2, 1]) }
end
end
end

View File

@ -1,44 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper'
require_relative './swagger_response_helper'
include SwaggerResponseHelper
RSpec.configure do |config|
# Specify a root folder where Swagger JSON files are generated
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need
# to ensure that it's configured to serve Swagger from the same folder
config.openapi_root = Rails.root.join('swagger').to_s
# Define one or more Swagger documents and provide global metadata for each one
# When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will
# be generated at the provided relative path under openapi_root
# By default, the operations defined in spec files are added to the first
# document below. You can override this behavior by adding a openapi_spec tag to the
# the root example_group in your specs, e.g. describe '...', openapi_spec: 'v2/swagger.json'
config.openapi_specs = {
'v1/swagger.yaml' => {
openapi: '3.0.1',
info: {
title: 'API V1',
version: 'v1'
},
paths: {},
servers: [
{
url: 'http://libre-wedding-planner.app.localhost/api',
description: 'Suitable for development'
}
]
}
}
# Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
# The openapi_specs configuration option has the filename including format in
# the key, this may want to be changed to avoid putting yaml in json files.
# Defaults to json. Accepts ':json' and ':yaml'.
config.openapi_format = :yaml
end

View File

@ -1,46 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
module SwaggerResponseHelper
def response_422
response(422, 'Validation errors in input parameters') do
produces 'application/json'
error_schema
xit
end
end
def response_empty_200
response(200, 'Success') do
produces 'application/json'
schema type: :object
xit
end
end
def response_empty_201
response(201, 'Created') do
produces 'application/json'
schema type: :object
xit
end
end
def response_404
response(404, 'Record not found') do
produces 'application/json'
error_schema
xit
end
end
private
def error_schema
schema type: :object,
required: %i[message errors],
properties: {
message: { type: :string },
errors: { type: :array, items: { type: :string } }
}
end
end