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/.keep
/public/assets
.docker-compose.yml

View File

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

View File

@ -3,9 +3,6 @@ on:
pull_request:
permissions:
contents: write
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
copyright_notice:
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:
- main
pull_request:
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
unit_tests:
runs-on: ubuntu-latest
@ -22,7 +19,7 @@ jobs:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: ruby/setup-ruby@v1.202.0
- uses: ruby/setup-ruby@v1
- run: bundle install
- name: Wait until Postgres is ready to accept connections
run: |

3
.gitignore vendored
View File

@ -33,6 +33,3 @@
# Ignore master key for decrypting credentials and more.
/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
# 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
# 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
remote: https://rubygems.org/
specs:
actioncable (7.2.2)
actionpack (= 7.2.2)
activesupport (= 7.2.2)
actioncable (7.2.1.2)
actionpack (= 7.2.1.2)
activesupport (= 7.2.1.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.2)
actionpack (= 7.2.2)
activejob (= 7.2.2)
activerecord (= 7.2.2)
activestorage (= 7.2.2)
activesupport (= 7.2.2)
actionmailbox (7.2.1.2)
actionpack (= 7.2.1.2)
activejob (= 7.2.1.2)
activerecord (= 7.2.1.2)
activestorage (= 7.2.1.2)
activesupport (= 7.2.1.2)
mail (>= 2.8.0)
actionmailer (7.2.2)
actionpack (= 7.2.2)
actionview (= 7.2.2)
activejob (= 7.2.2)
activesupport (= 7.2.2)
actionmailer (7.2.1.2)
actionpack (= 7.2.1.2)
actionview (= 7.2.1.2)
activejob (= 7.2.1.2)
activesupport (= 7.2.1.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.2)
actionview (= 7.2.2)
activesupport (= 7.2.2)
actionpack (7.2.1.2)
actionview (= 7.2.1.2)
activesupport (= 7.2.1.2)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
@ -32,37 +32,36 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.2)
actionpack (= 7.2.2)
activerecord (= 7.2.2)
activestorage (= 7.2.2)
activesupport (= 7.2.2)
actiontext (7.2.1.2)
actionpack (= 7.2.1.2)
activerecord (= 7.2.1.2)
activestorage (= 7.2.1.2)
activesupport (= 7.2.1.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.2)
activesupport (= 7.2.2)
actionview (7.2.1.2)
activesupport (= 7.2.1.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.2.2)
activesupport (= 7.2.2)
activejob (7.2.1.2)
activesupport (= 7.2.1.2)
globalid (>= 0.3.6)
activemodel (7.2.2)
activesupport (= 7.2.2)
activerecord (7.2.2)
activemodel (= 7.2.2)
activesupport (= 7.2.2)
activemodel (7.2.1.2)
activesupport (= 7.2.1.2)
activerecord (7.2.1.2)
activemodel (= 7.2.1.2)
activesupport (= 7.2.1.2)
timeout (>= 0.4.0)
activestorage (7.2.2)
actionpack (= 7.2.2)
activejob (= 7.2.2)
activerecord (= 7.2.2)
activesupport (= 7.2.2)
activestorage (7.2.1.2)
actionpack (= 7.2.1.2)
activejob (= 7.2.1.2)
activerecord (= 7.2.1.2)
activesupport (= 7.2.1.2)
marcel (~> 1.0)
activesupport (7.2.2)
activesupport (7.2.1.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
@ -72,8 +71,8 @@ GEM
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
acts-as-taggable-on (12.0.0)
activerecord (>= 7.1, < 8.1)
acts-as-taggable-on (11.0.0)
activerecord (>= 7.0, < 8.0)
zeitwerk (>= 2.4, < 3.0)
ast (2.4.2)
babel-source (5.8.35)
@ -81,7 +80,6 @@ GEM
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.8)
bindex (0.8.1)
bootsnap (1.18.4)
@ -92,7 +90,7 @@ GEM
connection_pool (2.4.1)
crass (1.0.6)
csv (3.3.0)
date (3.4.0)
date (3.3.4)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
@ -136,7 +134,7 @@ GEM
jsonapi-renderer (~> 0.2.0)
language_server-protocol (3.17.0.3)
logger (1.6.1)
loofah (2.23.1)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@ -145,13 +143,13 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
method_source (1.1.0)
method_source (1.0.0)
mini_mime (1.1.5)
minitest (5.25.2)
minitest (5.25.1)
money (6.19.0)
i18n (>= 0.6.4, <= 2)
msgpack (1.7.2)
net-imap (0.5.1)
net-imap (0.4.17)
date
net-protocol
net-pop (0.1.2)
@ -160,7 +158,7 @@ GEM
timeout
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nio4r (2.7.3)
nokogiri (1.16.7-aarch64-linux)
racc (~> 1.4)
nokogiri (1.16.7-arm-linux)
@ -178,12 +176,12 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.9)
pry (0.15.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
psych (5.2.0)
psych (5.1.2)
stringio
puma (6.5.0)
puma (6.4.3)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.8)
@ -193,22 +191,23 @@ GEM
rack (>= 3.0.0)
rack-test (2.1.0)
rack (>= 1.3)
rackup (2.2.1)
rackup (2.1.0)
rack (>= 3)
rails (7.2.2)
actioncable (= 7.2.2)
actionmailbox (= 7.2.2)
actionmailer (= 7.2.2)
actionpack (= 7.2.2)
actiontext (= 7.2.2)
actionview (= 7.2.2)
activejob (= 7.2.2)
activemodel (= 7.2.2)
activerecord (= 7.2.2)
activestorage (= 7.2.2)
activesupport (= 7.2.2)
webrick (~> 1.8)
rails (7.2.1.2)
actioncable (= 7.2.1.2)
actionmailbox (= 7.2.1.2)
actionmailer (= 7.2.1.2)
actionpack (= 7.2.1.2)
actiontext (= 7.2.1.2)
actionview (= 7.2.1.2)
activejob (= 7.2.1.2)
activemodel (= 7.2.1.2)
activerecord (= 7.2.1.2)
activestorage (= 7.2.1.2)
activesupport (= 7.2.1.2)
bundler (>= 1.15.0)
railties (= 7.2.2)
railties (= 7.2.1.2)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@ -216,9 +215,9 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.2.2)
actionpack (= 7.2.2)
activesupport (= 7.2.2)
railties (7.2.1.2)
actionpack (= 7.2.1.2)
activesupport (= 7.2.1.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@ -239,7 +238,7 @@ GEM
redis-client (0.22.2)
connection_pool
regexp_parser (2.9.2)
reline (0.5.11)
reline (0.5.10)
io-console (~> 0.5)
rspec-core (3.13.2)
rspec-support (~> 3.13.0)
@ -249,7 +248,7 @@ GEM
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.0.2)
rspec-rails (7.0.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
@ -273,7 +272,7 @@ GEM
ruby-progressbar (1.13.0)
rubytree (2.1.0)
json (~> 2.0, > 2.3.1)
securerandom (0.3.2)
securerandom (0.3.1)
sprockets (4.2.1)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
@ -283,10 +282,10 @@ GEM
sprockets (>= 3.0.0)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.2)
stringio (3.1.1)
thor (1.3.2)
tilt (2.4.0)
timeout (0.4.2)
timeout (0.4.1)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
@ -299,6 +298,7 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webrick (1.8.2)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
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
- Expense management
- Seating chart
* Configuration
* 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)
- Website with wedding information
- Attendance confirmation forms
- Multitenancy
* Services (job queues, cache servers, search engines, etc.)
# Development setup
Libre Wedding Planner is made of two main pieces:
- The backend (this repo), built with Ruby (on Rails)
- The frontend (repo [here](https://gitea.bustikiller.com/bustikiller/wedding-planner-frontend/)), built with NextJS and React. You will need both to have the service fully working.
Both repositories are expected to live have a common parent directory:
```
projects <or anything else>
|-> wedding-planner
|-> wedding-planner-frontend
```
## Docker compose
Docker compose is the recommended way to run Libre Wedding Planner for development purposes. After downloading both repositories, `cd` to the root of `wedding-planner` and run:
```bash
docker compose up --build
```
Several containers will be started:
- backend: starts a Rails server that will act as an API.
- workers: starts a runner of [solid queue](https://github.com/rails/solid_queue/) that takes .care of async tasks.
- frontend: starts a NextJS application in charge of the frontend.
- nginx: A reverse proxy that the backend and frontend under the same domain, and routes all requests to the upstream services.
- db: A Postgres instance used by the backend service.
The backend service will seed the database with fake data. It's worth noting that the Postgres container does not have a volume, so the application will be seeded every time the container is created.
The backend, frontend and workers have hot-reloading enabled, so changes made to the codebase should be reflected in the application on the next request.
Once all containers have started, visit http://libre-wedding-planner.app.localhost/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.
* Deployment instructions
* ...

View File

@ -1,43 +1,15 @@
# Copyright (C) 2024 Manuel Bustillo
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|
render json: {
message: 'Record invalid',
errors: exception.record.errors.full_messages
}, status: :unprocessable_entity
end
rescue_from ActionController::ParameterMissing do |exception|
render json: {
message: 'Parameter missing',
errors: [exception.message]
}, status: :bad_request
end
rescue_from ActiveRecord::RecordNotFound do |exception|
render json: {
message: 'Record not found',
errors: [exception.message]
}, status: :not_found
end
private
def 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
def set_csrf_cookie
cookies["csrf-token"] = {
value: form_authenticity_token,
secure: Rails.env.production?,
same_site: :strict,
}
end
end

View File

@ -1,22 +1,76 @@
# Copyright (C) 2024 Manuel Bustillo
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
render json: Expenses::TotalQuery.new.call
end
def index
render json: Expense.all.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type])
# GET /expenses/1 or /expenses/1.json
def show
end
# GET /expenses/new
def new
@expense = Expense.new
end
# GET /expenses/1/edit
def edit
end
# POST /expenses or /expenses.json
def create
@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
Expense.find(params[:id]).update!(expense_params)
render json: {}, status: :ok
respond_to do |format|
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
private
# Use callbacks to share common setup or constraints between actions.
def set_expense
@expense = Expense.find(params[:id])
end
def expense_params
params.require(:expense).permit(:name, :amount, :pricing_type)
end
# Only allow a list of trusted parameters through.
def expense_params
params.require(:expense).permit(:name, :amount, :pricing_type)
end
end

View File

@ -2,6 +2,7 @@
class GroupsController < ApplicationController
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

View File

@ -3,31 +3,94 @@
require 'csv'
class GuestsController < ApplicationController
before_action :set_guest, only: %i[show edit update destroy]
# GET /guests or /guests.json
def index
render json: Guest.all.includes(:group)
.joins(:group)
.order('groups.name' => :asc, name: :asc)
.as_json(only: %i[id name status], include: { group: { only: %i[id name] } })
@guests = Guest.all
.joins(:group)
.order('groups.name' => :asc, first_name: :asc, last_name: :asc)
render jsonapi: @guests
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
Guest.create!(guest_params)
render json: {}, status: :created
@guest = Guest.new(guest_params)
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
# PATCH/PUT /guests/1 or /guests/1.json
def update
Guest.find(params[:id]).update!(guest_params)
render json: {}, status: :ok
respond_to do |format|
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
# DELETE /guests/1 or /guests/1.json
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
end
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
params.require(:guest).permit(:name, :group_id, :status)
params.require(:guest).permit(:first_name, :last_name, :email, :phone)
end
end

View File

@ -2,22 +2,13 @@
class TablesArrangementsController < ApplicationController
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
def show
Seat.joins(guest: :group)
.where(tables_arrangement_id: params[:id])
.order('guests.group_id')
.pluck(
: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 }
@tables_arrangement = TablesArrangement.find(params[:id])
@seats = @tables_arrangement.seats
.includes(guest: %i[affinity_groups unbreakable_bonds])
.group_by(&:table_number)
end
end

View File

@ -1,22 +1,4 @@
# 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
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

View File

@ -1,27 +1,5 @@
# 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
validates :name, uniqueness: true
validates :name, :order, presence: true
@ -29,33 +7,5 @@ class Group < ApplicationRecord
has_many :children, class_name: 'Group', foreign_key: 'parent_id'
belongs_to :parent, class_name: 'Group', optional: true
before_create :set_color
scope :roots, -> { where(parent_id: nil) }
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

View File

@ -1,50 +1,18 @@
# 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
acts_as_taggable_on :affinity_groups, :unbreakable_bonds
belongs_to :group
enum :status, {
enum status: {
considered: 0,
invited: 10,
confirmed: 20,
declined: 30,
tentative: 40
}, validate: true
tentative: 40,
}
validates :name, presence: true
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 })
def full_name
"#{first_name} #{last_name}"
end
end

