Compare commits

..

1 Commits

Author SHA1 Message Date
Renovate Bot
00015e53f4 Update dependency solid_queue to v1.1.2
Some checks failed
Run unit tests / unit_tests (pull_request) Failing after 1m21s
Add copyright notice / copyright_notice (pull_request) Successful in 2m10s
Check usage of free licenses / check-licenses (pull_request) Successful in 1m5s
2024-12-28 01:19:15 +00:00
97 changed files with 341 additions and 861 deletions

View File

@ -24,7 +24,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- uses: ruby/setup-ruby@v1.202.0 - uses: ruby/setup-ruby@v1.202.0
- run: bundle install - run: bundle install
- run: bundle exec rubocop --force-exclusion --parallel
- name: Wait until Postgres is ready to accept connections - name: Wait until Postgres is ready to accept connections
run: | run: |
apt-get update && apt-get install -f -y postgresql-client apt-get update && apt-get install -f -y postgresql-client

View File

@ -1,29 +0,0 @@
require:
- rubocop-rails
- rubocop-factory_bot
- rubocop-rspec
- rubocop-rspec_rails
AllCops:
NewCops: enable
Exclude:
- 'db/**/*'
- 'config/**/*'
- 'script/**/*'
- 'bin/*'
- '*.yml'
Layout/LineLength:
Max: 120
RSpec/ExampleLength:
Enabled: false
Metrics/ModuleLength:
Enabled: false
RSpec/MultipleMemoizedHelpers:
Enabled: false
Style/Documentation:
Enabled: false
Metrics/MethodLength:
Max: 20
Rails/SkipsModelValidations:
Enabled: false
Metrics/AbcSize:
Enabled: false

18
Gemfile
View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.3.6' ruby '3.3.6'
@ -17,15 +15,15 @@ gem 'stimulus-rails'
gem 'turbo-rails' gem 'turbo-rails'
gem 'tzinfo-data', platforms: %i[windows jruby] gem 'tzinfo-data', platforms: %i[windows jruby]
gem 'acts_as_tenant'
gem 'faker' gem 'faker'
gem 'httparty'
gem 'jsonapi-rails' gem 'jsonapi-rails'
gem 'pluck_to_hash'
gem 'rack-cors' gem 'rack-cors'
gem 'react-rails' gem 'react-rails'
gem 'rswag'
gem 'rubytree' gem 'rubytree'
gem 'acts_as_tenant'
gem 'httparty'
gem 'rswag'
gem 'pluck_to_hash'
group :development, :test do group :development, :test do
gem 'annotaterb' gem 'annotaterb'
@ -38,16 +36,12 @@ group :development, :test do
end end
group :development do group :development do
gem 'letter_opener_web'
gem 'rubocop' gem 'rubocop'
gem 'rubocop-factory_bot', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
gem 'rubocop-rspec_rails', require: false
gem 'web-console' gem 'web-console'
gem 'letter_opener_web'
end end
gem 'chroma' gem 'chroma'
gem 'solid_queue', '~> 1.0' gem 'solid_queue', '~> 1.0'
gem 'devise', '~> 4.9' gem "devise", "~> 4.9"

View File

@ -339,18 +339,6 @@ GEM
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.36.2) rubocop-ast (1.36.2)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-factory_bot (2.26.1)
rubocop (~> 1.61)
rubocop-rails (2.28.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.3.0)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
rubocop-rspec (~> 3, >= 3.0.1)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
rubytree (2.1.0) rubytree (2.1.0)
json (~> 2.0, > 2.3.1) json (~> 2.0, > 2.3.1)
@ -438,10 +426,6 @@ DEPENDENCIES
rspec-rails (~> 7.1.0) rspec-rails (~> 7.1.0)
rswag rswag
rubocop rubocop
rubocop-factory_bot
rubocop-rails
rubocop-rspec
rubocop-rspec_rails
rubytree rubytree
shoulda-matchers (~> 6.0) shoulda-matchers (~> 6.0)
solid_queue (~> 1.0) solid_queue (~> 1.0)
@ -577,10 +561,6 @@ CHECKSUMS
rswag-ui (2.16.0) sha256=a1f49e927dceda92e6e6e7c1000f1e217ee66c565f69e28131dc98b33cd3a04f rswag-ui (2.16.0) sha256=a1f49e927dceda92e6e6e7c1000f1e217ee66c565f69e28131dc98b33cd3a04f
rubocop (1.69.2) sha256=762fb0f30a379bf6054588d39f1815a2a1df8f067bc0337d3fe500e346924bb8 rubocop (1.69.2) sha256=762fb0f30a379bf6054588d39f1815a2a1df8f067bc0337d3fe500e346924bb8
rubocop-ast (1.36.2) sha256=566405b7f983eb9aa3b91d28aca6bc6566e356a97f59e89851dd910aef1dd1ca rubocop-ast (1.36.2) sha256=566405b7f983eb9aa3b91d28aca6bc6566e356a97f59e89851dd910aef1dd1ca
rubocop-factory_bot (2.26.1) sha256=8de13cd4edcee5ca800f255188167ecef8dbfc3d1fae9f15734e9d2e755392aa
rubocop-rails (2.28.0) sha256=4967bed9ea13e6dcab566fea4265a6dd0381db739b305e48930aba1282da2715
rubocop-rspec (3.3.0) sha256=79e1b281a689044d1516fefbc52e2e6c06cd367c25ebeaf06a7a198e9071cd7d
rubocop-rspec_rails (2.30.0) sha256=888112e83f9d7ef7ad2397e9d69a0b9614a4bae24f072c399804a180f80c4c46
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
rubytree (2.1.0) sha256=30e8759ba060dff0dabf7e40cbaaa4df892fa34cbe9f1b3fbb00e83a3f321e4b rubytree (2.1.0) sha256=30e8759ba060dff0dabf7e40cbaaa4df892fa34cbe9f1b3fbb00e83a3f321e4b
rubyzip (2.3.2) sha256=3f57e3935dc2255c414484fbf8d673b4909d8a6a57007ed754dde39342d2373f rubyzip (2.3.2) sha256=3f57e3935dc2255c414484fbf8d673b4909d8a6a57007ed754dde39342d2373f

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
# Add your own tasks in files placed in lib/tasks ending in .rake, # Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative 'config/application' require_relative "config/application"
Rails.application.load_tasks Rails.application.load_tasks

View File

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

View File

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

View File

