Compare commits

...

30 Commits

Author SHA1 Message Date
Renovate Bot
998706da97 Update dependency factory_bot_rails to v6.4.4
All checks were successful
Add copyright notice / copyright_notice (pull_request) Successful in 2m14s
Check usage of free licenses / check-licenses (pull_request) Successful in 4m55s
Run unit tests / unit_tests (pull_request) Successful in 6m36s
2024-12-15 01:17:10 +00:00
bab5cd3161 Merge pull request 'Define an endpoint with a global summary of expenses and attendance' (#184) from dashboard into main
All checks were successful
Check usage of free licenses / check-licenses (push) Successful in 54s
Run unit tests / unit_tests (push) Successful in 3m22s
Build Nginx-based docker image / build-static-assets (push) Successful in 22m47s
Reviewed-on: #184
2024-12-11 22:44:35 +00:00
4e61ee2f22 Include real data in the guests summary of the dashboard
All checks were successful
Add copyright notice / copyright_notice (pull_request) Successful in 1m19s
Check usage of free licenses / check-licenses (pull_request) Successful in 1m30s
Run unit tests / unit_tests (pull_request) Successful in 2m5s
2024-12-11 23:42:25 +01:00
f68caca5a4 Add copyright notice
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 1m15s
Add copyright notice / copyright_notice (pull_request) Successful in 2m8s
Run unit tests / unit_tests (pull_request) Successful in 3m8s
2024-12-11 08:03:02 +00:00
da8f3c7618 Define an endpoint with a global summary of expenses and attendance
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 1m31s
Add copyright notice / copyright_notice (pull_request) Successful in 1m34s
Run unit tests / unit_tests (pull_request) Successful in 3m55s
2024-12-11 09:01:35 +01:00
Renovate Bot
d18adb2806 Update dependency rails to v8.0.0.1
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 9m35s
Add copyright notice / copyright_notice (pull_request) Successful in 27m12s
Run unit tests / unit_tests (pull_request) Successful in 30m49s
Check usage of free licenses / check-licenses (push) Successful in 3m37s
Run unit tests / unit_tests (push) Successful in 10m17s
Build Nginx-based docker image / build-static-assets (push) Successful in 51m29s
2024-12-11 01:08:44 +00:00
70bbf79a5a Merge pull request 'Group attendance properties into a json key' (#181) from groups-attendance-format into main
Some checks failed
Run unit tests / unit_tests (push) Failing after 1m47s
Check usage of free licenses / check-licenses (push) Successful in 1m9s
Build Nginx-based docker image / build-static-assets (push) Successful in 25m7s
Reviewed-on: #181
2024-12-10 07:46:20 +00:00
98c1c0d18c Group attendance properties into a json key
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 33s
Add copyright notice / copyright_notice (pull_request) Successful in 1m20s
Run unit tests / unit_tests (pull_request) Successful in 2m15s
2024-12-10 08:41:10 +01:00
71eecf94a6 Merge pull request 'Define and document CRUD endpoints for expenses' (#180) from expenses-crud into main
All checks were successful
Check usage of free licenses / check-licenses (push) Successful in 1m15s
Run unit tests / unit_tests (push) Successful in 2m39s
Build Nginx-based docker image / build-static-assets (push) Successful in 22m1s
Reviewed-on: #180
2024-12-09 18:31:59 +00:00
be40c97f2f Define and document CRUD endpoints for expenses
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 56s
Add copyright notice / copyright_notice (pull_request) Successful in 1m33s
Run unit tests / unit_tests (pull_request) Successful in 2m55s
2024-12-09 19:28:32 +01:00
ac09d67f4f Merge pull request 'Configure librecaptcha configuration for the development environment' (#179) from disable-librecaptcha-demo into main
All checks were successful
Check usage of free licenses / check-licenses (push) Successful in 1m4s
Run unit tests / unit_tests (push) Successful in 2m32s
Build Nginx-based docker image / build-static-assets (push) Successful in 23m0s
Reviewed-on: #179
2024-12-09 17:29:04 +00:00
e8543d8fb4 Configure librecaptcha configuration for the development environment
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 1m5s
Add copyright notice / copyright_notice (pull_request) Successful in 1m47s
Run unit tests / unit_tests (pull_request) Successful in 2m33s
2024-12-09 18:25:47 +01:00
ead48c2588 Merge pull request 'Avoid stack too deep erros due to excessive recursion' (#178) from stack-error-vns into main
Some checks failed
Check usage of free licenses / check-licenses (push) Successful in 25s
Run unit tests / unit_tests (push) Successful in 1m21s
Build Nginx-based docker image / build-static-assets (push) Has been cancelled
Reviewed-on: #178
2024-12-09 17:16:40 +00:00
dfb50ed2dc Avoid stack too deep erros due to excessive recursion
All checks were successful
Add copyright notice / copyright_notice (pull_request) Successful in 56s
Check usage of free licenses / check-licenses (pull_request) Successful in 1m32s
Run unit tests / unit_tests (pull_request) Successful in 1m52s
2024-12-09 18:14:21 +01:00
ff0e0b6931 Merge pull request 'Document tables arrangements controller' (#175) from tables-arrangements-api into main
All checks were successful
Check usage of free licenses / check-licenses (push) Successful in 1m23s
Run unit tests / unit_tests (push) Successful in 3m17s
Build Nginx-based docker image / build-static-assets (push) Successful in 24m40s
Reviewed-on: #175
2024-12-08 13:06:00 +00:00
5cbc81c498 Add copyright notice
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 1m14s
Add copyright notice / copyright_notice (pull_request) Successful in 2m5s
Run unit tests / unit_tests (pull_request) Successful in 3m46s
2024-12-08 13:02:08 +00:00
9d90ade40c Document tables arrangements controller
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 34s
Add copyright notice / copyright_notice (pull_request) Successful in 1m9s
Run unit tests / unit_tests (pull_request) Successful in 3m51s
2024-12-08 14:00:53 +01:00
b38e845b90 Merge pull request 'Allow the creation of guests associated to no group' (#174) from allow-creation-groupless-guests into main
All checks were successful
Check usage of free licenses / check-licenses (push) Successful in 1m1s
Run unit tests / unit_tests (push) Successful in 2m57s
Build Nginx-based docker image / build-static-assets (push) Successful in 23m24s
Reviewed-on: #174
2024-12-08 12:13:03 +00:00
83e36df14e Allow the creation of guests associated to no group
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 45s
Add copyright notice / copyright_notice (pull_request) Successful in 1m31s
Run unit tests / unit_tests (pull_request) Successful in 2m0s
2024-12-08 13:10:49 +01:00
cbcb7b70e3 Merge pull request 'Define CUD endpoints for the Groups model' (#173) from groups-endpoints into main
All checks were successful
Check usage of free licenses / check-licenses (push) Successful in 56s
Run unit tests / unit_tests (push) Successful in 2m28s
Build Nginx-based docker image / build-static-assets (push) Successful in 22m36s
Reviewed-on: #173
2024-12-08 10:52:59 +00:00
9f0773647f Add copyright notice
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 2m37s
Add copyright notice / copyright_notice (pull_request) Successful in 3m11s
Run unit tests / unit_tests (pull_request) Successful in 4m23s
2024-12-08 10:41:24 +00:00
20127398c6 Fix summary query to leverage ActsAsTenant scopes
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 1m10s
Add copyright notice / copyright_notice (pull_request) Successful in 1m35s
Run unit tests / unit_tests (pull_request) Successful in 5m11s
2024-12-08 11:39:50 +01:00
9e097361d0 Define endpoints to create, update, and delete groups 2024-12-08 11:30:38 +01:00
dae2e3bace Merge pull request 'Define a dummy endpoint to return a valid CSRF token' (#172) from token-endpoint into main
All checks were successful
Check usage of free licenses / check-licenses (push) Successful in 57s
Run unit tests / unit_tests (push) Successful in 2m42s
Build Nginx-based docker image / build-static-assets (push) Successful in 22m1s
Reviewed-on: #172
2024-12-08 08:39:39 +00:00
98877166dd Add copyright notice
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 1m34s
Add copyright notice / copyright_notice (pull_request) Successful in 3m1s
Run unit tests / unit_tests (pull_request) Successful in 4m39s
2024-12-08 08:34:55 +00:00
438de103ec Define a dummy endpoint to return a valid CSRF token
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 1m18s
Add copyright notice / copyright_notice (pull_request) Successful in 2m29s
Run unit tests / unit_tests (pull_request) Successful in 5m33s
2024-12-08 09:32:34 +01:00
9fab79044d Merge pull request 'Configure allowed hosts' (#171) from configure-host into main
All checks were successful
Check usage of free licenses / check-licenses (push) Successful in 1m14s
Run unit tests / unit_tests (push) Successful in 2m57s
Build Nginx-based docker image / build-static-assets (push) Successful in 23m4s
Reviewed-on: #171
2024-12-08 07:56:02 +00:00
84684b90d7 Configure allowed hosts
All checks were successful
Add copyright notice / copyright_notice (pull_request) Successful in 54s
Check usage of free licenses / check-licenses (pull_request) Successful in 1m33s
Run unit tests / unit_tests (pull_request) Successful in 1m57s
2024-12-08 08:53:51 +01:00
64f34a71dc Merge pull request 'Temporarily allow insecure cookies' (#169) from temp-allow-insecure-cookie into main
Some checks failed
Check usage of free licenses / check-licenses (push) Successful in 1m9s
Run unit tests / unit_tests (push) Successful in 3m9s
Build Nginx-based docker image / build-static-assets (push) Failing after 10m47s
Reviewed-on: #169
2024-12-07 23:49:29 +00:00
1fb6c483ed Temporarily allow insecure cookies
All checks were successful
Check usage of free licenses / check-licenses (pull_request) Successful in 31s
Add copyright notice / copyright_notice (pull_request) Successful in 1m16s
Run unit tests / unit_tests (pull_request) Successful in 2m55s
2024-12-08 00:48:42 +01:00
28 changed files with 580 additions and 216 deletions

View File

@ -23,6 +23,7 @@ gem 'rubytree'
gem 'acts_as_tenant'
gem 'httparty'
gem 'rswag'
gem 'pluck_to_hash'
group :development, :test do
gem 'annotaterb'

View File

@ -1,29 +1,29 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.0)
actionpack (= 8.0.0)
activesupport (= 8.0.0)
actioncable (8.0.0.1)
actionpack (= 8.0.0.1)
activesupport (= 8.0.0.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.0)
actionpack (= 8.0.0)
activejob (= 8.0.0)
activerecord (= 8.0.0)
activestorage (= 8.0.0)
activesupport (= 8.0.0)
actionmailbox (8.0.0.1)
actionpack (= 8.0.0.1)
activejob (= 8.0.0.1)
activerecord (= 8.0.0.1)
activestorage (= 8.0.0.1)
activesupport (= 8.0.0.1)
mail (>= 2.8.0)
actionmailer (8.0.0)
actionpack (= 8.0.0)
actionview (= 8.0.0)
activejob (= 8.0.0)
activesupport (= 8.0.0)
actionmailer (8.0.0.1)
actionpack (= 8.0.0.1)
actionview (= 8.0.0.1)
activejob (= 8.0.0.1)
activesupport (= 8.0.0.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.0)
actionview (= 8.0.0)
activesupport (= 8.0.0)
actionpack (8.0.0.1)
actionview (= 8.0.0.1)
activesupport (= 8.0.0.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@ -31,35 +31,35 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.0)
actionpack (= 8.0.0)
activerecord (= 8.0.0)
activestorage (= 8.0.0)
activesupport (= 8.0.0)
actiontext (8.0.0.1)
actionpack (= 8.0.0.1)
activerecord (= 8.0.0.1)
activestorage (= 8.0.0.1)
activesupport (= 8.0.0.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.0)
activesupport (= 8.0.0)
actionview (8.0.0.1)
activesupport (= 8.0.0.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.0)
activesupport (= 8.0.0)
activejob (8.0.0.1)
activesupport (= 8.0.0.1)
globalid (>= 0.3.6)
activemodel (8.0.0)
activesupport (= 8.0.0)
activerecord (8.0.0)
activemodel (= 8.0.0)
activesupport (= 8.0.0)
activemodel (8.0.0.1)
activesupport (= 8.0.0.1)
activerecord (8.0.0.1)
activemodel (= 8.0.0.1)
activesupport (= 8.0.0.1)
timeout (>= 0.4.0)
activestorage (8.0.0)
actionpack (= 8.0.0)
activejob (= 8.0.0)
activerecord (= 8.0.0)
activesupport (= 8.0.0)
activestorage (8.0.0.1)
actionpack (= 8.0.0.1)
activejob (= 8.0.0.1)
activerecord (= 8.0.0.1)
activesupport (= 8.0.0.1)
marcel (~> 1.0)
activesupport (8.0.0)
activesupport (8.0.0.1)
base64
benchmark (>= 0.3)
bigdecimal
@ -137,7 +137,7 @@ GEM
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.0)
irb (1.14.1)
irb (1.14.2)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.13.0)
@ -176,7 +176,7 @@ GEM
tomlrb (>= 1.3, < 2.1)
with_env (= 1.1.0)
xml-simple (~> 1.1.9)
logger (1.6.2)
logger (1.6.3)
loofah (2.23.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
@ -222,6 +222,9 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.9)
pluck_to_hash (1.0.2)
activerecord (>= 4.0.2)
activesupport (>= 4.0.2)
pry (0.15.0)
coderay (~> 1.1)
method_source (~> 1.0)
@ -242,30 +245,30 @@ GEM
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.0)
actioncable (= 8.0.0)
actionmailbox (= 8.0.0)
actionmailer (= 8.0.0)
actionpack (= 8.0.0)
actiontext (= 8.0.0)
actionview (= 8.0.0)
activejob (= 8.0.0)
activemodel (= 8.0.0)
activerecord (= 8.0.0)
activestorage (= 8.0.0)
activesupport (= 8.0.0)
rails (8.0.0.1)
actioncable (= 8.0.0.1)
actionmailbox (= 8.0.0.1)
actionmailer (= 8.0.0.1)
actionpack (= 8.0.0.1)
actiontext (= 8.0.0.1)
actionview (= 8.0.0.1)
activejob (= 8.0.0.1)
activemodel (= 8.0.0.1)
activerecord (= 8.0.0.1)
activestorage (= 8.0.0.1)
activesupport (= 8.0.0.1)
bundler (>= 1.15.0)
railties (= 8.0.0)
railties (= 8.0.0.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.1)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (8.0.0)
actionpack (= 8.0.0)
activesupport (= 8.0.0)
railties (8.0.0.1)
actionpack (= 8.0.0.1)
activesupport (= 8.0.0.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@ -413,6 +416,7 @@ DEPENDENCIES
license_finder
money
pg (~> 1.1)
pluck_to_hash
pry
puma (>= 5.0)
rack-cors

View File

@ -59,7 +59,7 @@ class ApplicationController < ActionController::Base
def set_csrf_cookie
cookies['csrf-token'] = {
value: form_authenticity_token,
secure: Rails.env.production?,
secure: false,
same_site: :strict
}
end

View File

@ -9,11 +9,21 @@ class ExpensesController < ApplicationController
render json: Expense.all.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type])
end
def create
Expense.create!(expense_params)
render json: {}, status: :created
end
def update
Expense.find(params[:id]).update!(expense_params)
render json: {}, status: :ok
end
def destroy
Expense.find(params[:id]).destroy!
render json: {}, status: :ok
end
private
def expense_params

View File

@ -2,6 +2,43 @@
class GroupsController < ApplicationController
def index
render json: Groups::SummaryQuery.new.call.as_json
query_result = Groups::SummaryQuery.new.call.as_json.map(&:deep_symbolize_keys).map do |group|
{
id: group[:id],
name: group[:name],
icon: group[:icon],
color: group[:color],
parent_id: group[:parent_id],
attendance: group.slice(:total, :considered, :invited, :confirmed, :declined, :tentative)
}
end
render json: query_result
end
def create
group = Group.create!(**group_params, parent:)
render json: group.as_json(only: %i[id name icon color parent_id]), status: :created
end
def update
group = Group.find(params[:id])
group.update!(**group_params, parent:)
render json: group.as_json(only: %i[id name icon color parent_id]), status: :ok
end
def destroy
Group.find(params[:id]).destroy!
render json: {}, status: :ok
end
private
def parent
params[:group][:parent_id].present? ? Group.find(params[:group][:parent_id]) : nil
end
def group_params
params.expect(group: [:name, :icon, :color])
end
end

View File

@ -5,7 +5,7 @@ require 'csv'
class GuestsController < ApplicationController
def index
render json: Guest.all.includes(:group)
.joins(:group)
.left_joins(:group)
.order('groups.name' => :asc, name: :asc)
.as_json(only: %i[id name status], include: { group: { only: %i[id name] } })
end

View File

@ -0,0 +1,30 @@
# Copyright (C) 2024 Manuel Bustillo
class SummaryController < ApplicationController
def index
expense_summary = Expenses::TotalQuery.new(wedding: ActsAsTenant.current_tenant).call
guest_summary = Guest.group(:status).count
render json: {
expenses: {
projected: {
total: expense_summary['total_projected'],
guests: expense_summary['projected_guests']
},
confirmed: {
total: expense_summary['total_confirmed'],
guests: expense_summary['confirmed_guests']
},
status: {
paid: 0
}
},
guests: {
total: guest_summary.except('considered').values.sum,
confirmed: guest_summary['confirmed'].to_i,
declined: guest_summary['declined'].to_i,
tentative: guest_summary['tentative'].to_i,
invited: guest_summary['invited'].to_i
}
}
end
end

View File

@ -0,0 +1,10 @@
# Copyright (C) 2024 Manuel Bustillo
class TokensController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :set_tenant
def show
head :ok
end
end

View File

@ -38,7 +38,7 @@ class Group < ApplicationRecord
scope :roots, -> { where(parent_id: nil) }
has_many :guests
has_many :guests, dependent: :nullify
def colorize_children(generation = 1)
derived_colors = generation == 1 ? color.paint.palette.analogous(size: children.count) : color.paint.palette.decreasing_saturation

View File

@ -10,7 +10,7 @@
# status :integer default("considered")
# created_at :datetime not null
# updated_at :datetime not null
# group_id :uuid not null
# group_id :uuid
# wedding_id :uuid not null
#
# Indexes
@ -25,7 +25,7 @@
#
class Guest < ApplicationRecord
acts_as_tenant :wedding
belongs_to :group
belongs_to :group, optional: true
enum :status, {
considered: 0,

View File

@ -2,8 +2,15 @@
module Expenses
class TotalQuery
private attr_reader :wedding
def initialize(wedding:)
@wedding = wedding
end
def call
ActiveRecord::Base.connection.execute(query).first
ActiveRecord::Base.connection.execute(
ActiveRecord::Base.sanitize_sql_array([query, { wedding_id: wedding.id }])
).first
end
private
@ -12,16 +19,10 @@ module Expenses
<<~SQL
WITH guest_count AS (#{guest_count_per_status}),
expense_summary AS (#{expense_summary})
SELECT expense_summary.fixed,
expense_summary.fixed_count,
expense_summary.variable,
expense_summary.variable_count,
expense_summary.total_count,
guest_count.confirmed as confirmed_guests,
guest_count.projected as projected_guests,
expense_summary.fixed + expense_summary.variable * guest_count.confirmed as total,
expense_summary.fixed + expense_summary.variable * guest_count.projected as max_projected,
(expense_summary.fixed + expense_summary.variable * guest_count.confirmed) / guest_count.confirmed as per_person
SELECT guest_count.confirmed as confirmed_guests,
guest_count.projected as projected_guests,
expense_summary.fixed + expense_summary.variable * guest_count.confirmed as total_confirmed,
expense_summary.fixed + expense_summary.variable * guest_count.projected as total_projected
FROM guest_count, expense_summary;
SQL
end
@ -29,19 +30,18 @@ module Expenses
def expense_summary
<<~SQL
SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed,
coalesce(count(amount) filter (where pricing_type = 'fixed'), 0) as fixed_count,
coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable,
coalesce(count(amount) filter (where pricing_type = 'per_person'), 0) as variable_count,
count(*) as total_count
coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable
FROM expenses
WHERE wedding_id = :wedding_id
SQL
end
def guest_count_per_status
<<~SQL
SELECT COALESCE(count(*) filter(where status = #{Guest.statuses["confirmed"]}), 0) as confirmed,
COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at("confirmed", "invited", "tentative").join(",")})), 0) as projected
SELECT COALESCE(count(*) filter(where status = #{Guest.statuses['confirmed']}), 0) as confirmed,
COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at('confirmed', 'invited', 'tentative').join(',')})), 0) as projected
FROM guests
WHERE wedding_id = :wedding_id
SQL
end
end

View File

@ -3,29 +3,19 @@
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
Group.left_joins(:guests).group(:id).pluck_to_hash(
:id,
:name,
:icon,
:parent_id,
:color,
Arel.sql('count(*) filter (where status IS NOT NULL) as total'),
Arel.sql('count(*) filter (where status = 0) as considered'),
Arel.sql('count(*) filter (where status = 10) as invited'),
Arel.sql('count(*) filter (where status = 20) as confirmed'),
Arel.sql('count(*) filter (where status = 30) as declined'),
Arel.sql('count(*) filter (where status = 40) as tentative'),
)
end
end
end

View File

@ -29,7 +29,7 @@ module VNS
@best_score = @target_function.call(@best_solution)
self.class.sequence(@perturbations).each do |perturbation|
optimize(perturbation.new(@best_solution))
optimize(perturbation)
end
@best_solution
@ -37,15 +37,22 @@ module VNS
private
def optimize(perturbation)
perturbation.each do |alternative_solution|
score = @target_function.call(alternative_solution)
next if score >= @best_score
def optimize(perturbation_klass)
loop do
optimized = false
@best_solution = alternative_solution.deep_dup
@best_score = score
perturbation_klass.new(@best_solution).each do |alternative_solution|
score = @target_function.call(alternative_solution)
next if score >= @best_score
return optimize(perturbation.class.new(@best_solution))
@best_solution = alternative_solution.deep_dup
@best_score = score
optimized = true
break
end
return unless optimized
end
end
end

View File

@ -92,6 +92,9 @@ Rails.application.configure do
# "example.com", # Allow requests from example.com
# /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
# ]
config.hosts << "app.libreweddingplanner.org"
# Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end

View File

@ -2,6 +2,16 @@
Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
get 'token' => 'tokens#show', as: :token
get 'up' => 'rails/health#show', as: :rails_health_check
resources :captcha, only: :create do
get 'v2/media', to: 'captcha#media', on: :collection, as: :media
end
mount Rswag::Ui::Engine => '/api-docs'
mount Rswag::Api::Engine => '/api-docs'
scope ":slug", constraints: { slug: Wedding::SLUG_REGEX } do
devise_for :users, skip: [:registration, :session, :confirmation]
devise_scope :user do
@ -13,24 +23,16 @@ Rails.application.routes.draw do
get '/users/confirmation', to: 'users/confirmations#show', as: :confirmation
end
resources :groups, only: :index
resources :groups, only: %i[index create update destroy]
resources :guests, only: %i[index create update destroy] do
post :bulk_update, on: :collection
end
resources :expenses, only: %i[index update] do
resources :expenses, only: %i[index create update destroy] do
get :summary, on: :collection
end
resources :tables_arrangements, only: %i[index show]
resources :summary, only: :index
root to: redirect("/%{slug}")
end
resources :captcha, only: :create do
get 'v2/media', to: 'captcha#media', on: :collection, as: :media
end
mount Rswag::Ui::Engine => '/api-docs'
mount Rswag::Api::Engine => '/api-docs'
get 'up' => 'rails/health#show', as: :rails_health_check
end

View File

@ -0,0 +1,7 @@
# Copyright (C) 2024 Manuel Bustillo
class AllowUngroupedGuests < ActiveRecord::Migration[8.0]
def change
change_column_null :guests, :group_id, true
end
end

4
db/schema.rb generated
View File

@ -12,7 +12,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2024_12_07_112305) do
ActiveRecord::Schema[8.0].define(version: 2024_12_08_102932) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@ -48,7 +48,7 @@ ActiveRecord::Schema[8.0].define(version: 2024_12_07_112305) do
t.string "phone"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.uuid "group_id", null: false
t.uuid "group_id"
t.integer "status", default: 0
t.string "name"
t.uuid "wedding_id", null: false

View File

@ -40,6 +40,7 @@ services:
image: librecaptcha/lc-core:latest
volumes:
- "./tmp/libre-captcha-data:/lc-core/data"
- "./libre-captcha-config.json:/lc-core/data/config.json"
ports:
- 8888
nginx:

29
libre-captcha-config.json Normal file
View File

@ -0,0 +1,29 @@
{
"randomSeed": -1534087241,
"port": 8888,
"address": "0.0.0.0",
"captchaExpiryTimeLimit": 5,
"bufferCount": 1000,
"threadDelay": 2,
"playgroundEnabled": false,
"corsHeader": "",
"maxAttemptsRatio": 0.009999999776482582,
"captchas": [
{
"name": "FilterChallenge",
"allowedLevels": [
"hard"
],
"allowedMedia": [
"image/png"
],
"allowedInputType": [
"text"
],
"allowedSizes": [
"350x100"
],
"config": {}
}
]
}

View File

@ -16,7 +16,7 @@ RSpec.describe Guest, type: :model do
end
end
it { should belong_to(:group) }
it { should belong_to(:group).optional }
describe 'scopes' do
describe '.potential' do

View File

@ -5,88 +5,66 @@ require 'rails_helper'
module Expenses
RSpec.describe TotalQuery do
describe '#call' do
let(:response) { described_class.new.call }
let(:wedding) { create(:wedding) }
let(:response) { described_class.new(wedding:).call }
before do
create_list(:guest, 2, status: :confirmed)
create_list(:guest, 3, status: :considered)
create_list(:guest, 4, status: :invited)
create_list(:guest, 5, status: :tentative)
create_list(:guest, 6, status: :declined)
create_list(:guest, 2, wedding:, status: :confirmed)
create_list(:guest, 3, wedding:, status: :considered)
create_list(:guest, 4, wedding:, status: :invited)
create_list(:guest, 5, wedding:, status: :tentative)
create_list(:guest, 6, wedding:, status: :declined)
end
context "when there is no expense" do
it "returns zero in all values", :aggregate_failures do
expect(response["fixed"]).to be_zero
expect(response["fixed_count"]).to be_zero
expect(response["variable"]).to be_zero
expect(response["variable_count"]).to be_zero
expect(response["total"]).to be_zero
expect(response["total_count"]).to be_zero
expect(response["max_projected"]).to be_zero
expect(response["per_person"]).to be_zero
expect(response["confirmed_guests"]).to eq(2)
expect(response["projected_guests"]).to eq(2 + 4 + 5)
context 'when there is no expense' do
it 'returns zero in all values', :aggregate_failures do
expect(response['total_confirmed']).to be_zero
expect(response['total_projected']).to be_zero
expect(response['confirmed_guests']).to eq(2)
expect(response['projected_guests']).to eq(2 + 4 + 5)
end
end
context "when there are only fixed expenses" do
context 'when there are only fixed expenses' do
before do
create(:expense, :fixed, amount: 100)
create(:expense, :fixed, amount: 200)
create(:expense, :fixed, wedding:, amount: 100)
create(:expense, :fixed, wedding:, amount: 200)
end
it "returns the sum of fixed expenses", :aggregate_failures do
expect(response["fixed"]).to eq(300)
expect(response["fixed_count"]).to eq(2)
expect(response["variable"]).to be_zero
expect(response["variable_count"]).to be_zero
expect(response["total"]).to eq(300)
expect(response["total_count"]).to eq(2)
expect(response["max_projected"]).to eq(300)
expect(response["per_person"]).to eq(150)
expect(response["confirmed_guests"]).to eq(2)
expect(response["projected_guests"]).to eq(2 + 4 + 5)
it 'returns the sum of fixed expenses', :aggregate_failures do
expect(response['total_confirmed']).to eq(300)
expect(response['total_projected']).to eq(300)
expect(response['confirmed_guests']).to eq(2)
expect(response['projected_guests']).to eq(2 + 4 + 5)
end
end
context "when there are only variable expenses" do
context 'when there are only variable expenses' do
before do
create(:expense, :per_person, amount: 100)
create(:expense, :per_person, amount: 200)
create(:expense, :per_person, wedding:, amount: 100)
create(:expense, :per_person, wedding:, amount: 200)
end
it "returns zero in the values and nonzero in the count", :aggregate_failures do
expect(response["fixed"]).to be_zero
expect(response["fixed_count"]).to be_zero
expect(response["variable"]).to eq(300)
expect(response["variable_count"]).to eq(2)
expect(response["total"]).to eq(2*300)
expect(response["total_count"]).to eq(2)
expect(response["max_projected"]).to eq(11*300)
expect(response["confirmed_guests"]).to eq(2)
expect(response["projected_guests"]).to eq(2 + 4 + 5)
it 'returns zero in the values and nonzero in the count', :aggregate_failures do
expect(response['total_confirmed']).to eq(2 * 300)
expect(response['total_projected']).to eq(11 * 300)
expect(response['confirmed_guests']).to eq(2)
expect(response['projected_guests']).to eq(2 + 4 + 5)
end
end
context "when there are both fixed and variable expenses" do
context 'when there are both fixed and variable expenses' do
before do
create(:expense, :fixed, amount: 100)
create(:expense, :fixed, amount: 200)
create(:expense, :per_person, amount: 50)
create(:expense, :fixed, wedding:, amount: 100)
create(:expense, :fixed, wedding:, amount: 200)
create(:expense, :per_person, wedding:, amount: 50)
end
it "returns the sum of fixed and variable expenses", :aggregate_failures do
expect(response["fixed"]).to eq(300)
expect(response["fixed_count"]).to eq(2)
expect(response["variable"]).to eq(50)
expect(response["variable_count"]).to eq(1)
expect(response["total"]).to eq(100 + 200 + 50 * 2)
expect(response["total_count"]).to eq(3)
expect(response["max_projected"]).to eq(100 + 200 + 11*50)
expect(response["per_person"]).to eq(200)
expect(response["confirmed_guests"]).to eq(2)
expect(response["projected_guests"]).to eq(2 + 4 + 5)
it 'returns the sum of fixed and variable expenses', :aggregate_failures do
expect(response['total_confirmed']).to eq(100 + 200 + 50 * 2)
expect(response['total_projected']).to eq(100 + 200 + 11 * 50)
expect(response['confirmed_guests']).to eq(2)
expect(response['projected_guests']).to eq(2 + 4 + 5)
end
end
end

View File

@ -16,9 +16,7 @@ RSpec.describe 'expenses', type: :request do
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 }
**Swagger::Schema::EXPENSE
}
}
@ -26,10 +24,31 @@ RSpec.describe 'expenses', type: :request do
end
regular_api_responses
end
post 'create expense' do
tags 'Expenses'
consumes 'application/json'
produces 'application/json'
parameter Swagger::Schema::SLUG
parameter name: :body, in: :body, schema: {
type: :object,
required: %i[expense],
properties: {
expense: {
type: :object,
required: %i[name amount pricing_type],
properties: Swagger::Schema::EXPENSE
}
}
}
response_empty_201
response_422
regular_api_responses
end
end
path '/{slug}/expenses/{id}' do
patch('update expense') do
tags 'Expenses'
consumes 'application/json'
@ -38,11 +57,7 @@ RSpec.describe 'expenses', type: :request do
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 }
}
properties: Swagger::Schema::EXPENSE
}
response_empty_200
@ -50,5 +65,15 @@ RSpec.describe 'expenses', type: :request do
response_404
regular_api_responses
end
delete('delete expense') do
tags 'Expenses'
produces 'application/json'
parameter Swagger::Schema::SLUG
parameter Swagger::Schema::ID
response_empty_200
response_404
regular_api_responses
end
end
end