View File

@ -1,26 +1,5 @@
# 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
belongs_to :guest
belongs_to :table_arrangement

View File

@ -1,24 +1,5 @@
# 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
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

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
type 'guest'
attributes :id, :group_id, :status
attributes :id, :email, :group_id, :status
attribute :name do
@object.name
"#{@object.first_name} #{@object.last_name}"
end
attribute :group_name do

View File

@ -6,40 +6,24 @@ class AffinityGroupsHierarchy < Array
def initialize
super
@references = {}
Group.roots.each do |group|
self << group.id
hydrate(group)
end
end
def find(id)
@references[id]
def find(name)
@references[name]
end
def <<(id)
new_node = Tree::TreeNode.new(id)
super(new_node).tap { @references[id] = new_node }
def <<(name)
new_node = Tree::TreeNode.new(name)
super(new_node).tap { @references[name] = new_node }
end
def register_child(parent_id, child_id)
@references[parent_id] << Tree::TreeNode.new(child_id).tap { |child_node| @references[child_id] = child_node }
def register_child(parent_name, child_name)
@references[parent_name] << Tree::TreeNode.new(child_name).tap { |child_node| @references[child_name] = child_node }
end
def distance(id_a, id_b)
return nil if @references[id_a].nil? || @references[id_b].nil?
def distance(name_a, name_b)
return nil if @references[name_a].nil? || @references[name_b].nil?
@references[id_a].distance_to_common_ancestor(@references[id_b])
end
private
def hydrate(group)
group.children.each do |child|
register_child(group.id, child.id)
hydrate(child)
end
@references[name_a].distance_to_common_ancestor(@references[name_b])
end
end