@ -1,41 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class AffinitiesController < ApplicationController
before_action :set_group
def index
overridden = @group.affinities.each_with_object({}) do |affinity, acc|
acc[affinity.another_group(@group).id] = affinity.discomfort
end
Group.where.not(id: @group.id)
.pluck(:id)
.index_with { |group_id| GroupAffinity::MAX_DISCOMFORT - (overridden[group_id] || GroupAffinity::NEUTRAL) }
.then { |affinities| render json: affinities }
end
def bulk_update
affinities = params.expect(affinities: [%i[group_id affinity]]).map(&:to_h).map do |affinity|
{
group_a_id: @group.id,
group_b_id: affinity[:group_id],
discomfort: GroupAffinity::MAX_DISCOMFORT - affinity[:affinity]
}
end
GroupAffinity.upsert_all(affinities)
render json: {}, status: :ok
rescue ActiveRecord::InvalidForeignKey
render json: { error: 'At least one of the group IDs provided does not exist.' }, status: :bad_request
rescue ActiveRecord::StatementInvalid
render json: { error: 'Invalid group ID or discomfort provided.' }, status: :bad_request
end
private
def set_group
@group = Group.find(params[:group_id])
end
end

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
set_current_tenant_through_filter set_current_tenant_through_filter
before_action :set_tenant before_action :set_tenant
@ -42,7 +40,7 @@ class ApplicationController < ActionController::Base
end end
def captcha_params def captcha_params
params.expect(captcha: %i[id answer]) params.expect(captcha: [:id, :answer])
end end
def default_url_options(options = {}) def default_url_options(options = {})
@ -55,7 +53,7 @@ class ApplicationController < ActionController::Base
def development_swagger? def development_swagger?
Rails.env.test? || Rails.env.test? ||
(Rails.env.development? && request.headers['referer']&.include?('/api-docs/index.html')) Rails.env.development? && request.headers['referer']&.include?('/api-docs/index.html')
end end
def set_csrf_cookie def set_csrf_cookie

View File

@ -1,12 +1,10 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class CaptchaController < ApplicationController class CaptchaController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_before_action :set_tenant skip_before_action :set_tenant
def create def create
id = LibreCaptcha.new.id id = LibreCaptcha.new.get_id
render json: { render json: {
id:, id:,
media_url: media_captcha_index_url(id:) media_url: media_captcha_index_url(id:)

View File

@ -1,14 +1,12 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class ExpensesController < ApplicationController class ExpensesController < ApplicationController
def summary def summary
render json: Expenses::TotalQuery.new.call render json: Expenses::TotalQuery.new.call
end end
def index def index
render json: Expense.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type]) render json: Expense.all.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type])
end end
def create def create

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class GroupsController < ApplicationController class GroupsController < ApplicationController
def index def index
query_result = Groups::SummaryQuery.new.call.as_json.map(&:deep_symbolize_keys).map do |group| query_result = Groups::SummaryQuery.new.call.as_json.map(&:deep_symbolize_keys).map do |group|
@ -41,6 +39,6 @@ class GroupsController < ApplicationController
end end
def group_params def group_params
params.expect(group: %i[name icon color]) params.expect(group: [:name, :icon, :color])
end end
end end

View File

@ -1,12 +1,10 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'csv' require 'csv'
class GuestsController < ApplicationController class GuestsController < ApplicationController
def index def index
render json: Guest.includes(:group) render json: Guest.all.includes(:group)
.left_joins(:group) .left_joins(:group)
.order('groups.name' => :asc, name: :asc) .order('groups.name' => :asc, name: :asc)
.as_json(only: %i[id name status], include: { group: { only: %i[id name] } }) .as_json(only: %i[id name status], include: { group: { only: %i[id name] } })

View File