View File

@ -10,26 +10,99 @@ RSpec.describe 'groups', type: :request do
parameter Swagger::Schema::SLUG
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 }
items: {
type: :object,
required: %i[id name icon parent_id color attendance],
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}$' },
attendance: {
type: :object,
required: %i[total considered invited confirmed declined tentative],
properties: {
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
regular_api_responses
end
post('create group') do
tags 'Groups'
consumes 'application/json'
produces 'application/json'
parameter Swagger::Schema::SLUG
parameter name: :body, in: :body, schema: {
type: :object,
required: %i[group],
properties: {
group: {
type: :object,
required: %i[name],
properties: Swagger::Schema::GROUP
}
}
}
response(201, 'created') do
schema type: :object, properties: {
id: { type: :string, format: :uuid, required: true },
**Swagger::Schema::GROUP
}
xit
end
regular_api_responses
end
path '/{slug}/groups/{id}' do
put('update group') do
tags 'Groups'
consumes 'application/json'
produces 'application/json'
parameter Swagger::Schema::SLUG
parameter name: :id, in: :path, type: :string, format: :uuid
parameter name: :body, in: :body, schema: {
type: :object,
required: %i[group],
properties: {
group: {
type: :object,
required: %i[name],
properties: Swagger::Schema::GROUP
}
}
}
response(200, 'updated') do
schema type: :object, properties: {
id: { type: :string, format: :uuid, required: true },
**Swagger::Schema::GROUP
}
xit
end
regular_api_responses
end
delete('delete group') do
tags 'Groups'
produces 'application/json'
parameter Swagger::Schema::SLUG
parameter name: :id, in: :path, type: :string, format: :uuid
response_empty_200
regular_api_responses
end
end
end
end

View File

@ -41,7 +41,7 @@ RSpec.describe 'guests', type: :request do
properties: {
guest: {
type: :object,
required: %i[name group_id status],
required: %i[name status],
properties: {
name: { type: :string },
group_id: { type: :string, format: :uuid },
@ -70,6 +70,7 @@ RSpec.describe 'guests', type: :request do
properties: {
guest: {
type: :object,
required: %i[name status],
properties: {
name: { type: :string },
group_id: { type: :string, format: :uuid },

View File

@ -4,10 +4,29 @@ module Swagger
module Schema
USER = {
id: { type: :string, format: :uuid },
email: { type: :string, format: :email },
created_at: SwaggerResponseHelper::TIMESTAMP,
updated_at: SwaggerResponseHelper::TIMESTAMP
email: { type: :string, format: :email },
created_at: SwaggerResponseHelper::TIMESTAMP,
updated_at: SwaggerResponseHelper::TIMESTAMP
}
ID = {
name: 'id',
in: :path,
type: :string,
format: :uuid,
}
GROUP = {
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}$' }
}
EXPENSE = {
name: { type: :string },
amount: { type: :number, minimum: 0 },
pricing_type: { type: :string, enum: Expense.pricing_types.keys }
}
SLUG = {

View File

@ -0,0 +1,61 @@
# Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper'
RSpec.describe 'summary', type: :request do
path '/{slug}/summary' do
get('list summaries') do
tags 'Summary'
produces 'application/json'
consumes 'application/json'
parameter Swagger::Schema::SLUG
response(200, 'successful') do
schema type: :object,
required: %i[expenses guests],
properties: {
expenses: {
type: :object,
required: %i[projected confirmed status],
properties: {
projected: {
type: :object,
required: %i[total guests],
properties: {
total: { type: :number },
guests: { type: :number }
}
},
confirmed: {
type: :object,
required: %i[total guests],
properties: {
total: { type: :number },
guests: { type: :number }
}
},
status: {
type: :object,
required: [:paid],
properties: {
paid: { type: :number }
}
}
}
},
guests: {
type: :object,
required: %i[total confirmed declined tentative invited],
properties: {
total: { type: :number },
confirmed: { type: :number },
declined: { type: :number },
tentative: { type: :number },
invited: { type: :number }
}
}
}
xit
end
end
end
end

View File

@ -0,0 +1,61 @@
# Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper'
RSpec.describe 'tables_arrangements', type: :request do
path '/{slug}/tables_arrangements' do
get('list tables arrangements') do
tags 'Tables Arrangements'
produces 'application/json'
parameter Swagger::Schema::SLUG
response(200, 'successful') do
schema type: :array,
items: {
type: :object,
required: %i[id name discomfort],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string },
discomfort: { type: :integer }
}
}
xit
end
regular_api_responses
end
end
path '/{slug}/tables_arrangements/{id}' do
get('show tables arrangement') do
tags 'Tables Arrangements'
produces 'application/json'
parameter Swagger::Schema::SLUG
parameter Swagger::Schema::ID
response(200, 'successful') do
schema type: :array,
items: {
type: :object,
required: %i[number guests],
properties: {
number: { type: :integer },
guests: {
type: :array,
items: {
type: :object,
required: %i[id name color],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string },
color: { type: :string }
}
}
}
}
}
xit
end
regular_api_responses
end
end
end

View File

@ -0,0 +1,15 @@
# Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper'
RSpec.describe 'tokens', type: :request do
path '/token' do
get('get a cookie with CSRF token') do
tags 'CSRF token'
consumes 'application/json'
produces 'application/json'
response_empty_200
end
end
end