View File

@ -3,40 +3,18 @@
module Tables
class DiscomfortCalculator
private attr_reader :table
def initialize(table:)
def initialize(table)
@table = table
end
def calculate
table_size_penalty + 10 * (cohesion_penalty * 1.0 / table.size)
cohesion_penalty
end
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
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)
next count_a * count_b if distance.nil?

View File

@ -4,7 +4,7 @@ require_relative '../../extensions/tree_node_extension'
module Tables
class Distribution
attr_accessor :tables, :min_per_table, :max_per_table
attr_accessor :tables
def initialize(min_per_table:, max_per_table:)
@min_per_table = min_per_table
@ -13,12 +13,9 @@ module Tables
end
def random_distribution(people)
min_tables = (people.count * 1.0 / @max_per_table).ceil
max_tables = (people.count * 1.0 / @min_per_table).ceil
@tables = people.in_groups(rand(min_tables..max_tables), false)
.map { |group| Table.new(group) }
.each { |table| table.min_per_table = @min_per_table }
.each { |table| table.max_per_table = @max_per_table }
@tables = []
@tables << Table.new(people.slice!(0..rand(@min_per_table..@max_per_table))) while people.any?
end
def discomfort
@ -33,7 +30,7 @@ module Tables
def pretty_print
@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
@ -64,7 +61,7 @@ module Tables
private
def local_discomfort(table)
table.discomfort ||= DiscomfortCalculator.new(table:).calculate
table.discomfort ||= DiscomfortCalculator.new(table).calculate
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
@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_b = table_b.reset