@ -1,33 +1,11 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class SummaryController < ApplicationController class SummaryController < ApplicationController
def index def index
render json: {
expenses:,
guests:
}
end
private
def guests
guest_summary = Guest.group(:status).count
{
total: guest_summary.except('considered').values.sum,
confirmed: guest_summary['confirmed'].to_i,
declined: guest_summary['declined'].to_i,
tentative: guest_summary['tentative'].to_i,
invited: guest_summary['invited'].to_i
}
end
def expenses
expense_summary = Expenses::TotalQuery.new(wedding: ActsAsTenant.current_tenant).call expense_summary = Expenses::TotalQuery.new(wedding: ActsAsTenant.current_tenant).call
guest_summary = Guest.group(:status).count
{ render json: {
expenses: {
projected: { projected: {
total: expense_summary['total_projected'], total: expense_summary['total_projected'],
guests: expense_summary['projected_guests'] guests: expense_summary['projected_guests']
@ -39,6 +17,14 @@ class SummaryController < ApplicationController
status: { status: {
paid: 0 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
end end

View File

@ -1,10 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class TablesArrangementsController < ApplicationController class TablesArrangementsController < ApplicationController
def index def index
render json: TablesArrangement.order(discomfort: :asc).limit(3).as_json(only: %i[id name discomfort]) render json: TablesArrangement.all.order(discomfort: :asc).limit(3).as_json(only: %i[id name discomfort])
end end
def show def show

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class TokensController < ApplicationController class TokensController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_before_action :set_tenant skip_before_action :set_tenant

View File

@ -1,9 +1,6 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true class Users::ConfirmationsController < Devise::ConfirmationsController
module Users
class ConfirmationsController < Devise::ConfirmationsController
clear_respond_to clear_respond_to
respond_to :json respond_to :json
@ -23,5 +20,4 @@ module Users
return return
end end
end end
end
end end

View File

@ -1,9 +1,6 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true class Users::RegistrationsController < Devise::RegistrationsController
module Users
class RegistrationsController < Devise::RegistrationsController
clear_respond_to clear_respond_to
respond_to :json respond_to :json
@ -28,5 +25,4 @@ module Users
def set_tenant def set_tenant
set_current_tenant(nil) set_current_tenant(nil)
end end
end
end end

View File

@ -1,10 +1,6 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true class Users::SessionsController < Devise::SessionsController
module Users
class SessionsController < Devise::SessionsController
clear_respond_to clear_respond_to
respond_to :json respond_to :json
end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class TableSimulatorJob < ApplicationJob class TableSimulatorJob < ApplicationJob
queue_as :default queue_as :default

View File

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

View File

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

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# == Schema Information # == Schema Information
# #
# Table name: expenses # Table name: expenses

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# == Schema Information # == Schema Information
# #
# Table name: groups # Table name: groups
@ -33,7 +31,7 @@ class Group < ApplicationRecord
validates :name, uniqueness: true validates :name, uniqueness: true
validates :name, :order, presence: true validates :name, :order, presence: true
has_many :children, class_name: 'Group', foreign_key: 'parent_id', dependent: :nullify, inverse_of: :parent has_many :children, class_name: 'Group', foreign_key: 'parent_id'
belongs_to :parent, class_name: 'Group', optional: true belongs_to :parent, class_name: 'Group', optional: true
before_create :set_color before_create :set_color
@ -43,7 +41,10 @@ class Group < ApplicationRecord
has_many :guests, dependent: :nullify has_many :guests, dependent: :nullify
def colorize_children(generation = 1) def colorize_children(generation = 1)
children.zip(palette(generation)) do |child, raw_color| 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 = raw_color.paint
final_color.brighten(60) if final_color.dark? final_color.brighten(60) if final_color.dark?
@ -53,20 +54,8 @@ class Group < ApplicationRecord
end end
end end
def affinities
GroupAffinity.where(group_a_id: id).or(GroupAffinity.where(group_b_id: id))
end
private private
def palette(generation)
if generation == 1
color.paint.palette.analogous(size: children.count)
else
color.paint.palette.decreasing_saturation
end
end
def set_color def set_color
return if color.present? return if color.present?

View File

@ -1,43 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# == Schema Information
#
# Table name: group_affinities
#
# id :bigint not null, primary key
# discomfort :float not null
# created_at :datetime not null
# updated_at :datetime not null
# group_a_id :uuid not null
# group_b_id :uuid not null
#
# Indexes
#
# index_group_affinities_on_group_a_id (group_a_id)
# index_group_affinities_on_group_b_id (group_b_id)
# uindex_group_pair (LEAST(group_a_id, group_b_id), GREATEST(group_a_id, group_b_id)) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (group_a_id => groups.id)
# fk_rails_... (group_b_id => groups.id)
#
class GroupAffinity < ApplicationRecord
NEUTRAL = 1
MIN_DISCOMFORT = 0
MAX_DISCOMFORT = 2
belongs_to :group_a, class_name: 'Group'
belongs_to :group_b, class_name: 'Group'
validates :discomfort,
numericality: { greater_than_or_equal_to: MIN_DISCOMFORT, less_than_or_equal_to: MAX_DISCOMFORT }
def another_group(group)
return nil if group != group_a && group != group_b
group == group_a ? group_b : group_a
end
end

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# == Schema Information # == Schema Information
# #
# Table name: guests # Table name: guests
@ -41,8 +39,8 @@ class Guest < ApplicationRecord
scope :potential, -> { where.not(status: %i[declined considered]) } scope :potential, -> { where.not(status: %i[declined considered]) }
after_destroy :recalculate_simulations
after_save :recalculate_simulations, if: :saved_change_to_status? after_save :recalculate_simulations, if: :saved_change_to_status?
after_destroy :recalculate_simulations
has_many :seats, dependent: :delete_all has_many :seats, dependent: :delete_all

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# == Schema Information # == Schema Information
# #
# Table name: seats # Table name: seats

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# == Schema Information # == Schema Information
# #
# Table name: tables_arrangements # Table name: tables_arrangements
@ -23,7 +21,7 @@
# #
class TablesArrangement < ApplicationRecord class TablesArrangement < ApplicationRecord
acts_as_tenant :wedding acts_as_tenant :wedding
has_many :seats, dependent: :delete_all has_many :seats
has_many :guests, through: :seats has_many :guests, through: :seats
before_create :assign_name before_create :assign_name

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# == Schema Information # == Schema Information
# #
# Table name: users # Table name: users

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# == Schema Information # == Schema Information
# #
# Table name: weddings # Table name: weddings

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
module Expenses module Expenses
class TotalQuery class TotalQuery
private attr_reader :wedding private attr_reader :wedding
@ -18,7 +16,7 @@ module Expenses
private private
def query def query
<<~SQL.squish <<~SQL
WITH guest_count AS (#{guest_count_per_status}), WITH guest_count AS (#{guest_count_per_status}),
expense_summary AS (#{expense_summary}) expense_summary AS (#{expense_summary})
SELECT guest_count.confirmed as confirmed_guests, SELECT guest_count.confirmed as confirmed_guests,
@ -30,7 +28,7 @@ module Expenses
end end
def expense_summary def expense_summary
<<~SQL.squish <<~SQL
SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed, SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed,
coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable
FROM expenses FROM expenses
@ -39,7 +37,7 @@ module Expenses
end end
def guest_count_per_status def guest_count_per_status
<<~SQL.squish <<~SQL
SELECT COALESCE(count(*) filter(where status = #{Guest.statuses['confirmed']}), 0) as confirmed, SELECT COALESCE(count(*) filter(where status = #{Guest.statuses['confirmed']}), 0) as confirmed,
COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at('confirmed', 'invited', 'tentative').join(',')})), 0) as projected COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at('confirmed', 'invited', 'tentative').join(',')})), 0) as projected
FROM guests FROM guests

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
module Groups module Groups
class SummaryQuery class SummaryQuery
def call def call
@ -11,21 +9,13 @@ module Groups
:icon, :icon,
:parent_id, :parent_id,
:color, :color,
*count_expressions
)
end
private
def count_expressions
[
Arel.sql('count(*) filter (where status IS NOT NULL) as total'), Arel.sql('count(*) filter (where status IS NOT NULL) as total'),
Arel.sql('count(*) filter (where status = 0) as considered'), Arel.sql('count(*) filter (where status = 0) as considered'),
Arel.sql('count(*) filter (where status = 10) as invited'), Arel.sql('count(*) filter (where status = 10) as invited'),
Arel.sql('count(*) filter (where status = 20) as confirmed'), Arel.sql('count(*) filter (where status = 20) as confirmed'),
Arel.sql('count(*) filter (where status = 30) as declined'), Arel.sql('count(*) filter (where status = 30) as declined'),
Arel.sql('count(*) filter (where status = 40) as tentative') Arel.sql('count(*) filter (where status = 40) as tentative'),
] )
end end
end end
end end

View File

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

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class SerializableGuest < JSONAPI::Serializable::Resource class SerializableGuest < JSONAPI::Serializable::Resource
type 'guest' type 'guest'

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
class AffinityGroupsHierarchy < Array class AffinityGroupsHierarchy < Array
include Singleton include Singleton

View File

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

View File

@ -1,19 +1,7 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
module Tables module Tables
class DiscomfortCalculator class DiscomfortCalculator
class << self
def cohesion_discomfort(id_a:, id_b:)
distance = AffinityGroupsHierarchy.instance.distance(id_a, id_b)
return 1 if distance.nil?
Rational(distance, distance + 1)
end
end
private attr_reader :table private attr_reader :table
def initialize(table:) def initialize(table:)
@table = table @table = table
@ -57,7 +45,12 @@ module Tables
# #
def cohesion_discomfort def cohesion_discomfort
table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)| table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)|
count_a * count_b * self.class.cohesion_discomfort(id_a: a, id_b: b) distance = AffinityGroupsHierarchy.instance.distance(a, b)
next count_a * count_b if distance.nil?
next 0 if distance.zero?
count_a * count_b * Rational(distance, distance + 1)
end end
end end
end end

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require_relative '../../extensions/tree_node_extension' require_relative '../../extensions/tree_node_extension'
module Tables module Tables

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
module Tables module Tables
class Shift class Shift
private attr_reader :initial_solution private attr_reader :initial_solution

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
module Tables module Tables
class Swap class Swap
private attr_reader :initial_solution private attr_reader :initial_solution

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
module Tables module Tables
class Table < Set class Table < Set
attr_accessor :discomfort, :min_per_table, :max_per_table attr_accessor :discomfort, :min_per_table, :max_per_table

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
module VNS module VNS
class Engine class Engine
class << self class << self

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
# This file is used by Rack-based servers to start the application. # This file is used by Rack-based servers to start the application.
require_relative 'config/environment' require_relative "config/environment"
run Rails.application run Rails.application
Rails.application.load_server Rails.application.load_server

View File

@ -23,12 +23,7 @@ Rails.application.routes.draw do
get '/users/confirmation', to: 'users/confirmations#show', as: :confirmation get '/users/confirmation', to: 'users/confirmations#show', as: :confirmation
end end
resources :groups, only: %i[index create update destroy] do resources :groups, only: %i[index create update destroy]
resources :affinities, only: %i[index] do
put :bulk_update, on: :collection
end
end
resources :guests, only: %i[index create update destroy] do resources :guests, only: %i[index create update destroy] do
post :bulk_update, on: :collection post :bulk_update, on: :collection
end end

