Compare commits
2 Commits
5ac9506cb3
...
d2b5f92636
Author | SHA1 | Date | |
---|---|---|---|
d2b5f92636 | |||
![]() |
423bda7a86 |
@ -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:
|
|
||||||
- ''
|
|
@ -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
|
|
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -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:
|
||||||
|
3
.github/workflows/copyright_notice.yml
vendored
3
.github/workflows/copyright_notice.yml
vendored
@ -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
|
||||||
|
23
.github/workflows/license_finder.yml
vendored
23
.github/workflows/license_finder.yml
vendored
@ -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
|
|
5
.github/workflows/tests.yml
vendored
5
.github/workflows/tests.yml
vendored
@ -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
3
.gitignore
vendored
@ -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
|
|
||||||
|
@ -1 +1 @@
|
|||||||
ruby-3.3.6
|
ruby-3.3.5
|
||||||
|
@ -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
|
||||||
|
@ -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"]
|
|
144
Gemfile.lock
144
Gemfile.lock
@ -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)
|
||||||
|
89
README.md
89
README.md
@ -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.
|
|
||||||
|
|
||||||
|
* ...
|
||||||
|
@ -3,41 +3,13 @@
|
|||||||
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?
|
|
||||||
|
|
||||||
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
|
private
|
||||||
|
|
||||||
def development_swagger?
|
|
||||||
Rails.env.test? ||
|
|
||||||
Rails.env.development? && request.headers['referer'].include?('/api-docs/index.html')
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_csrf_cookie
|
def set_csrf_cookie
|
||||||
cookies['csrf-token'] = {
|
cookies["csrf-token"] = {
|
||||||
value: form_authenticity_token,
|
value: form_authenticity_token,
|
||||||
secure: Rails.env.production?,
|
secure: Rails.env.production?,
|
||||||
same_site: :strict
|
same_site: :strict,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,21 +1,75 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
# Only allow a list of trusted parameters through.
|
||||||
def expense_params
|
def expense_params
|
||||||
params.require(:expense).permit(:name, :amount, :pricing_type)
|
params.require(:expense).permit(:name, :amount, :pricing_type)
|
||||||
end
|
end
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,7 +23,7 @@ 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
|
||||||
|
19
app/views/expenses/_expense.html.erb
Normal file
19
app/views/expenses/_expense.html.erb
Normal 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>
|
34
app/views/expenses/_form.html.erb
Normal file
34
app/views/expenses/_form.html.erb
Normal 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 %>
|
12
app/views/expenses/edit.html.erb
Normal file
12
app/views/expenses/edit.html.erb
Normal 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>
|
30
app/views/expenses/index.html.erb
Normal file
30
app/views/expenses/index.html.erb
Normal 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 %>
|
11
app/views/expenses/new.html.erb
Normal file
11
app/views/expenses/new.html.erb
Normal 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>
|
12
app/views/expenses/show.html.erb
Normal file
12
app/views/expenses/show.html.erb
Normal 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>
|
39
app/views/guests/_form.html.erb
Normal file
39
app/views/guests/_form.html.erb
Normal 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 %>
|
24
app/views/guests/_guest.html.erb
Normal file
24
app/views/guests/_guest.html.erb
Normal 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>
|
12
app/views/guests/edit.html.erb
Normal file
12
app/views/guests/edit.html.erb
Normal 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>
|
39
app/views/guests/index.html.erb
Normal file
39
app/views/guests/index.html.erb
Normal 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 %>
|
11
app/views/guests/new.html.erb
Normal file
11
app/views/guests/new.html.erb
Normal 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>
|
12
app/views/guests/show.html.erb
Normal file
12
app/views/guests/show.html.erb
Normal 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>
|
18
app/views/layouts/application.html.erb
Normal file
18
app/views/layouts/application.html.erb
Normal 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>
|
11
app/views/tables_arrangements/index.html.erb
Normal file
11
app/views/tables_arrangements/index.html.erb
Normal 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>
|
18
app/views/tables_arrangements/show.html.erb
Normal file
18
app/views/tables_arrangements/show.html.erb
Normal 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 %>
|
6
bin/jobs
6
bin/jobs
@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env ruby
|
|
||||||
|
|
||||||
require_relative "../config/environment"
|
|
||||||
require "solid_queue/cli"
|
|
||||||
|
|
||||||
SolidQueue::Cli.start(ARGV)
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
41
config/initializers/affinity_groups.rb
Normal file
41
config/initializers/affinity_groups.rb
Normal 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')
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,7 +0,0 @@
|
|||||||
# Copyright (C) 2024 Manuel Bustillo
|
|
||||||
|
|
||||||
class RemoveEmailFromGuests < ActiveRecord::Migration[7.2]
|
|
||||||
def change
|
|
||||||
remove_column :guests, :email, :string
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,7 +0,0 @@
|
|||||||
# Copyright (C) 2024 Manuel Bustillo
|
|
||||||
|
|
||||||
class AddColorToGroup < ActiveRecord::Migration[7.2]
|
|
||||||
def change
|
|
||||||
add_column :groups, :color, :string
|
|
||||||
end
|
|
||||||
end
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||||||
# Copyright (C) 2024 Manuel Bustillo
|
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 1) do
|
|
||||||
end
|
|
169
db/schema.rb
generated
169
db/schema.rb
generated
@ -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
|
||||||
|
12
db/seeds.rb
12
db/seeds.rb
@ -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)
|
|
@ -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
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
|
||||||
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
Loading…
x
Reference in New Issue
Block a user