View File

@ -1,9 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo
module Tables
class Table < Set
attr_accessor :discomfort, :min_per_table, :max_per_table
class Table < Array
attr_accessor :discomfort
def initialize(*args)
super
reset

View File

@ -2,13 +2,6 @@
module VNS
class Engine
class << self
def sequence(elements)
elements = elements.to_a
(elements + elements.reverse).chunk(&:itself).map(&:first)
end
end
def target_function(&function)
@target_function = function
end
@ -30,7 +23,7 @@ module VNS
puts "Initial score: #{@best_score.to_f}"
self.class.sequence(@perturbations).each do |perturbation|
@perturbations.each do |perturbation|
puts "Running perturbation: #{perturbation.name}"
optimize(perturbation.new(@best_solution))
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
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.
#
# 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.
# 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.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
class Set
class Array
def to_table
Tables::Table.new(self)
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
Rails.application.routes.draw do
mount Rswag::Ui::Engine => '/api-docs'
mount Rswag::Api::Engine => '/api-docs'
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
end
resources :expenses, only: %i[index update] do
resources :expenses do
get :summary, on: :collection
end
resources :tables_arrangements, only: %i[index show]
resources :tables_arrangements, only: [:index, :show]
get 'up' => 'rails/health#show', as: :rails_health_check
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.
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
enable_extension "pg_catalog.plpgsql"
enable_extension "plpgsql"
# Custom types defined in this 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 "updated_at", null: false
t.uuid "parent_id"
t.string "color"
t.index ["name"], name: "index_groups_on_name", unique: true
t.index ["parent_id"], name: "index_groups_on_parent_id"
end
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.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.uuid "group_id", null: false
t.integer "status", default: 0
t.string "name"
t.index ["group_id"], name: "index_guests_on_group_id"
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"
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|
t.integer "discomfort"
t.datetime "created_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
add_foreign_key "groups", "groups", column: "parent_id"
add_foreign_key "guests", "groups"
add_foreign_key "seats", "guests"
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 "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
add_foreign_key "taggings", "tags"
end