View File

@ -1,29 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
class CreateGroupAffinities < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def change
create_table :group_affinities, if_not_exists: true do |t|
t.references :group_a, type: :uuid, null: false, foreign_key: { to_table: :groups }
t.references :group_b, type: :uuid, null: false, foreign_key: { to_table: :groups }
t.float :discomfort, null: false
t.timestamps
end
add_check_constraint :group_affinities, 'group_a_id != group_b_id', name: :check_distinct_groups, if_not_exists: true
add_check_constraint :group_affinities, 'discomfort >= 0 AND discomfort <= 2', if_not_exists: true
reversible do |dir|
dir.up do
execute <<~SQL
CREATE UNIQUE INDEX CONCURRENTLY uindex_group_pair ON group_affinities (least(group_a_id, group_b_id), greatest(group_a_id, group_b_id));
SQL
end
dir.down do
remove_index :group_affinities, name: :uindex_group_pair, if_exists: true
end
end
end
end

17
db/schema.rb generated
View File

@ -12,7 +12,7 @@
# #
# 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_12_16_231415) 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 # These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
@ -30,19 +30,6 @@ ActiveRecord::Schema[8.0].define(version: 2024_12_16_231415) do
t.index ["wedding_id"], name: "index_expenses_on_wedding_id" t.index ["wedding_id"], name: "index_expenses_on_wedding_id"
end end
create_table "group_affinities", force: :cascade do |t|
t.uuid "group_a_id", null: false
t.uuid "group_b_id", null: false
t.float "discomfort", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index "LEAST(group_a_id, group_b_id), GREATEST(group_a_id, group_b_id)", name: "uindex_group_pair", unique: true
t.index ["group_a_id"], name: "index_group_affinities_on_group_a_id"
t.index ["group_b_id"], name: "index_group_affinities_on_group_b_id"
t.check_constraint "discomfort >= 0::double precision AND discomfort <= 2::double precision"
t.check_constraint "group_a_id <> group_b_id", name: "check_distinct_groups"
end
create_table "groups", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| create_table "groups", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "icon" t.string "icon"
@ -241,8 +228,6 @@ ActiveRecord::Schema[8.0].define(version: 2024_12_16_231415) do
end end
add_foreign_key "expenses", "weddings" add_foreign_key "expenses", "weddings"
add_foreign_key "group_affinities", "groups", column: "group_a_id"
add_foreign_key "group_affinities", "groups", column: "group_b_id"
add_foreign_key "groups", "groups", column: "parent_id" add_foreign_key "groups", "groups", column: "parent_id"
add_foreign_key "groups", "weddings" add_foreign_key "groups", "weddings"
add_foreign_key "guests", "groups" add_foreign_key "guests", "groups"

View File

@ -11,13 +11,6 @@ services:
environment: environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/postgres DATABASE_URL: postgres://postgres:postgres@db:5432/postgres
RAILS_ENV: development RAILS_ENV: development
tty: true
stdin_open: true
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/up"]
interval: 10s
timeout: 5s
retries: 5
volumes: volumes:
- .:/rails - .:/rails
workers: workers:
@ -39,11 +32,6 @@ services:
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
ports: ports:
- 3000 - 3000
healthcheck:
test: wget -qO - http://localhost:3000/api/health || exit 1
interval: 10s
timeout: 5s
retries: 5
depends_on: depends_on:
- backend - backend
volumes: volumes:
@ -62,10 +50,8 @@ services:
volumes: volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on: depends_on:
frontend: - frontend
condition: service_healthy - backend
backend:
condition: service_healthy
db: db:
image: postgres:17 image: postgres:17
ports: ports:

View File

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

View File

@ -1,72 +1,70 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
module Tree module Tree
RSpec.describe TreeNode do RSpec.describe TreeNode do
describe '#distance_to_common_ancestor' do describe '#distance_to_common_ancestor' do
def assert_distance(node1, node2, distance) def assert_distance(node_1, node_2, distance)
aggregate_failures do aggregate_failures do
expect(node1.distance_to_common_ancestor(node2)).to eq(distance) expect(node_1.distance_to_common_ancestor(node_2)).to eq(distance)
expect(node2.distance_to_common_ancestor(node1)).to eq(distance) expect(node_2.distance_to_common_ancestor(node_1)).to eq(distance)
end end
end end
context 'when the two nodes are the same' do context 'when the two nodes are the same' do
it 'returns 0 when comparing the root itself' do it 'returns 0 when comparing the root itself' do
root = described_class.new('root') root = Tree::TreeNode.new('root')
assert_distance(root, root, 0) assert_distance(root, root, 0)
end end
it 'returns 0 when comparing a child to itself' do it 'returns 0 when comparing a child to itself' do
root = described_class.new('root') root = Tree::TreeNode.new('root')
child = root << described_class.new('child') child = root << Tree::TreeNode.new('child')
assert_distance(child, child, 0) assert_distance(child, child, 0)
end end
end end
context 'when the two nodes are siblings' do context 'when the two nodes are siblings' do
it 'returns 1 when comparing siblings' do it 'returns 1 when comparing siblings' do
root = described_class.new('root') root = Tree::TreeNode.new('root')
child1 = root << described_class.new('child1') child1 = root << Tree::TreeNode.new('child1')
child2 = root << described_class.new('child2') child2 = root << Tree::TreeNode.new('child2')
assert_distance(child1, child2, 1) assert_distance(child1, child2, 1)
end end
end end
context 'when one node is parent of the other' do context 'when one node is parent of the other' do
it 'returns 1 when comparing parent to child' do it 'returns 1 when comparing parent to child' do
root = described_class.new('root') root = Tree::TreeNode.new('root')
child = root << described_class.new('child') child = root << Tree::TreeNode.new('child')
assert_distance(root, child, 1) assert_distance(root, child, 1)
end end
end end
context 'when one node is grandparent of the other' do context 'when one node is grandparent of the other' do
it 'returns 2 when comparing grandparent to grandchild' do it 'returns 2 when comparing grandparent to grandchild' do
root = described_class.new('root') root = Tree::TreeNode.new('root')
child = root << described_class.new('child') child = root << Tree::TreeNode.new('child')
grandchild = child << described_class.new('grandchild') grandchild = child << Tree::TreeNode.new('grandchild')
assert_distance(root, grandchild, 2) assert_distance(root, grandchild, 2)
end end
end end
context 'when the two nodes are cousins' do context 'when the two nodes are cousins' do
it 'returns 2 when comparing cousins' do it 'returns 2 when comparing cousins' do
root = described_class.new('root') root = Tree::TreeNode.new('root')
child1 = root << described_class.new('child1') child1 = root << Tree::TreeNode.new('child1')
child2 = root << described_class.new('child2') child2 = root << Tree::TreeNode.new('child2')
grandchild1 = child1 << described_class.new('grandchild1') grandchild1 = child1 << Tree::TreeNode.new('grandchild1')
grandchild2 = child2 << described_class.new('grandchild2') grandchild2 = child2 << Tree::TreeNode.new('grandchild2')
assert_distance(grandchild1, grandchild2, 2) assert_distance(grandchild1, grandchild2, 2)
end end
end end
context 'when the two nodes are not related' do context 'when the two nodes are not related' do
it 'returns nil' do it 'returns nil' do
root = described_class.new('root') root = Tree::TreeNode.new('root')
another_root = described_class.new('another_root') another_root = Tree::TreeNode.new('another_root')
assert_distance(root, another_root, nil) assert_distance(root, another_root, nil)
end end
end end

View File

@ -1,22 +1,19 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :expense do factory :expense do
wedding wedding
sequence(:name) { |i| "Expense #{i}" } sequence(:name) { |i| "Expense #{i}" }
pricing_type { 'fixed' } pricing_type { "fixed" }
amount { 100 } amount { 100 }
end end
trait :fixed do trait :fixed do
pricing_type { 'fixed' } pricing_type { "fixed" }
end end
trait :per_person do trait :per_person do
pricing_type { 'per_person' } pricing_type { "per_person" }
end end
end end

View File

@ -1,13 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
FactoryBot.define do
factory :group_affinity do
group_a factory: %i[group]
group_b factory: %i[group]
discomfort { GroupAffinity::NEUTRAL }
end
end

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :group do factory :group do
wedding wedding

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :guest do factory :guest do
group group

View File

@ -1,11 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :tables_arrangement do factory :tables_arrangement do
wedding wedding
end end
end end

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :user do factory :user do
wedding wedding

View File

@ -1,9 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :wedding do factory :wedding do
sequence(:slug) { |i| "wedding-#{i}" } sequence(:slug) { |i| "wedding-#{i}" }

View File

@ -1,16 +1,12 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.describe Expense do RSpec.describe Expense, type: :model do
describe 'validations' do describe 'validations' do
it { is_expected.to validate_presence_of(:name) } it { should validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:amount) } it { should validate_presence_of(:amount) }
it { is_expected.to validate_numericality_of(:amount).is_greater_than(0) } it { should validate_numericality_of(:amount).is_greater_than(0) }
it { is_expected.to validate_presence_of(:pricing_type) } it { should validate_presence_of(:pricing_type) }
end end
end end

View File

@ -1,46 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe GroupAffinity do
subject(:affinity) { build(:group_affinity, group_a:, group_b:) }
let(:wedding) { create(:wedding) }
let(:group_a) { create(:group, wedding:) }
let(:group_b) { create(:group, wedding:) }
let(:group_c) { create(:group, wedding:) }
describe 'validations' do
it do
expect(affinity).to validate_numericality_of(:discomfort)
.is_greater_than_or_equal_to(0)
.is_less_than_or_equal_to(2)
end
end
describe '.create' do
before do
create(:group_affinity, group_a: group_a, group_b: group_b)
end
it 'disallows the creation of a group affinity with the same group on both sides' do
expect do
create(:group_affinity, group_a: group_c, group_b: group_c)
end.to raise_error(ActiveRecord::StatementInvalid)
end
it 'disallows the creation of a group affinity that already exists' do
expect do
create(:group_affinity, group_a: group_a, group_b: group_b)
end.to raise_error(ActiveRecord::StatementInvalid)
end
it 'disallows the creation of a group affinity with the same groups in reverse order' do
expect do
create(:group_affinity, group_a: group_b, group_b: group_a)
end.to raise_error(ActiveRecord::StatementInvalid)
end
end
end

View File

@ -1,14 +1,10 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.describe Group do RSpec.describe Group, type: :model do
describe 'callbacks' do describe 'callbacks' do
it 'sets color before create' do it 'should set color before create' do
expect(create(:group).color).to be_present expect(create(:group).color).to be_present
end end
end end

View File