View File

@ -5,6 +5,8 @@ NUMBER_OF_GUESTS = 50
TablesArrangement.delete_all
Expense.delete_all
Guest.delete_all
ActsAsTaggableOn::Tagging.delete_all
ActsAsTaggableOn::Tag.delete_all
Group.delete_all
Expense.create!(name: 'Photographer', amount: 3000, pricing_type: 'fixed')
@ -56,15 +58,11 @@ groups = Group.all
NUMBER_OF_GUESTS.times do
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,
group: groups.sample,
status: Guest.statuses.keys.sample
)
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
class TableSimulatorJob < ApplicationJob
queue_as :default
def perform(*_args)
namespace :vns do
task distribute_tables: :environment do
engine = VNS::Engine.new
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.random_distribution(Guest.potential.shuffle)
initial_solution.random_distribution(Guest.all.shuffle)
engine.initial_solution = initial_solution

View File

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

View File

@ -4,7 +4,9 @@ FactoryBot.define do
factory :guest do
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 }
end
end

View File

@ -3,10 +3,5 @@
require 'rails_helper'
RSpec.describe Expense, type: :model do
describe 'validations' do
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
pending "add some examples to (or delete) #{__FILE__}"
end

View File

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

View File

@ -3,32 +3,5 @@
require 'rails_helper'
RSpec.describe Guest, type: :model do
describe 'validations' do
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
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@ -3,9 +3,5 @@
require 'rails_helper'
RSpec.describe TablesArrangement, type: :model do
describe 'callbacks' do
it 'assigns a name before creation' do
expect(described_class.create!.name).to be_present
end
end
pending "add some examples to (or delete) #{__FILE__}"
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.include FactoryBot::Syntax::Methods
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'
module Tables
RSpec.describe DiscomfortCalculator do
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
let(:calculator) { described_class.new(table) }
describe '#cohesion_penalty' do
before do
@ -80,16 +14,16 @@ module Tables
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(group, group).and_return(0)
end
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(family.id, friends.id).and_return(nil)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, work.id).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.id, school.id).and_return(3)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, school.id).and_return(4)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(work.id, school.id).and_return(5)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('family', 'friends').and_return(nil)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('friends', 'work').and_return(1)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('family', 'work').and_return(2)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('family', 'school').and_return(3)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('friends', 'school').and_return(4)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('work', 'school').and_return(5)
end
context 'when the table contains just two guests' 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) }
end
@ -97,8 +31,8 @@ module Tables
context 'when they belong to completely unrelated groups' do
let(:table) do
[
create(:guest, group: family),
create(:guest, group: friends)
create(:guest, affinity_group_list: ['family']),
create(:guest, affinity_group_list: ['friends'])
]
end
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
let(:table) do
[
create(:guest, group: friends),
create(:guest, group: work)
create(:guest, affinity_group_list: ['friends']),
create(:guest, affinity_group_list: ['work'])
]
end
@ -118,8 +52,8 @@ module Tables
context 'when they belong to groups at a distance of 2' do
let(:table) do
[
create(:guest, group: family),
create(:guest, group: work)
create(:guest, affinity_group_list: ['family']),
create(:guest, affinity_group_list: ['work'])
]
end
@ -129,8 +63,8 @@ module Tables
context 'when they belong to groups at a distance of 3' do
let(:table) do
[
create(:guest, group: family),
create(:guest, group: school)
create(:guest, affinity_group_list: ['family']),
create(:guest, affinity_group_list: ['school'])
]
end
@ -141,9 +75,9 @@ module Tables
context 'when the table contains three guests' do
let(:table) do
[
create(:guest, group: family),
create(:guest, group: friends),
create(:guest, group: work)
create(:guest, affinity_group_list: ['family']),
create(:guest, affinity_group_list: ['friends']),
create(:guest, affinity_group_list: ['work'])
]
end
@ -155,10 +89,10 @@ module Tables
context 'when the table contains four guests of different groups' do
let(:table) do
[
create(:guest, group: family),
create(:guest, group: friends),
create(:guest, group: work),
create(:guest, group: school)
create(:guest, affinity_group_list: ['family']),
create(:guest, affinity_group_list: ['friends']),
create(:guest, affinity_group_list: ['work']),
create(:guest, affinity_group_list: ['school'])
]
end
@ -171,8 +105,8 @@ module Tables
context 'when the table contains four guests of two evenly split groups' do
let(:table) do
[
create_list(:guest, 2, group: family),
create_list(:guest, 2, group: friends)
create_list(:guest, 2, affinity_group_list: ['family']),
create_list(:guest, 2, affinity_group_list: ['friends'])
].flatten
end
@ -184,8 +118,8 @@ module Tables
context 'when the table contains six guests of two unevenly split groups' do
let(:table) do
[
create_list(:guest, 2, group: family),
create_list(:guest, 4, group: friends)
create_list(:guest, 2, affinity_group_list: ['family']),
create_list(:guest, 4, affinity_group_list: ['friends'])
].flatten
end
@ -197,9 +131,9 @@ module Tables
context 'when the table contains six guests of three evenly split groups' do
let(:table) do
[
create_list(:guest, 2, group: family),
create_list(:guest, 2, group: friends),
create_list(:guest, 2, group: work)
create_list(:guest, 2, affinity_group_list: ['family']),
create_list(:guest, 2, affinity_group_list: ['friends']),
create_list(:guest, 2, affinity_group_list: ['work'])
].flatten
end
@ -211,9 +145,9 @@ module Tables
context 'when the table contains six guests of three unevenly split groups' do
let(:table) do
[
create_list(:guest, 3, group: family),
create_list(:guest, 2, group: friends),
create_list(:guest, 1, group: work)
create_list(:guest, 3, affinity_group_list: ['family']),
create_list(:guest, 2, affinity_group_list: ['friends']),
create_list(:guest, 1, affinity_group_list: ['work'])
].flatten
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
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
distribution.tables << %i[a b].to_table
distribution.tables << %i[c d].to_table
end
end
it 'yields all possible swaps between the tables' do
expect(swaps).to contain_exactly(
[Set[:a, :d], Set[:c, :b]],
[Set[:b, :c], Set[:d, :a]],
[Set[:a, :c], Set[:d, :b]],
[Set[:b, :d], Set[:c, :a]]
[%i[a d], %i[c b]],
[%i[b c], %i[d a]],
[%i[a c], %i[d b]],
[%i[b d], %i[c a]]
)
end
end
@ -34,22 +34,22 @@ module Tables
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
distribution.tables << %i[a b c].to_table
distribution.tables << %i[d e f].to_table
end
end
it 'yields all possible swaps between the tables' do
expect(swaps).to contain_exactly(
[Set[:b, :c, :d], Set[:e, :f, :a]],
[Set[:b, :c, :e], Set[:f, :d, :a]],
[Set[:b, :c, :f], Set[:d, :e, :a]],
[Set[:c, :a, :d], Set[:e, :f, :b]],
[Set[:c, :a, :e], Set[:f, :d, :b]],
[Set[:c, :a, :f], Set[:d, :e, :b]],
[Set[:a, :b, :d], Set[:e, :f, :c]],
[Set[:a, :b, :e], Set[:f, :d, :c]],
[Set[:a, :b, :f], Set[:d, :e, :c]]
[%i[b c d], %i[e f a]],
[%i[b c e], %i[f d a]],
[%i[b c f], %i[d e a]],
[%i[c a d], %i[e f b]],
[%i[c a e], %i[f d b]],
[%i[c a f], %i[d e b]],
[%i[a b d], %i[e f c]],
[%i[a b e], %i[f d c]],
[%i[a b f], %i[d e c]]
)
end
end
@ -57,26 +57,26 @@ module Tables
context 'when there are three 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
distribution.tables << Set[:e, :f].to_table
distribution.tables << %i[a b].to_table
distribution.tables << %i[c d].to_table
distribution.tables << %i[e f].to_table
end
end
it 'yields all possible swaps between the tables' do
expect(swaps).to contain_exactly(
[Set[:b, :c], Set[:d, :a], Set[:e, :f]],
[Set[:b, :d], Set[:c, :a], Set[:e, :f]],
[Set[:a, :c], Set[:d, :b], Set[:e, :f]],
[Set[:a, :d], Set[:c, :b], Set[:e, :f]],
[Set[:b, :e], Set[:c, :d], Set[:f, :a]],
[Set[:b, :f], Set[:c, :d], Set[:e, :a]],
[Set[:a, :e], Set[:c, :d], Set[:f, :b]],
[Set[:a, :f], Set[:c, :d], Set[:e, :b]],
[Set[:a, :b], Set[:d, :e], Set[:f, :c]],
[Set[:a, :b], Set[:d, :f], Set[:e, :c]],
[Set[:a, :b], Set[:c, :e], Set[:f, :d]],
[Set[:a, :b], Set[:c, :f], Set[:e, :d]]
[%i[b c], %i[d a], %i[e f]],
[%i[b d], %i[c a], %i[e f]],
[%i[a c], %i[d b], %i[e f]],
[%i[a d], %i[c b], %i[e f]],
[%i[b e], %i[c d], %i[f a]],
[%i[b f], %i[c d], %i[e a]],
[%i[a e], %i[c d], %i[f b]],
[%i[a f], %i[c d], %i[e b]],
[%i[a b], %i[d e], %i[f c]],
[%i[a b], %i[d f], %i[e c]],
[%i[a b], %i[c e], %i[f d]],
[%i[a b], %i[c f], %i[e d]]
)
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