@ -1,17 +1,12 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.describe Guest do RSpec.describe Guest, type: :model do
describe 'validations' do describe 'validations' do
subject(:guest) { build(:guest) } it { should validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:name) }
it do it do
expect(guest).to define_enum_for(:status).with_values( should define_enum_for(:status).with_values(
considered: 0, considered: 0,
invited: 10, invited: 10,
confirmed: 20, confirmed: 20,
@ -21,7 +16,7 @@ RSpec.describe Guest do
end end
end end
it { is_expected.to belong_to(:group).optional } it { should belong_to(:group).optional }
describe 'scopes' do describe 'scopes' do
describe '.potential' do describe '.potential' do
@ -32,7 +27,7 @@ RSpec.describe Guest do
confirmed_guest = create(:guest, status: :confirmed) confirmed_guest = create(:guest, status: :confirmed)
tentative_guest = create(:guest, status: :tentative) tentative_guest = create(:guest, status: :tentative)
expect(described_class.potential).to contain_exactly(invited_guest, confirmed_guest, tentative_guest) expect(Guest.potential).to match_array([invited_guest, confirmed_guest, tentative_guest])
end end
end end
end end

View File

@ -1,11 +1,7 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.describe Seat do RSpec.describe Seat, type: :model do
pending "add some examples to (or delete) #{__FILE__}" pending "add some examples to (or delete) #{__FILE__}"
end end

View File

@ -1,12 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.describe TablesArrangement do RSpec.describe TablesArrangement, type: :model do
describe 'callbacks' do describe 'callbacks' do
it 'assigns a name before creation' do it 'assigns a name before creation' do
expect(create(:tables_arrangement).name).to be_present expect(create(:tables_arrangement).name).to be_present

View File

@ -1,11 +1,7 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.describe User do RSpec.describe User, type: :model do
pending "add some examples to (or delete) #{__FILE__}" pending "add some examples to (or delete) #{__FILE__}"
end end

View File

@ -1,27 +1,22 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.describe Wedding do RSpec.describe Wedding, type: :model do
describe 'validations' do describe 'validations' do
subject { build(:wedding) } subject { build(:wedding) }
describe 'slug' do describe 'slug' do
it { is_expected.to allow_value('foo').for(:slug) } it { should allow_value('foo').for(:slug) }
it { is_expected.to allow_value('foo-bar').for(:slug) } it { should allow_value('foo-bar').for(:slug) }
it { is_expected.to allow_value('foo-123').for(:slug) } it { should allow_value('foo-123').for(:slug) }
it { is_expected.to allow_value('foo-123-').for(:slug) } it { should allow_value('foo-123-').for(:slug) }
it { is_expected.to allow_value('foo--123').for(:slug) } it { should allow_value('foo--123').for(:slug) }
it { is_expected.not_to allow_value('Foo').for(:slug) } it { should_not allow_value('Foo').for(:slug) }
it { is_expected.not_to allow_value('/foo').for(:slug) } it { should_not allow_value('/foo').for(:slug) }
it { is_expected.not_to allow_value('foo/123').for(:slug) } it { should_not allow_value('foo/123').for(:slug) }
it { is_expected.not_to allow_value('foo_123').for(:slug) } it { should_not allow_value('foo_123').for(:slug) }
it { is_expected.not_to allow_value('foo/').for(:slug) } it { should_not allow_value('foo/').for(:slug) }
end end
end end
end end

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
module Expenses module Expenses
@ -63,8 +61,8 @@ module Expenses
end end
it 'returns the sum of fixed and variable expenses', :aggregate_failures do 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_confirmed']).to eq(100 + 200 + 50 * 2)
expect(response['total_projected']).to eq(100 + 200 + (11 * 50)) expect(response['total_projected']).to eq(100 + 200 + 11 * 50)
expect(response['confirmed_guests']).to eq(2) expect(response['confirmed_guests']).to eq(2)
expect(response['projected_guests']).to eq(2 + 4 + 5) expect(response['projected_guests']).to eq(2 + 4 + 5)
end end

View File

@ -1,13 +1,11 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
module Groups module Groups
RSpec.describe SummaryQuery do RSpec.describe SummaryQuery do
describe '#call' do describe '#call' do
subject(:result) { described_class.new.call } subject { described_class.new.call }
context 'when there are no groups' do context 'when there are no groups' do
it { is_expected.to eq([]) } it { is_expected.to eq([]) }
@ -19,7 +17,7 @@ module Groups
context 'when there are no guests' do context 'when there are no guests' do
it 'returns the summary of groups' do it 'returns the summary of groups' do
expect(result).to contain_exactly( is_expected.to contain_exactly(
{ 'id' => parent.id, { 'id' => parent.id,
'name' => 'Friends', 'name' => 'Friends',
'icon' => 'icon-1', 'icon' => 'icon-1',
@ -60,11 +58,11 @@ module Groups
create_list(:guest, 8, group: child, status: :invited) create_list(:guest, 8, group: child, status: :invited)
create_list(:guest, 9, group: child, status: :confirmed) create_list(:guest, 9, group: child, status: :confirmed)
create_list(:guest, 10, group: child, status: :declined) create_list(:guest, 10, group: child, status: :declined)
create_list(:guest, 11, group: child, status: :tentative) # rubocop:disable FactoryBot/ExcessiveCreateList create_list(:guest, 11, group: child, status: :tentative)
end end
it 'returns the summary of groups' do it 'returns the summary of groups' do
expect(result).to contain_exactly( is_expected.to contain_exactly(
{ {
'id' => parent.id, 'id' => parent.id,
'name' => 'Friends', 'name' => 'Friends',

View File

@ -1,13 +1,11 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# This file is copied to spec/ when you run 'rails generate rspec:install' # This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper' require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test' ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment' require_relative '../config/environment'
# Prevent database truncation if the environment is production # Prevent database truncation if the environment is production
abort('The Rails environment is running in production mode!') if Rails.env.production? abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails' require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point! # Add additional requires below this line. Rails is not loaded until this point!

View File

@ -1,52 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper'
RSpec.describe 'affinities' do
path '/{slug}/groups/{group_id}/affinities' do
parameter Swagger::Schema::SLUG
parameter name: 'group_id', in: :path, type: :string, format: :uuid, description: 'group_id'
get('list affinities') do
tags 'Affinities'
produces 'application/json'
response(200, 'successful') do
schema type: :object, additionalProperties: { type: :integer, minimum: 0, maximum: 2 }
xit
end
end
end
path '/{slug}/groups/{group_id}/affinities/bulk_update' do
parameter Swagger::Schema::SLUG
parameter name: 'group_id', in: :path, type: :string, format: :uuid, description: 'group_id'
put('bulk update affinities') do
tags 'Affinities'
produces 'application/json'
consumes 'application/json'
parameter name: :body, in: :body, schema: {
type: :object,
required: [:affinities],
properties: {
affinities: {
type: :array,
items: {
type: :object,
required: %i[group_id affinity],
properties: {
group_id: { type: :string, format: :uuid, description: 'ID of the associated group' },
affinity: { type: :integer, minimum: 0, maximum: 2 }
}
}
}
}
}
response_empty200
end
end
end

View File

@ -1,11 +1,10 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'captcha' do RSpec.describe 'captcha', type: :request do
path '/captcha' do path '/captcha' do
post('create a CAPTCHA challenge') do post('create a CAPTCHA challenge') do
tags 'CAPTCHA' tags 'CAPTCHA'
consumes 'application/json' consumes 'application/json'
@ -16,7 +15,7 @@ RSpec.describe 'captcha' do
required: %i[id], required: %i[id],
properties: { properties: {
id: { type: :string, format: :uuid }, id: { type: :string, format: :uuid },
media_url: { type: :string, format: :uri } media_url: { type: :string, format: :uri },
} }
xit xit
end end

View File

@ -1,10 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'expenses' do RSpec.describe 'expenses', type: :request do
path '/{slug}/expenses' do path '/{slug}/expenses' do
get('list expenses') do get('list expenses') do
tags 'Expenses' tags 'Expenses'
@ -44,8 +42,8 @@ RSpec.describe 'expenses' do
} }
} }
response_empty201 response_empty_201
response422 response_422
regular_api_responses regular_api_responses
end end
end end
@ -62,9 +60,9 @@ RSpec.describe 'expenses' do
properties: Swagger::Schema::EXPENSE properties: Swagger::Schema::EXPENSE
} }
response_empty200 response_empty_200
response422 response_422
response404 response_404
regular_api_responses regular_api_responses
end end
@ -73,8 +71,8 @@ RSpec.describe 'expenses' do
produces 'application/json' produces 'application/json'
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
parameter Swagger::Schema::ID parameter Swagger::Schema::ID
response_empty200 response_empty_200
response404 response_404
regular_api_responses regular_api_responses
end end
end end

View File

@ -1,10 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'groups' do RSpec.describe 'groups', type: :request do
path '/{slug}/groups' do path '/{slug}/groups' do
get('list groups') do get('list groups') do
tags 'Groups' tags 'Groups'
@ -102,7 +100,7 @@ RSpec.describe 'groups' do
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
parameter name: :id, in: :path, type: :string, format: :uuid parameter name: :id, in: :path, type: :string, format: :uuid
response_empty200 response_empty_200
regular_api_responses regular_api_responses
end end
end end

View File

@ -1,10 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'guests' do RSpec.describe 'guests', type: :request do
path '/{slug}/guests' do path '/{slug}/guests' do
get('list guests') do get('list guests') do
tags 'Guests' tags 'Guests'
@ -53,8 +51,8 @@ RSpec.describe 'guests' do
} }
} }
response_empty201 response_empty_201
response422 response_422
regular_api_responses regular_api_responses
end end
end end
@ -82,9 +80,9 @@ RSpec.describe 'guests' do
} }
} }
response_empty200 response_empty_200
response422 response_422
response404 response_404
regular_api_responses regular_api_responses
end end
@ -94,8 +92,8 @@ RSpec.describe 'guests' do
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
parameter name: 'id', in: :path, type: :string, format: :uuid parameter name: 'id', in: :path, type: :string, format: :uuid
response_empty200 response_empty_200
response404 response_404
regular_api_responses regular_api_responses
end end
end end

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
module Swagger module Swagger
module Schema module Schema
USER = { USER = {
@ -9,13 +7,13 @@ module Swagger
email: { type: :string, format: :email }, email: { type: :string, format: :email },
created_at: SwaggerResponseHelper::TIMESTAMP, created_at: SwaggerResponseHelper::TIMESTAMP,
updated_at: SwaggerResponseHelper::TIMESTAMP updated_at: SwaggerResponseHelper::TIMESTAMP
}.freeze }
ID = { # rubocop:disable Style/MutableConstant -- rswag modifies in: :path parameters ID = {
name: 'id', name: 'id',
in: :path, in: :path,
type: :string, type: :string,
format: :uuid format: :uuid,
} }
GROUP = { GROUP = {
@ -23,15 +21,15 @@ module Swagger
icon: { type: :string, example: 'pi pi-crown', description: 'The CSS classes used by the icon' }, icon: { type: :string, example: 'pi pi-crown', description: 'The CSS classes used by the icon' },
parent_id: { type: :string, format: :uuid }, parent_id: { type: :string, format: :uuid },
color: { type: :string, pattern: '^#(?:[0-9a-fA-F]{3}){1,2}$' } color: { type: :string, pattern: '^#(?:[0-9a-fA-F]{3}){1,2}$' }
}.freeze }
EXPENSE = { EXPENSE = {
name: { type: :string }, name: { type: :string },
amount: { type: :number, minimum: 0 }, amount: { type: :number, minimum: 0 },
pricing_type: { type: :string, enum: Expense.pricing_types.keys } pricing_type: { type: :string, enum: Expense.pricing_types.keys }
}.freeze }
SLUG = { # rubocop:disable Style/MutableConstant -- rswag modifies in: :path parameters SLUG = {
name: 'slug', name: 'slug',
in: :path, in: :path,
type: :string, type: :string,
@ -49,6 +47,6 @@ module Swagger
answer: { type: :string } answer: { type: :string }
} }
} }
}.freeze }
end end
end end

View File

@ -1,10 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'summary' do RSpec.describe 'summary', type: :request do
path '/{slug}/summary' do path '/{slug}/summary' do
get('list summaries') do get('list summaries') do
tags 'Summary' tags 'Summary'

View File

@ -1,10 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'tables_arrangements' do RSpec.describe 'tables_arrangements', type: :request do
path '/{slug}/tables_arrangements' do path '/{slug}/tables_arrangements' do
get('list tables arrangements') do get('list tables arrangements') do
tags 'Tables Arrangements' tags 'Tables Arrangements'

View File

@ -1,5 +1,15 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' 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

View File

@ -1,10 +1,9 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'users/confirmations' do RSpec.describe 'users/confirmations', type: :request do
path '/{slug}/users/confirmation' do path '/{slug}/users/confirmation' do
get('confirm user email') do get('confirm user email') do
tags 'Users' tags 'Users'
@ -18,7 +17,7 @@ RSpec.describe 'users/confirmations' do
xit xit
end end
response422 response_422
end end
end end
end end

View File

@ -1,10 +1,9 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'users/registrations' do RSpec.describe 'users/registrations', type: :request do
path '/{slug}/users' do path '/{slug}/users' do
post('create registration') do post('create registration') do
tags 'Users Registrations' tags 'Users Registrations'
@ -14,13 +13,13 @@ RSpec.describe 'users/registrations' do
parameter Swagger::Schema::SLUG parameter Swagger::Schema::SLUG
parameter name: :body, in: :body, schema: { parameter name: :body, in: :body, schema: {
type: :object, type: :object,
required: %i[user wedding], required: [:user, :wedding],
properties: { properties: {
user: { user: {
type: :object, type: :object,
required: %i[email password password_confirmation], required: %i[email password password_confirmation],
properties: { properties: {
email: { type: :string, format: :email }, email: { type: :string, format: :email},
password: SwaggerResponseHelper::PASSWORD, password: SwaggerResponseHelper::PASSWORD,
password_confirmation: SwaggerResponseHelper::PASSWORD password_confirmation: SwaggerResponseHelper::PASSWORD
} }

View File

@ -1,11 +1,11 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'swagger_helper' require 'swagger_helper'
RSpec.describe 'users/sessions' do RSpec.describe 'users/sessions', type: :request do
path '/{slug}/users/sign_in' do path '/{slug}/users/sign_in' do
post('create session') do post('create session') do
tags 'Users Sessions' tags 'Users Sessions'
consumes 'application/json' consumes 'application/json'
@ -32,7 +32,7 @@ RSpec.describe 'users/sessions' do
xit xit
end end
response401(message: 'Invalid Email or password.') response_401(message: 'Invalid Email or password.')
end end
end end

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
module Tables module Tables
RSpec.describe DiscomfortCalculator do RSpec.describe DiscomfortCalculator do
@ -14,7 +12,8 @@ module Tables
describe '#calculate' do describe '#calculate' do
before do before do
allow(calculator).to receive_messages(table_size_penalty: 2, cohesion_discomfort: 3) allow(calculator).to receive(:table_size_penalty).and_return(2)
allow(calculator).to receive(:cohesion_discomfort).and_return(3)
end end
let(:table) { Table.new(create_list(:guest, 6)) } let(:table) { Table.new(create_list(:guest, 6)) }
@ -30,7 +29,6 @@ module Tables
table.min_per_table = 5 table.min_per_table = 5
table.max_per_table = 7 table.max_per_table = 7
end end
context 'when the number of guests is in the lower bound' do context 'when the number of guests is in the lower bound' do
let(:table) { Table.new(create_list(:guest, 5)) } let(:table) { Table.new(create_list(:guest, 5)) }
@ -90,7 +88,6 @@ module Tables
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, school.id).and_return(4) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, school.id).and_return(4)
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(work.id, school.id).and_return(5) allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(work.id, school.id).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, group: family) }
@ -105,7 +102,6 @@ module Tables
create(:guest, group: friends) create(:guest, group: friends)
] ]
end end
it { expect(calculator.send(:cohesion_discomfort)).to eq(1) } it { expect(calculator.send(:cohesion_discomfort)).to eq(1) }
end end
@ -209,7 +205,7 @@ module Tables
end end
it 'returns the sum of the penalties for each pair of guests' do it 'returns the sum of the penalties for each pair of guests' do
expect(calculator.send(:cohesion_discomfort)).to eq((4 * 1) + (4 * Rational(1, 2)) + (4 * Rational(2, 3))) expect(calculator.send(:cohesion_discomfort)).to eq(4 * 1 + 4 * Rational(1, 2) + 4 * Rational(2, 3))
end end
end end
@ -223,7 +219,7 @@ module Tables
end end
it 'returns the sum of the penalties for each pair of guests' do it 'returns the sum of the penalties for each pair of guests' do
expect(calculator.send(:cohesion_discomfort)).to eq((6 * 1) + (2 * Rational(1, 2)) + (3 * Rational(2, 3))) expect(calculator.send(:cohesion_discomfort)).to eq(6 * 1 + 2 * Rational(1, 2) + 3 * Rational(2, 3))
end end
end end
end end

View File

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

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
module Tables module Tables
@ -9,7 +7,7 @@ module Tables
describe '#each' do describe '#each' do
let(:shifts) do let(:shifts) do
acc = [] acc = []
described_class.new(initial_solution).each do |solution| # rubocop:disable Style/MapIntoArray -- #map is not implemented described_class.new(initial_solution).each do |solution|
acc << solution.tables.map(&:dup) acc << solution.tables.map(&:dup)
end end
acc acc

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
module Tables module Tables
@ -9,7 +7,7 @@ module Tables
describe '#each' do describe '#each' do
let(:swaps) do let(:swaps) do
acc = [] acc = []
described_class.new(initial_solution).each do |solution| # rubocop:disable Style/MapIntoArray -- #map is not implemented described_class.new(initial_solution).each do |solution|
acc << solution.tables.map(&:dup) acc << solution.tables.map(&:dup)
end end
acc acc

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
module VNS module VNS

View File

@ -1,7 +1,5 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
# This file was generated by the `rails generate rspec:install` command. Conventionally, all # This file was generated by the `rails generate rspec:install` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause # The generated `.rspec` file contains `--require spec_helper` which will cause
@ -48,49 +46,51 @@ RSpec.configure do |config|
# triggering implicit auto-inclusion in groups with matching metadata. # triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups config.shared_context_metadata_behavior = :apply_to_host_groups
# The settings below are suggested to provide a good initial experience # The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content. # with RSpec, but feel free to customize to your heart's content.
# # This allows you to limit a spec run to individual examples or groups =begin
# # you care about by tagging them with `:focus` metadata. When nothing # This allows you to limit a spec run to individual examples or groups
# # is tagged with `:focus`, all examples get run. RSpec also provides # you care about by tagging them with `:focus` metadata. When nothing
# # aliases for `it`, `describe`, and `context` that include `:focus` # is tagged with `:focus`, all examples get run. RSpec also provides
# # metadata: `fit`, `fdescribe` and `fcontext`, respectively. # aliases for `it`, `describe`, and `context` that include `:focus`
# config.filter_run_when_matching :focus # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
# config.filter_run_when_matching :focus
# # Allows RSpec to persist some state between runs in order to support
# # the `--only-failures` and `--next-failure` CLI options. We recommend # Allows RSpec to persist some state between runs in order to support
# # you configure your source control system to ignore this file. # the `--only-failures` and `--next-failure` CLI options. We recommend
# config.example_status_persistence_file_path = "spec/examples.txt" # you configure your source control system to ignore this file.
# config.example_status_persistence_file_path = "spec/examples.txt"
# # Limits the available syntax to the non-monkey patched syntax that is
# # recommended. For more details, see: # Limits the available syntax to the non-monkey patched syntax that is
# # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ # recommended. For more details, see:
# config.disable_monkey_patching! # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
# config.disable_monkey_patching!
# # Many RSpec users commonly either run the entire suite or an individual
# # file, and it's useful to allow more verbose output when running an # Many RSpec users commonly either run the entire suite or an individual
# # individual spec file. # file, and it's useful to allow more verbose output when running an
# if config.files_to_run.one? # individual spec file.
# # Use the documentation formatter for detailed output, if config.files_to_run.one?
# # unless a formatter has already been configured # Use the documentation formatter for detailed output,
# # (e.g. via a command-line flag). # unless a formatter has already been configured
# config.default_formatter = "doc" # (e.g. via a command-line flag).
# end config.default_formatter = "doc"
# end
# # Print the 10 slowest examples and example groups at the
# # end of the spec run, to help surface which specs are running # Print the 10 slowest examples and example groups at the
# # particularly slow. # end of the spec run, to help surface which specs are running
# config.profile_examples = 10 # particularly slow.
# config.profile_examples = 10
# # Run specs in random order to surface order dependencies. If you find an
# # order dependency and want to debug it, you can fix the order by providing # Run specs in random order to surface order dependencies. If you find an
# # the seed, which is printed after each run. # order dependency and want to debug it, you can fix the order by providing
# # --seed 1234 # the seed, which is printed after each run.
# config.order = :random # --seed 1234
# config.order = :random
# # Seed global randomization in this process using the `--seed` CLI option.
# # Setting this allows you to use `--seed` to deterministically reproduce # Seed global randomization in this process using the `--seed` CLI option.
# # test failures related to randomization by passing the same `--seed` value # Setting this allows you to use `--seed` to deterministically reproduce
# # as the one that triggered the failure. # test failures related to randomization by passing the same `--seed` value
# Kernel.srand config.seed # as the one that triggered the failure.
Kernel.srand config.seed
=end
end end

View File

@ -3,10 +3,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
require_relative 'swagger_response_helper' require_relative './swagger_response_helper'
require_relative 'requests/schemas' require_relative './requests/schemas.rb'
include SwaggerResponseHelper # rubocop:disable Style/MixinUsage include SwaggerResponseHelper
RSpec.configure do |config| RSpec.configure do |config|
# Specify a root folder where Swagger JSON files are generated # Specify a root folder where Swagger JSON files are generated

View File

@ -1,19 +1,18 @@
# Copyright (C) 2024 Manuel Bustillo # Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
module SwaggerResponseHelper module SwaggerResponseHelper
TIMESTAMP_FORMAT = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z' TIMESTAMP_FORMAT = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'
TIMESTAMP_EXAMPLE = Time.zone.now.iso8601(3) TIMESTAMP_EXAMPLE = Time.zone.now.iso8601(3)
TIMESTAMP = { type: :string, pattern: TIMESTAMP_FORMAT, example: TIMESTAMP_EXAMPLE }.freeze TIMESTAMP = {type: :string,pattern: TIMESTAMP_FORMAT,example: TIMESTAMP_EXAMPLE}.freeze
PASSWORD = { type: :string, minLength: User.password_length.begin, maxLength: User.password_length.end }.freeze PASSWORD = { type: :string, minLength: User.password_length.begin, maxLength: User.password_length.end }
def regular_api_responses def regular_api_responses
response401 response_401
end end
def response422 def response_422
response(422, 'Validation errors in input parameters') do response(422, 'Validation errors in input parameters') do
produces 'application/json' produces 'application/json'
error_schema error_schema
@ -21,7 +20,7 @@ module SwaggerResponseHelper
end end
end end
def response_empty200 def response_empty_200
response(200, 'Success') do response(200, 'Success') do
produces 'application/json' produces 'application/json'
schema type: :object schema type: :object
@ -29,7 +28,7 @@ module SwaggerResponseHelper
end end
end end
def response_empty201 def response_empty_201
response(201, 'Created') do response(201, 'Created') do
produces 'application/json' produces 'application/json'
schema type: :object schema type: :object
@ -37,7 +36,7 @@ module SwaggerResponseHelper
end end
end end
def response404 def response_404
response(404, 'Record not found') do response(404, 'Record not found') do
produces 'application/json' produces 'application/json'
error_schema error_schema
@ -45,7 +44,7 @@ module SwaggerResponseHelper
end end
end end
def response401(message: nil) def response_401(message: nil)
response(401, 'Unauthorized') do response(401, 'Unauthorized') do
produces 'application/json' produces 'application/json'
schema type: :object, schema type: :object,